黙った接続をいつ切るか
一言でいうと
静かな接続と死んだ接続は、ソケットだけを見ても区別できないので、最後の活動時刻を、プログラムが自分で書き留めておく必要があります。
なぜ必要なのか
多重化サーバーは、接続を長く持つように作られています。それが利点であり、問題でもあります。お客さんがリクエストを送って答えを受け取ったあと、何も言わなければ、サーバーはその接続を見張り続けます。ファイル記述子1つ、受信・送信バッファー、そして接続ごとにプログラムが作った状態が、そのまま残ります。こうした接続がたまると、ある時点で新しい接続を受け取れなくなりますが、そのとき見えるエラーは、たいていリソース不足なので、原因がずっと離れた場所を指します。
さらに悪いのは、相手がすでに消えている場合です。ノートパソコンのふたを閉じたり、途中の機器が状態を忘れたりすると、相手はFINもRSTも送れません。サーバー側のソケットはそのまま開いていて、読み取りの準備完了は永遠に来ません。この状態をhalf-openと呼びます。ソケットの状態を照会するだけでは、静かなのか死んでいるのかわかりません。
どう動くのか
解決策は、ソケットに尋ねるのではなく、時間を記録することです。接続ごとに、最後に意味のあることが起きた時刻を書き留めておき、定期的に現在の時刻と比べて、古いものを切ります。
conn.last_active = clock() # 읽거나 보낸 뒤에 갱신
stale = [key for key, conn in conns.items()
if now - conn.last_active >= idle_timeout]
3つのことを決める必要があります。何を活動として数えるのか、どのくらいの頻度で検査するのか、そして、どの時計を使うのかです。
活動の定義は、サービスごとに違います。バイトを受け取ったことだけを活動とみなすこともできますし、応答を送ったことまで含めることもできます。重要なのは、定義を1か所にまとめておくことです。複数の場所でばらばらに時刻を更新すると、なぜこの接続が切られなかったのか、あとで説明できなくなります。
検査の周期は、多重化の呼び出しの待ち時間と結びつきます。イベントが1つもなければ、プログラムはselectの中で眠っているので、どれだけ正確な期限切れのリストを作っても、起きなければ誰も切られません。そのため、待ち時間に上限を置きます。イベントがなくても、定期的に起きて一巡するようにするのです。
時計は経過時間を測る用途なので、壁時計ではなく単調時計を使います。時刻の同期で壁時計が後ろへ調整されると、まだ期限切れではない接続が期限切れに見えたり、逆に永遠に期限切れにならなかったりすることがあります。検査のために時計を注入できるようにしておけば、何秒も実際に待たなくても、期限切れの判断をテストできます。
カーネルにも、似た仕組みがあります。tcp(7)とsocket(7)が説明するTCP keepaliveは、長く静かな接続に空のプローブを送って、相手が生きているかを確認します。ただし、これはトランスポート層の生存確認であり、アプリケーションのアイドルポリシーではありません。相手のプロセスが止まっていても、カーネルは応答できるので、keepaliveは通過し、それでも業務は進みません。2つの仕組みは、互いの代わりにはなりません。
切るときには、順序があります。関心リストから外し、プログラムが持っていた接続ごとの状態を消し、ソケットを閉じます。ソケットだけを閉じてリストに残しておくと、次の起床でない接続を探すことになり、逆にリストから外すだけで閉じなければ、記述子が漏れます。ラボでは、この後始末を1つの関数にまとめます。
期限切れの対象を選ぶことと、実際に切ることを分けるのも、同じ理由です。選びながら同時に消すと、反復している途中でデータ構造が変わり、ある接続を飛ばしてしまいます。より実質的な理由は、テストです。選ぶ関数が純粋なら、時計を渡すだけで境界条件を確認でき、実際にソケットを開いたり何秒も待ったりする必要がありません。経過時間が制限とちょうど同じときに切るのか、といった問いは、こうしてはじめて答えを固定できます。
後始末のあとでログを残すときは、理由もあわせて書きます。相手が閉じたために切れたことと、アイドル制限で自分たちが切ったことは、原因がまったく違うのに、ログに「接続終了」としか書かれていなければ、あとで2つを区別する方法がありません。運用でアイドル制限を調整するかどうかを判断する根拠が、まさにこの区別です。
現場での姿
運用でよく見る組み合わせは、こうです。接続数のグラフは、ギザギザなしに単調に上がり、1秒あたりのリクエスト数は普段どおりです。つまり、新しいお客さんは来るのに、古いお客さんが出ていかないのです。こうしたグラフは、アイドル整理がないか、あってもイベントがないときに起きない構造のときに現れます。
アイドル制限を短くしすぎたために起きる、逆方向の事故もよくあります。接続を再利用するように作られたクライアントが、毎回新しい接続を開くことになり、レイテンシが増えて、ポートが急速に消耗します。そのため、数字は、クライアントの再利用の周期とあわせて決める必要があります。サーバーだけを見て選んだアイドル制限は、たいてい片側で間違います。
次のクイズですること
接続が静かなことと切れたことを、何で区別するのか、イベントが1つもないときに期限切れの検査がどう回るのか、そしてkeepaliveが何を保証し、何を保証しないのかを、整理してみてください。ラボで作るidle_keysは選ぶだけで消さないのですが、その理由もあわせて考えてみるとよいでしょう。