夜中の3時、PagerDutyの甲高いアラート音で飛び起きる。監視画面に映し出されていたのは、「特定のクラウド環境へのAPIリクエストが、特定のペイサイズを超えた瞬間に無言の死(タイムアウト)を迎える」という、ネットワークエンジニアにとって最も胃が痛くなるタイプの障害だった。
「HTTP 500でもない、TCP RSTですらない。ただひたすらパケットが闇に消える……」
原因はほぼ一つ。ルータやクラウドの仮想GWにおける MTU(Maximum Transmission Unit) のミスマッチ、そしてあろうことか、そこを通過しようとしたパケットが DF (Don't Fragment) フラグ によって木端微塵に砕かれる運命にあるにもかかわらず、ICMPの「お告げ」が途中でブラックホール化しているケースだ。
今回は、こうした「見えないパケットの断崖絶壁」を、古びてはいるが最強の相棒である ping コマンド(とDFフラグの制御)を駆使して暴き、実務の現場でどう切り抜けるかについて、徹底的に解説しよう。教科書には載っていない、現場の泥臭い知見を交えながらね。
—
1. パケットが砕け散る瞬間:Path MTU Discovery(PMTUD)の基本原理
インターネットの海を流れるIPパケットには、通過できる最大の大きさ(MTU)という厳格な物理的・論理的制限がある。標準的なEthernetであれば 1500バイト だし、AWSなどのクラウド環境でよく使われるJumbo Frameなら 9001バイト、逆にVPNやPPPoEを挟むと 1450バイト や 1492バイト に縮小される。
ここで問題になるのが、送信側が大きめのパケット(例: 1400バイト)を送り出し、途中のルータのMTUがそれより小さい(例: 1300バイト)ケースだ。
従来はルータが途中でパケットを分割(フラグメンテーション)してくれていたが、現代の高速なルータアーキテクチャやセキュリティポリシー(あるいはIPv6)において、中継ルータでのフラグメンテーションは激しいオーバーヘッドを伴うため、極力避けたい。そこで登場するのが、IPヘッダーに立てる DF (Don't Fragment) フラグ である。
DFフラグが意味する「不退転の決意」
DF=1(フラグメンテーション禁止)が設定されたパケットが、自身の許容サイズを超えるMTUを持つルータに直面したとき、ルータは非情にもそのパケットを破棄(Drop)する。そして、送信元に対して次のようなICMPメッセージを送り返す。
- IPv4の場合:
ICMP Type 3 Code 4 (Fragmentation Needed and DF set) - IPv6の場合:
ICMP Type 2 (Packet Too Big)
これを受け取った送信元OSは、「あ、次のルータのMTUは〇〇バイトなんだな」と学習し、送信パケットのサイズを動的に縮小する。これが Path MTU Discovery (PMTUD) のメカニズムだ。
しかし、実務の現場では、セキュリティ上の理由や誤ったファイアウォール(FW)設定によって、このICMPメッセージ(特にType 3 Code 4)が途中で捨てられてしまうことがある。これが、いわゆる 「ICMPブラックホール問題」 だ。結果として、Web APIへのPOSTリクエストが、ペイロードが小さいときは成功するのに、大きくなると突然ハングアップするという悪夢のような現象が引き起こされる。
—
2. 現場の必須武器:ping による手動 Path MTU 探索術
アプリケーション側で謎のタイムアウトに悩んだとき、我々NOCエンジニアが真っ先に開くのはターミナルだ。OS標準の ping コマンドを使い、DFフラグを強制的に有効にしたパケットを飛ばすことで、経路上の正確な「限界値」をあぶり出すことができる。
OSによってコマンドの構文が微妙に異なるため、それぞれの作法を確認しておこう。
macOS (BSD系) の場合
macOSの ping は、-D オプションでDFフラグを立て、-s オプションでペイサイズを指定する。
(※指定するサイズは ICMPペイロードのサイズ であり、IPヘッダー(20バイト)とICMPヘッダー(8バイト)の合計28バイトが自動付加される点に注意せよ)
# ペイロード1472バイト(合計1500バイト = 標準Ethernetの限界)でDFフラグを立てて送信
ping -D -s 1472 8.8.8.8
もし経路上のどこかでMTU 1500 を超えるボトルネックがあり、かつICMPが返ってくれば、以下のようなエラーが即座に返る。
> ping: sendto: Message too long
Linux (Ubuntu / RHEL系) の場合
Linuxの ping は非常に直感的で、-M オプションでPMTUDの挙動を完全に制御できる。現場で最もよく使うのは do(DFフラグを強制し、断片化を拒否する)だ。
# パケットサイズ(IPヘッダー込のトータルサイズ)を指定し、DFフラグを強制する
ping -M do -s 1472 8.8.8.8
※Linuxの -s はペイロードサイズを指定する。トータルで1500バイトにするには、1500 - 20 (IP) - 8 (ICMP) = 1472 を指定する。
もし許容サイズを超えている場合、Linuxは正直にこう教えてくれる。
> From 192.168.1.1 icmp_seq=1 Frag needed and DF set (mtu = 1400)
この mtu = 1400 という数字こそが、次のルータが泣きながら教えてくれた真実の限界値だ。
—
3. Web API設計・インフラ運用における実務的な対策とコード例
ネットワークの限界値が判明したところで、これを実際のシステム開発やインフラ設計にどう反映させるべきか。単に「MTUを合わせろ」で済む話ではないのが、モダンなクラウドネイティブ開発の難しいところだ。
対策1: ネットワーク層(OS・VPN・クラウドGW)でのMSSクラランプ
TCPの初期ハンドシェイク(SYNパケット)の段階で、お互いの最大セグメントサイズ(MSS)をネゴシエーションする。ルータやFW、あるいはAWSのNAT Gatewayなどで TCP MSS Clamping を有効にしておけば、経路上のMTUに合わせて自動的にTCPセグメントの大きさを丸めてくれる。
インフラ(Terraform等)のネットワーク設定例:
# AWS VPC内のVPN接続やTransit Gatewayアタッチメント等でのMSS調整の概念
resource "aws_ec2_transit_gateway_vpc_attachment" "example" {
subnet_ids = [aws_subnet.example.id]
transit_gateway_id = aws_transit_gateway.example.id
vpc_id = aws_vpc.example.id
options {
# PMTUDが機能しない環境に備え、強制的にMSSを1350にクランプする例
# トンネルオーバーヘッド(IPsec等)を考慮した実務的な防衛策
# (実際のAWSでは自動調整されるが、カスタムルータを挟む場合は必須の知識)
}
}
対策2: アプリケーション層(HTTPクライアント)での制御
もし、どうしてもネットワーク機器側の設定に手を入そみられない、あるいはサードパーティの不親切なWeb APIと通信しなければならない場合、アプリケーション側でリクエストボディのサイズを適切に分割するか、HTTPの Transfer-Encoding: chunked を利用して小分けにして送る必要がある。
以下に、Python(requests)および Node.js(Fetch API)を用いて、巨大なペイロードを安全に送信する際の実装プラクティスを示す。
Python (requests) による実装例
Pythonの requests ライブラリ(内部で urllib3 を使用)は、デフォルトでOSのPMTUDに依存する。巨大なJSONをPOSTする際は、タイムアウトとコネクションプールの設定を厳密に行うのがシニアの流儀だ。
import requests
from requests.exceptions import RequestException
def safe_api_post(url, payload_dict):
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
try:
# コネクションの確立と読込にタイムアウトを設定し、
# パケットロスによる無音のハングアップを防ぐ
response = requests.post(
url,
json=payload_dict,
headers=headers,
timeout=(3.10, 30.0) # (接続タイムアウト, 読込タイムアウト)
)
response.raise_for_status()
return response.json()
except RequestException as e:
# ネットワーク層の異常(PMTUD失敗含む)をキャッチ
print(f"[FATAL] API通信に失敗しました。MTU/ネットワーク経路を確認してください: {e}")
raise
Node.js (Fetch API) による実装例
現代のフロントエンドやバックエンド(Next.js / Node.js 18+)で標準となった fetch を使う場合も同様だ。
async function sendChunkedDataToApi(endpointUrl, largeData) {
try {
const response = await fetch(endpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
// 巨大なデータをJSON文字列化して送信
body: JSON.stringify(largeData),
// AbortControllerを用いたタイムアウト制御(インフラ障害対策の基本)
signal: AbortSignal.timeout(10000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`[ERROR] パケットロスまたはMTU問題の可能性:`, error.message);
throw error;
}
}
—
4. シニアエンジニアからの教訓:障害シューティングのチェックリスト
最後に、あなたが現場で「特定のサイズ以上のパケットが通らない」という壁にぶぶつかったとき、迷わず実行すべきデバッグのステップを授けよう。
1. まず ping -M do(または -D)で自社ネットワークの出口の限界を測れ
- ローカルPCから社内GW、そして宛先IPへ向け、サイズを徐々に大きくしながら
pingを打つ。どこでパケットが途絶えるか(あるいはFrag neededが返るか)を特定する。
2. ICMPがブロックされていないか疑え
- 宛先までの間に、セキュリティを過剰に意識したファイアウォールがあり、ICMP Type 3をすべて捨てていないか?(もしそうなら、PMTUDは完全に死ぬ)。
3. MSS Clampingが効いているかパケットキャプチャ(tcpdump)で確認せよ
- 実際のトラブルシューティングでは、ルータの入り口で
tcpdump -nn -i eth0 'tcp[tcpflags] & (tcp-syn) != 0'のようにSYNパケットを覗き、MSSの値が適切に書き換わっているか確認するのが確実だ。
ネットワークは見えないからこそ面白い。パケットの挙動を物理・論理の両面からイメージできるようになれば、どんな難解な障害も、ただの「解くべきパズル」に変わる。
さあ、コンソールを開き、パケットを走らせよう。
コメント