HTTPは、その下層にあたるトランスポートレイヤーのプロトコルとして、通常TCPを使用します。 したがって、TCPのレイヤで速度が改善することは、そのままWebの高速化につながる可能性があるといえます。
GoogleはWebを速くするための活動として、TCPのようなプロトコルレイヤの改善にも取り組んでいます。 今回はその中の一つ、TCP Fast Openを取り上げ、解説と動作検証、簡単なベンチマークを行います。 検証環境等は最下部に記載します.
Make the Web Faster: TCP Fast Open
3 Way Handshake
TCPは、「正確、確実にデータを届ける」ことを重視した設計になっています。 特に接続確立時には、双方の状態をきちんと確認しながら実施するための 3 Way Handshake (以下3WH)という方式を採用しています。
例えば、クライアントがサーバに対してHTTPリクエストを送信したい場合は、以下のような流れになります。
クライアントは、3WHが終わってはじめてHTTPリクエストを送信できるため、HTTPのリクエストを送るまでにはTCPのレイヤで三回の通信が必要になります。 HTTPでは、Keep-AliveなどでTCPコネクションを使いまわすこともできますが、それでも切断と接続の頻度は多いのが現状です。
そこで、3WHの通信を「なるべく少なく」することで、接続確立の高速化を図る方法が、今回紹介する TCP Fast Open (以下TFO)です。
なぜ3WHを使うのか
わざわざ三回も通信を行う3WHを用いる理由はいくつかありますが、その一つとして、クライアントIPの確認があります。
クライアントSYNが含まれるパケットにはクライアントのIPアドレスが載るため、サーバはそのアドレスにレスポンスを返すのですが、このIPは簡単に偽装することができます。
そこで、HTTPのやりとりに移る前に、クライアントのアドレスに対してサーバからもSYNを送ります。 もしこのとき、アドレスが偽装されたものであった場合、その機器が実在したとしてもサーバからのSYNは無視されるため、HTTPのやりとりに移る前にアドレスが正しいことを確認できるのです。
TCP Fast Open
TFOは、簡単に言えばTCPレイヤでCookieを用いることで、すでに接続を確立したことがあるIPアドレスのホストに対しては、3WHを簡略化するという方式です。
Cookieを用いるため、まったく初めて接続する(Cookieを保持していない)ホストに対しては通常の3WHを実行し、その時ホストがCookieを発行します。 二回目以降の接続確立では、クライアントはそのCookieをサーバに送信するという流れになります。
では、通信の詳細を見てみましょう。
次に解説する流れは、TCPコネクションを確立した後にHTTPのGETリクエストを発行し、レスポンスを受け取るところまでの範囲とします。 この時、HTTPのリクエストの受信、レスポンスの送信はサーバの責務ですが、リクエストデータを処理しレスポンスデータを生成するのは、アプリレイヤの責務であるという前提を踏まえて読んでください。
最初の接続
この時点ではサーバとクライアントが有効なCookieをお互いに保存していません。
- クライアントは、SYNに Fast Open Cookie オプションにCookie Requestをつけて送信する。
- サーバは、 TFO-Cookie を生成し、それをFast Open CookieオプションにつけたSYN-ACKを送信する。
- クライアントは、TFO-Cookieをキャッシュする。
- クライアントは、ACKを返し、通常の接続確立を終える。
- クライアントは、HTTP GETリクエストを送信する。
- サーバは、HTTP GETクエストをアプリレイヤに渡し、アプリが生成したレスポンスデータをレスポンスとして送信する。
通常の3WHと違うのは、SYNとSYN-ACKにFast Open Cookieオプションが付く点です。 通信の回数は三回と変わりません。 また、最初のSYNを受け取ったサーバがTFOに対応していない場合、サーバはオプションを無視して通常の3WHが実行(フォールバック)されます。 HTTPの通信は3WHが完全に終わった後になります。
ここでサーバが生成するTFO-Cookieは、クライアントのIPをベースにします。 クライアントはこのTFO-Cookieを、次回以降の接続で仕様するためにキャッシュします。
次回以降の接続
サーバとクライアントが有効なCookieをお互いに保持している状態です。
- クライアントは、SYNパケットにキャッシュしたTFO-CookieとHTTP GETのリクエストデータを含めて送信する。
- サーバは、TFO-Cookieを受け取る。この時初回接続で生成した方法と同じようにSYNからTFO-Cookieを生成し、受け取ったTFO-Cookieと比較する。同じであればTFO-Cookieは正しいものであり、同時にクライアントのIPは偽装されていないことがわかる。
- TFO-Cookieが正しいことを確認したら、サーバはリクエストデータをアプリケーションレイヤに渡す。クライアントにはSYN-ACKを返すが、もしHTTPレスポンスデータが準備できていればSYN-ACKに載せることも可能。
- アプリレイヤがHTTPレスポンスデータを生成し終えたら、サーバはそれをクライアントに送信する。
- クライアントは、ACKを送信する。
通常の3WHと違うのは、SYNに初回接続でキャッシュしたTFO-CookieとHTTP GETのリクエストデータが付くことです。 サーバはTFO-Cookieを元に、クライアントがIPを偽装していないかを知ることができるので、正しいIPであればその後のクライアントからのACKがくる前にGETのデータをアプリのレイヤに渡してしまいます。 アプリから見れば一回の通信で、リクエストデータを取得したのと同等の状態になります。
また、TFOは3WHの省略と解説されることが多いですが、実際にはサーバは従来どおりSYN-ACKを返し、クライアントも最後のACKをきちんと送ります。 ただし、もしアプリが生成したレスポンスデータがサーバに届いたらSYN-ACKに載せて返すこともできますし、SYN-ACKを先に返しておいて別途送ることもできます。 最後のクライアントACKを待たずにレスポンスを開始できるのが、3WHを短縮するからくりになっているのです。
もし、サーバがTFOに対応していなかったりクライアントのTFO-Cookieが無効と判断された場合は、通常の3WHにフォールバックされ、通常のフローでHTTPのGETが行われます。
TFO-Cookie
TFO-CookieはクライアントのIPを保証できなければなりませんが、その生成アルゴリズムは仕様では定義されておらず、実装依存です。 参考までにLinux Kernel 3.11では、以下のようにクライアント/サーバ双方のIPアドレスを暗号化して生成しています。
http://lxr.linux.no/linux+v3.11/net/ipv4/tcp_fastopen.c#L67
IPアドレスを元に生成しているため、クライアントのIPが変わるとCookieが無効になることを意味します。
注意点
Intermedialies(中間サーバ問題)
こうしたネットワークプロトコルの改善には、必ずIntermedialies(middleboxともいう)と呼ばれる中間サーバが問題になります。 具体的にはプロトコルをいじることで、NATやFWなどが通信を不正と見なして遮断したり、内容を書き換えてしまう問題です。
TFOの場合は、NATでアドレスが書き換わる状況などではCookieが不正とみなされ、TFOが成功しない可能性が考えられます。 しかし、問題があれば通常の3WHへのフォールバックによって、通信を続行することができるよう設計されているため、この問題は最小限に抑えられています。
ただし、リクエストデータを付与したSYNが拒否され、3WH成立後に改めてリクエストを送信する場合は、リクエストデータが二重に送信されるためオーバーヘッドになる可能性があります。
導入方法
TFOはクライアントAPIがLinux kernel 3.6に、サーバAPIが3.7にマージされています。 執筆時点ではWindowsおよびMac OSでは使用することができません。
TFOを有効にするには、設定ファイルにフラグを立て、ソケットプログラムにおいて適切なシステムコールを呼ぶ必要があります。
サーバ
サーバでは、生成したソケットをバインドした後に、setsocketopt(2)を用いてTCP_FASTOPENオプションを付与する必要があります。 qlenの値は、TFOによって3WHが終了していないソケットのキューサイズです。
1 2 3 4 5 6 7 8 |
s = socket(AF_INET, SOCK_STREAM, 0); // ソケットの生成 bind(s, ...); // アドレスへのバインド int qlen = 5; setsockopt(s, SOL_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen)); // TCP_FASTOPEN オプションの設定 listen(s, ...); // ソケットのリッスン |
クライアント
クライアントでは、生成したソケットに対するconnect(2)とsend(2)の代わりに、sendto(2)を使用してデータを送信します。 これは通常UDP通信などで使用されるシステムコールですが、TFOでは3WHが終了していない状態でのデータ送信が必要なため、これを用います。
このsendto(2)は、初回接続時はTFO-Cookie Requestを送信し、既にCookieがある場合はそれを使用してSYNにデータを載せて送信を行います。
1 2 3 4 5 |
// ソケットに対する connect(2) + write(2) の代わりに sendto(2) を使用 sendto(s, data, data_len, MSG_FASTOPEN, (struct sockaddr *) &server_addr, addr_len); // 以降の送受信は通常どおり read(2)/write(2) を使用 |
設定
Linux KernelではTFOの設定ファイルとして /proc/sys/net/ipv4/tcp_fastopen が用意されています。 このファイルにはビットでクライアント/サーバごとに有効/無効の設定が可能です。 Linux Kernelでは以下の設定値が定義されています。
0x0: 無効
0x1: クライアントのみ有効
0x2: サーバのみ有効
0x3: クライアント/サーバともに有効
0x4: CookieがなくてもSYNでデータを送信
0x100: Cookie を検証せずにデータの載ったSYNを受け入れる
0x200: Cookie オプションがなくてもSYNにデータを載せる
0x400/0x800: TCP_FASTOPENオプションを設定してないソケットでもTFOを有効にする。
実装
簡単なTFOサーバとクライアントのスクリプトを作成して、パケットの流れを確認してみます。 現時点で言語レベルでTFOをサポートしているものは少ないため *1、Pythonで直接システムコールを呼ぶスクリプトを使用します。
クライアントは “hello\n” を二回送信し、サーバはそれをエコーバックします。 二台のマシン(MacOS)上のVartual Box上のUbuntuで実行します。
1 2 3 4 |
# server $ sudo python server.py # listen port 80 # client $ python client.py 192.168.1.10 # server address |
実行結果をWiresharkで確認します。
最初のClient SYNにFast Open Cookie Requestが付き、SYN ACKでFast Open Cookiが付与されていることがわかります。 データは別のパケットで送られています。
二回目のClient SYNには、このCookieとデータが一緒に載っています。 しかもサーバはSYN ACKを返した後に、サーバのACKを待たずにデータを返しています。 正常に動いていることがわかりますね。
ApacheとChrome
ApacheやNginxなどはまだTFOに対応していませんが、kernel側でリスニングソケットにTFOを強制するオプションがあるので、それを使用して検証します。 また、最新のChromeはTFOに対応しているため(OSもTFO対応が必要) 、これをクライアントとして利用します。
まず、サーバ側でTFO強制の設定をしApacheをインストールします。
1 2 |
$ echo 0x403 | sudo tee /proc/sys/net/ipv4/tcp_fastopen $ sudo apt-get install apache2 |
クライアント側は最新のchromeを用意し chrome://flags で “TCP Fast Openを有効にする” を設定します。
この状態でChromeからapacheのデフォルトページに二回アクセスします。 Chromeは、一回目のGETでCookieをリクエストし、二回目のGETでSYNにCookieとリクエストヘッダを載せて投げています。
同様の方法を使えば、設定ファイルを書くだけで既存のサーバをTFOに対応させることができるでしょう。 しかし、このオプションはあくまでもテスト目的に使用するもので、本番運用ではkernel APIレベルで対応したものを使用するべきです。
ベンチマーク
TFOはRTTが大きい環境でこそ効果が期待されます。そこでAWSを用いて、Tokyoリージョンをクライアント、N.Virginiaリージョンをサーバとし、ベンチマークをとってみます。使用するAMIは以下です。(インスタンスにはElastic IPをふっています)
- TOKYO: ubuntu-raring-13.04-amd64-server-20130820(ami-0b8c1f0a)
- N.Virginia: ubuntu-raring-12.04-amd64-server-20130820(ami-77fcbc1e)
サーバにはApacheを、クライアントはTFOに対応しているHttpingを用いて十回のリクエストを投げる簡単なベンチを行います。
1 2 3 4 5 6 7 8 |
$ echo 3 | sudo tee /proc/sys/net/ipv4/tcp_fastopen $ wget http://www.vanheusden.com/httping/httping-2.3.3.tgz $ tar zxvf httping-2.3.3.tgz $ cd httping-2.3.3.tgz $ ./configure --with-tfo # TFO を有効にしてビルド $ make $ ./httping -g http://192.0.2.0 -c 10 # TFO off $ ./httping -g http://192.0.2.0 -c 10 -F # TFO on |
結果
# TFO off
10 connects, 10 ok, 0.00% failed, time 13830ms
round-trip min/avg/max = 344.7/382.6/424.2 ms
TFO on
10 connects, 10 ok, 0.00% failed, time 12052ms round-trip min/avg/max = 161.8/204.9/364.2 ms (TFO onのパターンは、最初の一回はCookie Requestです)
この検証では、RTTは平均で46%短くなっている事がわかります。
まとめ
TFOは、まだドラフトの段階で実装も限られていますが、仕様が固まればサーバやOSも対応が進むと考えられます。 各言語の標準ソケットモジュールレベルでの対応も少しずつ進んでいるため、今後は普通にネットワークプログラムを書けば自動的にTFO対応されるでしょう。
注意点として、通常の3WHにフォールバックした場合にリクエストデータが二重になる可能性がありますが、 一方で通信を「なるべく少なく」することによる効果は非常に大きいため、そことのトレードオフとなるでしょう。
普及にはまだ時間がかかるかもしれませんが、今のうちから自身のもつサービス検証をしてみてはいかがでしょうか。
検証環境
- Ubuntu 13.10
- kernel 3.11.0-13-generic
- Python 2.7.3
- Wireshark 1.10.2
- HTTPing 2.3.3
- Google Chrome 32(beta)