TCP — 接続と信頼性はどう作られるのか
一言でいうと
TCPは、シーケンス番号、確認応答、再送、そして2種類のウィンドウで、「失われず、順序も入れ替わらないバイトストリーム」という錯覚を作り出します。
なぜ必要なのか
IPはベストエフォートを尽くすだけで、何も約束しません。パケットは消えることがあり、順序が入れ替わることがあり、重複することがあります。ところが、大半のアプリケーションは、「送ったとおりに受け取る」ことを前提に作られています。その隙間を埋める層が、TCPです。
どう動くのか
接続の確立。クライアントがSYNを送り、サーバーがSYN-ACKで応答し、クライアントがACKを送ります。3回のやり取りが必要な理由は、双方がそれぞれの初期シーケンス番号を相手に知らせて、確認してもらう必要があるからです。
接続の終了。各方向を別々に閉じるので、FINとACKが2回ずつ、4回のやり取りが必要です。先に閉じた側は、TIME-WAIT状態で、2MSL(最大セグメント生存時間の2倍)の間残ります。遅れて届いたセグメントが、同じポートのペアを使う次の接続を汚染しないようにし、最後のACKが失われたときに、再送してやるためです。負荷の大きいサーバーで、TIME-WAITソケットが数万個溜まるのは正常で、これをなくそうとして、カーネルパラメーターをむやみにいじると、別の問題を呼びます。
信頼性。受け取る側は、うまく受け取れた位置までをACKで知らせます。送る側は、一定時間(RTO)以内に確認がなければ再送し、同じACKが3回重複したら、タイムアウトを待たずにすぐ再送します(高速再送)。
2つのウィンドウ。フロー制御は、受け取る側がアドバタイズする受信ウィンドウ(rwnd)で、「自分はこれだけしか受け取れない」を知らせる仕組みです。輻輳制御は、送る側が自分で保持する輻輳ウィンドウ(cwnd)で、「ネットワークがこれだけしか耐えられない」を推定する仕組みです。2つは目的が違います。実際の送信量は、2つの値のうち小さいほうに制限されます。輻輳制御は、スロースタートで指数的に増やし、しきい値を超えると線形に増やし、損失が検知されると大きく減らします(AIMD)。
現場での姿
接続失敗のメッセージ2種類の違いが、診断の半分を決めます。
Connection refusedは、相手がRSTを返してきたという意味です。パケットが宛先まで行って戻ってきたという証拠なので、ルーティング、セキュリティグループ、ネットワークポリシーは、すでに通過しています。残る候補は、宛先ホストの内側です。プロセスが死んでいるか、別のポートにバインドされているか、0.0.0.0ではなく127.0.0.1にだけバインドしているか、ロードバランサーの後ろに、正常なバックエンドが1つもない場合です。ここで、よくある誤解を正す必要があります。「refusedが出るのでファイアウォール」という説明は、ほとんどが誤りです。実務のファイアウォールは、ほとんど常に、黙って捨てる(DROP)ポリシーで、捨てられたパケットは応答がないので、timeoutとして現れます。
Connection timed outは、SYN再送をすべて使い切るまで、何の応答もなかったという意味です。Linuxはデフォルトでtcp_syn_retriesの回数だけ再送し、間隔を2倍ずつ延ばします。1+2+4+8+16+32+64を足すと、127秒です。アプリケーションログのタイムアウト設定が30秒なのに、実際には127秒で失敗したなら、その設定が適用されていないというサインです。
リスナーがあるのにtimeoutになる場合もあります。acceptキューがいっぱいになると、カーネルが新しいSYNを黙って捨てます。ss -ltnでリスニングソケットを見ると、Recv-Qが現在のキューの長さ、Send-Qが最大サイズです。Recv-QがSend-Qを超えているなら、原因はネットワークではなく、アプリケーションの処理スループットです。
続くクイズで確認すること
refusedとtimeoutが、それぞれどんなパケットのやり取りの結果なのか、フロー制御と輻輳制御の違いが何なのかを説明できるかを確認します。