5. ファイル記述子の制限
受信したすべての接続を正常に処理できるように、HAProxyは読み込み時に、プロセスの存続期間中に必要となるファイル記述子の総数を計算します。通常のUnixプロセスには、デフォルトで一般に1024個のファイル記述子が許可され、特権プロセスはこの上限を自分で引き上げられます。これが、HAProxyをrootで起動して上限を調整させる理由の1つです。デフォルトの上限である1024個のファイル記述子では、約500の同時接続を処理できます。計算には、プロセスごとの接続総数を制限するグローバルのmaxconnパラメーター、リスナー数、ヘルスチェックが有効なサーバー数、エージェントチェック、ピア、ロガー、場合によってはその他の技術的要件が使われます。簡単な概算として、maxconnの値を2倍して数十を加えると、必要なファイル記述子数のおおよその値が得られます。
当初のHAProxyはこの値を計算できず、globalセクションの"ulimit-n"設定で値を渡す必要がありました。現在でも多くの設定にこの項目が残っているのは、このためです。残念ながら、この値はしばしば誤って計算されていました。その結果、maxconnに近づくと、必要なリソースを待つ間に受信接続を抑制する代わりに、接続が失敗していました。このため、非常に古いバージョンから残っている"ulimit-n"設定は削除することが重要です。
中程度の負荷であっても、受け付けるにはファイル記述子数を増やすことが必須ですが、OS固有の調整も伴います。まず、select()によるポーリングは1024個のファイル記述子に制限されます。実際にはLinuxのselect()はかつてそれ以上を扱えましたが、一部のOSに付属する過度に厳しいSELinuxポリシーが、1024個を超えるファイル記述子でのselect()の使用を禁止しています。そのため、実行時の問題を避けるため、HAProxyはこの場合に起動を拒否するようになりました。サポート対象のすべてのOSではpoll()が利用でき、この制限を受けません。自動的に選択されるため、動作させるための特別な設定は不要です。しかし、poll()はファイル記述子数が増えると非常に遅くなります。HAProxyは内部のファイル記述子キャッシュやバッチ処理などで性能への影響を抑えるよう最善を尽くしますが、目安として、1000を超える同時接続でpoll()を使うと多くのCPUを消費します。
カーネル2.6以降を使うLinuxシステムでは、epoll()システムコールが使われます。これは、カーネル内のコールバックに基づく、はるかに拡張性の高い仕組みで、監視対象として登録されたファイル記述子の数によらず、一定の起床時間を保証します。HAProxyがLinux向けのいずれかの設定でビルドされていれば、検出時に自動的に使われます。利用可否とサポート状況は"haproxy -vv"で確認できます。
対応するBSDシステムでは、代替としてkqueue()が利用できます。poll()よりはるかに高速で、変更をバッチ処理するためepoll()よりもわずかに高速です。少なくともFreeBSDとOpenBSDがサポートしています。Linuxのepoll()と同様に、サポート状況と利用可否は"haproxy -vv"の出力に表示されます。
優れたポーラーを使うだけでは十分ではなく、プロセスが必要な上限まで利用できることが必須です。HAProxyは起動するとすぐに新しいプロセスのファイル記述子上限を設定し、成功したか確認します。失敗した場合は、管理者が問題を確認できるよう、forkする前に報告します。rootとして起動している限り、この設定が失敗する理由は通常ありません。ただし、非特権ユーザーとして起動した場合は失敗することがあります。rootとしてhaproxyを起動しないやむを得ない理由(エンドユーザーやアプリケーション別アカウントで起動する場合など)がある場合は、システム管理者がそのユーザーに対するファイル記述子上限を引き上げられます。設定が有効かどうかは、そのユーザーのコマンドラインで"ulimit -n"を実行して確認できます。新しい上限が表示されるはずです。
警告:非特権ユーザーのアカウントで上限を変更しても、その値はログイン時にのみ反映され、システム起動時の一部のスクリプトやcrontabではまったく考慮されないことがよくあります。これは完全にOSに依存します。この方法で実行する場合は、haproxyを起動する前に"ulimit -n"を確認することを忘れないでください。一般的な推奨は、本番用途ではhaproxyを非特権ユーザーとして起動しないことです。非特権ユーザーで起動するとhaproxyが一部のセキュリティ保護を有効にできなくなることも、重要な理由です。
haproxyプロセスが要求した数のファイル記述子をシステム上で使えると確認した後も、システム固有の別の上限に2つ遭遇する可能性があります。1つはシステム全体のファイル記述子上限で、すべてのプロセスを合わせてシステム上で開けるファイル記述子の総数です。この上限に達すると、通常、accept()やsocket()はENFILEを返します。もう1つは、プロセスごとのファイル記述子数のハード上限で、setrlimit()でそれより大きな値を設定することを防ぎます。どちらもOSに大きく依存します。Linuxでは、システム全体の上限は起動時にメモリ量に基づいて設定され、“fs.file-max"のsysctlで変更できます。プロセスごとのハード上限はデフォルトで1048576ですが、“fs.nr_open"のsysctlで変更できます。
ファイル記述子の上限が低すぎる場合は、実行中のプロセスでその制限を確認できます。プロセスの上限に達すると、straceユーティリティはaccept()とsocket()が”-1 EMFILE"を返したと報告します。この場合は、“ulimit-n"の値を引き上げる(または削除する)だけで解決します。これらのシステムコールが”-1 ENFILE"を返す場合は、カーネルの上限に達しており、システム全体のパラメーターに対処する必要があります。これらの問題は、CPU使用率の上昇(accept()が失敗した場合)や、通常ユーザーにも見える接続失敗につながるため、必ず解決しなければなりません。グローバルのmaxconn値を下げて処理を直列化し、必要に応じてHTTP keep-aliveを無効にして接続をより早く解放・再利用させる方法もあります。