なぜ「API呼び出しが途中で死ぬ」のか? ― MTU/MSS不整合と闘うネットワークの流儀
現場で叩き上げのエンジニアなら一度は経験があるはずだ。小さなテキストデータなら問題なく取得できるAPIが、大きなJSONやバイナリを返した瞬間に、まるで電源が落ちたかのように沈黙する。curlで叩けば Empty reply from server、ブラウザのコンソールには net::ERR_CONNECTION_RESET。
「サーバーが落ちているわけじゃない。ネットワークも疎通している。なのに通信できない」。この現象の背後に潜む「MTU(Maximum Transmission Unit)」と「MSS(Maximum Segment Size)」の不整合は、現代のクラウド環境でも依然としてエンジニアの胃を痛める、最も泥臭く、かつ最も本質的なパケットの死角だ。
今日は、パケットがネットワークの荒波に揉まれ、断片化という悲劇に直面したとき、何が起きているのかを紐解いていこう。
—
1. MTUとMSS:パケットの「積載量」と「中身」の規律
まず、基本を整理しよう。
- MTU (Maximum Transmission Unit): ネットワーク機器(NICやルーター)が一度に送信できる物理的なパケットの最大サイズ。イーサネットの標準は
1500 byteだ。これを超えるパケットは、基本的には通れない。 - MSS (Maximum Segment Size): TCPヘッダーを除いた、TCPペイロード(データ部分)の最大サイズ。
TCPのコネクションを確立する「3ウェイ・ハンドシェイク」の際、お互いに「俺はこれくらいのサイズのデータなら一気に処理できるぜ」と申告し合うのが MSS だ。
計算式は非常にシンプルだ。
MSS = MTU - IPヘッダー(20byte) - TCPヘッダー(20byte)
標準的なMTU 1500 byte の場合、MSS は 1460 byte となる。しかし、VPNトンネルやクラウド上の仮想ネットワーク(VXLANなど)を経由すると、カプセル化によるオーバーヘッドが発生し、実効的なMTUが 1500 を割ってしまうことが多々ある。ここが不整合の入り口だ。
—
2. PMTUDの理想と、現実に潜む「ブラックホール」
この問題を解決するために設計されたのが、パスMTUディスカバリー(PMTUD)だ。
送信元はIPヘッダーに「断片化禁止(DF: Don’t Fragment)」ビットを立ててパケットを送る。もし経路上のどこかでMTU制限に引っかかると、ルーターは「ICMP Type 3 Code 4(Destination Unreachable: Fragmentation Needed)」という悲鳴を上げる。これを受け取った送信元は、「あ、デカすぎたのか」と理解し、送信サイズを縮小する。
……というのが教科書の理屈だが、現実はそう甘くない。セキュリティポリシーで ICMP を全拒否しているファイアウォールやロードバランサーが多すぎるんだ。結果、パケットは「断片化禁止」の烙印を押されたままドロップされ、送信元には何のフィードバックも戻らない。これが有名な「PMTUDブラックホール問題」だ。
—
3. 現場の特効薬:MSSクランプ(MSS Clamping)
PMTUDが機能しない環境で、我々が取るべき最も実用的な防衛策が「MSSクランプ」だ。これは、ルーターやロードバランサーがSYNパケットを通過させる際、MSS 値を強制的に書き換えるテクニックである。
例えば、iptables を使って、パケットの MSS を 1400 byte に強制的に制限する設定はこうなる。
# 経路上のMTUが1450などの環境で、余裕を持って1400に制限する例
# mangleテーブルでFORWARDチェーンに介入し、MSSを書き換える
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1400
これを踏み台サーバーや境界ルーターで設定するだけで、それまで謎の断絶を繰り返していたAPI通信が嘘のように安定することがある。
—
4. デバッグの最前線:パケットの「声」を聞く
問題が発生したとき、闇雲にコードを弄るのはやめよう。まずはパケットサイズを確認するのが鉄則だ。
curl でヘッダーを確認する
まず、curl で詳細なトレースを見る。
# -v で詳細を表示、--trace-ascii でパケットの内容を追う
curl -v https://api.example.com/huge-data --trace-ascii debug.dump
もし通信が途中で止まるなら、Wireshark や tcpdump の出番だ。
tcpdump で MSS を確認する
サーバー側で、接続確立時のSYNパケットをキャプチャしてみる。
# 80番ポートでSYNパケットのみをキャプチャし、MSSオプションを確認
sudo tcpdump -ni eth0 'tcp[tcpflags] & tcp-syn != 0' -vv
出力結果の中に mss 1460 という文字列が見えるはずだ。もし、クライアントが 1460 を要求しているのに、経路のどこかでパケットが消失しているなら、そこがボトルネックだ。
Python で再現試験を行う
クライアント側で、あえて大きなペイロードを送信してみるスクリプトを書いてみるのもいい。
import requests
# 巨大なJSONデータを送信してみるテスト
payload = {"data": "A" * 2000} # MTUを意識した大きなデータ
try:
# タイムアウトを短く設定し、コネクションがハングアップするか確認
response = requests.post("https://api.example.com/upload", json=payload, timeout=5)
print(f"Status: {response.status_code}")
except requests.exceptions.ConnectionError as e:
print(f"ネットワークエラーが発生しました: {e}")
—
最後に:ネットワークを「疑う」勇気を
Web APIの設計において、MTUやMSSを意識することは一見、レイヤーの異なる「低レイヤーの仕事」に見えるかもしれない。だが、クラウドネイティブな時代になればなるほど、物理的な制限は仮想ネットワークという皮をかぶって、より巧妙に隠蔽される。
アプリケーションがどれほど美しく設計されていても、それを運ぶパケットが物理的な制約という「壁」に激突していては、すべては水泡に帰す。
「コードは正しいはずだ。原因はどこだ?」と迷ったとき、一度レイヤーを下げて、パケットのサイズと経路に思いを馳せてみてほしい。ネットワークは、常に正直だ。不整合があれば、必ずどこかでパケットが悲鳴を上げている。その声を聞き取れるようになることこそが、凄腕のエンジニアへの第一歩なのだから。
コメント