TCP高速化プロキシ「AccelTCP」を公開しました
昨年末からずっとこんなことをしてまして、この時期になってようやく今年初のブログ記事です。 進捗的なアレがアレでごめんなさい。そろそろ3年目に突入の @pandax381です。
RTT > 100ms との戦い
経緯はこのへんとか見ていただけるとわかりますが「日本と海外の間を結ぶ長距離ネットワーク(いわゆるLong Fat pipe Network)において、通信時間を削減するにはどうしたらいいか?」ということを、昨年末くらいからずっとアレコレやっていました。
送信したパケットが相手に到達するまでの時間(伝送遅延)を削減するのは、光ファイバーの効率の研究とかしないと物理的に無理なので、ここで言う通信時間とは「TCP通信」における一連の通信を完了するまでの時間です。
伝送遅延については、日本国内のホスト同士であれば、RTT(往復遅延時間)はだいたい10〜30ms程度ですが、日本・北米間だと100ms以上、日本・欧州間だと200ms以上になります。
TCPは、実際にデータを送る前に3Wayハンドシェイクによってコネクションの確立する必要がありますが、伝送遅延の大きな回線では、このハンドシェイクを行うだけでそれなりの時間が掛かってしまいます。
また、データ送信時に発生するACK(確認応答)待ちによるアイドルタイムの増加もTCPのパフォーマンス低下の大きな要因です。TCPは、信頼性のある通信を担保するために、受信側からのACK応答を待ちながらデータを送信します。つまり、ACK応答が届くまでに時間が掛かると、それだけ何もせずに待っている時間が増えてしまい、その結果として通信全体に掛かる時間が大幅に増加してしまうのです。そして、通信全体に掛かる時間が増加するということは、スループットが低下するということです。
TCPのスループットの理論値は以下の計算式で求められます。
TCPスループット(bps)= ウィンドウサイズ(bit)/ RTT(sec)
例えば、ウィンドウサイズを64KBと仮定した場合、RTTが10msだとTCPスループットの理論値は51.2Mbpsとなりますが、RTTが100msの場合には5.12Mbpsになってしまいます。つまり、RTTが10倍になればスループットは1/10になるのです。
これは、例え1Gbpsや10Gbpsといった帯域の広い回線を使っていても同じです。TCPのスループットは伝送遅延に大きく影響されます。
このように、伝送遅延の大きなネットワークではTCPは性能を十分に発揮できないため、伝送遅延が大きいことを前提として、通信時間を短縮するための方法を模索するところが今回の出発点です。
TCPがダメならUDPを使えばいいじゃない!
TCPのハンドシェイクによるオーバーヘッドは、その後にやりとりするデータ量が少ないほど、通信全体に掛かる時間に対して占める割合が大きくなります。
例えば、HTTP通信でリクエストもレスポンスもデータ量が少ない場合、通信時間の50%はハンドシェイクによるオーバーヘッドが占めることになります。(※ KeepAliveを使わない場合)
ゲーム内で発生する通信の傾向を見てみると、ほとんどが数KB〜数十KBの小さなデータのやりとりだったため、ハンドシェイクによるオーバーヘッドの影響を大きく受けていそう、というかむしろハンドシェイクを省略するだけでかなり効果がありそうな感じです。
「Google先生のQUICみたいな感じで、UDPベースで信頼性のあるオレオレプロトコル自作すれば結構早くなりそうだよなー」とか妄想が膨らみますね。
そんなわけで、勢いでこんなもの作ってみました。
「IDTP(Iikanji ni Datagram de Transport Protocol)」(大人の事情で公開はなしですm(_)m)
名前の通り、データをUDPでイイ感じに運んでくれるプロトコルです。プロキシサーバでTCPのペイロードをUDPに乗せかえて運ぶことを想定して作りました。QUICに手を出さなかったのは、上に乗せられるプロトコルがSPDYに限定される(らしい)からということと、自分で作ってみるのも楽しいよなーと思ったからです。
とりあえず、数KBのファイルをRTTに近い時間でHTTP GET出来ることを目標にプロトタイプを作成したのですが、最初の実装では以下の機能を盛り込みました。
- IPアドレス+ポート番号+セッションIDによるセッションの識別
- SACKのような選択式確認応答
- タイムアウトによる強制再送(SRTTベースのRTO計算めんどいので固定値)
とりあえず動くようになったので、北米のサーバと日本国内のサーバでベンチマークとった結果が⇩です。
お、10KB未満のデータに関してはバッチリRTTと同じくらい(=理論値でのほぼ最速)の時間になってますね!めでたしめでたし。
はい、これで完成とか言っちゃったら怒られちゃいますよね...
フロー制御も輻輳制御もない、つまり送信側は常に全力で送信しているので早くて当然なんですね。そして、トラフィックが一定量を超えるとパケロスが顕著に出はじめます。こうなると再送→パケロス→再送...と、絵に描いたような輻輳により通信が破綻してしまいます。
その後、フロー制御や輻輳制御、スロースタートの機能を盛り込み、一応「信頼性のあるトランスポートプロトコル」と名乗っても大丈夫そうなところまで作り込みました。
TCPへの回帰
ここまでやって、ふとあることに気づきます。
「コレ、もうTCPじゃね?」
確認応答、再送、フロー制御、輻輳制御、スロースタート...どこからどう見ても完全にTCPです。
TCPの欠点を補うために「UDPベースで信頼性のあるトランスポートプロトコルを作ろう」という試みは、誰もが一度はやったことあるんじゃないかと思いますが、ぼくのような素人だと真面目に作り込めば作り込むほどTCPになってしまうんですよね。
こうなると、ハンドシェイクを省略している意外はTCPとの差がほとんどありません。むしろ、標準のプロトコルスタックの方が圧倒的に洗練されているため、どう考えてもそっちを使うべきです。
そんなこんなで、半分ネタで作ったIDTPは、速攻でお役御免になりました。
結局、ふりだしに戻り「ベースの通信はTCPに任せたい!でもハンドシェイクは省略したい!」という、なんとも他力本願な考えに至ります。
TCPでハンドシェイクを省略...あれ、どっかで聞いたことあるような...
そうだよ!TFO(TCP Fast Open)だよ!TFOでやればいいんだよ!
結論:素直にクライアントとサーバのカーネルを3.6/3.7以上にしてTFOを使う。めでたしめでたし。
...まぁ、それで片付いてくれれば苦労しないんですが...^^;
TFOを採用するにあたって、以下のような懸念事項があります
- 比較的新しいカーネルでなければ利用できない
- クライアントアプリの改修が必要(iOSって対応してたっけ?)
- NATやF/Wの介在によってTFOが失敗し、通常のハンドシェイクにフォールバックする可能性がある
全くの新規で環境を用意するなら問題にならないかもしれませんが、既に運用中のサービスに適用するとなると、めちゃくちゃハードルが高くなります。
そんなわけで、単純にTFOで全て解決!とはならないのでした。
ようやく本題の「AccelTCP」の話
長々と前置きを書いてしまいましたが、紆余曲折(だいぶ遠回り)した結果、今回の記事のタイトルにある「AccelTCP」を開発するに至りました。
AccelTCP(ACCELerlate TCP proxy)
AccelTCPは、伝送遅延の大きな回線におけるTCP通信を高速化するためのプロキシサーバ型のソフトウェアです。下記の図のように、クライアント側とオリジンサーバ側にそれぞれAccelTCPのプロキシを立てて、伝送遅延の大きな長距離ネットワーク上での通信を代理で行います。
コネクションプーリングによりTCP接続のオーバーヘッドを削減
プロキシサーバ間で、あらかじめ確立されたTCPコネクションを再利用する「コネクションプーリング」を行います。プロキシサーバ間のコネクションプーリングにより、TCPコネクションの確立時に発生する3Wayハンドシェイクのオーバーヘッドを削減し、比較的小さなデータのやりとりを行う通信の待ち時間を大幅に短縮できます。
TCPパラメータの最適化による高速化
プロキシサーバ間のTCP通信は、ウィンドウサイズなどのTCPパラメータをネットワークの特性に合わせ最適化することにより、データ転送効率が大きく向上します。
プロキシサーバ型によるメリット
プロキシサーバ型を採用することにより、クライアントおよびサーバサイドのプログラムを改修することなく「AccelTCP」を利用できます。また、プロキシサーバの設置により通信区間が分割され、各通信区間の往復遅延時間が減少します。これは、パケット消失時の再送時間の短縮につながり、通信全体の高速化が期待できます。
HTTPプロキシモード
ネームベースのバーチャルホストに対応するために、プロキシサーバによるHTTPリクエストのホストヘッダ書換えとXFFヘッダ挿入を行うHTTPプロキシモードを備えています。
通信データの暗号化とSSLオフロード機能を搭載
プロキシサーバ間の通信はSSL/TLSにより暗号化され、安全にやりとりできます。また、SSLオフロード機能を搭載しているためSSL非対応サーバのSSL化や、サーバからSSLの処理を分離することも可能です。
ぶっちゃけ大したことやってないけど、データ量が小さい場合はそれなりに効果あります。
使い方
READMEだけ見ても分かりにくいと思うので、簡単な使い方の説明です。
ビルド
AccelTCPをビルドするには以下のライブラリが必要です。
ライブラリが揃っていれば、makeするだけでビルド終了です。$ git clone https://github.com/KLab/AccelTCP.git
$ cd AccelTCP
$ make
プロキシの起動方法
まず、サーバプロキシを起動します。クライアントプロキシからの接続を 10381 ポートで待ち受け、オリジンサーバ(133.242.5.116:80)に転送するには、下記のように起動します。--server がサーバプロキシとして動くためのオプションです。
$ ./acceltcp -- --server 10381:133.242.5.116:80
次に、クライアントプロキシを起動します。クライアントからの接続を8080ポートで待ち受け、サーバプロキシ(192.168.0.1:10381)に転送するには、下記のように起動します。--http オプションは、HTTPプロキシモードを有効にするためのオプションです。HOSTヘッダを --http-host オプションで指定した内容に書き換えます。--connection-num オプションは、クライアントプロキシからサーバプロキシに対して事前に接続しておくTCPコネクションの数です。
$ ./acceltcp -- --http --http-host=www.klab.com --connection-num=100 8080:192.168.0.1:10381
これで、クライアントプロキシの8080ポートへのアクセスが、オリジンサーバ(133.242.5.116)の80ポートへ転送されます。
オプションの説明とか
オリジンサーバに対してSSLで接続する場合には、サーバプロキシを以下のように起動します。
$ ./acceltcp -- --server --ssl-connect 10381:133.242.5.116:80
クライアントプロキシとサーバプロキシの間もSSL化する場合には、それぞれ以下のように起動させます。なお、デフォルトではカレントディレクトリにある server.crt と server.key が使用されますが、それぞれ --ssl-certificate と --ssl-privatekey で上書き指定できます。
$ ./acceltcp -- --server --ssl-accept --ssl-connect 10381:133.242.5.116:80
$ ./acceltcp -- --ssl-connect --http --http-host=www.klab.com --connection-num=100 8080:192.168.0.1:10381
更に、クライアントプロキシもSSLで待ち受ける場合には下記のようになります。これで全区間の通信がSSL化されます。
$ ./acceltcp -- --server --ssl-accept --ssl-connect 10381:133.242.5.116:80
$ ./acceltcp -- --ssl-accept --ssl-connect --http --http-host=www.klab.com --connection-num=100 8080:192.168.0.1:10381
その他に、--rbuf と --sbuf オプションで受信送信それぞれのソケットバッファのサイズが変更できたりもします。
おわりに
本件はIDCフロンティアさんとの共同研究ということで、超特急で検証用のサーバ設定してもらったり、IDTPが暴発して大量のトラフィック流し過ぎてご迷惑をお掛けしたりしちゃってホントごめんなさいなこともありましたが、久しぶりに没頭できるネタを振ってもらって有り難い限りです。まだまだこれで終わりではないので、もっと突っ込んで輻輳制御のモジュールとかも書いてみたりしたいなぁと思っています。
外部の方から見たら「KLab=ケータイのゲーム作ってる会社」というイメージが強いと思うんですけど、中にはこんなことやってる人もいるよーってことで、こういうのが好きな人がKLabに興味持ってくれると個人的にとっても嬉しいです。
おしまい。
@pandax381