2 ポイント 投稿者 GN⁺ 2024-05-10 | 1件のコメント | WhatsAppで共有
  • 分散システムのレイテンシ問題は、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件のコメント

 
GN⁺ 2024-05-10
Hacker Newsのコメント
  • キャリアの中でNagleアルゴリズムが原因の遅延問題を何度も直してきたので、今ではまず最初に疑うようになった
    ロジック自体は妥当だが、一部のワークロードには合わない。ソケットを作るときにエンジニアが明示的に選ぶべきで、OSのデフォルトに任せるべきではないと思う
    問題は良いオプションか悪いオプションかではなく、データ転送の仕方をかなり積極的に変える設定が存在するのに、多くの人がその存在を知らないことにある

    • 自分も似たようなもので、新しいRPCフレームワークを見るたびにGitHub Issueで「TCP_NODELAYを検討しましたか、それともこのフレームワークは毎秒20呼び出ししかできないのですか?」と投稿するのが趣味になっている
      これまで毎回バグを見つけてきた
      例: 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風のバッファリングを自作したりするほどには賢くないソフトウェアエンジニアが必要、ということになる
    • 同意。高頻度/低遅延取引の世界では、Nagleアルゴリズムを切ることはかなり昔から、おそらく15年以上前からよく知られていて、自分も最初に確認することの一つだ
    • 本当に欲しいのは遅延をnマイクロ秒にすることだが、システムコールの前に自前でユーザー空間バッファリングを入れる以外に良い方法はない
      io_uring のようにシステムコールのコストを相殺してくれるものがなければ、ユーザー空間側の方がうまく動く
    • このロジックはもともとTelnetセッションのようなものを想定したものだ
      記憶では、それが主な動機だった
  • 結論が少し奇妙だ。Nagleアルゴリズムは明らかに書き込みをまとめようとする試みであり、ハードウェア・ネットワーク・アプリケーション・ユースケースに関係なく、場合によってはまとめ書きの方が優れている
    今日でも多くのコンピューティングはまとめ書きを使っており、ネットワークアプリケーションも恩恵を受ける
    QUICのようなより新しい上位レベルのプロトコルは書き込みをまとめ、TCPの独立した接続処理やエラー処理を事実上ユーザー空間へ移し、プロトコルができるだけ早くアプリケーションへデータを押し込み、個々のストリームの接続処理やエラー処理はホストのTCP/IPスタックやルーターではなくアプリケーションに任せるようにしている
    昔のようにネットワークが再び飽和すれば、NagleアルゴリズムはQUIC向けに修正された形で戻ってくるだろうし、おそらくアプリケーションコードのより深いところで、特定の基準に達するまでQUICパケットの送信を待つような形になるだろう
    技術におけるあらゆるものは、ハードウェアかソフトウェアがボトルネックに達すると再発明される。両者の性能は同じ速度では伸びないので、結局いつもそうなる
    帯域幅だけでなく、小さなパケットのせいで秒間パケット数が飽和する場合にも、Nagleアルゴリズムは有用だ

    • QUICとTCPの違いは、TCPとその前身の原罪、つまりメッセージ層が見えない非同期シリアルポート接続をまねた点にある
      そのおかげで物理的なテレタイプライターからサービスに接続することはできたが、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_acknet.inet.tcp.delacktime がある

    • TCP_QUICKACK は最悪の形は直すが、問題全体を解決するわけではない
      Nagle アルゴリズムは依然として、データを送る前に最大で 1 往復時間分待つ可能性があり、RFC に従うなら、ほとんど利点なく遅延を追加するだけになる
    • その通り。TCP_QUICKACK を受信のたびに設定しなければならないとは、何を考えていたのだろう
      なぜ一部の時間だけオフにしておきたいのか
    • CentOS/RedHat では、ルートの末尾に quickack 1 を追加して、その経路の 遅延 ACK をオフにできる
  • 帯域幅が限られていて、パケットの最小サイズが 64 バイトで、さらにフレーム間ギャップまで必要だった世界では、1 バイトごとに TCP パケットを送るのは途方もない帯域幅の無駄だった
    ほとんどの Ethernet ネットワークでは今でも最小サイズはそうで、空の ACK を送る場合も同じである
    ただし私の基本的な立場はこうだ。TCP_NODELAY ではなく、単に TCP なのだ

    • 相手側のパイプが何らかの理由で切れたことに気づく内蔵メカニズムを持つプロトコルがあるとよい
    • QUIC(https://en.wikipedia.org/wiki/QUIC) は、遅延のような TCP の問題を解決するものではないのか
  • Nagle がもう不要だという論理には、あまり納得できない
    Telnet は今日では重要ではないが、それでも次のようなアプリケーションはまだ多そうだ
    write(fd, "Host: "), write(fd, hostname), write(fd, "\r\n"), write(fd, "Content-type: ") など
    これが 40 倍のオーバーヘッドではないとしても、5 倍程度にはなり得る

    • アプリケーションを直せばよい
      ファイルに書き込むときにこのようなことをして、魔法のような性能を期待したりはしない。OS に独自のバッファがあってもそうである
      ソケットに書き込むときだけ別の期待をする理由はなく、そもそも Nagle はシステムコールのオーバーヘッドから救ってくれるわけでもない
    • Telnet の話を見て OpenSSH が何をしているのか気になったが、対話型セッションも含め、すべての接続に TCP_NODELAY を設定している
      コードを読んだことと strace の動作観察の両方で確認した
    • 非同期 I/O をしていると考えると、小さな write(2) ごとにブロックする代わりにバッファに入れるのが唯一筋の通る方法なので、このようなパターンは今ではそれほど一般的ではないと思う
      サーバーではうまくスケールさせるには通常、非同期 I/O が必要であり、クライアントでもネットワーク呼び出しでブロックされるのは体験が悪い
      特に今日のようにネットワーク変更が多く、圏外になることが多い環境ではなおさらだ
    • 一部の開発者がひどいコードを書くからといって、インターネット全体が罰を受けるべきではない
    • そもそもこのようにすべきではない
      ネットワーク面を除いても、システムコールはかなり高コストなので性能に悪い
  • アプリケーションのソースにアクセスできないときに、ソケットで TCP_NODELAY を有効にするよい方法を知っているか気になる
    永続的に適用するカーネル設定や、後から変更するコマンドは見つけられなかった
    ルーティングテーブルに quickack 1 を入れて遅延 ACK はオフにできたが、アプリケーションの外側から TCP_NODELAY を有効にするのは特に難しそうだ
    最近、自分が所有するアプリケーションと、それが相互作用するクローズドソースのアプリケーションとの間で、ここに説明されている問題をまさに経験している

    • socket(2) に対する LD_PRELOAD による横取り のようなものが動くのではないか
      実際の関数を呼び出した後に setsockopt のようなことをして、修正済みのソケットを返す形である
    • 具体的な状況によっては、間に socat を挟めるかもしれない
      元が your_app —> server なら、your_app -> localhost_socat -> server になるようにする
      socat には tcp_nodelay を設定するコマンドラインオプションがある
      ただし、クローズドソースのアプリに localhost へ接続するよう納得させる必要がある
      DNS ルックアップをするなら、/etc/hosts の項目で localhost に接続させられる可能性がある
      アプリはローカルソケットで socat と通信するので、アプリ側の tcp_nodelay は影響しない
    • デバッガをアタッチして、ptracesetsockopt を呼べばよいのではないか
    • /proc//fd/ を開いてソケットオプションを設定する方法が動くかもしれない。試してはいない
    • LD_PRELOAD
  • 15 年ほど前、非常にリアルタイム性の強い MMO をやっていたが、すべての通信が TCP だった
    ボタンをクリックしても、応答パケットが戻ってくるまで自分の行動が画面に表示されることすらなかった
    最終的に、このゲームをやっていた子どもたち、私を含め、皆が TCP_NODELAY を有効にするとゲームが非常に滑らかになることを見つけ出した
    特にゲームサーバーに近い California 側のプレイヤーには効果が大きかった

    • WoW のことを言っているのかは分からないが、その頃にゲームのアップデートがまさにこの変更を行い、おそらく他にも何かを変えたはずだ
      興味深い副作用は、変更前は TCP ストリームが止まるとゲームがしばらく固まり、その後、取りこぼした受信イベントを非常に速く再生したことだ
      たいていそのイベントは自分が死ぬ場面だった
      変更後は、代わりに単に接続が切れるようになった
  • 関連する Oxide and Friends ポッドキャストのエピソード: https://www.youtube.com/watch?v=mqvVmYhclAg

    • 素晴らしいエピソードで、可視化の重要性を本当に強く示していた
  • Go のようにデフォルトで TCP_NODELAY を有効にするモダンな言語を使っていれば該当しない :-)

  • 毎回そうとは限らない。時には DNS である

    • ある時は、ルーターの故障したラインカードが IPv4 アドレスの最後のビットを 0 にしてしまい、「偶数の IPv4 アドレスだけアクセス可能」というチケットになった
    • 私の場合、ある時はガラスが汚れていた
      工事現場の近くにあるルーターで、レーザーと光ファイバーの間の隙間にほこりが入り込み、信号を十分に減衰させていて、40〜50% のパケットロスが見られた
      損失箇所を突き止めて NOC がその伝送事業者にメールを送り、翌日派遣された技術者がその顛末を返信で教えてくれた
    • 50 年に一度、20 億 km 離れた場所では、故障したメモリチップかもしれない
      それでも普通は迂回パッチを当てられるので、大事ではない
    • BGP や、通知なしにディスクがいっぱいになることも忘れてはいけない
    • 失敗したら DNS で、単に動きが止まるなら TCP_NODELAYストリームバッファリングである
      本当に複雑なシステムである Web は、キャッシュによっても失敗する