Tech Learning Daily

2026-10-05 (Mon) — 第 80 号
AI が毎朝届ける、ソフトウェア技術の基礎解説
Infra 🌿 基礎 ⏱ 約 7 分

ファイルなんて開いていないのに、なぜ「Too many open files」でサーバーが落ちるのか?

アクセスが増えた夜、API サーバーのログに accept: too many open files が並び、新しいリクエストが一切通らなくなる。そのアプリはログ出力以外でファイルを開いた覚えがない。開いているのは HTTP の接続と DB のコネクションだけのはずだ。では、何の「ファイル」が多すぎたのか。

🎯 3 行まとめ
  • Linux ではファイルだけでなく TCP ソケットやパイプも「ファイルディスクリプタ(fd)」という番号で扱われ、プロセスごとに持てる数に上限がある。
  • 上限は RLIMIT_NOFILE のソフトリミットで、systemd 配下のデフォルトは 1024。これは古い select() が 1024 未満の番号しか扱えないことへの互換措置である。
  • 対処は「上限を上げる」と「fd の漏れを止める」の 2 つで、漏れが原因なら上限をいくら上げても時間稼ぎにしかならない。

前提: カーネルが渡す「番号札」がファイルディスクリプタ

プロセスがファイルを開くと、カーネルは開いた対象そのものではなく小さな整数を返す。これがファイルディスクリプタ(fd)で、「プロセスごとの、開いているものの一覧表」の添字である。以降の read や write はこの番号を指定して呼ぶ。0・1・2 番は標準入力・標準出力・標準エラーに予約されている。

新しく割り当てられる番号は「そのプロセスで現在使われていない最小の番号」と決まっている。open(2) の man ページにそう明記されており、閉じた番号はすぐ再利用される。だから fd の番号を眺めても「累計で何回開いたか」は分からず、「今いくつ開いているか」の目安にしかならない。

 プロセスの fd 表     カーネル内の実体
 ┌────┬─────────┐
 │ 0  │ ────────┼──▶ 端末 (stdin)
 │ 1  │ ────────┼──▶ 端末 (stdout)
 │ 2  │ ────────┼──▶ 端末 (stderr)
 │ 3  │ ────────┼──▶ app.log
 │ 4  │ ────────┼──▶ listen ソケット :8080
 │ 5  │ ────────┼──▶ TCP 接続 (客A)
 │ 6  │ ────────┼──▶ TCP 接続 (DB)
 │ ...│         │
 └────┴─────────┘
  ▲ 上限 = RLIMIT_NOFILE (ソフト)

ソケットもパイプも「ファイル」として数えられる

エラーの名前から「ディスク上のファイルを開きすぎた」と思える。しかし実際は、Unix 系 OS では TCP ソケット・パイプ・epoll のインスタンスなど、カーネルが管理する入出力の窓口がすべて同じ fd 表に載る。Web サーバーなら、待ち受けソケットが 1 つ、クライアント接続 1 本ごとに 1 つ、DB コネクションプールの接続 1 本ごとに 1 つ、外部 API への HTTP 接続ごとに 1 つ、fd を消費する。

つまり lead の「ファイルを開いた覚えがない」サーバーは、同時接続数がそのまま fd 消費になっていた。上限に達すると open() や accept() は EMFILE(エラー番号 24)を返し、これを文字列にしたのが「Too many open files」である。accept() が失敗するとカーネル側では接続が成立していても、アプリはそれを受け取れない。結果として新規リクエストだけが詰まり、既存の接続は動き続けるという分かりにくい壊れ方をする。

なお似たエラーに ENFILE がある。こちらはプロセス単位ではなくシステム全体の上限(/proc/sys/fs/file-max)に達した場合で、別のプロセスまで巻き込んで失敗する。日常的に出会うのは前者の EMFILE がほとんどだ。

🍱 たとえるなら

クロークの番号札である。客(プロセス)が荷物(ファイルでも電話回線でも)を預けるたびに、クロークは空いている一番小さい番号の札を渡す。札は 1 人あたりの枚数に上限があり、返却しないまま預け続けると、荷物の種類に関係なく「札がもう無い」と断られる。断られた理由が「荷物が重すぎる」ではなく「札が足りない」である点が、このエラーの正体だ。

上限はなぜ 1024 なのか: 3 段の天井

プロセスごとの上限は RLIMIT_NOFILE というリソース制限で、ソフトリミットとハードリミットの 2 つの値を持つ。カーネルが実際に強制するのはソフトリミットで、ハードリミットはその天井である。一般ユーザーのプロセスは自分のソフトリミットをハードリミットまでなら自由に上げられるが、ハードリミット自体を上げるには CAP_SYS_RESOURCE 権限が要る。さらにハードリミットは /proc/sys/fs/nr_open(カーネルのドキュメント上のデフォルトは 1,048,576)を超えられない。

systemd の DefaultLimitNOFILE= のデフォルトは 1024:524288、つまりソフト 1024・ハード 524288 だ。ハードは大きいのにソフトだけ低い理由は、select() というシステムコールにある。select() は 1024(FD_SETSIZE)未満の番号しか監視できず、この制約は今後も変わらないと man ページに書かれている。ソフトを最初から上げると、select() を使う古いプログラムが 1024 番以上の fd を受け取って壊れる。そこで「低いソフトで安全側に起動し、平気なプログラムは自分でハードまで上げよ」という設計になっている。

この方針に従っている例が Go だ。Go 1.19 以降、os パッケージを import したプログラムは起動時にソフトリミットをハードリミットまで自動で引き上げる。同じ systemd 配下で同じ負荷をかけても、ランタイムが自分で上限を引き上げるかどうかで 1024 の壁に当たるかが変わる。自分の言語ランタイムがどちらなのかは、下のコマンドで /proc/<PID>/limits の実際の値を見るのが確実だ。

# 今のシェルの上限を見る(ソフト / ハード)
ulimit -Sn
ulimit -Hn

# 動いているプロセスの上限と現在の使用数(Linux)
grep "Max open files" /proc/<PID>/limits
ls /proc/<PID>/fd | wc -l

どこで上げるか、そして上げる前に漏れを疑う

上限はプロセスの起動元から継承されるため、どこで起動しているかで設定場所が変わる。systemd のサービスなら unit ファイルの LimitNOFILE=、Docker なら docker run --ulimit で指定する。Docker は値を指定しないとデーモン側のデフォルトを引き継ぎ、ハードを省略するとソフトと同じ値がハードにも入る。シェルの ulimit -n はそのシェルから起動したプロセスにしか効かないので、本番の常駐プロセスには反映されない。

# systemd の unit([Service] セクション)
[Service]
LimitNOFILE=65536

# Docker(soft:hard)
docker run --ulimit nofile=65536:65536 myapp

ただし上限を上げる前に、fd 数が時間とともに増え続けていないかを確認したい。接続数が横ばいなのに /proc/<PID>/fd の数だけが右肩上がりなら、どこかで閉じ忘れている。典型は、Go の http.Client でレスポンスの Body を Close しないケースだ。net/http のドキュメントは呼び出し側に Body を閉じるよう求めており、読み切って閉じないと TCP 接続を keep-alive で再利用できないとしている。再利用されない接続が次々と作られれば、その分だけソケットの fd が積み上がる。漏れている状態で上限を 1024 から 65536 に上げても、落ちるまでの時間が延びるだけで、メモリを食いながら同じ結末を迎える。

💼 実務でどう出会うか

負荷試験で同時接続数を上げた瞬間にエラー率が跳ね、アプリログに EMFILE や「Too many open files」が出る。あるいは本番で数日おきに API が応答しなくなり、再起動すると直る。前者は上限不足、後者は fd 漏れを疑う。切り分けは /proc/<PID>/fd の数を時系列で取ることで、Prometheus のクライアントライブラリが多くの言語で標準出力する process_open_fds と process_max_fds をダッシュボードに並べておくと、再起動で直る系の障害を事前に検知できる。nginx のようなリバースプロキシは 1 リクエストでクライアント側と upstream 側の 2 本の接続を持つので、同時接続数の倍近い fd を見込む必要がある。

⌨️ 手を動かす(5 分)

上限を意図的に 32 まで下げたサブシェルで Python にファイルを開かせ続け、どこで EMFILE が出るかを観察する。Mac のターミナルでそのまま動く(括弧で囲んでいるので、元のシェルの上限は変わらない)。

( ulimit -n 32; python3 -c '
import os
fs = []
try:
    while True:
        fs.append(open(os.devnull))
except OSError as e:
    print(len(fs), "files opened before:", e)
' )

上限は 32 なのに、開けたファイル数は 32 より少し少ないはずだ。0〜2 番の標準入出力と、Python 自身が起動時に開いた fd がすでに枠を使っているからである。続けて Docker で Linux の fd 表を直接覗いてみる。

docker run --rm --ulimit nofile=64:64 alpine sh -c 'ulimit -n; ls -l /proc/self/fd'

64 の後に、0・1・2 番などが何に繋がっているか(パイプや /dev/null)がシンボリックリンクとして並ぶ。これがプロセスの番号札の実物である。

🙅 よくある誤解
  • 「Too many open files」はファイル操作が多いアプリだけの問題 — ソケットも fd を消費するので、ファイルを扱わない API サーバーでも同時接続数が増えれば出る。
  • ターミナルで ulimit -n 65536 を打てば本番も直る — 上限は起動元から継承される。systemd や Docker から起動したプロセスには、それぞれの設定(LimitNOFILE= / --ulimit)で指定する必要がある。
  • 上限を大きくすれば解決する — 原因が fd の閉じ忘れなら、上限を上げても落ちるまでの時間が延びるだけだ。fd 数が接続数と無関係に増え続けていないかを先に確認する。
📖 用語ミニ辞典
ファイルディスクリプタ(fd)
プロセスが開いているファイル・ソケット・パイプなどを指す小さな整数。プロセスごとの一覧表の添字。
RLIMIT_NOFILE
1 プロセスが持てる fd の上限を決めるリソース制限。ソフト(実際に強制される値)とハード(ソフトの天井)がある。
EMFILE
プロセス単位の fd 上限に達したときのエラー。文字列表現が「Too many open files」。
ENFILE
システム全体で開けるファイル数の上限に達したときのエラー。
select()
複数の fd を同時に監視する古いシステムコール。1024 未満の番号しか扱えないため、ソフトリミットが 1024 に据え置かれる理由になっている。
🔗 もっと深く
  • getrlimit(2) — Linux manual page — ソフト/ハードリミットの権限ルールと nr_open による天井が一次情報で確認できる。
  • systemd.exec(5) — Linux manual page — LimitNOFILE= の項に、ソフトを 1024 に据え置く理由と、アプリ側で上げるべきという推奨が書かれている。
  • open(2) — Linux manual page — 「最小の空き番号を返す」規則と、fd と open file description の違いが説明されている。
  • Go 1.19 Release Notes — Go が起動時にソフトリミットを自動で引き上げるようになった経緯と、子プロセスへの影響。