ひとつの席から多数のソケットを見る
一言でいうと
多重化の呼び出しは、複数のソケットを一度に見張ってくれますが、準備完了という答えは、いま読んでも塞がらないという意味にすぎません。
なぜ必要なのか
1つの接続がほかを塞がないようにするには、待つ場所を接続ごとに置かず、1か所にまとめる必要があります。接続ごとにスレッドを与える方法もありますが、スレッドはそれぞれスタックを持ち、コンテキストスイッチのコストがあるので、接続数千を目標にすると、別の種類の限界に先に当たります。多重化は逆の方向です。実行の流れは1つのままにして、待つ仕事だけをカーネルに任せます。
どう動くのか
プログラムはカーネルに、ソケットのリストと、それぞれのソケットについて何が知りたいかを伝えます。読むものができるか、書く場所ができるかです。カーネルは、そのうち1つでも条件が合うまでプログラムを眠らせ、起こすときに、条件が合ったものだけを返します。そのため、接続がいくつあっても、待つ場所は1つです。
Linuxには、この仕事をする呼び出しがいくつかあり、古いものから限界が違います。select(2)は、ドキュメントの最初の行に警告を付けています。glibcの実装でfd_setが固定サイズなので、FD_SETSIZEである1024より小さい番号の記述子しか見張れず、この制限は変わらないので、最新のアプリケーションはpollやepollを使うようにという内容です。epoll(7)は、関心リストと準備リストをカーネルの中に別々に持ち、見張る記述子が増えても、うまくスケールするように作られています。
Pythonでは、この違いを自分で選ぶ必要はありません。selectorsモジュールのDefaultSelectorが、そのプラットフォームで最も効率的な実装を選んでくれます。プログラムが扱う概念は、3つに絞られます。
selector.register(sock, selectors.EVENT_READ, data)
for key, events in selector.select(timeout=1.0):
if events & selectors.EVENT_READ:
...
selector.modify(sock, selectors.EVENT_READ | selectors.EVENT_WRITE, data)
ここで最もよく誤解されるのが、準備完了という言葉の意味です。読み取りの準備完了は、データがあるという意味ではなく、いま読もうとしても止まらないという意味です。相手が接続を閉じたときも、読み取りの準備完了で起きて、そのときrecvは0バイトを返します。0バイトは空のメッセージではなく、もう来るものがないという合図です。この2つを混同すると、切れた接続を見張り続けて、ループが休まずに回るプログラムになります。
さらに、select(2)のBUGSの節は、読み取りの準備完了として報告された記述子に対して、続く読み取りがそれでも塞がることがあると書いています。例に挙げられている状況は、データが届いたものの、検査の結果、チェックサムが合わずに捨てられる場合で、ドキュメントは、そのため塞がってはいけないソケットにはO_NONBLOCKを使うほうが安全だと勧めています。まとめると、多重化は非ブロッキングの代わりではなく、一緒に使うものです。準備完了の通知は、いつ試すかを決めてくれて、非ブロッキングは、試みが外れたときにプログラムを止めないようにしてくれます。
起きたあとにすることにも、ルールがあります。リッスンソケットが読み取りの準備完了で起きたとき、待っている接続が1つだけだという保証はありません。1回の起床で、複数の接続がキューに入っていることがあるので、acceptがEAGAINまたはEWOULDBLOCKを出すまで、繰り返して空にする必要があります。同じドキュメントは、すでに切れた接続に対してECONNABORTEDが起こりうるとも書いています。その1つのために、残りの待ち行列をあきらめてはいけません。
現場での姿
事故は、たいてい、CPU使用率100%の形で現れます。接続は増えていないのに、イベントループが休まずに回っているのです。原因をたどると、たいてい次の2つのどちらかです。切れた接続を関心リストから外していないか、送るものがないのに書き込みの準備完了を見張り続けているかです。書き込む場所はほとんど常に空いているので、書き込みの準備完了はほとんど常に真で、そうするとselectがすぐに戻ってきます。次のモジュールで、この問題を正面から扱います。
逆方向の事故もあります。イベント1つを処理しながら、別のブロッキング呼び出しをしてしまうことです。ファイルを同期で読んだり、ほかのサービスにブロッキングでリクエストを送ったり、重い計算をその場で行ったりする場合です。多重化サーバーは実行の流れが1つだけなので、この1回の停止が、見張っていた接続すべての停止になります。構造を変えたからといって、ブロッキングがなくなるわけではなく、ブロッキングがずっと高くつくようになるのです。
次のクイズですること
準備完了の通知が保証することと、保証しないことを区別してみてください。読み取りの準備完了で起きたソケットで、recvが0を返した状況、リッスンソケットが1回起きたときに、待っていた接続が3つあった状況、そして書き込みの準備完了がずっと真の状況を、それぞれどう扱うべきかが、クイズの内容です。