TL;DR — クイックフィックス
プロセスがOSの許可数を超えるファイル(またはソケット)を開いたまま閉じていません。ファイルディスクリプタの上限に達しています。
**本番環境での最速修正:**実行中のユーザーの制限を引き上げる:
ulimit -n 65536
これで一時的に余裕ができます。ただし、次のデプロイ前に根本原因を修正してください — ulimitだけでは永遠に乗り切れません。
このエラーの見た目
Traceback (most recent call last):
File "worker.py", line 47, in process
f = open(path, 'r')
OSError: [Errno 24] Too many open files
ソケットでも同様のエラーが発生します:
OSError: [Errno 24] Too many open files
File "server.py", line 112, in accept
conn, addr = self.sock.accept()
どちらも根本原因は同じです。ファイルディスクリプタ(FD)はファイル、ソケット、パイプ、一部のIPCメカニズムで共有されています。単純に枯渇してしまったのです。
根本原因
開いているファイルやソケットはそれぞれ1つのファイルディスクリプタを消費します。LinuxとmacOSはプロセスごとにソフトリミットを設けており、Linuxでは通常 1024 です。現在の値を確認するには:
ulimit -n
# または
cat /proc/sys/fs/file-max # システム全体のハードキャップ
ほぼすべてのケースで2つの原因が考えられます:
- FDリーク:コードがファイルやソケットを開いたまま閉じていない。上限に達するまで静かに積み重なっていく。
- 正当な枯渇:クローラー、ログプロセッサー、コネクションプールがデフォルトの許容量を超えるFDを本当に必要としている。
現在プロセスが開いているFDの数を確認するには:
# PIDをプロセスIDに置き換える
ls /proc/PID/fd | wc -l
# またはlsofを使う
lsof -p PID | wc -l
上限に近づくばかりで下がらない?それはリークです。
修正1 — リークを止める(最も一般的な修正)
with文を使いましょう。Pythonのコンテキストマネージャーは、ブロックの途中で例外が発生してもclose()を呼び出します — 手動クリーンアップは不要です。
問題のあるパターン:
f = open('data.csv', 'r')
content = f.read()
# f.close()を忘れている — 上で例外が発生するとFDがリークする
修正後:
with open('data.csv', 'r') as f:
content = f.read()
# ここでFDが解放される(何があっても)
ソケットも同様です:
import socket
with socket.create_connection(('example.com', 80)) as sock:
sock.sendall(b'GET / HTTP/1.0\r\n\r\n')
response = sock.recv(4096)
# ソケットは自動的に閉じられる
修正2 — 長時間実行コードでは明示的に閉じる
withが使えない場合もあります — オブジェクト属性やクラスレベルのハンドルがその例です。代わりにtry/finallyでラップしましょう:
f = open('output.log', 'a')
try:
f.write(line)
finally:
f.close()
HTTPセッションも同じ落とし穴があります。コンテキストマネージャーとして使いましょう:
import requests
with requests.Session() as session:
resp = session.get('https://api.example.com/data')
print(resp.json())
修正3 — OSの制限を引き上げる
高並列サーバーや大量のファイル処理では、数千のFDが本当に必要になることがあります。デフォルトの1024では低すぎます。
一時的(現在のシェルセッションのみ):
ulimit -n 65536
ユーザーへの永続的な設定 — /etc/security/limits.confを編集:
# /etc/security/limits.conf
www-data soft nofile 65536
www-data hard nofile 65536
変更を有効にするにはログアウトして再ログインしてください。ulimit -nで確認できます。
systemdサービスの場合 — [Service]セクションに追加:
[Service]
LimitNOFILE=65536
適用するには:sudo systemctl daemon-reload && sudo systemctl restart yourservice
修正4 — PythonのresourceモジュールをJapanese
スクリプト内から制限を引き上げたい場合 — systemdもシェルアクセスもない?resourceモジュールを起動時に使えます:
import resource
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
print(f'Current limits: soft={soft}, hard={hard}')
# ソフトリミットをハードリミットまで引き上げる
resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
注意点:rootなしでは、ソフトリミットをハードリミットまでしか引き上げられません。それ以上はulimitまたはlimits.confが必要です。
修正5 — コネクションプールサイズを制限する(requests、aiohttp、データベース)
高負荷時、HTTPクライアントやDBドライバーはプールへの返却より速く接続を開くことがあります。プールサイズを明示的に固定しましょう:
import requests
from requests.adapters import HTTPAdapter
session = requests.Session()
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=20)
session.mount('https://', adapter)
session.mount('http://', adapter)
asyncioでは、セマフォで同時オープン数を制御できます:
import asyncio
async def process_files(paths):
sem = asyncio.Semaphore(100) # 同時FDを100に制限
async def open_with_limit(path):
async with sem:
# ここでファイルまたはネットワーク操作を行う
pass
await asyncio.gather(*[open_with_limit(p) for p in paths])
この100はulimitに基づいて調整してください。安全な目安として、ソフトリミットの50%未満に抑えましょう。
修正の確認
ワークロード実行中にFD数をリアルタイムで監視する:
# PIDのFDカウントをライブ表示
watch -n1 'ls /proc/PID/fd | wc -l'
# プロセスが実際に認識している制限を確認
cat /proc/PID/limits | grep 'open files'
カウントが横ばいになればリークは解消されています。まだ増え続けている場合はlsofで原因を特定しましょう:
lsof -p PID | sort -k9 | head -40
同じファイルパスのエントリが繰り返し出てくる箇所がリークです — そこを重点的に調査してください。
クイック診断チェックリスト
ulimit -nを実行 — まだデフォルトの1024ですか?引き上げましょう。ls /proc/PID/fd | wc -lを確認 — 上限にどれだけ近いですか?- コード内で
withブロック外にあるopen(呼び出しを検索する。 - 閉じていない
socket、裸のrequests.get()呼び出し、DBカーソルを探す。 - スレッドや非同期を使っている?並列オープン数が制限されているか確認 — 無制限の
gather()やワーカー数無制限のThreadPoolExecutorは使わない。

