目次
アクセスが増えたある日、アプリのログにEMFILE: too many open filesやjava.io.IOException: Too many open filesが並び、再起動すると直るがしばらくするとまた発生する。こうした症状に出会ったとき、「ファイルなんてそんなに開いていないのに」と首をかしげる方は少なくありません。実はこのエラーの「ファイル」は、ソケットやパイプまで含んだ広い意味の言葉です。
この記事では、ファイルディスクリプタ(FD)とは何かをLinuxの仕組みから説明し、ulimit・/etc/security/limits.conf・systemdのLimitNOFILEがそれぞれどこに効くのかを整理します。そのうえでlsofを使った調査手順、リークが起きる典型的なコードパターン、Nginx・Node.js・Javaでの設定を解説し、上限を増やして終わりにしない根本対策までたどり着けるようにします。
ファイルディスクリプタとは何か
「開いているもの」すべてに付く番号
ファイルディスクリプタ(File Descriptor、FD)は、プロセスが開いているリソースをカーネルが識別するための整数です。Linuxでは「すべてはファイル」という設計思想のもと、通常のファイルだけでなく、ネットワークソケット、パイプ、デバイス、epollインスタンス、タイマーなどもFDとして扱われます。プロセスが起動すると0(標準入力)・1(標準出力)・2(標準エラー)が割り当てられ、以降open()やsocket()のたびに次の番号が使われます。
つまりWebサーバーやAPIサーバーにとって、FDの消費源の大半はソケットです。クライアントとの接続1本、DBへの接続1本、外部APIへのHTTP接続1本、それぞれがFDを1つ消費します。同時接続が1,000本あれば、ファイルを1つも開いていなくてもFDは1,000以上必要になるわけです。
3段階の上限
FDの上限は3つの層で決まっています。
| 階層 | 設定 | 意味 |
|---|---|---|
| プロセスごと | ulimit -n(RLIMIT_NOFILE) | 1プロセスが開けるFD数。soft(現在値)とhard(softを上げられる上限)がある |
| プロセスごとの絶対上限 | fs.nr_open | hardリミットとして設定できる最大値(既定1048576) |
| システム全体 | fs.file-max | カーネル全体で開けるファイルの総数 |
「Too many open files」の原因は、ほとんどの場合1段目のプロセスごとのsoftリミットです。多くのディストリビューションでログインシェルの既定値は1024と小さく、サーバー用途ではすぐに足りなくなります。
設定はどこに効くのか|ulimit・limits.conf・systemd
まず現状を確認する
# 現在のシェルのsoft / hardリミット
ulimit -Sn
ulimit -Hn
# 動作中プロセスの実際の上限(シェルの値とは別物)
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'
# システム全体の上限と現在の使用数
cat /proc/sys/fs/file-max
cat /proc/sys/fs/file-nr # 割当済み 未使用 上限
重要なのは、ulimit -nで見えるのは「そのシェルの値」であって、systemdが起動したサービスには関係がないという点です。プロセスの調べ方で対象のPIDを特定し、/proc/PID/limitsを見るのが唯一の正確な確認方法です。
limits.confはログインセッション用
/etc/security/limits.conf(と/etc/security/limits.d/)はPAMを通じてログイン時に適用される設定です。SSHでログインして手動で起動するプロセスや、cronから起動されるジョブには効きますが、systemdのサービスには適用されません。「limits.confを書いたのに直らない」の原因の大半はこれです。
# /etc/security/limits.d/90-nofile.conf
# ログインユーザー app に対する設定(再ログイン後に有効)
app soft nofile 65536
app hard nofile 65536
systemdサービスにはLimitNOFILE
systemdで管理しているサービスは、ユニットファイルの[Service]セクションにLimitNOFILEを書きます。既存のユニットを直接編集せず、systemctl editでドロップインを作るのが作法です。
sudo systemctl edit myapp.service
# 開いたエディタに以下を記述
[Service]
LimitNOFILE=65536
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
cat /proc/$(systemctl show -p MainPID --value myapp.service)/limits | grep 'open files'
すべてのサービスの既定値を変えたい場合は/etc/systemd/system.confのDefaultLimitNOFILEを設定します。Dockerコンテナの場合はdocker run --ulimit nofile=65536:65536、Composeならulimits: { nofile: { soft: 65536, hard: 65536 } }で指定します。
lsofで「何が開いているか」を調べる
数を数え、種類を分類する
上限を上げる前に、「本当にその数が必要なのか」を確かめます。同時接続に見合った数ならスケールの問題ですが、時間とともに増え続けるならリークであり、上限を上げても先送りにしかなりません。
PID=$(pgrep -o node)
# 開いているFDの数(lsofより軽くて正確)
ls /proc/$PID/fd | wc -l
# 種類ごとに集計(REG=ファイル, IPv4/IPv6=ソケット, FIFO=パイプ など)
sudo lsof -p $PID -nP | awk 'NR>1 {print $5}' | sort | uniq -c | sort -rn
# ソケットの接続先と状態を確認
sudo lsof -p $PID -nP -i | awk '{print $9, $10}' | sort | uniq -c | sort -rn | head
# 1分ごとに推移を記録して増え続けるか観察
while true; do echo "$(date +%T) $(ls /proc/$PID/fd | wc -l)"; sleep 60; done
集計結果にCLOSE_WAIT状態のソケットが大量に出ていれば、相手が切断したのに自分がclose()していない典型的なリークです。TCPの状態遷移はTCPの3ウェイハンドシェイクとTIME_WAITで解説しています。
リークの典型パターン
- ファイルのクローズ忘れ:例外が発生したときに
close()が呼ばれない。Pythonのwith、Javaのtry-with-resources、Goのdefer f.Close()のように、言語のスコープ機構で必ず閉じる書き方に統一する - HTTPレスポンスボディの未消費:Goの
http.Clientではresp.Bodyを読み切って閉じないと接続が再利用されず溜まる。Node.jsのストリームもresume()やdestroy()を忘れると残る - DBコネクションの未返却:プールから取得した接続をエラー経路で返却しないと、プールが枯渇するだけでなくソケットも増え続ける
- ログファイルの多重オープン:リクエストごとにロガーを生成するコードで、ファイルハンドルが積み上がる
- 子プロセスのパイプ:
spawnした子プロセスの標準出力を読まないまま放置すると、パイプのFDが残る
Nginx・Node.js・Javaでの設定
Nginx
Nginxはワーカーごとにworker_connections本の接続を扱い、リバースプロキシとして動く場合はクライアント側とアップストリーム側で1接続あたり2つのFDを使います。そのためworker_rlimit_nofileをworker_connectionsの2倍以上に設定し、systemd側のLimitNOFILEもそれを下回らないようにします。基本の構築はNginx入門、性能面の調整はNginxパフォーマンスチューニングを参照してください。
# /etc/nginx/nginx.conf
worker_processes auto;
worker_rlimit_nofile 65536; # ワーカープロセス自身がsoftリミットを引き上げる
events {
worker_connections 16384;
}
Node.jsとJava
Node.jsは自分でリミットを引き上げないため、systemdやDockerの設定に依存します。イベントループで大量の同時接続を扱う設計なので、1024のままでは数百接続で頭打ちになることが珍しくありません。Java(HotSpot)はLinux上でsoftリミットをhardリミットまで自動で引き上げる挙動があるため、hard側を十分に確保しておくことが重要です。いずれの場合も、アプリ側の設定ではなく「起動した主体(systemd・Docker)の設定」で決まる、と覚えておくと迷いません。
トラブル事例:上限を上げても翌週にまた発生した
症状:Node.jsのAPIサーバーでEMFILEが発生。LimitNOFILE=65536に上げて再起動したところ収まったが、1週間後に再発した。
原因:ls /proc/PID/fd | wc -lを定期的に記録すると、アクセス数に関係なく1時間に数十ずつ増え続けていた。lsof -iで確認すると外部の決済APIへの接続がCLOSE_WAITのまま数千本残っており、タイムアウト時にレスポンスストリームを破棄せずリトライしていたコードが原因だった。上限を上げたことで、枯渇までの時間が延びただけだった。
対処:タイムアウト時にAbortControllerでリクエストを確実に中断し、ストリームをdestroy()するように修正。あわせてFD数をメトリクスとして監視に追加し、上限の70%で警告が出るようにした。上限の引き上げは「猶予を作る」ための暫定対処であり、増加傾向の監視とコードの修正が根本対策です。
まとめ
「Too many open files」の「ファイル」にはソケットやパイプが含まれ、サーバーでは同時接続数がそのままFD消費になります。上限はプロセスごとのsoft/hardリミットで決まり、ログインセッションにはlimits.conf、systemdのサービスにはLimitNOFILE、Dockerには--ulimitと、起動主体ごとに設定場所が違う点を押さえてください。そして必ず/proc/PID/limitsと/proc/PID/fdで実際の値を確認し、増え続けているならリークとしてlsofで種類と接続先を特定します。上限の引き上げと監視、コード修正の3つを揃えて初めて根本対策になります。
サーバーの安定運用やアプリケーションのリソース設計でお困りの際は、Harmonic Societyのシステム開発・インフラ支援にご相談ください。
Harmonic Society
この記事の内容、自社の業務でも活かせそうですか?
ローカルLLM・AI・クラウドなどの技術導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK。まずは30分の無料相談から。売り込みはしません。
関連記事
Related / 9 articles
Notes & Insights
- プログラミング
DDoS攻撃の仕組みと対策入門|レイヤー別の防御とCDN・クラウドの活用
DDoS攻撃をボリューム型・プロトコル型・アプリ層に分けて仕組みを解説し、自前サーバーで防げない理由、CloudflareやAWS Shieldの標準防御、オリジンIPの隠し方、レートリミットとBot対策、攻撃を受けたときの初動、費用が跳ね上がるDenial of Walletへの備えまでわかります。
- プログラミング
WAFとは?仕組み・導入パターン・誤検知対策|Webアプリを攻撃から守る実践ガイド
WAFがファイアウォールやIDSと何が違うのか、シグネチャとマネージドルールの仕組み、Cloudflare WAF・AWS WAF・ModSecurityの比較、フォーム送信がブロックされる誤検知の調査と例外設定、ログ監視、WAFが代替できないことまで実践的に解説します。
- プログラミング
セキュリティヘッダー入門|CSP・HSTS・X-Frame-Optionsの設定と効果を実践解説
CSP・HSTS・X-Frame-Options・X-Content-Type-Optionsなど主要セキュリティヘッダーが防ぐ攻撃と、CSPのReport-Onlyからの段階導入、nonce/hash、HSTS preloadの不可逆リスク、Nginx・Next.js・Astroでの設定例、確認方法を解説します。
- プログラミング
クラウドの通信費(Egress)入門|データ転送量課金の仕組みと転送コストを抑える設計
クラウドの「受信無料・送信有料」の原則、AZ間・リージョン間・インターネット向けの単価差、NATゲートウェイ処理料の罠、CDNで転送量を減らす方法、バックアップやログ転送の見落とし、請求書で転送料を特定する手順を解説。想定外の請求を防げます。
- プログラミング
秘密情報をGitに入れない仕組み|.gitignore・git-secrets・履歴から漏れた鍵の削除
APIキーや.envをGitにコミットしてしまう典型経路と、.gitignore・.env.exampleの運用、pre-commitでのgitleaks検知、GitHub secret scanningの活用、漏れた鍵の無効化と履歴書き換え(git filter-repo)の手順を解説。仕組みで再発を防げます。
- プログラミング
開発・ステージング・本番環境の分離設計|環境差分をなくす構成とアクセス制御
開発・ステージング・本番それぞれの目的と、構成をコードで揃える方法、環境別の設定注入、本番データを使わないテストデータ戦略、ステージングの保護(Basic認証・IP制限・noindex)、コストを抑える運用までを解説。環境差分による本番障害を防げます。
- プログラミング
ngrok・Cloudflare Tunnelでローカルを公開|Webhook開発とデモ環境の作り方
NAT内のローカル環境にStripeやLINEのWebhookを届けるトンネリングの仕組みを解説。ngrok・Cloudflare Tunnel・localtunnelの比較、固定ドメインと認証、リクエスト検査、公開時のセキュリティ、自宅サーバー公開への応用までわかります。
- プログラミング
ローカル開発環境のHTTPS化|mkcert・hostsファイル・自己署名証明書の正しい使い方
ローカル開発をHTTPS前提にすべき理由(Secure Cookie・Service Worker・OAuth)と、mkcertでローカルCAを作りhostsで独自ドメインを割り当ててVite・Next.js・Dockerで使う手順を解説。証明書警告を無視する癖の危険も理解できます。
- プログラミング
localhost・0.0.0.0・127.0.0.1の違い|ポートとUnixソケットを理解して「つながらない」を解決
localhost・127.0.0.1・0.0.0.0の意味の違い、Dockerで外から接続できない原因、host.docker.internal、ポート競合の調べ方、Unixソケットの利点と権限、1024未満ポートの制約を解説。「つながらない」を仕組みから解決できます。
Harmonic Society
「読んで終わり」にせず、自社の業務で試してみませんか?
AI・ローカルLLM・クラウドの導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK・売り込みなし。
無料・30分・オンラインOK|1営業日以内に返信します