Go言語とHTTP2
http2 in Go 1.6; dotGo 2015 - Google スライド
2015年の5月にRFCが出たばかりのHTTP2が2016年の2月にリリース予定のGo1.6で早くも利用可能になることになっている.HTTP2の勉強も兼ねてGo言語におけるHTTP2実装を追ってみる.
以下ではまず実際にHTTP2サーバを動かしChromeで接続してみる.次に現状コードがどのように管理されているかを追う.最後に実際にコードを動かしながらHTTP2の各種機能を追う.なお参照するコードはすべて以下のバージョンを利用している(まだWIPなのでコードなどは今後変わる可能性があるので注意).
$ go version
go version devel +9b299c1 darwin/amd64
HTTP2とは?
HTTP/2に関してはスライドやブログ記事,Podcastなど非常に豊富な情報がインターネット上に存在する.そもそもHTTP2とは何か?なぜ必要なのか?などを理解したい場合は参考に挙げた記事などを参照するのがよい.
実際に使ってみる
最小限のコードでHTTP2サーバーを起動しChromeで接続してみる.
まず最新のGoをソースからビルドする(ビルドにはGo1.5.1を利用する).以下では2015年11月16日時点の最新を利用した.
$ git clone --depth=1 https://go.googlesource.com/go ~/.go/latest
$ export GOROOT_BOOTSTRAP=~/.go/1.5.1
$ cd ~/.go/latest/src && ./make.bash
現時点でGoにおけるHTTP2はover TLSが前提になっている.そのためサーバー証明書と鍵が必要になる(なければ事前にopenssl
コマンドやcrypto/x509
パッケージなどを使って自己署名証明書をつくる).
コードは以下.
func main() {
certFile, _ := filepath.Abs("server.crt")
keyFile, _ := filepath.Abs("server.key")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Protocol: %s\n", r.Proto)
})
err := http.ListenAndServeTLS(":3000", certFile, keyFile, nil)
if err != nil {
log.Printf("[ERROR] %s", err)
}
}
証明書と鍵を読み込んでListenAndServeTLS
を呼ぶだけ.HTTP2のために特別なことをする必要はない.クライアントがHTTP2に対応していれば勝手にHTTP2が使われる.起動して接続すると以下のように「Protocol: HTTP/2.0」が確認できる(Chrome拡張のHTTP/2 and SPDY indicatorが反応しているのも確認できる).
コードの行方
現在HTTP2のコードはどのように管理されているのか? もともとはbradfitz氏によりbradfitz/http2で実装が進められていた.そしてgolang.org/x/net/http2に移動した.ちなみにGo1.5以前でもこちらのパッケージを使えばHTTP2を使うことはできる.
このhttp2
パッケージの位置付けはローレベルなHTTP2の実装であり普通のひとは触ることがない.Go1.6では普通のひとが触るハイレベルなインターフェースは今まで通りのnet/http
となる.
では最新のGoのコードにはどのようにマージされたのか? まずヘッダ圧縮のHPACKはhttp2/hpack
という名前でサブディレクトリに別パッケージとして実装されている.これはsrc/vendor
以下にvendoringされている.http2
も同様にvendoringされているだけかと思ったが,こちらはnet/http
パッケージにh2_bundle.go
という1つのファイルとして組み込まれている.
具体的な経緯はhttp -> http2 -> http import cycleを読むとわかるが,単純にvendoringするとnet/http
->http2
とhttp2
->net/http
というimport cycleが起こってしまう.これは上の例で示したようにAPIの変更なしにHTTP2を有効にするというゴールを達成するためには避けられない.
これを解決するために使われたのがbundleコマンドである.これはパッケージを別パッケージとして1つのファイルにするコマンド.以下のように使われる.
$ bundle golang.org/x/net/http2 net/http http2
これでgolang.org/x/net/http2
をnet/http
パッケージとしてhttp2
というprefixをつけて一つのファイルにまとめるいうことがおこる.
変更はどうするのか? bundle
はテストを無視するため変更はテストがちゃんとあるgolang.org/x/net/http2に入り,その都度bundle
してマージとなるらしい(リリースまでは).
… というのが現状.正式なリリースまでに時間はあるのでどうなるのかはわからない.
HTTP2の機能を追う
以下ではHTTP2の主な機能がどのようにGo言語で実装されているのかを見ていく.
フレームとストリーム
HTTP1.xではリクエスト/レスポンスのフォーマットにテキストが利用されてきた.HTTP2ではフレームと呼ばれるバイナリのフォーマットが利用される.これにより転送量の低減を実現している.
フレームにはいくつかのタイプが定義されている.例えばHTTP1.xのヘッダにあたるHEADERS,HTTP1.xのBody部にあたるDATAなどがある.フレームは以下のようなフォーマットで表現される(cf. 4.HTTP Frames).
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
HTTP1.xでは1つのリソースを取得するために1つのTCPコネクションが必要である.つまり3つの画像が必要であれば3つのTCPコネクションが必要である.TCPはThree-way handshakingやスロースタートにより通信のオーバーヘッドが避けられない.そのため各コネクションはなるべく並列で確立されるのが望ましい.しかし同一オリジンへの同時接続数はたいてい6つに制限されている.つまり7つ目は先の6つのどれかが完了するまでブロックされる.これに対応するためにHTTP1.x時代では画像などを別ドメインから読み込むDomain Shardingという手法が一般的にはとられてきた(cf.HTTPリクエストを減らすために).
HTTP2ではストリームという概念を導入し上記の問題を解決している.ストリームとは1つのTCPコネクション上に作られる仮想的な双方向シーケンスである.このストリームによりリクエストは多重化され複数のリソース取得も並列で実行可能になる.それぞれのリクエストとレスポンスはひとつのストリームで処理され,それぞれがユニークなIDをもつ.
Go言語ではどう実装されているか.まずFramer
というstructがフレームの書き込みと読み込みを担う.そしてそれぞれのタイプのフレームの書き込みのために専用のメソッド,例えばDATAならWriteData
,HEADERSならWriteHeaders
,が準備されている.
実際にDATAフレームを作って中身を覗いてみる.
buf := new(bytes.Buffer)
fr := http2.NewFramer(buf, buf)
var streamID uint32 = 1<<24 + 2<<16 + 3<<8 + 4
fr.WriteData(streamID, true, []byte("Hello"))
b := buf.Bytes()
fmt.Printf("Frame: %q\n", b)
fmt.Printf("Type: %x\n", b[4:5]) // Type: 01
fmt.Printf("StremID: %x\n", b[5:9]) // StremID: 01020304
fmt.Printf("DATA: %x\n", b[9:]) // DATA: 48656c6c6f
NewFramer
でFramer
をつくり,WriteData
でストリームのIDとともにデータを書き込む.あとは書き込まれたデータを定義に基づき覗くと中身が見れる.
ヘッダ圧縮
HTTPはステートレスなプロトコルである.そのためHTTP1.xでは1つのセッションで毎回似たようなヘッダを送る必要があり冗長である.HTTP2ではヘッダの圧縮を行う.ヘッダの圧縮にはHPACKと呼ばれる手法を用いる.HPACKはハフマン符号化と静的/動的テーブルという仕組みで圧縮を行う手法である.HPACKはHTTP2とは別にRFC 7541で仕様化されている.
Go言語ではhttp2/hpack
という名前でhttp2
パッケージのサブディレクトリに別パッケージとして実装されている.それぞれ実際に使ってみる.
まずハフマン符号化.ハフマン符号は文字の出現頻度の偏りに合わせてビット列を割り当てる符号化である.ここでは例としてwww.example.com
という文字列をハフマン符号でEncode/Decodeしてみる.
s := "www.example.com"
fmt.Println(len(s))
fmt.Println(hpack.HuffmanEncodeLength(s))
b := hpack.AppendHuffmanString(nil, s)
fmt.Printf("%x\n", b)
var buf bytes.Buffer
hpack.HuffmanDecode(&buf, b)
fmt.Printf("%s\n", buf.String())
上の例では15バイトの文字列を12バイトに符号化できる.
次にテーブルによる圧縮.HTTP2ではよく利用するヘッダをKey-Valueの辞書としてもちそのインデックスを示すことでヘッダを表現する.テーブルは仕様として事前に定義された(Static Table Definition)静的テーブルとリクエストのやりとりの中で更新する動的テーブルがある.
まず静的テーブルにエントリがある:method GET
をEncodeしてみる.
var buf bytes.Buffer
e := hpack.NewEncoder(&buf)
e.WriteField(hpack.HeaderField{
Name: ":method",
Value: "GET",
})
fmt.Printf("Encoded: %x (%d) \n", buf.Bytes(), len(buf.Bytes()))
これは1バイトに圧縮される.
次に静的テーブルにエントリのない:authority www.example.com
をEncodeしてみる.動的テーブルの効果をみるために2度実行する.
var buf bytes.Buffer
e := hpack.NewEncoder(&buf)
e.WriteField(hpack.HeaderField{
Name: ":authority",
Value: "www.example.com",
})
fmt.Printf("Encoded: %x (%d) \n", buf.Bytes(), len(buf.Bytes()))
buf.Reset()
e.WriteField(hpack.HeaderField{
Name: ":authority",
Value: "www.example.com",
})
fmt.Printf("Encoded: %x (%d) \n", buf.Bytes(), len(buf.Bytes()))
同じヘッダに対して2度Encodeを実行する.まず1度目は動的テーブルにエントリがないため14バイトにしかならない.もしテーブルにエントリがない場合は,動的テーブルにそれが追加される.そして2度目の実行時はそのテーブルが参照され1バイトに圧縮される.
優先度制御
HTTP1.xでは全てのリクエストは平等に処理される.つまり画像もCSSもJSも全て平等に処理される.HTTP2ではクライアントがリクエストに優先度を指定することができる.例えばサイトのレンダリングが必要なCSSやJSを優先的にリクエストすることができる.これによりページの描画を改善しユーザの体感速度を向上することが期待できる.
Go言語ではどうなっているのか.クライアントはPriorityParam
というstructを用いて優先度の指定を行う.そしてHEADERSもしくはPRIOTIRYフレームでこれを送信する.
次にサーバーの動き.これは外からは触れない.どう処理されるかはテストコードを見るのが良い.priority_test.go
を見る.
// A -> B
// move A's parent to B
streams := make(map[uint32]*stream)
a := &stream{
parent: nil,
weight: 16,
}
streams[1] = a
b := &stream{
parent: a,
weight: 16,
}
streams[2] = b
adjustStreamPriority(streams, 1, PriorityParam{
Weight: 20,
StreamDep: 2,
})
if a.parent != b {
t.Errorf("Expected A's parent to be B")
}
if a.weight != 20 {
t.Errorf("Expected A's weight to be 20; got %d", a.weight)
}
if b.parent != nil {
t.Errorf("Expected B to have no parent")
}
if b.weight != 16 {
t.Errorf("Expected B's weight to be 16; got %d", b.weight)
}
各a
とb
というstream
がmapで管理されている.mapのkeyは各stream
のIDである.そしてadjustStreamPriority
でIDが1であるa
の優先度を16から20に更新し依存するストリームのIDを2に変更する.
…と優先度の更新は追えたが,この優先度をどのように使っているのかは見つけることができなかった.知ってる人がいたら教えてください.
Server Push
HTTP1.xではクライアントからリソースのリクエストがあって初めてサーバー側からそれを送ることができる.HTTP2ではServer Pushという仕組みを使い,クライアントがリクエストする前にサーバー側からクライアントにリソースを送りつけることができる.例えばサーバーがHTMLのリクエストを受けたとする.サーバーはHTMLの内容を知っているので次にCSSやJSのリクエストがクライアントから送られることを予想できる.Server Pushを使えばそれらを先に送信することができる.
kazuhoさんによるH2Oに関する発表を見ているとServer Pushは難しそう.Server Pushで送られたリソースはクライアントでキャッシュされ,クライアントはそのキャッシュを利用することになる.既にキャッシュが存在する場合にPushするのは無駄になるが,それを制御するのは難しい(H2OではCache aware-server-pushをしているとのこと).
Go言語ではどうなっているのか.Server Pushを行うときはPUSH_PROMISEというフレームをサーバーは送信する.このフレームを作成するWritePushPromise
というメソッドは準備されている.がServer Pushを行うためのハイレベルなインターフェースは現時点では見当たらなかった.“Please test Go’s HTTP/2 support”などを読む限り今後のバージョンでのリリースになりそう.
まとめ
Go1.6に予定されているHTTP2の実装を追ってみた.Server Pushのハイレベルなインターフェースを除いた基本的な機能は実装されている.そしてGo1.5以前と同じインターフェースで利用できることがわかった.
Go言語のHTTP2をすぐに使うのか? と聞かれるとまだ議論が必要であると感じた.少なくとも現時点ではTLS終端はアプリケーションの前段のnginxにある.最初はnginxなどで利用することから始めると思う.
PaaSを運用している立場からみると状況はもう少し複雑になる.例えばGoogle App EngineのようにTLSとHTTP2は全てGoogleのサーバーが面倒見るからその上のアプリケーションは何もしなくてもHTTP2が有効になりますと言うこともできる(cf. Full Speed Ahead with HTTP/2 on Google Cloud Platform).その一方でServer Pushなどはアプリケーションごとにコントロールしたいかもしれない.そういう場合にどのようにハンドルするべきなのかなど考えることは多い.
- HTTP/2時代のウェブサイト設計
- HTTP/2でも初めてみます?
- HTTP2 時代の Web - web over http2
- HTTP/2 入門 - Yahoo! JAPAN Tech Blog
- Software Design 2015年11月号
- HTTP2 時代のサーバサイドアーキテクチャ考察
- HTTP2 の RFC7540 が公開されました
- HTTP/2のRFCを読んだ感想 - WAF Tech Blog | クラウド型 WAFサービス Scutum 【スキュータム】
- スレッドプログラミングによる HTTP/2 の実装
- HTTP2 時代のサーバサイドアーキテクチャ考察
- HTTP/2 Deep Dive: Priority & Server Push
- HTTP/2 and Server Push
- http://www.integralist.co.uk/posts/http2.html
- Full Speed Ahead with HTTP/2 on Google Cloud Platform
- http2/quic meetup - 資料一覧 - connpass
- mozaic.fm #2
- Rebuild: 99: The Next Generation Of HTTP (kazuho)
- #161: HTTP/2 with Ilya Grigorik