トランスポート層における Web 最適化

グーグルがWebを高速化するために何をしているか - Publickey を読んで,ちょっと自分なりの解釈をメモしておきます.尚,私はトランスポート層 (TCP) 以外は素人同然なので,トランスポート層の部分だけを見ていきます.

1 往復減らすと平均通信時間を 10% 短縮できる

TCP の各チューニングを見る前に,クライアント-サーバ間での通信を 1 往復(1 RTT)減らす事へのインパクトを考えます.これについて,もっともはっきりと明示しているのは以下の SSL のスライドです.

グーグルがWebを高速化するために何をしているか - Publickey

このスライドを見ると,ハンドシェイクにかかる回数を 1往復分削減する事によって 10% 速くなったと明記されています.ここから分かるのは,クライアント-サーバ間での(トランスポート層での)やり取りは平均で 10往復 (10RTT) 程度であると言う事です.算出方法は後述しますが,伝統的な TCP の場合,データ転送開始直後からクライアント-サーバ間を 8往復すると 255 パケット送信する事ができます(1+2+4+8+16+32+64+128).1パケットに含める事のできるデータサイズは 1.4KB 程度なので 8往復で送信できるデータ量は 357KB となり,冒頭で紹介されていた「Web ページの平均的なサイズは 320KB」の言説とも合致します(データ通信 8往復 + SYN, FIN の 2〜3往復で大体 10往復か?).

ここから分かることは,「1度の通信でのやり取りを 1往復減らすだけで,かなりの性能改善となる」と言うことです*1

トランスポート層で改善できる事

TCP で行っている Web 最適化は以下の 3つだそうです(“Makes Google products 12% faster” は,へぇーすごいですねーと言う事で).

グーグルがWebを高速化するために何をしているか - Publickey
1. 輻輳ウィンドウサイズの初期値を大きくする

TCP の特徴の一つとして「データ転送開始直後は非常に速度が遅い」と言うものがあります.これは,スロースタートフェーズと呼ばれています.

データ転送開始直後は,TCP はどの程度の転送速度でデータを送信して良いのか分かりません.そのため,最初(1往復目)は 1RTT に 1パケットしか送信しません.これが成功すると,次(2往復目)は 1RTT に 2パケット送信し,その後,4パケット,8パケット,16パケット,32パケット,・・・とパケット廃棄が発生するまで倍々に増加させていきます.

この方法は,送信するデータが非常に大きい場合は,このフェーズにかかる時間は全体から見ると微々たるものなのであまり問題となりません.しかし,Web ページのようにスロースタートフェーズだけでデータ転送が完了してしまうようなデータサイズの場合,このフェーズでかかる時間が無視できなくなります.

これに対する最も単純な解決方法は,「輻輳ウィンドウサイズの初期値(通常 1)を大きくしてしまう」と言うものです.例えば,初期値を 2にした場合(スロースタートでデータ送信が完了すると仮定すると)完了までにかかるクライアント-サーバのやり取りを 1 往復減らす事ができます.Web ページのデータ転送において,1 往復減らす事に対するインパクトは先に述べた通りです.

もちろん,やみくもに大きくすれば良いと言うものではありません.ネットワークのキャパシティ以上の初期値を設定してしまうと輻輳崩壊を発生させ,通信を行う事ができなるなどネットワークに深刻な被害を及ぼします.Google のように接続するクライアント数が膨大な場合,輻輳ウィンドウサイズの初期値をたった 1 上げるだけでもその影響はかなりのものになる事が予想されます.

ただ,昨今の回線速度の向上などによって輻輳ウィンドウサイズの初期値はもう少し大きくしようと言う方向にはあるようです.あまり詳細を調べたことはないのですが,linux kernel 2.6.x 辺りは初期値が 4 だったような気がします(デフォルトなのかオプションなのかは未調査).

2. Transaction TCP (T/TCP)

TCP は通信開始時に 3-way handshake と言うやり取りを行います.伝統的な TCP では,

  1. サーバ(データ送信側)が SYN パケットと呼ばれる特別なパケットを送信する.
  2. クライアントが SYN パケットに対する ACK パケットを返送すると同時に自らも SYN パケットを送信する (SYN+ACK).
  3. サーバがクライアントの SYN パケットに対する ACK パケットを返送する.

と言う手順を踏みます.つまり,実際のデータ通信のやり取りに加えて 3往復分余計なやり取りが増えると言う事です.

Transaction TCP (T/TCP) は,このやり取りを短縮しようと考え出されたものです.手法自体は,詳細 TCP/IP Vol.1 プロトコル にも紹介されているように(24.7節),かなり初期の頃から提案されていました.

http://dictionary.rbbtoday.com/Details/term548.html

T/TCP は,「データのやり取りが 1パケットで終了するような状況」を想定しており FIN パケットも SYN パケットに含めてしまうと言うものですが,FIN パケットを含める事ができないようなデータサイズでも 1往復程度はやり取りを少なくできるのはと思います.

3. タイムアウト時間(の初期値)を動的に変更する

TCP は,「パケットが廃棄された」と認識する方法として,「重複 ACK の受信」と「タイムアウト」の 2通りを使用しています.「重複 ACK の受信」の話は今回は置いておくとして,今回はタイムアウトのお話.

伝統的な TCP の場合,タイムアウト時間の初期値は 2秒程度の固定値に設定されてあります.しかし国内でのデータ通信の場合,1RTT が大体 10ms 〜 30ms オーダ,大陸間でのデータ通信でも 100ms 〜 300ms 程度のオーダなので,これらに比べて 2秒と言う値は,ほとんど場合で大きすぎると言う問題があります.

これの改善方法として考えられるのが,「RTT の値によってタイムアウト時間(の初期値)を動的に変えてしまう」と言うものがあります.グーグルがWebを高速化するために何をしているか - Publickey の参考 URL として紹介されていた インターネット・ドラフト もこの方法についてのものでした.

(2.2) When the first RTT measurement R is made, the host MUST set

SRTT <- R
RTTVAR <- R/2
RTO <- SRTT + max (G, K*RTTVAR)

where K = 4.

(2.3) When a subsequent RTT measurement R' is made, a host MUST set

RTTVAR <- (1 - beta) * RTTVAR + beta * |SRTT - R'|
SRTT <- (1 - alpha) * SRTT + alpha * R'

・・・(中略)・・・

After the computation, a host MUST update

RTO <- SRTT + max (G, K*RTTVAR)
http://tools.ietf.org/id/draft-paxson-tcpm-rfc2988bis-00.txt

SRTT が RTT の平均,RTTVAR が RTT の分散を表します(ともに,加重移動平均で算出).また,RTO が実際に使用するタイムアウト時間となります.RTT の平均・分散を基にタイムアウト時間を動的に変更している事が分かります.

ちょっとぐぐって見たところ,Windows などでもこの類の方法が用いられているようで(参考:TCP/IP の最大再送信タイムアウトを変更する方法),伝統的なタイムアウト時間を採用している OS がどの程度あるのかは不明ですが,場合によってはこの辺りも検討事項になるようです.

TCP は,Web のようなデータサイズの小さいものをやり取りする事はあまり主眼には置かれていません.また,昨今のネットワーク事情を考えると,TCP の各パラメータ(輻輳ウィンドウサイズの初期値,輻輳回避フェーズでの増加量,・・・)は安全すぎるて性能を十分に発揮できないと言う問題もいろいろと指摘されています.こういった事を考えながら,注意深くチューニングする必要があるようです.

ユーザ側での Web 最適化

ちなみに,受信側(ユーザ側)でも速度を改善できる可能性はあります.Windows の場合,デフォルトでは Delayed ACK が有効になっていますが,Delayed ACK は送信側の輻輳ウィンドウサイズの増加を鈍化させると言う問題があります(輻輳ウィンドウサイズの増加方法が「ACK パケットを受信する毎」であるため).

これを無効にする事の有効性は,RAGNAROK Online を始めとして通信のリアルタイム性が問題となるネットワークゲームで主に語られていますが(参考:TcpAckFrequencyについて - ふぇるくれ~ると@うぃき - アットウィキ),Web ページの受信時にもある程度の通信時間の短縮が見込めるのではないかと思います.

*1:10% が「かなり」なのかどうかは良く分かりませんが.