Nagle アルゴリズムと遅延 ACK の相互作用
対話型アプリで突然現れる約200msのストールを原理から退治できる。Nagleと遅延ACKがなぜ噛み合って遅延を生むか、TCP_NODELAYを切るべき境界まで判断できる。
- 1.Nagleは未ACKの小セグメントがある間は次の小データを溜める送信側の最適化。遅延ACKは受信側がACKを最大約200ms遅らせる最適化。両者は独立に正しいが、噛み合うと互いを待ち合う。
- 2.リクエスト→レスポンス型でwriteが分割されると、送信側はACK待ちで止まり受信側はデータ待ちでACKを遅らせ、約40〜200msのストールが周期的に表面化する。
- 3.対話・RPC・小リクエスト即応答型はTCP_NODELAYでNagleを切るのが定石。バルク転送ではNagleを残す。根治はアプリ側でwriteを1回に束ねること。
二つの最適化が、別々に正しい
TCPには送信効率を上げる小細工が送受の両側に1つずつあります。どちらも単独では理にかなっていますが、組み合わさると遅延を生みます。
- Nagle アルゴリズム(送信側): 細切れの小セグメント乱発を抑え、ヘッダ比率を下げる。
- 遅延 ACK(delayed ACK、受信側): ACK単独パケットを減らし、データやウィンドウ更新に相乗りさせる。
両者はそれぞれ別の問題(細切れ送信、ACKの氾濫)を解くために生まれました。問題は、片方の「待ち」がもう片方の「待ち」の前提を崩す ことです。本稿はその噛み合わせを内部動作から解きほぐします。前提として小パケット抑制の全体像は TCPのフロー制御とウィンドウスケーリング を、TCPとUDPの役割分担は TCP と UDP の違い を参照してください。
Nagle アルゴリズムの判断規則
Nagleの狙いは Silly Window Syndrome の送信側対策、すなわち「1バイトのデータを40バイト超のヘッダで運ぶ」極端な非効率を避けることです。規則は一行で言えます。
未確認(未ACK)の送信済みデータが存在する間は、
MSS 未満のセグメントを送らずバッファに溜める。
溜めたデータは次のいずれかで送出する:
(a) 溜まったデータが MSS に達した、または
(b) 未確認データがすべて ACK された。
裏を返すと、未確認データが無いとき(パイプが空のとき)の最初の小セグメントは即送られる。Nagleが効くのは「小データを送った直後に、ACKが返る前にもう一度小データを送ろうとした」場合だけです。telnetで打鍵を往復のたびに束ねられるのはこの性質によります。
Nagleの本質は「未確認の小セグメントは常に高々1つ」という制約です。送信側は小セグメントを1つ投げたら、それがACKされるまで次の小セグメントを出しません。フルサイズ(MSS)セグメントはこの制約の対象外で、いつでも送れます。だからバルク転送はNagleがあってもほぼ影響を受けません。問題はデータがMSSに満たない対話型に限られます。
遅延 ACK の判断規則
受信側はACKを必ず即返すわけではありません。遅延ACK は、ACK単独の小パケットを減らすために確認応答を少し遅らせ、直後に送る応答データやウィンドウ更新に「相乗り(piggyback)」させます。規則は概ね次の通りです。
受信したセグメントに対し ACK を最大約 200ms 遅らせてよい。
ただし次の場合は即時 ACK する:
- 連続して 2 個ぶんのフルサイズデータを受信した(2 セグメントごとに 1 ACK)
- 順序の乱れ(out-of-order)を検出した
- 遅延タイマ(約 40〜200ms、実装依存)が満了した
「2セグメントに1 ACK」が基本動作で、これ自体は健全です。問題は データが1セグメントしか来ず、しかも応答データもすぐ生成されないとき。受信側は相乗りの機会を待って遅延タイマ満了までACKを抱え込みます。
噛み合わせ:互いの「待ち」が直列化する
ここで両者を同じコネクション上で動かします。典型は「小さなリクエストを送って即レスポンスを待つ」往復型で、かつ送信側がリクエストを 複数回のwriteに分割 した場合です。
送信側 write を 2 回に分割 (例: ヘッダ部 → 本体部) したとき
t0 送信側: 1個目の小セグメントを送信 (パイプは空だったので即送出)
t1 受信側: 1個目を受信。応答データはまだ無い
→ 遅延 ACK 発動。ACK を抱えてタイマ起動
t2 送信側: 2個目の小セグメントを送ろうとする
→ だが 1個目が未 ACK。Nagle 発動で送出を保留
--- ここで両者が互いを待つ ---
送信側: 「ACK が来たら 2個目を送る」
受信側: 「データ(2個目)が来たら ACK に相乗りさせる」
t3 受信側: 遅延タイマ満了 (約 40〜200ms)。やむなく単独 ACK 送出
t4 送信側: ACK 受信。Nagle 解除。ようやく 2個目を送出
リクエスト全体が受信側に届くのは t4 以降。サーバーはそこで初めて完全なリクエストを処理できます。1往復あたり 遅延タイマぶん(実装により約40ms、古典的には約200ms)の純粋な待ち が挿入され、それが毎リクエスト繰り返されます。CPUもネットワークも空いているのにスループットだけが落ちる、再現性が高く原因の見えにくい遅延です。
「ローカルでは速いのに本番(高RTT)で異様に遅い」「転送が約40msや約200msの倍数で刻まれる」「往復回数に比例して遅延が増える」ならこの相互作用を疑います。キャプチャでは、小セグメント送信の直後にACKが約40〜200ms遅れて単独で現れ、その後に次の小セグメントが続くパターンが見えます。帯域は余っていてRTTだけが効くのが特徴です。
なぜ「分割write」が引き金なのか
重要なのは、writeを1回にまとめれば噛み合わない ことです。リクエスト全体を1回のwriteで渡せば、パイプが空の状態から1つのセグメント(MSS以下なら即送出)として出ていき、受信側はそれを受けて応答を生成、ACKに相乗りできます。Nagleが牙をむくのは「1つ目を送った直後、ACK前に2つ目を送ろうとする」ときだけだからです。
| 書き方 | 送信の挙動 | 遅延の有無 |
|---|---|---|
| リクエストを1回のwriteで送る | パイプが空→即送出。Nagleは2個目を待たない | 噛み合わない(速い) |
| ヘッダと本体を2回に分けてwrite | 1個目送出→2個目がNagleで保留→ACK待ち | 遅延タイマぶんストール |
| 毎回フルサイズ(MSS)で送るバルク転送 | MSSセグメントはNagle対象外で連続送出 | ほぼ影響なし |
つまり根本原因はNagle単体でも遅延ACK単体でもなく、「往復型 × 分割write × 両アルゴリズムの待ち合い」 という三つ巴です。
TCP_NODELAY を切る判断
レイテンシ優先のアプリでは TCP_NODELAY ソケットオプションでNagleを無効化するのが定石です。これは「未ACKの小セグメントがあっても小データを即送る」設定で、待ち合いの送信側を解消します。
TCP_NODELAY 有効 → Nagle を切る。小セグメントを即送信。
用途: 対話 / RPC / 小リクエスト即応答型 / WebSocket / ゲーム / DB クライアント。
TCP_NODELAY 無効(既定) → Nagle 有効のまま。
用途: 大ファイル等のバルク転送。小セグメントを束ねて効率を稼ぐ。
ただし TCP_NODELAY は万能薬ではありません。判断基準を整理します。
| 観点 | TCP_NODELAY で切る | Nagle を残す |
|---|---|---|
| 通信パターン | 往復が多くRTTが支配的(対話/RPC) | 一方向に大量データを流す(バルク) |
| 最適化したい指標 | レイテンシ(応答の速さ) | スループット(帯域効率) |
| 副作用 | 小パケットが増え帯域効率は下がりうる | 分割writeで遅延が表面化しうる |
| より良い根治 | writeを束ねれば切らずに済む場合もある | — |
TCP_NODELAY は送信側の待ちを消すだけで、分割write自体は残ります。小セグメントが2個に分かれてネットワークへ出ていくため、パケット数とCPU割り込みは増えます。理想は アプリ側でwriteを1回に束ねる(バッファリングして一括送信、またはwritev/sendmsgでベクタ送信)こと。これならNagleを切らずにヘッダ効率もレイテンシも両立します。TCP_NODELAYは「束ねられない事情があるとき」の調停弁と捉えるのが正確です。ソケットバッファ周りの最適化は ソケットバッファとゼロコピー も参照してください。
設計指針:層ごとに役割を分ける
実務では次の優先順で考えると判断を誤りません。
1. アプリ層: 1論理メッセージは1回のwriteで送る(最優先)
→ 送信前にバッファへ組み立て、完成形を一括 write
2. ソケット層: 往復型で束ねられないなら TCP_NODELAY を有効化
3. 観測: 遅延が約40/200msの倍数で刻まれていないかキャプチャで確認
HTTP/1.1のような往復型プロトコルがこの罠に嵌りやすく、多くのHTTPサーバー/クライアントは既定で TCP_NODELAY を有効にしています。コネクション確立や状態の前提は TCPの状態遷移とコネクション確立・切断の内部 を押さえておくと、ストールがハンドシェイク由来か転送中の待ち合い由来かを切り分けられます。
キャプチャ(tcpdump / Wireshark)で、小セグメント送出の直後にACKが約40〜200ms遅れ、その後に次の小セグメントが続く三段パターンを探します。Wiresharkなら該当ストリームのI/Oグラフが階段状に刻まれます。RTTが小さいローカルでは遅延タイマぶんの差が相対的に大きく見え、症状が誇張されて観測される点に注意してください。
まとめ
Nagleは「未確認の小セグメントがある間は小データを溜める」送信側最適化、遅延ACKは「ACKを約200ms遅らせて相乗りさせる」受信側最適化で、どちらも単独では正しい設計でした。両者が同じ往復型コネクションで、しかもwriteが分割されたときだけ、送信側はACK待ち・受信側はデータ待ちで直列化し、遅延タイマぶんのストールが毎往復に挿入されます。対話・RPC・小リクエスト即応答型は TCP_NODELAY でNagleを切るのが定石ですが、根治はアプリ層で1メッセージを1 writeに束ねること。層ごとに役割を分けて考えれば、「帯域は空いているのに遅い」という症状を原理から切り分けられます。
「Nagleの規則」→ 未ACKの小セグメントがある間はMSS未満を溜め、全ACKかMSS到達で送出。宙に浮く小セグメントは高々1つ。「遅延ACKの規則」→ 最大約200ms遅延、2セグメントごと・順序乱れ・タイマ満了で即時。「両者でなぜ遅くなるか」→ 往復型で分割writeのとき送信側ACK待ち×受信側データ待ちが直列化し遅延タイマぶんストール。「対策」→ TCP_NODELAYでNagle無効化、ただし根治はwriteの一括化。バルク転送ではNagleを残す。
ネットワーク Article
Nagle アルゴリズムと遅延 ACK の相互作用を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
TCP
比較で見る軸
難易度: advanced / カテゴリ: ネットワーク / タグ数: 6
導入後に効く点
リクエスト→レスポンス型でwriteが分割されると、送信側はACK待ちで止まり受信側はデータ待ちでACKを遅らせ、約40〜200msのストールが周期的に表面化する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ネットワーク
- タグ数
- 6
判断チェックリスト
- 自社の用途が「TCP / Nagle」に近いか確認する。
- 強みである「Nagleは未ACKの小セグメントがある間は次の小データを溜める送信側の最適化。遅延ACKは受信側がACKを最大約200ms遅らせる最適化。両者は独立に正しいが、噛み合うと互いを待ち合う。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。