SPDYで複数のTCPコネクションをひとつにまとめるとはどういうことか

SPDYが流行っていて,複数のTCPコネクションを1つにまとめて高速化を図るらしいということは知っていた.
しかし,単にTCPのコネクション数を抑えるだけならHTTP 1.1のKeep Aliveやpipeliningを使えばよいし,既存技術のどこが問題でSPDYはどう解決しているのかを調べてみた.

SPDYの人でもWeb標準の人でもなんでもないので,間違いが多分含まれています.

並列TCPコネクション

並列にTCPコネクションを張る状況として,Webの世界においては以下の2つを思いつく.

  • ブラウザがあるページをロードして,そのページに複数の画像ファイルが含まれており,それらを同時に取得するために並列にTCPコネクションを張り,HTTPリクエストを投げる.

f:id:y_uuki:20130308184727p:plain

  • JSで非同期に複数のHTTPリクエストを投げる.1個のリクエストを投げるときに1個のTCPコネクションを張る.

並列TCPコネクションの問題点

SPDYのドラフト( SPDY Protocol - Draft 3 - The Chromium Projects SPDY Protocol - Draft 3 日本語訳 )を読むと,

One of the bottlenecks of HTTP implementations is that HTTP relies on multiple connections for concurrency.

SPDY Protocol Draft 3 - 1. Overview

HTTP 実装のボトルネックの1つに、並列処理のために複数コネクションを必要とすることがあります。

とあり,これがなぜボトルネックとなるかというと

This causes several problems, including additional round trips for connection setup, slow-start delays, and connection rationing by the client, where it tries to avoid opening too many connections to any single server.

SPDY Protocol Draft 3 - 1. Overview

これは、接続確立のために追加で発生するラウンドトリップや、スロースタートによる遅延、そして1つのサーバーに対して複数の接続をおこなうことを避けるためのクライアントによるコネクションの割り当て、といったいくつかの問題を引き起こします。

とある.

  • ① 最初の"additional round trips for connection setup"とは,TCP接続を確立するために行う3-wayハンドシェイクのためのパケットの往復のことである.
    並列にコネクションを確立する場合は,3-wayハンドシェイクを並列に実行することになる.もし1コネクションにまとめていれば,追加の3-wayハンドシェイクは必要なくなる.これはレイテンシの問題というより,(2016/03/23 修正.レイテンシのほうがむしろ問題.) サーバの負荷とインターネット上を流れる無駄なパケットが増えてしまうということが問題な気がする.

  • ② 次の"slow-start delays"とは,TCPのウィンドウサイズが大きくなるまでの遅延時間のことである.
    TCPではネットワークの輻輳を回避するために,徐々にウィンドウサイズ(同時に送信するパケット数)を増加させる.
    ウィンドウサイズが増加すれば基本的にはスループットは向上する.
    単一のTCPコネクションを使い回せば,ネットワークが輻輳していない限りウィンドウサイズはパケットが往復するたびに指数関数的に増加していく.
    しかし,並列にTCPコネクションを張ってしまうと,それぞれのコネクションにおけるウィンドウサイズが大きくなる前にデータの送信が完了してしまう.
    要するに,単一のTCPコネクションにより,ウィンドウサイズが大きい状態でデータ送信するほうがパケットの往復が少なくて済むが,並列にTCPコネクションを張ると,ウィンドウサイズが小さいままの状態でデータ送信しなければならず,パケットの往復が増えてしまう.

  • ③ 最後の"connection rationing by the client"は,よくわからないけどクライアントがコネクションを張り過ぎてサーバに迷惑をかけないように同時接続数を制限して管理しなければならないとかそういうことな気がする.

HTTP 1.1

これらは,HTTP 1.0から問題になっていて,HTTP 1.1での解決策は以下のとおりである.

  • HTTP 1.0のKeep Alive (HTTP/1.1: Connectionsにより,一度確立したTCPコネクションをクローズせずに使いまわす.これで①,②をある程度解決できる.

  • Keep Aliveされた単一のTCPコネクションにおいて,HTTPは基本的にレスポンスが返ってくるまで次のリクエストが投げられない(新しくTCPコネクションを確立すればもちろんリクエストは投げられる).同時に複数のリクエストを投げるために,同じくHTTP 1.1のpipelining( HTTP/1.1: Connections を使う.

Keep Aliveとpipeliningについては下記の記事の説明がわかりやすい.

pipeliningの問題点

The problem with pipelining, it says, is that even when multiple requests are pipelined into one HTTP connection, the entire connection remains first-in-first-out, so a lost packet or delay in processing one request results in the delay of every subsequent request in the pipeline. LWN.netの記事 Reducing HTTP latency with SPDY [LWN.net] から引用

自分なりに解釈すると,pipeliningは,HTTPレスポンスの受信に失敗または受信が遅延すると,pipeline上の他のHTTPリクエストやレスポンスの送受信が失敗または遅延してしまうということだと思う.

例えばあるレスポンスの受信(図では②)に失敗すると,TCPコネクションを接続確立をやり直すなどの対処が必要である.
また,パケットロスなどの原因により②の受信が遅延すると③,④の受信も遅延してしまう.

f:id:y_uuki:20130308184809p:plain

レスポンスの受信の失敗については,なにも最初から接続の確立をやり直さなくても失敗したレスポンスだけサーバが再送すればいいのではと思った.
しかし,再送のためには,サーバがクライアントの受信失敗を検知しなければならず,検知する仕組み(TCPの確認応答的なもの)を実装しなければならない.

レスポンスの受信の遅延については,②を無視して③,④を先に受信させられないのと思ったけど,HTTP 1.1の仕様では①..④の順にレスポンスを送信しなければならないという制約がある.

A server MUST send its responses to those requests in the same order that the requests were received.

RFC 2616 (HTTP/1.1) 8.1.2.2 Pipelining http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.1.2.2 から引用

そこでSPDY

SPDYではTCPの上にTCPのようなステートフルなプロトコル層を追加して,pipeliningの問題点である

  • 受信失敗時にTCPコネクションを再確立する
  • リクエストの順番と同じ順番で対応するレスポンスを送信しなければならない

を解決している.

f:id:y_uuki:20130308185933p:plain

(出典:SPDY: SPDY: An experimental protocol for a faster web - The Chromium Projects

前者・後者ともに,単一のTCPコネクションの上に複数の論理的なストリームを構築することにより解決する.
ここでいうストリームはHTTPリクエストと対応するレスポンスのペアである.(SPDYが定義するストリームはより広義な定義がされている)

これにより,たとえ受信に失敗してもストリームが切れるだけで,TCPコネクションを再確立する必要がない.

さらに,個々のストリームは独立しているので,リクエストの順番と同じ順番にレスポンスを返さなければならないという制約はない.

The fully multiplexed approach taken by SPDY, however, allows multiple HTTP requests and responses to be interleaved in any order, more efficiently filling the TCP channel. A lost packet would still be retransmitted, but other requests could continue to be filled without pausing to wait for it. A request that requires server-side processing would form a bottleneck in an HTTP pipeline, but SPDY can continue to answer requests for static data over the channel while the server works on the slower request. LWN.netの記事 Reducing HTTP latency with SPDY [LWN.net] から引用

論理的なストリームの構築方法

では論理的なストリームをどうのように実現しているのか.

SPDY defines 3 control frames to manage the lifecycle of a stream:

  • SYN_STREAM - Open a new stream
  • SYN_REPLY - Remote acknowledgement of a new, open stream
  • RST_STREAM - Close a stream

SPDY Protocol Draft 3 - 2.3.1 Stream frames

SPDY ではストリームのライフサイクルを管理するために3つのコントロールフレームを定義します。 SYN_STREAM - 新しいストリームの開始 SYN_REPLY - 新しく開始するストリームのリモート承認 RST_STREAM - ストリームの終了

ストリーム制御のためのデータ本体を含まない専用のコントロールフレームを用いることにより,単一TCPコネクション上にストリームという概念を導入している.それぞれのコントロールフレームはStream-IDをもち,このIDにより各ストリームを区別する.

データ本体は,データフレームとして送信する.

SPDYにおけるHTTPリクエストとレスポンス

上記コントロールフレームとデータフレームを用いてどのようにしてHTTPリクエストとレスポンスを表現するのか.

大雑把にまとめると,リクエストがSYN_STREAMフレームに相当し,レスポンスがSYN_REPLYに相当する. それぞれbodyがある場合はSYN_STREAM/SYN_REPLYフレームを送信後にデータフレームを送信する.

詳細は下記参照.

リクエスト

For requests which do not contain a body, the SYN_STREAM frame MUST set the FLAG_FIN, indicating that the client intends to send no further data on this stream. For requests which do contain a body, the SYN_STREAM will not contain the FLAG_FIN, and the body will follow the SYN_STREAM in a series of DATA frames. The last DATA frame will set the FLAG_FIN to indicate the end of the body.

SPDY Protocol Draft 3 - 3.2.1 Request

body を含まないリクエストでは、クライアントがストリームに追加のデータを送信しないことを示すために、SYN_STREAM フレームに FLAG_FIN を設定しなければなりません。body を含むリクエストでは、SYN_STREAM は FLAG_FIN を含まず、body は SYN_STREAM のあとに続く一連の DATA フレームに含まれます。最後の DATA フレームには、body の終わりであることを示すための FLAG_FIN が設定されます。

レスポンス

The server responds to a client request with a SYN_REPLY frame. Symmetric to the client's upload stream, server will send data after the SYN_REPLY frame via a series of DATA frames, and the last data frame will contain the FLAG_FIN to indicate successful end-of-stream. If a response (like a 202 or 204 response) contains no body, the SYN_REPLY frame may contain the FLAG_FIN flag to indicate no further data will be sent on the stream.

SPDY Protocol Draft 3 - 3.2.2 Response

サーバーは、クライアントのリクエストに SYN_REPLY フレームで応じます。クライアントのアップロードストリームとは対象的に、サーバーは SYN_REPLY フレーム後の一連の DATA フレームによりデータを送信し、最後の DATA フレームにはストリームが正しく終了したことを示す FLAG_FIN が含まれます。(202や204レスポンスといった) body を含まないレスポンスの場合、SYN_REPLY フレームには、このストリームではこれ以上送られるデータがないことを示す FLAG_FIN が設定されるかもしれません。

雑感

  • SPDYのTCPコネクションを1つにまとめるという観点のみを切り出して調べた.

  • 並列にTCPコネクションを張ることの問題は前からあって,pipeliningとかいれてみたけど,本来ステートレスなHTTPのレイヤでステートフルなことをやろうとしてうまくいかないから,SPDYが登場したみたいな感じでは.

  • SPDYの特にフレーミング層についてはもう少し机上シミュレーションを繰り返さないとよく理解できなさそう.

  • SPDYはHTTPの上でHTTPをやろうとしているという言葉があるけど,個人的にはTCPの上でTCPをやろうとしているイメージがある.

追記 最後のHTTPの上でHTTPというのは以下の記事からの引用. Webはインターネットになった - naoyaのはてなダイアリー