- 分散システムのレイテンシ問題をデバッグする際、最初に確認すべき項目は TCP_NODELAY の設定である
- Nagleアルゴリズムは1984年のRFC896で提案された方式で、小さなパケット送信時のTCPヘッダーのオーバーヘッドを減らすために設計された
- しかし、遅延ACK (delayed ACK) メカニズムと組み合わさると、データ送信がACK受信まで遅延し、レイテンシに敏感なアプリケーションの性能を悪化させる
- 現代のデータセンター環境ではRTTが非常に短く、ほとんどのシステムがすでに大きなメッセージを送信しているため、Nagleアルゴリズムの利点はほとんど失われている
- したがって、現代の分散システムでは TCP_NODELAY をデフォルトで有効化すべきであり、Nagleアルゴリズムはもはや不要である
Nagleアルゴリズムの背景
- 1984年のJohn Nagleによる RFC896は、キーボード入力のような小さなデータ送信で発生する40バイトのヘッダーに対して1バイトのデータという4000%のオーバーヘッド問題を解決するために提案された
- 当時の問題は、ユーザーが1文字ずつ入力するたびに小さなパケットが送信され、ネットワーク効率が低下することだった
- 解決策は、前のデータがACKされていない状態では新しいセグメントを送信しないよう制限する方式だった
- このアプローチは当時のネットワーク環境では有効だったが、レイテンシが重要な現代システムには不向きである
NagleアルゴリズムとDelayed ACKの相互作用
- Delayed ACK (RFC813, RFC1122) は、受信側が即座にACKを送らず、応答データが生じるかタイマーが満了するまでACKを遅らせる方式である
- NagleアルゴリズムはACKを待って送信を止め、delayed ACK はACKを遅らせるため、双方が互いを待つ膠着状態が発生する
- John Nagle自身もこの組み合わせを「ひどい組み合わせ」と表現し、2つの機能は独立して導入されたが、併用すると遅延を引き起こすと指摘している
現代環境での問題点
- データセンター内のRTTは約500μsで、同一リージョン内でも数ミリ秒程度と非常に短い
- このような環境で1 RTTぶん送信を遅らせることは、性能低下につながる
- また現代の分散システムは、TLS、シリアライズ、プロトコルオーバーヘッドなどにより、すでに十分に大きなメッセージを送信しているため、1バイトパケット問題はほとんど存在しない
- 小さなメッセージの最適化は、今ではアプリケーション層で処理されている
TCP_NODELAYの必要性
- レイテンシに敏感な分散システムでは、TCP_NODELAY を有効にして Nagleアルゴリズムを無効化することが推奨される
- これは「非効率」でも「誤った設定」でもなく、現代のハードウェアとトラフィック特性に合った選択である
- 著者はTCP_NODELAYがデフォルト値になるべきだと主張する
- 一部の「
write() 呼び出しごとに送信する」コードは遅くなる可能性があるが、そのようなコードは根本的に修正されるべきである
その他の関連オプション
- TCP_QUICKACK オプションはACK遅延を減らすが、移植性の問題と一貫しない動作のため、根本的な解決策ではない
- 核心的な問題は、カーネルがアプリケーションの意図した時点より長くデータを保持することであり、
write() 呼び出し時に即座に送信されるべきである
結論
- Nagleアルゴリズムは、過去にネットワーク効率を高めるための優れた発明だったが、
現代の高速ネットワークと分散システム環境では、むしろ遅延を招く時代遅れの機能になっている
- したがって、TCP_NODELAY を常に有効化することが現代システム設計の基本原則として提示されている
1件のコメント
Hacker Newsの意見
当時は複数のホストが1つのイーサネットチャネルを共有していたため、衝突を避けるために CSMA/CD を使っていた
しかし今日のイーサネットの大半はポイントツーポイント構成で、送受信を同時に行えるフルデュプレックス環境である
したがって CSMA はもはや不要であり、TCP_NODELAY を設定して Nagle アルゴリズムを無効化するのは、ほとんどの場合で合理的だと思う
それがデフォルト設定になったのは、ネットワーク史上の大きな失敗の1つだと思う
2014年ごろにデータセンタースイッチを交換した際、10Mbit ハーフデュプレックスをサポートしていなかったため、一部の旧式機器を残さざるを得なかった経験がある
小さすぎるパケットの生成を防いでくれる
Nagle は TCP 層の最適化であり、小さなパケットをまとめて効率を上げる役割を持つ
CSMA は物理/データリンク層の問題で、Nagle とは別物だ
Go で書かれたバックエンドはデフォルトでTCP_NODELAYが設定されていたので原因ではなかったが、Nagle の問題認識に関する部分は興味深かった
以前の議論もあり、このスレッド を参照できる
DICOM プロトコルのようなチャット型通信では、TCP_NODELAY=1 に設定するとスループットが大きく向上する
関連リンク 参照
今どきのワークロードでは delayed ACK に大きな利点はないと思う
HTTP 中心の現代環境では、Nagle も delayed ACK も両方無効にするほうがよいと考える
データセンター間のRTT が数百マイクロ秒レベルなので、1 RTT でも遅らせるのはむしろ損になりうる
Wiki リンク
いつ送るか、いつバッファリングするかはアプリケーションが決めるべきだ
Linux では追加データがまもなく送られることをカーネルに知らせるヒントで、ヘッダーとデータを分けて送るときに便利だ
io_uring と組み合わせるとさらに効率がよい
即時応答が必要なメッセージの後でバッファを空にして送る機能があるとよい
最近の TCP チャネルでは同期・非同期メッセージが混在していて、さらに複雑だ
SCTP のようなプロトコルがもっと広く使われてほしい
TLS のようなラッピングでもメッセージ境界を見つけるのが面倒になる
理想的には「バッファリング可」ビットを立てて大きな送信を分割し、最後に「即時送信」を指定できるべきだ
TCP_CORK はそれに少し近い代替手段だが、やや雑な感じがする
ファイル I/O でも似た問題がある
かなり面白い内容だ
アプリケーションが遅延とスループットのバランスを自分で調整できるべきだ
しかしアプリケーションレベルで実装するにはunacked dataを知る必要があり、非効率になる
単純な20ms flush タイマーがあるだけでも、ずっとましだっただろう