HTTP/2 for Ruby

こんにちは、エンジニアの前田です。この記事は、 HTTP2 Advent Calendar 2014 - Qiita の22日目の記事です。

HTTP/2の実装一覧はこちらにあります → 「Implementations」 。このうち、pure Ruby実装 http-2 のdraft-14対応化を行いました。今回はその経緯を少し書いてみようと思います。

余談ですが、現在の仕様では “HTTP2.0” ではなく “HTTP/2” もしくは “HTTP2” が正しい名称です。

http-2 gem

http-2 gemは「High Performance Browser Network」の著者として有名なIlya Grigorikが作成したものです。github pageのコミットログによれば、2013年8月から開発をスタートしたようです。2014年1月時点でdraft-06に対応したものが公開されていましたが、その後開発は止まっていました。

そのころ、私はHTTP/2勉強会で「最速実装」(最も性能の高い実装、ではなく、機能を減らしてGETだけできるクライアントを最も素早く作る実装の意)について解説する機会があり、RubyでHTTP/2 GETの最速実装を試していました。RubyのちゃんとしたHTTP/2実装がないのかとさがしてみると、Ilyaのpure Rubyのものと、matsumoto-rさん作のnghttp2のmruby bindingが見つかりました。Ilyaのものは開発が止まっていたのが残念でした。nghttp2のCRuby bindingを作るという案もありましたが、自分の勉強の意味も含てpure Rubyでの実装をしてみたかったという思いがあり、Ilya実装に手を加えることを選択しました。社内プロジェクトとして、まずは当時の最新draftに追いつくべくHPACKから実装に着手しました。これが5月後半くらいのことです。

その後、断続的に開発を続け、h2-13 + HPACK-09対応版ができました。forkがでかくなりすぎてしまって、どう本家にfeedbackしようかとまよっていた9月、h2-14ドラフトが出たのを契機に、他の開発者からもIlya実装にPRが来はじめました。他にも興味のある人がいるなら、とh2-14対応版を作り、がんばってrebaseしてPRを送りました。Ilyaのレビューに対応するなどして、11月始めくらいにマージが完了、11月末にhttp-2 gem 0.7.0がリリースされました。11月のHTTP/2 Conferenceでは実際にIlyaと会って握手することもできました。

試しに実行してみるには、上記リポジトリをgit cloneして、以下のコマンドを試してみるとよいです。

bundle install
(cd example; bundle exec ruby -I../lib client.rb http://nghttp2.org:80/)|less

「こうやったらこけた!」などのレポートをお待ちしております。

http-2の実装で苦労した部分

http-2 gemは内部で大きくframe層、stream層、connection層に分かれています。frame層にはさらに下にHPACKがあります。分断されていてわかりやすいこともあり、まずはHPACKからとりかかりました。

HPACK-03では、literalとheader/static tableしかありませんでした。refsetもhuffmanもなしです。実装に着手した時点の最新ドラフト、h2-12 + HPACK-07 を対象として、まずはhuffmanコーディングを実装しました。huffmanコーディングの実装はkazuさんの資料がわかりやすく、おおいに参考になりました。#http2study グループの成果としてhpack-test-caseがありますが、これを使うにはreference setを実装する必要がありました。

reference setの実装は難しく、私もtatsuhiro-tさんのアルゴリズムをRubyで実装して使いました。reference setで特に難しい部分があったのですが、いまやなくなってしまった部分なので解説はやめておきます(同一のoctetを4回送出する必要がある部分です、と言えば実装したことのある人にはわかります)。HPACK-10でreference setが削除されたのは本当によかったです。HPACKのバージョンが上がるときにhuffmanコードテーブルが変更されることが度々あったことも含め、hpack-test-caseにはお世話になっています。

HPACKの後、下層から順番に実装を続けていきました。h2-12でやり残した部分をh2-13/-14に追いつく作業と交互に実施したため、ブランチがとっちらかって大変なことになりました。もっとしっかりトピックブランチ開発をすればよかったと後悔しています。

さて、frame層の実装では特に実装の難しい部分はありませんでした。PADDINGがちょっとややこしかったのと、h2-14になってデフォルトのフレーム最大長が16383からこっそり16384に変更になったのが落とし穴だった程度です。

難しかったのはSETTINGSの設定値の扱いです。設定値はconnection全体について記述しますが、送信側と受信側とで独立した設定が可能になっています。元の実装ではこれが考慮されておらず、双方向のSETTINGSがひとつの変数を共有していたので、ここを直しました。この誤りはわりと他の実装でも存在したそうです。自分からSETTINGSを送出する場合は、ackを受信して始めて適用しなければならない点も工夫が必要です。SETTINGSに関してもうひとつ難しかったのは、h2-14になってframe最大長がSETTINGSで設定可能になったことです。frameの最大長を必要とするのはframe層ですが、設定情報を持っているのはconnection層なので、間のstream層を通じて設定情報をやり取りできるようにする必要がありました。このあたりはもっとすっきりできる気もしています。

stream層ではflow controlはとりあえず実装しましたが、正しいのかどうか自分でもあまり自信がありません。ユニットテストをもう少し書かないといけません。ここでも元実装では送信側と受信側で十分な分離がなかったので苦労しました。片方向のflow controlをオブジェクトとして立てるか、あるいは同じようなコードを複製するかでなやんで、結局複製しています。DRYを追求したいところです。

connection層の実装では、難しい部分の実装は後回しでまだ着手していないため、苦しいのはこれからかと思います。特にpriorityとdependencyがつらそうです。その他にもこまごまと未実装の部分があります。

http-2の今後

現在のバージョンで、http-2 server - nghttp2 client間、およびnghttp2 server - http-2 clientの間での単体のGET, POST, PUTなどは動いています。HTTP/2仕様と比べて未実装の部分はまだまだ多く、もう少し単体での開発が必要です。

  • stream multiplexing + priority (dependency tree)
  • ALPN対応 (これはopensslを対応するバージョンにすることも含めて)
  • エラー処理 (仕様でエラーを返さなければならない(MUST)となっている部分ができていない)

また、実用になるにはHTTP/1.1との切りかえ部分も含めたインターフェースが必要になります。クライアントとしてはNet::HTTPあるいはHTTP::Clientへのdrop-in replacementとしてのAPIラッパーがほしいところです。サーバーはさらに難しく、Railsから使えるようにするには、次世代のRackであるThe Metalへのインテグレーションを考える必要があると思っています。

また、現在の実装は性能に関して何もしていないので、HPACKなどまだまだ遅い処理をしています。Pure Rubyであることの利点としてJRubyでそのまま動くことを大事にしたいので、extに出すのではなくRubyレベルでの最適化を行うことになるでしょう。そういえばマルチスレッドの排他制御をちゃんとしないとJRubyで大コケしそうです。

当面の目標は #http2study の http2 interop ハッカソン #4です。このためにh2-16対応をします。