握手の裏側で何が起きているのか?TCP 3ウェイハンドシェイクと「見えない罠」
ネットワークエンジニアとして現場に立っていると、「Web APIのレスポンスが妙に遅い」「特定の環境からだけ接続が切れる」といった相談をよく受けます。そんな時、多くのエンジニアはまずアプリケーションログを見に行きますが、本当の答えは、OSのカーネルが黙々と処理している「TCPの握手」の裏側にあることが少なくありません。
今日は、教科書で習ったはずの「3ウェイハンドシェイク」を、現場のトラブルシューティングという泥臭い視点から解剖してみましょう。
—
1. 握手(ハンドシェイク)の正しい理解:ステート遷移のリアル
TCPの接続確立は、単なるパケットの往復ではありません。サーバーとクライアントが「会話ができる状態か」を確認し合う、極めて緻密なステート(状態)遷移の旅です。
3ウェイハンドシェイクのシーケンス
1. SYN (Synchronize): クライアントが SYN パケットを送信。ここでクライアントは SYN_SENT 状態になります。「おーい、繋ぎたいんだけど!」という合図です。
2. SYN-ACK: サーバーはそれを受け取り、SYN-ACK を返して SYN_RCVD 状態へ。ここでサーバーは「いいよ、君の番号(シーケンス番号)はこれだね、僕の番号はこれにするよ」と握手に応じます。
3. ACK (Acknowledge): クライアントが最後に ACK を投げ、双方が ESTABLISHED 状態へ。これでようやくデータのやり取りが可能になります。
この一瞬のやり取りの間、サーバーのメモリ上には「接続待ち」のキュー(バックログ)が確保されます。ここがインフラ屋としての「戦場」です。
—
2. 泥沼の「ハーフオープン」とSYNフラッド攻撃
もし、クライアントが最後の ACK を返さなかったらどうなるでしょうか?
サーバーは SYN_RCVD のまま、「まだかな?」と待ち続けます。この状態を「ハーフオープン」と呼びます。悪意ある攻撃者がこの SYN パケットを大量に送りつけ、サーバーの接続待ちキューを埋め尽くすのが、かの有名な SYNフラッド攻撃 です。
サーバーの listen キューが溢れると、正当なユーザーからのアクセスはすべて Connection Refused またはタイムアウトで弾かれます。Web APIが突然沈黙する原因の多くは、ここにあるのです。
—
3. 現場で使えるデバッグの極意:CLIでの可視化
トラブルシューティングにおいて、今のサーバーがどんな状態にあるかを知ることは不可欠です。まずは ss コマンド(または古い環境なら netstat)で、今の接続状況を確認しましょう。
# 現在のTCP接続状態を一覧表示。SYN_RECVの状態が多い場合は要注意!
ss -ntu state syn-recv
# 特定のポート(例: 80番)の接続数をカウント
ss -nt sport = :80 | grep ESTAB | wc -l
また、クライアント側から接続が確立できない場合、curl で詳細なトレースを取るのが定石です。
# 接続の過程を詳しく追跡する
curl -Iv https://api.example.com
この時、Connection timed out が出るのか、Connection refused なのかで原因の切り分けが大きく変わります。前者はネットワーク経路やファイアウォールでの遮断(ドロップ)、後者はサーバー側でプロセスが死んでいるか、ポートが開いていない可能性が高いと判断できます。
—
4. カーネルパラメータでの守り方
SYNフラッドからサーバーを守るために、Linuxカーネルの設定を調整するのは運用エンジニアの嗜みです。/etc/sysctl.conf を確認してください。
# /etc/sysctl.conf への設定例
# SYN Cookieを有効化:キューが溢れそうな時に偽装パケットを弾く防衛策
net.ipv4.tcp_syncookies = 1
# SYN-ACKの再送回数を減らす:ハーフオープン状態を早く切り上げる
net.ipv4.tcp_synack_retries = 2
# キューの最大数を増やす:急激なトラフィック増に対応
net.core.somaxconn = 1024
設定を変えた後は、sysctl -p を忘れずに適用しましょう。これだけで、高負荷時の耐性が劇的に変わります。
—
5. アプリケーション視点:タイムアウト設計の重要性
最後に、開発者として気をつけるべきは「タイムアウト」です。Pythonの requests ライブラリなどでAPIを叩く際、デフォルトのタイムアウト設定を信じてはいけません。
import requests
# 接続タイムアウト(connect)と読み込みタイムアウト(read)を分けるのがプロの技
try:
response = requests.get(
"https://api.example.com",
timeout=(3.05, 10) # 接続に3秒、読み込みに10秒まで待つ
)
response.raise_for_status()
except requests.exceptions.ConnectTimeout:
print("サーバーのSYN待ちでタイムアウトしました。インフラを確認してください。")
TCPハンドシェイクを理解しているエンジニアは、単に「繋がらない」と嘆くのではなく、「どのフェーズで止まっているのか」「なぜそのタイムアウト値なのか」を論理的に説明できます。
ネットワークの境界は、パケットが通過する場所であると同時に、あなたの技術力が試される場所でもあります。ぜひ、日々の運用の中で tcpdump や ss を駆使し、パケットの声に耳を傾けてみてください。そこには、教科書には載っていない「ネットワークの真実」が流れていますから。
コメント