HTML5Experts.jp

TCP Fast Open – Webを速くするためにGoogleがやっていること Make the Web Faster 4 –

連載: Make the Web Faster (4)

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をお互いに保存していません。

  1. クライアントは、SYNに Fast Open Cookie オプションにCookie Requestをつけて送信する。
  2. サーバは、 TFO-Cookie を生成し、それをFast Open CookieオプションにつけたSYN-ACKを送信する。
  3. クライアントは、TFO-Cookieをキャッシュする。
  4. クライアントは、ACKを返し、通常の接続確立を終える。
  5. クライアントは、HTTP GETリクエストを送信する。
  6. サーバは、HTTP GETクエストをアプリレイヤに渡し、アプリが生成したレスポンスデータをレスポンスとして送信する。

通常の3WHと違うのは、SYNとSYN-ACKにFast Open Cookieオプションが付く点です。 通信の回数は三回と変わりません。 また、最初のSYNを受け取ったサーバがTFOに対応していない場合、サーバはオプションを無視して通常の3WHが実行(フォールバック)されます。 HTTPの通信は3WHが完全に終わった後になります。

ここでサーバが生成するTFO-Cookieは、クライアントのIPをベースにします。 クライアントはこのTFO-Cookieを、次回以降の接続で仕様するためにキャッシュします。

次回以降の接続

サーバとクライアントが有効なCookieをお互いに保持している状態です。

  1. クライアントは、SYNパケットにキャッシュしたTFO-CookieとHTTP GETのリクエストデータを含めて送信する。
  2. サーバは、TFO-Cookieを受け取る。この時初回接続で生成した方法と同じようにSYNからTFO-Cookieを生成し、受け取ったTFO-Cookieと比較する。同じであればTFO-Cookieは正しいものであり、同時にクライアントのIPは偽装されていないことがわかる。
  3. TFO-Cookieが正しいことを確認したら、サーバはリクエストデータをアプリケーションレイヤに渡す。クライアントにはSYN-ACKを返すが、もしHTTPレスポンスデータが準備できていればSYN-ACKに載せることも可能。
  4. アプリレイヤがHTTPレスポンスデータを生成し終えたら、サーバはそれをクライアントに送信する。
  5. クライアントは、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が終了していないソケットのキューサイズです。

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にデータを載せて送信を行います。

// ソケットに対する 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で直接システムコールを呼ぶスクリプトを使用します。

python sample code (gist)

クライアントは “hello\n” を二回送信し、サーバはそれをエコーバックします。 二台のマシン(MacOS)上のVartual Box上のUbuntuで実行します。

# 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をインストールします。

$ 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をふっています)

サーバにはApacheを、クライアントはTFOに対応しているHttpingを用いて十回のリクエストを投げる簡単なベンチを行います。

$ 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にフォールバックした場合にリクエストデータが二重になる可能性がありますが、 一方で通信を「なるべく少なく」することによる効果は非常に大きいため、そことのトレードオフとなるでしょう。

普及にはまだ時間がかかるかもしれませんが、今のうちから自身のもつサービス検証をしてみてはいかがでしょうか。

検証環境

  1. いくつかの言語は導入に関する議論やパッチを見つけたので載せておきます。 Ruby, Go, Python