【実務・中級編】 TCPコネクション切断のステート遷移(FIN_WAIT, CLOSE_WAIT, TIME_WAIT) – ネットワーク基礎とWebセキュリティ実践ガイド

なぜ君のサーバーは「ポート枯渇」で死ぬのか?―TCP切断の深淵とTIME_WAITの正体

エンジニアたるもの、APIのレスポンスが返ってこないというアラートに飛び起きた経験が一度はあるはずだ。そのとき、君はまず何を疑う?DBのロックか、それともアプリケーションのメモリリークか。

しかし、現場で長年戦っていると、往々にして犯人はもっと「足元」にいる。そう、TCPコネクションの管理だ。特に、高負荷なWeb APIサーバーで突如として発生する「接続不可」のトラブルは、その多くが TIME_WAIT 状態の積み上げによるポート枯渇に起因している。

今回は、教科書を読み飛ばして現場に飛び込んだ君たちへ、TCPの「お別れ」の作法と、それがインフラに与えるリアルな影響について深掘りしていく。

—

TCPの「4ウェイハンドシェイク」というお別れの儀式

TCPはコネクション型だ。接続時に SYN を送り合うのは知っての通りだが、切断時はもっと慎重だ。片方が「もう送るものはないよ」と言ったからといって、即座にブチッと切るわけにはいかない。相手がまだ何か言いたいかもしれないからだ。

ここで登場するのが「4ウェイハンドシェイク」だ。

1. FIN: 「もう送るデータはないよ」(アクティブクローズ側)
2. ACK: 「了解、受け取ったよ」(パッシブクローズ側)
3. FIN: 「じゃあ私も閉じるね」(パッシブクローズ側)
4. ACK: 「了解、完全に閉じるよ」(アクティブクローズ側)

このプロセスを経て、コネクションは静かに幕を下ろす。だが、問題はここからだ。

—

なぜ TIME_WAIT は存在するのか?

最後の ACK を送った側は、すぐには CLOSED 状態にならず、TIME_WAIT という状態に留まる。これはOSの仕様であり、RFC 793で定められた「誠実さ」だ。

理由は主に2つある。

  • 遅延パケットの消滅待ち: ネットワークのどこかで迷子になっていた古いパケットが、新しいコネクションに混入するのを防ぐため。
  • 最後のACKの再送: 自分が送った最後の ACK が相手に届かなかった場合、相手は「FIN」を再送してくる。その際、自分側が既に CLOSED になっていると「RST(強制リセット)」を返してしまい、相手にエラーを報告してしまう。それを避けるための猶予期間だ。

この期間は通常 2MSL(Maximum Segment Lifetime:セグメント生存時間)と呼ばれ、Linuxでは大抵1分(60秒)に設定されている。

—

現場で直面する「ポート枯渇」の罠

秒間数千のリクエストを処理するAPIサーバーにおいて、1分間もコネクションが TIME_WAIT で居座られたらどうなるか?

サーバーが使えるエフェメラルポート(一時的なポート)は、せいぜい6万弱だ。TIME_WAIT が溜まりきると、新しいコネクションを作ろうとしても「そんなポートは残っていないよ」とOSが拒絶する。これが、君たちが夜中に叩き起こされる「接続不可」の正体だ。

解決策:カーネルパラメータをいじる

まずは現場で即効性のあるパラメータチューニングだ。/etc/sysctl.conf に以下の設定を投入する。

# TIME_WAIT状態のソケットを再利用可能にする(クライアント側で特に有効)
net.ipv4.tcp_tw_reuse = 1

# エフェメラルポートの範囲を広げる(デフォルトは狭すぎる場合が多い)
net.ipv4.ip_local_port_range = 1024 65535

ただし、tcp_tw_reuse はタイムスタンプオプション(PAWS)が有効な場合にのみ安全に動作する。現代のネットワーク環境ではほぼ問題ないが、設計時には考慮が必要だ。

—

コードで見る「接続の使い回し」

そもそも、なぜこれほどコネクションが頻繁に切れるのか?原因の多くは、クライアント側のコードにある。

アンチパターン:都度コネクションを張る(Pythonの例)

import requests

# ループの中で毎回セッションを作っていると、そのたびにTCPハンドシェイクが発生し、切断時にTIME_WAITが生成される
for i in range(1000):
    response = requests.get("https://api.example.com/data")

ベストプラクティス:コネクションプールを使う

requests.Session() を使うことで、TCPコネクションを Keep-Alive で維持し、ハンドシェイクのオーバーヘッドと TIME_WAIT の発生を劇的に減らせる。

import requests

# Sessionオブジェクトを使い回すことで、コネクションが維持される
session = requests.Session()
for i in range(1000):
    response = session.get("https://api.example.com/data")

—

まとめ:ネットワークは「行儀よく」扱う

TCPの TIME_WAIT は、決して「悪者」ではない。ネットワークという不完全な世界で、通信の整合性を守るための「殿(しんがり)」だ。

君たちがインフラを設計する際は、以下の3点を意識してほしい。

1. コネクションプーリング: アプリケーション側で接続を使い回す。これが最大の防御策だ。
2. Keep-Aliveの活用: HTTPレベルでもTCPレベルでも、接続を無駄に切らない設定を入れる。
3. OSチューニングは最後の手段: カーネルパラメータを弄るのは、コードやアーキテクチャの改善をやり尽くしてからにしよう。

ネットワークは正直だ。パケットの挙動を想像し、一つひとつのコネクションを大切に扱う。それができるエンジニアこそが、真に「枯れた」安定したシステムを構築できると私は信じている。

さて、そろそろ次の障害調査の時間だ。君たちも、たまには netstat や ss コマンドで、サーバーの中で何が起きているか覗いてみるといい。意外な「お別れ」のドラマが見えてくるはずだぞ。

コメント

タイトルとURLをコピーしました