TT Lab
はじめる
学ぶ 学習パス コース

Linux障害対応

上限は一つではなく四つある

TT Labで続きを見る

一言でいうと

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. 限界は1つだと考えている。実際には4つです。
  2. シェルで変えた値がサービスに適用されると考えている。適用されません。
  3. 制限を引き上げれば解決すると考えている。たいていはリークなので、障害の発生時期が数時間後に先送りされるだけです。

どう動くのか

制限の包含関係は次のとおりです。

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上位のプロセスを取り出すリーク追跡ツールを作ります。