TCP_NODELAY オプションの継続的な使用
(brooker.co.za)- 分散システムのレイテンシ問題は、
TCP_NODELAYを有効にするだけで解決するケースが繰り返し見られ、TCP のデフォルト動作が現代のワークロードと合わない可能性がある - Nagle アルゴリズムは、1984年の RFC896 で小さな TCP パケットのヘッダーコストを減らすために作られたもので、ACK を受け取る前に新しいセグメントの送信を防ぐ方式である
- delayed ACK と併用すると、一方は ACK を待ち、もう一方は応答データまたはタイマーを待つため、レイテンシに敏感なパイプライン型アプリケーションには不利になる
- データセンター内の RTT が約 500μs でも、現代のサーバーはその時間で多くの処理をこなせるため、1 RTT 分だけ送信を遅らせる選択のメリットは不明確である
- 現代の分散システムでは、TLS、エンコーディング、シリアライズ、アプリケーションメッセージサイズにより、単一バイトパケットの問題は減っており、レイテンシに敏感な環境では Nagle アルゴリズムの無効化のほうが自然である
レイテンシのデバッグで最初に確認する設定
- 分散システムでレイテンシの問題が起きると、まず
TCP_NODELAYが有効になっているかを確認することになる - 多くの分散システム開発者が、この単純なソケットオプション1つで解決する問題に時間を失ってきた
- こうした繰り返しは、TCP のデフォルト動作が現在の分散システムに合っていない、または Nagle アルゴリズム自体が古くなっている可能性を示している
Nagle アルゴリズムが解決しようとした問題
- RFC896 は、1984年に小さなパケット問題を扱った文書である
- 当時は、キーボード入力のように1文字ずつ入ってくるデータを TCP で送る際、1バイトのデータごとに40バイトのヘッダーが付く非効率が発生していた
- 有用なデータ1バイトあたり40バイトのヘッダーが付き、4000% のオーバーヘッドが発生する
- 軽い負荷では耐えられても、ネットワークスループットには不利である
- Nagle アルゴリズムの目的は、TCP ヘッダーコストをよりよく償却してスループットを高めることにあった
- 小さなパケットは、シェルのような人間との対話型アプリケーションや、複数回の
write呼び出しでデータを少しずつカーネルへ渡す実装で主に発生していた
- 小さなパケットは、シェルのような人間との対話型アプリケーションや、複数回の
- 中核となる動作は、以前に送ったデータがまだ ACK を受け取っていないとき、新しい送信データを別個の TCP セグメントとしてすぐには送らないことだ
- Nagle アルゴリズムはタイマーと一緒に説明されることが多いが、RFC896 自体はネットワークの**往復時間(RTT)**以外に別のタイマーを使っていない
delayed ACK と組み合わさったときに生じる遅延
- delayed ACK は、パケット受信確認をすぐには送らず、返送するデータが発生するかタイマーが満了するまで待つ方式である
- RFC813 は、1982年に ACK の遅延を提案した初期の文書で、特定の状況で受信者が ACK 送信を遅らせ、後で送るためのタイマーを設定できると扱っている
- RFC1122 は delayed ACK をさらに正式化している
- 2つの機能はそれぞれ合理的だが、一緒に使うと遅延を生み出す可能性がある
- Nagle アルゴリズムは、さらに多くのデータを送る前に ACK の受信を待つ
- delayed ACK は、応答データが準備されるかタイマーが満了するまで ACK の送信を遅らせる
- パケットをいっぱいに詰めるには役立つが、レイテンシに敏感なパイプライン型アプリケーションには望ましくない
- John Nagle の Hacker News コメントも、この問題を tinygram の防止ではなく、ACK 遅延と固定タイマーの組み合わせとして見ている
- 2つの合理的なプロトコル機能が組み合わさって望まない動作を生む事例であり、このような相互作用がプロトコル設計を難しくしている
現代の分散システムと合わない点
- delayed ACK がなくても、Nagle アルゴリズムの動作は現代の分散システムが望む方式と異なる場合がある
- 現在の環境では、RTT 自体が無視しにくいコストである
- データセンター内の単一 RTT は通常、約 500μs
- 同じリージョンのデータセンター間 RTT は数 ms
- 世界規模の経路では数百 ms に達することもある
- 現代のサーバーは数百 μs の間にも多くの処理を実行できるため、データを1 RTT 分遅らせて送る選択が明確なメリットだとは言いにくい
- Nagle アルゴリズムのもともとの正当化は、単一バイトパケットで発生する40倍のヘッダーオーバーヘッドを減らすことにあった
- 現代の分散データベースや分散システムは、概して単一バイトパケットを送らない
- アプリケーションが送るデータ自体がより大きい
- TLS のようなプロトコルオーバーヘッドが追加される
- エンコーディングやシリアライズのオーバーヘッドも付く
- 小さなメッセージを避けるべきという問題は今も重要だが、その責任は実質的にアプリケーション層へ移っている
- JSON で包んだデータを1バイトずつ送る方式は、Nagle アルゴリズムとは関係なく効率的ではない
TCP_NODELAY をデフォルトの選択と見る理由
- レイテンシに敏感な分散システムを現代のデータセンター級ハードウェアで作るなら、
TCP_NODELAYを有効にして Nagle アルゴリズムを無効化してもよい - 現代システムのトラフィック、アプリケーション構成、ハードウェア性能を考えると、Nagle アルゴリズムはもはや必要ない可能性がある
TCP_NODELAYがデフォルト値であるべきだという立場も取りうる- バイトごとに
writeを呼び出すコードは、TCP_NODELAYがデフォルトだと遅くなる可能性がある - 効率が重要なら、そのようなコードは Nagle アルゴリズムに頼るのではなく、アプリケーション実装を修正すべきである
TCP_QUICKACK は補助的な選択肢に近い
TCP_QUICKACKは代替として挙げられることがあるが、移植性の低さと特殊な意味合いのため、最初の選択肢にはしにくい- Linux tcp man page の意味を直接確認する必要がある
- より大きな問題は、
TCP_QUICKACKが、カーネルがプログラムの意図よりも長くデータを保持するという根本問題を解決しない点である - プログラムが
write()を呼び出したなら、実際にwrite()が実行されることを期待する
1件のコメント
Hacker Newsのコメント
キャリアの中でNagleアルゴリズムが原因の遅延問題を何度も直してきたので、今ではまず最初に疑うようになった
ロジック自体は妥当だが、一部のワークロードには合わない。ソケットを作るときにエンジニアが明示的に選ぶべきで、OSのデフォルトに任せるべきではないと思う
問題は良いオプションか悪いオプションかではなく、データ転送の仕方をかなり積極的に変える設定が存在するのに、多くの人がその存在を知らないことにある
これまで毎回バグを見つけてきた
例: https://cloud-haskell.atlassian.net/browse/DP-108 または https://github.com/agentm/curryer/issues/3
ただし「良い/悪いオプションではない」には同意しない
これは不適切に書かれたアプリケーションを「魔法のように直す」ためのカーネル側のヒューリスティックであり、記事が言うように、正常なアプリケーションは1バイトのネットワーク向け
write()システムコールなどしないそういうソフトウェアは直すべきだ
この機能が意味を持つのは、カーネルのシステム管理者で、チーム内政治のような理由でマシン上で動いているソフトウェアを直せない、まれな状況くらいだと思う
それ以外では、まともなソフトウェアをより複雑にする
つまり、不適切に書かれたソフトウェアのスループットを少し上げるために入った奇妙な魔法を明示的にオフにしなければならず、正しく書かれたソフトウェアには大きく予想外の遅延を生むということだ
John Nagleはここからリンクされているスレッドで、遅延ACKの方がより悪いと言っているが、これには同意する
しかしNagleアルゴリズムが悪化させるSend/Send/Receiveパターンは完全に妥当で一般的なユースケースであり、TCP上でパイプライン化RPCを行うあらゆるものに当てはまる
遅延ACKとNagleアルゴリズムはどちらもデフォルトでオフであるべきだと思う
名前もTCP_DELAYくらいであるべきで、基本的なユーザー空間バッファリングを実装したくないときだけオンにすればよい
人々がこういうことを知っている必要があってはならず、デフォルトの挙動は驚きの少ないものであるべきだ
writeの振る舞いをするアプリケーションを直すことなら、TCP_DELAYをオンにするオプションはかなり奇妙なものになるこのオプションを知るくらいには賢いが、
write呼び出しをうまく分けたり、自分のアプリケーションに合ったより良いNagle風のバッファリングを自作したりするほどには賢くないソフトウェアエンジニアが必要、ということになるio_uringのようにシステムコールのコストを相殺してくれるものがなければ、ユーザー空間側の方がうまく動く記憶では、それが主な動機だった
結論が少し奇妙だ。Nagleアルゴリズムは明らかに書き込みをまとめようとする試みであり、ハードウェア・ネットワーク・アプリケーション・ユースケースに関係なく、場合によってはまとめ書きの方が優れている
今日でも多くのコンピューティングはまとめ書きを使っており、ネットワークアプリケーションも恩恵を受ける
QUICのようなより新しい上位レベルのプロトコルは書き込みをまとめ、TCPの独立した接続処理やエラー処理を事実上ユーザー空間へ移し、プロトコルができるだけ早くアプリケーションへデータを押し込み、個々のストリームの接続処理やエラー処理はホストのTCP/IPスタックやルーターではなくアプリケーションに任せるようにしている
昔のようにネットワークが再び飽和すれば、NagleアルゴリズムはQUIC向けに修正された形で戻ってくるだろうし、おそらくアプリケーションコードのより深いところで、特定の基準に達するまでQUICパケットの送信を待つような形になるだろう
技術におけるあらゆるものは、ハードウェアかソフトウェアがボトルネックに達すると再発明される。両者の性能は同じ速度では伸びないので、結局いつもそうなる
帯域幅だけでなく、小さなパケットのせいで秒間パケット数が飽和する場合にも、Nagleアルゴリズムは有用だ
そのおかげで物理的なテレタイプライターからサービスに接続することはできたが、TCPはメッセージ境界を知らないものになり、今ではその知識を一部押し込めるとしても、初期のソフトウェアではそうではなかった
一方でQUICやSCTP、TP4のような多くの非TCPプロトコルは、メッセージ境界を明示的に提供する
システムとのインターフェースはエミュレートされたシリアルポートではなく、多くても再組み立てされるメッセージベースだ
プロトコルには正しくまとめるための文脈がない
逆に 遅延 ACK をオフにするのはどうだろうか
問題は、小さなパケットの抑制と遅延 ACK が相互作用するときに起きる病的な挙動である
小さなパケットの抑制をオフにする公開オプションは
TCP_NODELAYだが、遅延 ACK はどうやってオフにできるのか4 通りの組み合わせをすべてベンチマークして、何が最も合うのか見たい場合の話である
少し調べたところ、Linux には
TCP_QUICKACKソケットオプションがあるが、受信のたびに設定しなければならない/proc/sys/net/ipv4/tcp_delack_minと/proc/sys/net/ipv4/tcp_ato_minもあるFreeBSD には
net.inet.tcp.delayed_ackとnet.inet.tcp.delacktimeがあるTCP_QUICKACKは最悪の形は直すが、問題全体を解決するわけではないNagle アルゴリズムは依然として、データを送る前に最大で 1 往復時間分待つ可能性があり、RFC に従うなら、ほとんど利点なく遅延を追加するだけになる
TCP_QUICKACKを受信のたびに設定しなければならないとは、何を考えていたのだろうなぜ一部の時間だけオフにしておきたいのか
quickack 1を追加して、その経路の 遅延 ACK をオフにできる帯域幅が限られていて、パケットの最小サイズが 64 バイトで、さらにフレーム間ギャップまで必要だった世界では、1 バイトごとに TCP パケットを送るのは途方もない帯域幅の無駄だった
ほとんどの Ethernet ネットワークでは今でも最小サイズはそうで、空の ACK を送る場合も同じである
ただし私の基本的な立場はこうだ。
TCP_NODELAYではなく、単に TCP なのだNagle がもう不要だという論理には、あまり納得できない
Telnet は今日では重要ではないが、それでも次のようなアプリケーションはまだ多そうだ
write(fd, "Host: "),write(fd, hostname),write(fd, "\r\n"),write(fd, "Content-type: ")などこれが 40 倍のオーバーヘッドではないとしても、5 倍程度にはなり得る
ファイルに書き込むときにこのようなことをして、魔法のような性能を期待したりはしない。OS に独自のバッファがあってもそうである
ソケットに書き込むときだけ別の期待をする理由はなく、そもそも Nagle はシステムコールのオーバーヘッドから救ってくれるわけでもない
コードを読んだことと
straceの動作観察の両方で確認したwrite(2)ごとにブロックする代わりにバッファに入れるのが唯一筋の通る方法なので、このようなパターンは今ではそれほど一般的ではないと思うサーバーではうまくスケールさせるには通常、非同期 I/O が必要であり、クライアントでもネットワーク呼び出しでブロックされるのは体験が悪い
特に今日のようにネットワーク変更が多く、圏外になることが多い環境ではなおさらだ
ネットワーク面を除いても、システムコールはかなり高コストなので性能に悪い
アプリケーションのソースにアクセスできないときに、ソケットで TCP_NODELAY を有効にするよい方法を知っているか気になる
永続的に適用するカーネル設定や、後から変更するコマンドは見つけられなかった
ルーティングテーブルに
quickack 1を入れて遅延 ACK はオフにできたが、アプリケーションの外側から TCP_NODELAY を有効にするのは特に難しそうだ最近、自分が所有するアプリケーションと、それが相互作用するクローズドソースのアプリケーションとの間で、ここに説明されている問題をまさに経験している
socket(2)に対する LD_PRELOAD による横取り のようなものが動くのではないか実際の関数を呼び出した後に
setsockoptのようなことをして、修正済みのソケットを返す形である元が
your_app —> serverなら、your_app -> localhost_socat -> serverになるようにするsocat には
tcp_nodelayを設定するコマンドラインオプションがあるただし、クローズドソースのアプリに localhost へ接続するよう納得させる必要がある
DNS ルックアップをするなら、
/etc/hostsの項目で localhost に接続させられる可能性があるアプリはローカルソケットで socat と通信するので、アプリ側の
tcp_nodelayは影響しないptraceでsetsockoptを呼べばよいのではないか/proc//fd/を開いてソケットオプションを設定する方法が動くかもしれない。試してはいないLD_PRELOAD15 年ほど前、非常にリアルタイム性の強い MMO をやっていたが、すべての通信が TCP だった
ボタンをクリックしても、応答パケットが戻ってくるまで自分の行動が画面に表示されることすらなかった
最終的に、このゲームをやっていた子どもたち、私を含め、皆が TCP_NODELAY を有効にするとゲームが非常に滑らかになることを見つけ出した
特にゲームサーバーに近い California 側のプレイヤーには効果が大きかった
興味深い副作用は、変更前は TCP ストリームが止まるとゲームがしばらく固まり、その後、取りこぼした受信イベントを非常に速く再生したことだ
たいていそのイベントは自分が死ぬ場面だった
変更後は、代わりに単に接続が切れるようになった
関連する Oxide and Friends ポッドキャストのエピソード: https://www.youtube.com/watch?v=mqvVmYhclAg
Go のようにデフォルトで TCP_NODELAY を有効にするモダンな言語を使っていれば該当しない :-)
https://github.com/golang/go/issues/57530
知らなかった
単に「モダンな」ネットワーキングライブラリを使えばよいのでは
毎回そうとは限らない。時には DNS である
工事現場の近くにあるルーターで、レーザーと光ファイバーの間の隙間にほこりが入り込み、信号を十分に減衰させていて、40〜50% のパケットロスが見られた
損失箇所を突き止めて NOC がその伝送事業者にメールを送り、翌日派遣された技術者がその顛末を返信で教えてくれた
それでも普通は迂回パッチを当てられるので、大事ではない
TCP_NODELAYや ストリームバッファリングである本当に複雑なシステムである Web は、キャッシュによっても失敗する