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

こんにちは!ネットワークやインフラの世界へようこそ。

インフラエンジニアとして現場に出ると、避けて通れないのが「TCPコネクションのトラブル」です。Webアプリケーションの性能チューニングや、高負荷なAPIサーバーの運用をしていると、「なぜか急に繋がらなくなった」「ポートが足りない(ポート枯渇)エラーが出るぞ……?」なんて壁にぶつかることがよくあります。

その黒幕の多くが、今回お話しするTCPの切断プロセス、特にTIME_WAITという状態こいつなんです。

「難しそうだな……」なんて身構えなくて大丈夫ですよ!今回は、パケットの飛び交う世界を身近な「郵便配達」に例えながら、一歩ずつ優しく紐解いていきましょう。

—

1. そもそもTCPってどんな仕組み?(おさらいと郵便配達のたとえ)

私たちがブラウザでWebサイトを見る時、裏側では「TCP(Transmission Control Protocol)」というルールを使って、データを確実に対象のサーバーへ届け合っています。

TCPを分かりやすく例えるなら、「手紙のやり取り」です。
ネットフリックスの動画やWebページのデータを、一文字も取りこぼさないように、相手としっかり意思疎通を取りながらやり取りする仕組みだと思ってください。

つながる時(3ウェイハンドシェイク)と、お別れする時(4ウェイハンドシェイク)

  • つながる時(3ウェイハンドシェイク): 「もしもし、今から手紙を送るね?」「はい、受け取る準備OKです!」「じゃあ送るよ!」という3ステップで回線を繋ぎます。
  • お別れする時(4ウェイハンドシェイク): 「もう送るデータはないから、お互いに手紙のやり取りを終わりにしようか」と、お互いの意思を確認し合うため、4つのステップ(4ウェイ)を踏んで丁寧に片付けを行います。

この「お別れする時」のステップの中で、私の担当するサーバーが裏側でどんな状態(ステート)に変化しているのかを、これから詳しく見ていきましょう!

—

2. 正常終了への道:4ウェイハンドシェイクとステートの旅

Webサーバーとブラウザが「もう通信を終わりにしようね」と決めてから、完全に接続が切れるまでの裏側のドラマを覗いてみましょう。

通信の終了は、どちらか一方から「終わります!」という合図(FINパケット:おしまいフラグ)を送ることから始まります。

[クライアント(ブラウザ)]                     [サーバー(Webアプリ等)]
        |                                       |
        |---- (1) FIN (もう送るデータないよ) ---->|  --> サーバー側は 【CLOSE_WAIT】へ
        |                                       |
        |<--- (2) ACK (了解!こっちも確認したよ) --|
        |     (しばらくデータ処理を待つ…)         |
        |                                       |
        |<--- (3) FIN (サーバー側も終了するね) ---|  <-- サーバー側は 【LAST_ACK】へ
        |                                       |
        |---- (4) ACK (了解!完全にバイバイ!) ->|  --> サーバー側は 【CLOSED】へ
        |                                       |
  (クライアント側は【TIME_WAIT】へ)

登場する主なステート(状態)たち

  • CLOSE_WAIT(クローズ・ウェイト)

クライアントから「もう終わろう」と言われたサーバーが、「おっ、了解。じゃあこっちも終わりの準備をするね」と返事をした後の状態です。サーバー側のプログラムがまだ処理を終えていない場合、この状態にしばらく留まります。

  • LAST_ACK(ラスト・アック)

サーバー側からも「よし、うちも終わりだ!」と相手にFINを送った後、最後の「了解!」の返事(ACK)を待っている、ほんの一瞬の状態です。

  • TIME_WAIT(タイム・WAIT)

すべてのやり取りが終わったクライアント(または接続を先に切った側)が、「本当にもう通信は終わったよね?念のため、少しの間だけ待機しておくか」とクールダウンしている状態です。

ここで特にインフラエンジニアの頭を悩ませるのが、最後のTIME_WAITなんです。なぜ、終わったはずなのにわざわざ待機しているのでしょうか?

—

3. なぜTIME_WAITなんて面倒な状態があるの?

「もうお別れしたんだから、すぐにスパッと縁を切ればいいじゃない!」と思いますよね。でも、このTIME_WAITには、ネットワークの現実世界を生き抜くための大切な理由が2つあります。

1. 消えかけのパケットが後から届くかもしれないから
ネットワークの海を漂っていた古いパケットが、少し遅れて遅れて届くことがあります。もし即座に同じIPアドレス・同じポート番号で別の通信を始めてしまうと、「さっきの古いおしゃべりの残りかす」が新しい通信に混ざってしまい、大誤作動を起こしてしまいます。
2. 最後の「バイバイ(ACK)」が相手に届かなかった時の保険
こちらが送った最後の「了解!」の返事が、途中で消えてしまったとします。相手は「あれ?返事が来ないぞ、もう一回『終わろう』って送らなきゃ」と、同じFINをもう一度送ってきます。その時、こっちが完全にスッカラカン(CLOSED状態)になっていたら、「なんだコイツ突然!」とエラーになってしまいます。TIME_WAITで待っていれば、「あ、さっきの返事が届いてなかったんだな」と優しくもう一度「了解!」を返してあげられるのです。

このTIME_WAIT、LinuxなどのOSでは通常60秒間ほど持続します。

—

4. 悪夢の始まり:TIME_WAITが引き起こす「ポート枯渇問題」

「たかが60秒待つだけなら問題ないのでは?」と思いますよね。
しかし、1秒間に何千ものリクエストをさばく超人気Webサイトや、大量のAPIリクエストを外部に投げまくるマイクロサービス環境では、話が変わってきます。

通信をするためには、パソコン(あるいはサーバー)のなかに「ポート番号」という「部屋番号」のようなものが必要です。この部屋番号は、OSごとに使える数に限りがあります(だいたい数万個程度)。

TIME_WAIT状態にあるポートは、「まだ完全に片付けが終わっていないから、鍵が閉まっていて新しい人が使えない部屋」になります。

恐ろしいシナリオ

1. 1秒間に10,000件のリクエストを外部に投げるサーバーがある。
2. すべての通信が終わり、10,000個のポートがTIME_WAIT状態になる。
3. 60秒間ポートが塞がり続けるため、単純計算で 10,000 × 60 = 600,000個 のポートが必要になる。
4. しかし、使えるポートの数(数万個)を遥かに超えているため、「新しい通信が作れない!(ポート枯渇)」というエラーが発生し、システムが突如としてダウンしてしまうのです!

—

5. 現場で使える!TIME_WAITとポート枯渇の対策・チューニング

「じゃあどうすればいいんだよ!」という絶望の淵にいるエンジニアの皆さんのために、実務で使える具体的な処方箋をお届けします。

Linux(UbuntuやCentOSなど)環境において、カーネルパラメータを調整することで、このTIME_WAIT地獄を華麗にかわすことができます。

対策1: TIME_WAITソケットの再利用を有効にする (net.ipv4.tcp_tw_reuse)

安全な条件のもとで、TIME_WAIT状態のポートを新しい通信に「リサイクル」して使う設定です。これが一番手堅く安全なチューニングと言えます。

設定ファイル(例: /etc/sysctl.d/99-tcp-tuning.conf など)に以下の1行を追加します。

# TIME_WAIT 状態のソケットを、安全な状況下で新しく立ち上げる接続に再利用する
net.ipv4.tcp_tw_reuse = 1

設定を反映させるには、以下のコマンドを叩きます。

# カーネルパラメータを即座に再読み込みする
sudo sysctl -p /etc/sysctl.d/99-tcp-tuning.conf

対策2: クライアント側のポート範囲を広げる (net.ipv4.ip_local_port_range)

そもそも使える部屋番号(ポート)の数が少なすぎるなら、使える範囲を広げてしまえばいいのです。エフェメラルポート(動的ポート)と呼ばれる領域をグッと広げます。

# 利用可能なローカルポートの範囲を拡張する(デフォルトよりも広範囲に設定)
net.ipv4.ip_local_port_range = 1024 65535

対策3: アプリケーション側の設計を見直す(Keep-Aliveの活用)

OSのパラメータをいじる前に、アプリケーション側でできる最大の防御が「HTTP Keep-Alive(コネクションの持続)」の活用です。
毎回新しく「手紙のやり取り(コネクション)」をゼロから作って切断して……と繰り返すからTIME_WAITが爆発するのです。1度繋いだパイプ(コネクション)を使い回す設定(Keep-Alive)にすることで、コネクションの生成・切断の回数自体を劇的に減らすことができます。

例えば、PHPのGuzzleやPythonのrequestsライブラリなどでAPIを叩く際も、セッション(Session)オブジェクトを使い回すことで、背後で上手にTCPコネクションを維持してくれます。

import requests

# 毎回 requests.get() を呼ぶと毎回コネクションが作られて切断される(TIME_WAITの嵐に)
# 代わりに Session を使うことで、コネクションが維持(Keep-Alive)され、ポート枯渇を防げる!
session = requests.Session()

for i in range(100):
    # 同じセッションを使い回すことで、TCPコネクションの張替えコストを削減!
    response = session.get('https://api.example.com/data')
    print(response.status_code)

—

まとめ

いかがでしたでしょうか?
TCPの切断プロセスである4ウェイハンドシェイクと、そこに潜むTIME_WAIT、そしてそれが引き起こすポート枯渇の恐怖。そしてそれを回避するためのカーネルチューニングとアプリ実装のコツまで、一連の流れがイメージできたでしょうか。

ネットワークのパケットやステートは、一見すると目に見えない冷たい数字の羅列のように思えますが、その裏側では「データを確実かつ安全に届けるため」の泥臭い配慮とルールが何重にも張り巡らされています。

現場で「なんだか急に繋がらなくなったぞ?」というインフラのトラブルに直面したときは、ぜひ今回の郵便配達のストーリーとステートたちのドラマを思い出してみてくださいね。きっと、冷静に原因を突き止めるヒントが見えてくるはずです。

それでは、また次回の技術探訪でお会いしましょう!快適で安全なネットワークライフを!

コメント

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