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

接続ひとつが遅くなり、残り全部が止まった

この方式が崩れる場所

TT Labで続きを見る

一言でいうと

1つのプロセスが耐えられる接続数は、ファイル記述子の上限と、接続あたりのメモリ、そしてコア1つが決め、3つとも測って確認できます。

なぜ必要なのか

多重化に変えると、接続数が急に増えても耐えられるように見えます。そのため、限界がどこかを尋ねなくなり、限界はたいてい、最も忙しい日に初めて発見されます。このモジュールは、その限界を3つに分けて、前もって数えてみます。3つとも、推測ではなく、コマンド1行で確認できる値です。

どう動くのか

1つ目は、ファイル記述子の上限です。getrlimit(2)によると、RLIMIT_NOFILEは、そのプロセスが開ける最も大きい記述子の番号より1大きい値を意味し、それを超えようとする試みはEMFILEで失敗します。接続1つが記述子1つなので、この値がそのまま同時接続の天井です。自分のプロセスの値はulimit -nで、実際にいくつ持っているかは/proc/<pid>/fdを数えて確認できます。

ここで数えられるのは、接続だけではありません。標準入出力3つ、リッスンソケット1つ、そして多重化オブジェクトが記述子を使う実装なら、それ1つが加わります。そのため、接続200個を付けて数えた値は、200ではなく、それより少し大きくなります。この小さな差が重要な理由は、その差を説明できてはじめて、上限の近くで何が先に尽きるかを予測できるからです。

2つ目は、見張る方式そのものの限界です。前に見たように、select(2)は、glibcの実装でFD_SETSIZEである1024より小さい番号しか扱えません。記述子の上限をどれだけ上げても、selectを使うかぎり、番号1023を超える記述子は見張れず、ドキュメントは、そのときはpollやepollを使うようにと書いています。PythonのDefaultSelectorを使えば、この選択をプラットフォームが代わりに行ってくれますが、どの実装が選ばれたかは、確認しておく価値があります。

3つ目は、接続あたりのメモリです。プログラムが作った状態だけでなく、カーネルのソケットバッファーも含まれます。tcp(7)は、ソケットバッファーのサイズを、/proc/sys/net/ipv4/tcp_rmemとtcp_wmem、またはソケットごとのSO_RCVBUFとSO_SNDBUFで決めると説明しながら、TCPが要求されたサイズの2倍を実際に割り当て、余った領域を管理用の構造に使うと書いています。そのため、getsockoptで読み直した値は、設定した値と同じではありません。接続1つが占める領域を計算するときに、この倍数を忘れると、予算が半分ずれます。

限界 どこで決まるか どう確認するか
同時接続数 RLIMIT_NOFILE ulimit -n、/proc/PID/fdを数える
見張れる番号 selectのFD_SETSIZE ドキュメントに1024、epollは該当なし
接続あたりのメモリ ソケットバッファーとプログラムの状態 tcp_rmemと実際の使用量

ここに、4つ目があります。イベントループは実行の流れが1つなので、コア1つしか使いません。接続が増えてCPUが1コアを埋めると、記述子もメモリも余っているのに、レイテンシだけが増えます。このときの解決策は、ループを最適化することではなく、プロセスを増やして、コアをより多く使うことです。そのため、実際のサービスは、多重化とマルチプロセスを一緒に使います。このコースは、そのうち前半を扱います。

現場での姿

同じコードが、ノートパソコンでは動くのにコンテナでは動かないことが、よくこの限界で起こります。記述子の上限はプロセスごとに付く値なので、実行環境が変わると一緒に変わり、上限に達したときに出るEMFILEは、たいてい接続を受ける場所ではなく、見当違いの場所で先に表面化します。ファイルを開いたり、ログをローテーションしたりするコードが先に失敗して、原因がネットワークだという事実が、遅れて明らかになります。

メモリ側の事故は、もっと静かです。接続1つあたり数キロバイトは小さく見えますが、数万個になるとギガバイト単位になり、この領域は、アプリケーションのヒープではなくカーネル側なので、言語ランタイムのメモリ指標には現れにくいのです。そのため、プロセスの常駐メモリは平凡なのに、ノードは圧迫を受けているという状況が生まれます。

そのため、キャパシティプランニングでは、目標の接続数を先に決め、その数に、記述子の上限と接続あたりの領域を、それぞれ掛けてみます。2つのうち先に引っかかるほうが、そのサービスの本当の限界です。数字を書き留めておけば、あとで限界に達したときに何を増やすべきかが明確になり、何よりも、限界に達する前にわかります。

次のラボですること

いよいよ自分で作ってみます。まず逐次サーバーで行列待ちを測り、同じ負荷を多重化サーバーでもう一度受けて、2つの数字を比べます。最後に、接続200個を付けた状態で、サーバープロセスが持っている記述子を数え、その値がなぜ200ではないのかを、説明できなければなりません。ラボの採点ツールは、書き出した数字をその場でもう一度測って照合するので、もっともらしい値を書くだけでは合格しません。