上限は一つではなく四つある
一言でいうと
Too many open filesを見ると、ほとんどの人がulimit -n 65536を実行して再起動します。そして同じエラーにまた出会います。制限は1つではなく4つあり、シェルで変えた値はすでに動いているサービスには適用されないからです。
なぜ必要なのか
nginx: accept4() failed (24: Too many open files)またはjava.net.SocketException: Too many open files。このメッセージには、3つの誤解が重なっています。
- 限界は1つだと考えている。実際には4つです。
- シェルで変えた値がサービスに適用されると考えている。適用されません。
- 制限を引き上げれば解決すると考えている。たいていはリークなので、障害の発生時期が数時間後に先送りされるだけです。
どう動くのか
制限の包含関係は次のとおりです。
fs.file-max 시스템 전체 합 (현대 리눅스에선 대개 병목이 아님)
fs.nr_open 한 프로세스의 RLIMIT_NOFILE 이 넘을 수 없는 커널 천장
RLIMIT_NOFILE hard 관리자가 정한 상한
RLIMIT_NOFILE soft 실제 적용값 (프로세스가 hard 까지 스스로 올릴 수 있다)
このコードブロックの韓国語コメントは、上から順に、システム全体の合計(現代のLinuxでは通常ボトルネックではありません)、1つのプロセスのRLIMIT_NOFILEが超えられないカーネルの天井、管理者が決めた上限、実際の適用値(プロセスがhardまで自分で引き上げられます)という意味です。
errnoが階層を分けてくれます。EMFILE(24)はToo many open filesで、プロセスの限界です。ENFILE(23)はToo many open files in systemで、システム全体です。in systemという2つの単語が、診断の半分を占めます。
シェルのulimitが効かない理由。/etc/security/limits.confはPAMモジュールが読み取り、PAMはログインセッションでのみ動作します(SSH、su、login)。systemdが起動時に立ち上げたサービスはログインセッションではないため、このファイルをそもそも読み取りません。サービスの制限は、ユニットファイルのLimitNOFILEで決めます。そしてrlimitはプロセスの生成時点で適用されるため、reloadではなくrestartでなければ反映されません。
systemdがソフトリミットを低く設定する理由。240以降、systemdはハードを大きく(524288)、ソフトを低く(1024)保ちます。互換性のためです。select()がfd番号1024以上を扱えず、一部の古いプログラムは、起動時に0からソフトリミットまですべてのfdを閉じるループを回します。その上限が100万なら、起動に数秒かかります。「必要なプログラムが自分で引き上げて使え」という設計です。Goランタイムは実際に、起動時にソフトをハードまで引き上げるため、同じマシンでGoのサービスだけが問題なく動く状況が生まれます。
確実な確認方法は1つだけです。
grep 'Max open files' /proc/<PID>/limits # soft / hard
ls /proc/<PID>/fd | wc -l # 실제 사용량
lsof -p PID | wc -lは、cwd、root、実行ファイル、mmapされたライブラリまで数えますが、これらはRLIMIT_NOFILEに含まれないため、数字が大きく出ます。制限と比較する値ではありません。
現場での姿
ソケットリークの典型的な形。fdの種類を数えてみるとsocketが6万個あり、ssで状態を見るとCLOSE-WAITが2万個で、相手のアドレス別に見るとすべて同じRedisサーバーです。CLOSE-WAITは、相手がFINを送ったのに、こちら側がclose()を呼び出していない状態で、カーネルは無期限に待ち続け、タイムアウトがありません。つまり、アプリケーションがコネクションを返却していないという意味で、コードレビューの範囲がその場で確定します。
反対に、TIME-WAITはカーネルが自動的に片付け、fdも消費しません。両者を混同すると、見当違いのカーネルパラメーターをいじることになります。
リークか負荷かを見分ける方法。1分間隔で、fdの数とESTABLISHEDの数を一緒に記録します。接続数は横ばいなのにfdだけが単調に増加するなら、リークです。そのとき制限を引き上げるのは、障害を数時間先送りするだけです。
ソケットもfdです。ファイルだけを数えていると見落としますが、ソケット・パイプ・epoll・inotifyインスタンスもすべてfdを消費します。/proc/PID/fdのシンボリックリンクをreadlinkしてみると、socket:[12345]、pipe:[67890]、anon_inode:[eventpoll]のように種類が現れます。
制限が実際にどこから来るのか
Too many open filesに出会ったとき、ulimit -nを引き上げるのが最初の反応ですが、それが効かない場合が半分あります。制限は4か所から来て、どれが勝ったのかは、実行中のプロセスでしか確認できません。
cat /proc/<pid>/limits | grep -i 'open files'
この値が真実です。シェルでulimit -nをいくら変えても、すでに動いているプロセスには適用されません。
| 場所 | 適用対象 |
|---|---|
ulimit -n |
そのシェルとその子孫 |
/etc/security/limits.conf |
PAMを経由してログインしたセッション |
systemdユニットのLimitNOFILE |
そのサービス(limits.confは参照しません) |
| コンテナランタイムの設定 | コンテナの中 |
systemdで起動するサービスが最もよくはまります。limits.confを修正して再起動しても変わらない理由がこれです。ユニットに直接書く必要があります。
[Service]
LimitNOFILE=65535
システム全体の制限も別にあります。プロセスごとの制限を上げても、fs.file-maxに達するとそこで止められます。
sysctl fs.file-max fs.nr_open
cat /proc/sys/fs/file-nr # 현재 사용, 미사용, 최대
何がfdを消費しているかを数えます。ソケットかファイルかによって、対処が異なります。
ls -l /proc/<pid>/fd | awk '{print $NF}' | sed 's/:.*//' | sort | uniq -c | sort -rn | head
socket:が大半なら接続を閉じていないということで、同じファイルが数千個あるなら、ファイルを開いて閉じないコードです。制限を上げるのは時間を稼ぐことであって、直すことではありません。
途方もなく大きな値を入れてはいけません。LimitNOFILE=infinityにすると、一部のプログラムは0からその数まですべてを閉じようと試み、何分も止まります。実際に必要な値より、少し大きめに設定します。
次のラボですること
/proc/1/limitsから本当の制限を読み取ることから始めて、同じファイルを50回開き、fdは複数あるのにinodeは1つに収束する仕組みを確認します。制限を下げたシェルでEMFILEを直接再現し、ソケットもfdであることを目で確かめたあと、fd上位のプロセスを取り出すリーク追跡ツールを作ります。