🍣

O’Reilly の Building Blocks of TCP から学ぶTCP通信

2023/03/17に公開

概要

こんにちは、ポートのSREを担当している @yukionodera です。

弊社では定期的にインフラ勉強会を開催しており、
今回はその中で読む機会があった High Performance Browser Networking | O'Reilly の中で、
Building Blocks of TCPの章を自分なりに噛み砕いてまとめていきます。

全体的な構成は下記の通りです。

  • TCP の仕組み
    • Three-Way Handshake
    • TCP Fast Open (TFO)
  • TCP の抱える問題
    • Congestion Control and Avoidance
      • Flow Control
      • Slow Start
    • Bandwidth-Delay Product
    • TCP head-of-line (HOL) blocking
  • TCP のパフォーマンス最適化
    • サーバー調整
    • アプリケーション調整

それでは早速。

はじめに

インターネットの中心には、下記2つのプロトコルがある。

IP: Internet Protocol: ホスト間でのルーティングとデータ転送を提供
TCP: Transmission Controll Protocol: 信頼性のないチャネル上で信頼性のあるネットワーク通信を提供

これらのプロトコルは、元々はTCP/IPとして一つのRFCで定義されていたが、1981 年を境に2つのRFCに分割された。

中でもTCPは、アプリケーション内で行われるネットワークコミュニケーションの複雑性を隠しながら、様々なことを実現している。

  • retransmission of lost data, in-order delivery, congestion control and avoidance, data integrity, and more.
  • ロストデータの再送信、順番通りの配信、輻輳制御と回避、データの完全性の担保、など

TCPは、データ配信の時間よりも正しくデータが配信されることに重きを置いているのが特徴である。

RFCとは
IETF が発行している、TCP/IPプロトコルに関する技術仕様に関する文書群

TCPの基本 - Three-Way Handshake

Three-Way Handshakeとは、TCP接続の基本となる仕組み。
random sequence number を利用することで、セキュアな接続の確保が実現できている。

通信の流れ

  • SYN(client)
    • 下記の情報を SYN packet として送信
      • random sequence number x (client)
      • TCP flags and options (client)
  • SYN ACK(server)
    • 下記の情報を SYN ACK packet として送信
      • x に1を足した番号
      • random sequence number y (server)
      • TCP flags and options (server)
  • ACK(client)
    • x, y に1足した番号を含む ACK packet を送信
    • handshake を完了
      <img title='スクリーンショット 2023-01-04 17.02.36.png' alt='スクリーンショット 2023-01-04 17.02.36' src='/attachments/82dc6175-5331-4743-9004-22a78e1a6a9e' width="662" data-meta='{"width":662,"height":579}'>

handshake 完了後、クライアントは ACK packet を送信した直後から、サーバーはACKが届いたあとからデータの送信が可能となる。
この手続きは、全てのTCP接続において必要になるもので、TCPを利用するアプリケーションのパフォーマンスに影響する。

これがTCP利用のアプリケーションで、TCPの再利用が求められる一つの大きな要因である。

TCP Fast Open とは

TCPに組み込まれている仕組みの一つ。
SYN packet にデータを入れて通信可能にすることで、パフォーマンスをあげるメカニズムである。

例えば、Webページのロードには、複数のホストから多くのリソースの読み込みが必要となる。
その全てで新たなTCP接続が必要となるため、モバイルネットワークなどではパフォーマンスに影響しやすい。

これを解決するメカニズムが TCP Fast Open (TFO) である。

もちろん下記のようないくつかの制限がある。

  • データサイズ
  • HTTP リクエストの種類
  • 再利用するTCP接続のみで利用可能
  • etc

輻輳回避と制御 とは

現代の複雑なネットワークには、輻輳崩壊 と呼ばれる既知の問題がある。
TCPとIPの階層間で相互作用し発生する異常な輻輳による問題のことで、異なる帯域幅のネットワークを接続したときに発生しやすい。

具体的には、往復送信時間が最大再送間隔を越えることで、ホストが複数のデータ再送を実行、それによりネットワークが混雑し、パケットがDrop、結果として、
この状態が発生すると、ネットワーク全体が使えなくなってしまう。

これを輻輳崩壊といい、この問題を回避するために、様々な仕組みがTCPに実装されている。

  • Flow Control (rwnd)
  • Slow-Start (cwnd)
  • congestion control
  • congestion avoidance

これらの仕組みについても解説していく。

Flow Control

Flow Control とは、データ送信側が、データ受信側のデータ処理上限を超えないようにする仕組み。
データ処理のために利用可能な buffer space のサイズを表す rwnd(receive window) を双方で公開し合うことで対応している。
このrwnd は、全てのACKに含まれている。

rwnd に関連するTCPオプション - Window Scaling(RFC1323)

TCPのオプションの一つであり、rwnd の限界値を大きくするためのもの。
当初のTCP仕様では、このrwnd のために16bit が割り当てられていて、その場合のrwnd の限界値は 65,535 bytes となっていた。
しかし、このrwnd だと不十分であることが判明しており、これを解消するためにRFC1323 でrwnd を最大1gb まで設定できるように修正された。
これがWindow Scaling オプションであり、現在このオプションはデフォルトで有効化されている.(従来の古い機器ではこのオプションが使えない場合もある)
ネットワーク帯域幅を十分に使えないなどの問題が発生した場合は、window size を確認するところから始めると良い。
Linuxの場合、下記のコマンドで確認,有効化できる。

$> sysctl net.ipv4.tcp_window_scaling
$> sysctl -w net.ipv4.tcp_window_scaling=1

Flow Control の抱える問題

Flow Control が導入されたにも関わらず、輻輳崩壊の問題が1980年代後期に現実的になってきた。

なぜかというと、確かにFlow Controlにより、送信側が受信側のデータ処理上限を超えることは無くなったが、
ネットワークの帯域幅を超えないようにする仕組みは存在しなかった。
つまり、データ送信側、受信側のどちらも、通信を開始する最初の段階で、
ネットワーク内で利用可能な帯域幅を知るための仕組みがなかった。
そのため、利用可能な帯域幅以上のデータが送信された場合、輻輳崩壊の可能性があった。

Slow-Start とは

Flow Control と同様、輻輳崩壊を防ぐために設けられたTCPの仕組みの一つ。
あらかじめネットワーク帯域幅を推測し、絶え間なく変化するネットワーク状況に応じて、
スピード(データ量)を調節することができる仕組みである。

Slow -Start とともに、congestion avoidance, fast retransmit, and fast recovery といったアルゴリズムも同時期に文書化されており、
これら4つはすぐにTCP仕様の必須項目となった。

Slow -Startを理解するために、実際の動きを見てみる

ニューヨークにいるClientが、ロンドンのファイルサーバーからファイルを取得しようとしている。
最初に、three-way handshake が実行され、ACK packet 内で rwnd size を公開する。
最後のACK packet が届いたら、アプリケーションデータの交換が始まる。

この時、ネットワーク上で利用可能なキャパシティを推測するためには、実際にデータをやり取りして測定するしかない。
Slow -Startはこれを利用してデータ量を調節する。

まず、サーバーはTCP接続毎に congestion window (cwnd) variable をリセットして、
初期値として、システム指定の保守的な値を設定する。Linuxの場合は initcwnd に設定された値が初期値となる。
このcwnd をさまざまなアルゴリズムを利用して徐々に大きくしていき、送信するデータ量を増やしてくのがSlow-Startである。

Congestion Window Size:
segments 単位で設定される。(現在は初期値 10 segments)
この値に基づいて、送信側(server)が送ることのできるデータ量が決まる
このcwnd はsender - receiver 間で共有されず、今回の場合はロンドンのサーバーにプライベート変数として保管される

また、最終的にclient server 間で送信できるデータ量の上限は、rwnd, cwnd どちらかの最小値となる。

cwnd はどのように調整されていくのか

cwnd を適切な値に調整するために、パケットが認識されるに従ってゆっくりとcwnd のサイズを大きくしていくのが、Slow -Startの仕組み。
では具体的な仕組みはどうなっているのか。

まず、cwnd の初期値は2023現在 10 から開始することができる。(cwnd が出たばかりの当時は初期値が1だった)
そこから、ACKが受け取るたびにcwnd の値を倍にしていき、ネットワークの帯域幅に合わせて調節されていく。

この仕組みがあるため、一般的には接続したばかりの通信よりも再利用されている通信の方がデータ量が多く、接続してすぐにネットワークの帯域幅をフルに使うことはできない。

Slow-Startの問題

Slow-Start は、大きいデータのストリーミングダウンロードなどでは最大のwindow size に到達するため問題にならない。
一方で、通信時間が短くて数が多いような通信の場合は、window size が最大に達する前にデータ転送が完了するため、サービスのパフォーマンスに影響が出やすい。
その場合は、Client -Server 間のround trip time を改善するしかない。

Slow-Start の機能

Slow -Startには、Slow -Start -Resetという機能がある。
TCP接続が一定時間 idle の状態だった場合、cwnd をReset するというものである。
idle 中にネットワークの状態が変化した時に、輻輳混雑が発生しないようにするための仕組みである。

しかし、この機能は有効化しているとサービスのパフォーマンスに影響を与える可能性がある。
例えば、 サイトのUserが一時的に inactive の状態に入り、またactive 状態に戻ってきた場合、その間にSSRが稼働してパフォーマンスが落ちてしまう。
こういったデメリットが懸念されることから、大抵の場合はこの機能を無効化することが推奨されている。

Linux であれば下記のコマンドで値の確認、設定ができる。

$> sysctl net.ipv4.tcp_slow_start_after_idle

$> sysctl -w net.ipv4.tcp_slow_start_after_idle=0

また、Three way handshake と Slow -Startを図に表すと、下記のようになる。
<img title='スクリーンショット 2023-01-19 16.42.03.png' alt='スクリーンショット 2023-01-19 16.42.03' src='/attachments/26336f04-9aa7-49a9-9fa4-e58896998fe4' width="636" data-meta='{"width":636,"height":619}'>

ここまで理解できると、keepalive, pipelining, and multiplexing とかも簡単になるよ!とのこと

Congestion Control and Avoidance Algorithms とは

packet-loss が発生した場合の仕組みを実現しているのが、Congestion Control and Avoidance Algorithmsである。
後述するAIMD, PRPなどもこれに含まれる。

TCP は、packet-loss を必ず発生するものとして捉えており、それを元にパフォーマンスを調整するフィードバックの仕組みがある。
Slow-Start は、rwnd を超えるか、packet-loss が発生するまで cwnd を倍にして増やしていく仕組みになっている。
そして、packet-loss が発生した場合は、Congestion Control and Avoidance Algorithms が働き、cwnd をうまく調整してくれている。

また、TCPにもさまざまな種類があるが、全てのTCPがこのCongestion Control and Avoidance に関する問題に取り組んでいる。

TCP Tahoe and Reno (original implementations), TCP Vegas, TCP New Reno, TCP BIC, TCP CUBIC (default on Linux), or Compound TCP (default on Windows), among many others.

Proportional Rate Reduction for TCP

packet-loss から復旧するための最適な方法を定義することは、簡単なことではない。
ギリギリを攻めるような設定にすれば膨大な数のpacket-loss が発生してパフォーマンスに影響が出るし、
早急に復旧することができなければpacket-loss が更なる packet -lossを産む可能性がある。

もともとTCPでは、パケットロス発生時に輻輳ウィンドウを半分にし、ラウンドトリップごとに一定量ずつゆっくりとウィンドウを増やしていくAIMD(Multiplicative Decrease and Additive Increase)アルゴリズムが採用されていたが、AIMDは保守的すぎる場合が多いため、新しいアルゴリズムが開発された。

それがPRR(Proportional Rate Reduction)(RFC 6937で規定された新しいアルゴリズム) だ。
その目的はパケット損失時の回復速度を向上させることで、これを開発したGoogleでの測定によると、パケットロスのある接続の平均待ち時間を3-10%短縮することができたという。
PRR は現在、Linux 3.2+ カーネルでデフォルトのCongestion Avoidance アルゴリズムでもある。

Bandwidth-Delay Product

BDP とは、日本語で帯域幅遅延積というもので、ACKの応答を受け取る前にネットワーク上で送信できるデータ量のことを表す。
データリンクの容量(ビット/秒)と往復遅延時間(秒)の積が帯域幅遅延積となる。
たとえば、往復時間が50ミリ秒、ADSL 2Mビット/秒 接続の場合、BDPは100000ビット(または12500バイト)となり、これがACK応答を受け取る前にネットワーク上で一度に送信できるデータ量となる。

ADSL とは、電話回線を利用したデータ通信技術

ここまで調整した

BDP = 理論上、ネットワークで一度に送信できるデータ量の上限

TCPにおける最適なウィンドウサイズは、両者間の往復時間とデータリンク容量に基づいて変化する。BDPと違って、ネットワークの利用状況により異なる。
送信者と受信者間の送信可能なデータの最大量は、受信(rwnd)と輻輳(cwnd)ウィンドウサイズの最小値であり、これらはACKごとに通信され、輻輳制御と回避アルゴリズムに基づいて送信者によって動的に調整される。また、それらは最適なウィンドウサイズに届くかどうかが分からない。

BDP に関連する問題点

送信可能なデータの最大量、つまり、受信(rwnd)と輻輳(cwnd)ウィンドウサイズの最小値 が、 最適なウィンドウサイズ に達していないと、遅延が発生する可能性がある。
送信データ量が送信可能なデータの最大量より少ない場合、もちろん遅延は発生しない。
実際の遅延時間は、両者の往復時間により異なる。

クライアントとサーバーの両方がより高いレートで通信できることがわかっているにもかかわらず、利用可能な帯域幅のほんの一部しか使用されていない場合、それはウィンドウサイズが小さいことが原因である可能性がある。

  • 低い rwnd サイズの設定
  • 悪いネットワーク状況と高いパケット損失による cwnd のリセット
  • 突発的なスパイクによるスループット制限

Ex.

  • 送信したいデータ量: 40
  • BDP: 20
    • データ送信にかかる往復時間: 20
    • データリンクの容量: 1
  • 最適なウィンドウサイズ: 20
  • 設定されたウィンドウサイズ: 10
    • 1 x 10 segments 送信 -> ウィンドウサイズが10のため、ACK が帰ってくるのを10 の時間(片道分)待つ -> 1 x 10 segments 送信 ->...= 合計4往復必要で、80の時間が必要
  • 設定されたウィンドウサイズ: 20
    • 1 x 20 segments 送信 -> 20個目のsegments を送ったときに、1つ目のACKが帰ってくる -> ACKが帰ってくる毎に新しいsegments を送信可能 -> = 合計40+10(ACK) = 50 の時間が必要(10 は最後に帰ってくるACK分の時間)

Head-of-Line Blocking

TCP head-of-line (HOL) blocking として知られる、予測できない遅延(一般的にジッターと呼ばれる)を引き起こす問題。

TCPでは、データが順番に受信機に渡されなければならないため、途中でパケットの一つが失われた場合、失われたパケットが再送信され受信機に到着するまで、後続のパケットは受信機のTCPバッファに保持されなければならないことがある。
これがTCP head-of-line (HOL) blocking として知られてる問題である。
この遅延は、一般的にはジッターとして知られており、アプリケーションのパフォーマンスに大きな影響を及ぼす可能性がある。

一方でこれはTCPのメリットでもあり、パケットが必ず順番に配送され、それによりアプリケーションはバケットの並べ替えや再組み立てを行う必要がなく、アプリケーションコードをよりシンプルにすることが可能な点が挙げられる。

HOL が発生しないプロトコル

アプリケーションによっては、信頼性のある配送も順番通りの配送も、必要ない場合がある。
例えば、すべてのパケットが独立したメッセージである場合、順番通りの配送は厳密に不要で、
すべてのメッセージが以前のすべてのメッセージを上書きする場合、信頼性のある配送の要件は完全に削除することが可能。

もちろん、TCPはそのような設定を提供おらず、すべてのパケットはシーケンス化され、順番に配送される。

UDPとの違いはここにある。
順序外の配送やパケットロスに対処でき、遅延やジッターに敏感なアプリケーションは、UDPのような代替トランスポートを使用したほうがよいでしょう。

UDPの活用例

UDPの活用例としては、オーディオ、ビデオ、ゲームの状態の更新など、信頼性の高い配信や順番通りの配信を必要としないアプリケーションデータが考えられる。

例えばオーディオに関して言えば、パケットが失われた場合、オーディオコーデックはオーディオに小さな切れ目を入れて、受信パケットの処理を続行する。
失われたパケットを待つと、音声出力にさまざまな一時停止が発生し、ユーザーにとってより悪い体験となる危険性があるためだ。
また同様に、ゲームの状態のアップデートを配信する場合、時間Tのパケットをすでに持っているのに、時間T-1の状態を記述したパケットを待つことは、単に不要になる。
理想的にはすべてのアップデートを受信することだが、ゲームプレイの遅延を避けるために、低遅延を優先して、断続的に損失を受け入れるようになっている。

TCPにおけるパケットロスの捉え方

パケットロスは、TCPの性能を最大限に引き出すために必要なことである。
実際、TCPにおいて、パケットロスはフィードバック機構として機能している。
受信者と送信者は、パケットロスなどの情報をもとに送信レートを調整してネットワークに負担をかけないようにすることで、遅延最小化を実現している。


TCP の最適化について

TCPは、すべてのネットワークに対して公平であり、基盤となるネットワークを最も効率的に使用するように設計された、適応性のあるプロトコル。
したがって、TCP を最適化する最善の方法は、TCP が現在のネットワークの状態を感知し、その種類と下位および上位の層の要件に基づいて動作を適応させる方法を、調整することである。
例えば、ワイヤレスネットワークは異なる輻輳アルゴリズムが必要かもしれないし、アプリケーションによっては最高の体験を提供するためにカスタム QoS セマンティックが必要かもしれない。

とはいえ、TCPの基本的な原理とその意味するところは変わらない。

TCP最適化において、ボトルネックになりやすいポイントは下記の通りである。

  • TCP スリーウェイハンドシェイクは、フルラウンドトリップのレイテンシーを発生させる
  • TCPスロースタートは、すべての新規接続に適用される
  • TCPフローと輻輳制御は、すべての接続のスループットを調整する
  • TCPのスループットは、現在の輻輳ウィンドウのサイズによって制御される

また、これらを考慮した上で、現代の高速ネットワーク上でのTCP接続によるデータ転送の速度は、受信者と送信者の間の往復時間によって制限されることが多い。
そして、帯域幅が増加し続ける一方で、遅延は光速によって制限され、すでに最大値から小さな定数ファクターの範囲内にある。
つまり、ほとんどの場合、帯域幅ではなくレイテンシがTCPのボトルネックとなっている。

カスタム QoS セマンティック とは、内部でQoSに関する共通の認識を持つための仕組み、ツール、手法。
QoS とは、Quality of Serviceの略で、Webサービスなどでユーザーの期待値を満たすために設定される非機能要件の記述である。
また、セマンティックとは、ITにおいては、データの持つ意味をコンピュータに理解させることである。
非機能要件とは、パフォーマンスやセキュリティ、信頼性、可用性、ユーザー体験などに関するもの。

サーバーの調整

ここまで調整した。こことサーバーの調整までで一通り終わりそう。

まず始めに、TCP のバッファやタイムアウトの値をチューニングする前に、ホストを最新 のシステムバージョンにアップグレードする。
TCP のベストプラクティスとその性能を決定するアルゴリズムは進化し続けており、これらの変更のほとんどは、最新のカーネルでのみ利用可能なためだ。
つまり、送信側と受信側の TCP スタック間の相互作用が最適になるように、サーバーを常に最新に保つことが重要です。

最新カーネルにアップグレードしたら、下記の項目を確認する。

  • TCPの初期輻輳ウィンドウ(init cwnd)
    • 開始時の輻輳ウィンドウを大きくすることで、TCPは最初のラウンドトリップでより多くのデータを転送し、ウィンドウの成長を著しく加速させることが可能。
  • スロースタート Reset
    • アイドル後のスロースタートを無効にすることで、cwnd を Reset させないようにして、周期的なバーストでデータを転送するTCP接続のパフォーマンスを向上。
  • ウィンドウスケーリング (RFC 1323)
    • ウィンドウスケーリングを有効にすると、最大受信ウィンドウ(rwnd)サイズが大きくなり、高遅延接続のスループットが向上。
  • TCP高速オープン(TCP Fast Open)
    • 特定の状況下で、最初のSYNパケットでアプリケーションデータを送信することが可能。
    • TFOは比較的新しい最適化で、クライアントとサーバーの両方でサポートが必要となるため、カーネルアップグレード必須。
    • アプリケーションでもこれを利用できるかどうか調査が必要。

上記の設定と最新のカーネルを組み合わせることで、個々のTCP接続において最高のパフォーマンス(低遅延と高スループット)を実現することが可能である。

アプリケーションによっては、高い接続速度、メモリ消費量などを最適化するために、サーバー上の他のTCP設定を調整する必要がある場合も。
その他のアドバイスについては、お使いのプラットフォームのドキュメントを参照し、HTTP Working Groupによって維持されている「TCP Tuning for HTTP」を参考にすべし。

Note:
Linuxユーザにとって、ssはオープンソケットの様々な統計情報を調査するのに便利なパワーツール.
コマンドラインから ss --options --extended --memory --processes --info を実行すると、現在のピアとそれぞれの接続設定が表示される.

アプリケーションの調整

TCP のパフォーマンスを調整することで、サーバーとクライアントは、個々の接続に対して最良のスループットとレイテンシを提供することができる。
それらに加えて、アプリケーションが新しいTCP接続や確立済のTCP接続をどのように使用するか調整することで、更なるパフォーマンス向上が望めるだろう。

具体的には、以下の3つを意識するのが良い。

  • No bit is faster than one that is not sent; send fewer bits. より少ないビットを送信.
    • 不要なデータ転送をなくすことが、唯一最善の最適化。
    • 例えば、不要なリソースをなくしたり、適切な圧縮アルゴリズムを適用して最小限のビットを転送するようにする。
  • We can’t make the bits travel faster, but we can move the bits closer. ビットをより近くに移動.
    • CDN を利用するなどして世界中のサーバーを地理的に分散させ、ビットをクライアントの近くに配置する.
      • それにより、ネットワークラウンドトリップのレイテンシーが減少し、TCP パフォーマンスが大幅に改善される。
  • TCP connection reuse is critical to improve performance. TCPコネクションの再利用.
    • スロースタートやその他の輻輳メカニズムによるオーバーヘッドを最小化するため、可能な限り既存のTCP接続を再利用する。

TCP最適化チェックリスト

TCPパフォーマンスを最適化することは、アプリケーションの種類に関係なく、サーバーへの新しい接続ごとに高い配当が得られる。
TCP最適化のためのチェックリストは下記の通り。

  • サーバーカーネルを最新バージョンにアップグレードする
  • cwndのサイズが10に設定されていることを確認する
  • ウィンドウスケーリングが有効であることを確認する
  • アイドル後のスロースタートを無効にする
  • TCP Fast Openの有効化を検討する
  • 冗長なデータ転送をなくす
  • 転送データを圧縮する
  • サーバーをユーザーの近くに配置し、ラウンドトリップタイムを短縮する
  • 可能な限りTCPコネクションを再利用する
  • TCP Tuning for HTTP」の推奨事項を確認する

Discussion