Web APIが突如として沈黙する?TCP MSSネゴシエーションとMTUの深淵
夜中の2時、PagerDutyの甲高いアラート音が静寂を切り裂いた。
「特定のクライアントから、巨大なJSONペイロードを持つPOSTリクエストだけがタイムアウトする」——そんな悪夢のようなトラブルシューティングに直面したことはないだろうか。
アプリケーション層のログを見てもエラーはなく、NginxやAPI Gatewayの手前でパケットがパタリと途絶えている。curlで叩くと正常に返ってくるのに、一部のユーザー環境や特定のクラウド間通信になると途端に繋がらなくなる。この手の「原因不明の通信不良」の9割は、レイヤー4のトランスポート層、すなわちTCP MSS(Maximum Segment Size)とMTU(Maximum Transmission Unit)のミスマッチが引き起こしている。
今回は、ネットワークの底流で何が起きているのか、そして私たちが実務でどうこれに対処すべきか、現場の泥臭い知見を交えて徹底的に解説しよう。
—
1. MTUとMSSの基本概念:パケットが形を変える理由
まずは言葉の定義から始めよう。私たちが日常的に扱っているHTTPリクエストやJSONレスポンスは、アプリケーション層から見れば「ただのバイト列」だが、ネットワーク層(レイヤー3)に降りると「IPパケット」という封筒に入れられる。
このIPパケットが一度の送出で運べる最大の大きさが MTU(Maximum Transmission Unit) だ。一般的なイーサネットの標準MTUは 1500 バイトである。
一方、今回主役となる MSS(Maximum Segment Size) は、トランスポート層(レイヤー4)であるTCPが「一度に受け入れられるペイロード(データ本体)の最大サイズ」を指す。
ここで重要な数式がある。
MSS = MTU - (IPヘッダーサイズ + TCPヘッダーサイズ)
IPv4の場合、IPヘッダーとTCPヘッダーは通常それぞれ 20 バイトずつ消費するため、標準的なMTU 1500 バイトの環境では、MSSのデフォルト値は以下のようになる。
1500 - 20 (IPv4) - 20 (TCP) = 1460 バイト
もし、あなたが設計したWeb APIが 1500 バイトを超える巨大なTCPセグメントを送り出そうとしたとき、途中のルーターのMTUが小さければ、パケットは無残にも引き裂かれる運命にある。これが断片化(Fragmentation)だ。
—
2. MSSのネゴシエーション:3wayハンドシェイクの裏側
TCPのコネクションが確立される瞬間、すなわちお馴染みの「3wayハンドシェイク」の裏側で、両者の端末はひそやかに「私、一度にこれくらいのサイズまでなら受け取れるよ」と名刺交換をしている。これが MSSネゴシエーション だ。
通信の流れをシーケンス図的に見てみよう。
Client Server
| |
|--- [SYN, MSS=1460] ------------------------------->| (クライアントの最大受取サイズを通知)
| |
|<-- [SYN-ACK, MSS=1430] ----------------------------| (サーバーの最大受取サイズを通知)
| |
|--- [ACK] ----------------------------------------->| (ハンドシェイク完了)
| |
クライアントが 1460 を提示し、サーバーが 1430(例えばPPPoE環境などでMTUが 1492 の場合など)を返した場合、双方のTCPスタックは賢いので、より小さい方(この場合は 1430)をそのコネクションの送信上限として採用する。
これにより、送信側は相手の許容範囲を超える巨大なセグメントを最初から作らずに済む。これが理想的な世界線だ。
—
3. Path MTU Discovery(PMTUD)の暗闘とブラックホール問題
しかし、世の中はそんなに甘くない。インターネットの途中には、あなたが管理していない無数のルーターが存在する。
送信元から宛先までの経路(Path)において、途中の最小MTUを動的に検出する仕組みが Path MTU Discovery (PMTUD) である。
PMTUDの正常な挙動
1. 送信側ホストが「断片化禁止(Don’t Fragment, DFフラグ = 1)」を立てたIPパケットを送信する。
2. 途中のルーターで「このパケット、次の回線のMTUよりデカいな……でもDFが立ってるから分割できないや」となる。
3. ルーターはパケットを破棄し、送信元へ ICMP “Fragmentation Needed”(Type 3, Code 4) という怒りのメッセージを送り返す。
4. 送信元はICMPを受け取り、「おっと、じゃあMSSを小さくしよう」と学習して再送する。
恐るべき「ICMPブラックホール問題」
ここで現代のセキュリティインフラの闇が立ちはだかる。DDoS攻撃やスキャン対策として、企業のファイアウォールやクラウドのセキュリティグループ(AWSのSecurity GroupやGCPの防火壁など)で、すべてのICMPパケットを容赦なくドロップ(破棄)しているケースが非常に多い。
ICMPが途中で握りつぶされると、送信元ホストは「パケットが届いたのか、途中で捨てられたのか」を知る術を失う。
結果として、クライアント側でローディンググルグル(無限ローディング)が続き、タイムアウトまで通信が完全に沈黙する。これが実務で最も恐れられる「PMTUDブラックホール問題」の正体だ。
—
4. 実務での対策:API設計とインフラ設定の現場知見
では、このネットワークの罠にどう立ち向かうべきか。現場のインフラエンジニアやバックエンドエンジニアが取るべき具体的な防衛策をいくつか紹介しよう。
A. サーバー側(Nginx / Linux OS)でのMSSクランピング設定
アプリケーションサーバーやAPI Gateway(Nginx等)を構築する際、OSレベルまたはリバースプロキシのレイヤーでMSSを強制的に書き換える(MSS Clamping)のが最も確実な延命措置となる。
Linuxのiptables(nftables)やiproute2を使用し、ルーターやロードバランサー通過時にSYNパケットのMSSを強制的に小さく(例えば 1360 や 1400 に)丸める設定を入れる。
Linuxカーネル(sysctl)での設定例:
# /etc/sysctl.d/99-tcp-mss.conf
# ネットワークインターフェースの自動MSSクランピングを有効化
net.ipv4.tcp_window_scaling = 1
# ※もし直接ルーター役を果てるサーバーであれば、iptablesで以下のようにMSSを強制書き換えする
# iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
B. Nginxのリバースプロキシ設定でのチューニング
Nginxをフロントに置く場合、バックエンドのオリジンサーバーとの通信やクライアントとの通信において、TCP_NODELAYやバッファサイズを適切に調整することが求められる。
http {
# ァストパス通信のための設定
tcp_nodelay on; # Nagleアルゴリズムを無効化し、小さなパケットも即座に送信
tcp_nopush on; # ヘッダーとファイルデータをまとめて送る(sendfile併用時)
server {
listen 443 ssl;
server_name api.example.com;
# クライアントからの大きなペイロードを受け入れるためのバッファ設定
client_max_body_size 10M;
client_body_buffer_size 128k;
location /v1/data {
proxy_pass http://backend_cluster;
proxy_http_version 1.1;
# プロキシ間の接続でもパケットの断片化リスクを最小化
proxy_set_header Connection "";
}
}
}
—
5. デバッグ実践:パケットをキャプチャして真実を見極める
「本当にMSSやMTUが原因なのか?」を証明するには、勘や推測を捨ててパケットキャプチャを取るのがエンジニアの王道だ。
現場で最も頼りになる tcpdump コマンドのレシピを授けよう。
ステップ1: 3wayハンドシェイク時のMSS値を確認する
サーバー上で以下のコマンドを実行し、該当するAPIエンドポイントへcurl等でアクセスを飛ばす。
# インターフェース(例: eth0)でポート443のSYNパケットをキャプチャし、オプション(MSS)を表示
sudo tcpdump -nnvvv -i eth0 'tcp[tcpflags] & (tcp-syn) != 0'
出力結果の中に、以下のような記述が見つかるはずだ。
options [mss 1460,sackOK,TS val 2538421 ecr 0,nop,wscale 7]
ここに表示されている mss 1460 が、その通信における初期ネゴシエーション値だ。もし相手のネットワーク環境(VPN経由やPPPoE)でMTUが制限されているにもかかわらず、ここが不自然に大きいままだと危険信号となる。
ステップ2: PythonスクリプトによるAPIリクエスト時の挙動検証
Web APIのクライアント側を開発する際、巨大なペイロードを投げるテストコードを書いて挙動を確認しておこう。Pythonの requests ライブラリを用いた検証用コードのサンプルを提示する。
import requests
import sys
def test_large_payload_api(endpoint_url):
# あえてMTU超過やフラグメンテーションの境界線になり得るサイズのJSONデータを生成
# 10KBのダミー文字列
large_payload = {
"data": "A" * (10 * 1024)
}
headers = {
"Content-Type": "application/json",
"X-Debug-Client": "MSS-MTU-Tester"
}
print(f"[*] 宛先へ巨大ペイロードを送信中: {endpoint_url}")
try:
# タイムアウトを短めに設定し、PMTUDブラックホールによる沈黙を検知する
response = requests.post(endpoint_url, json=large_payload, headers=headers, timeout=5)
print(f"[+] ステータスコード: {response.status_code}")
print(f"[+] レスポンスヘッダー: {response.headers.get('Content-Type')}")
except requests.exceptions.Timeout:
print("[-] 【警告】リクエストがタイムアウトしました!", file=sys.stderr)
print(" -> パケットが途中のルーターでドロップしている(ICMPブラックホールの可能性)か、", file=sys.stderr)
print(" -> MSS/MTUのミスマッチによる断片化失敗の疑いがあります。", file=sys.stderr)
except requests.exceptions.RequestException as e:
print(f"[-] 通信エラーが発生しました: {e}", file=sys.stderr)
if __name__ == "__main__":
target = "https://api.example.com/v1/data"
test_large_payload_api(target)
このスクリプトを実行した際、小さなペイロードでは200 OKが返るのに、10KB のペイロードにした途端に requests.exceptions.Timeout で沈黙する場合、それはアプリケーションのバグではなく、紛れもなくレイヤー4の幽霊(MSS/MTU問題)に取り憑かれている証拠だ。
—
6. まとめ:見えないレイヤーに思いを馳せるプロフェッショナルであれ
現代のクラウドネイティブな開発環境では、インフラの抽象化が進み、開発者がOSのパケットサイズやTCPの仕組みを意識する機会は減った。KubernetesやServerlessの裏側で、ネットワークは自動でいい感じに動いてくれているように見える。
しかし、グローバル展開するWeb API、厳格なセキュリティポリシーが敷かれた企業内ネットワークからのアクセス、あるいは特殊なVPN環境を跨ぐ通信において、レイヤー4の基本原則を理解しているか否かは、トラブルシューティングのスピードを何倍も左右する。
「コードは正しいのに、なぜか繋がらない」。そんな壁にぶぶんだときは、アプリケーションコードの深部ではなく、足元であるTCPセグメントの大きさに目を向けてみてほしい。パケットは、いつだって嘘をつかない。
コメント