HTTP/2のサーバーをゼロから実装した時に感じたこと

石澤基氏:今日は、GoとOCI Artifactsを使ってインターネットプロトコルのテストケースを共有可能にするという話をしていきたいと思います。

私はカンムという会社でソフトウェアエンジニアをしています、石澤基と申します。カンムでは主にサービスのバックエンドやインフラ領域を担当していて、Goは個人的に書いているのも含めて、5年ぐらい前から書いたり読んだりしています。

私が所属するカンムでは、VISAのプリペイドカードをアプリですぐに発行して使える、「バンドルカード」というサービスを展開しています。バンドルカードのバックエンドはGoで書かれていて、Goを使ってプリペイドカードの仕組みを改善していきたい、もっとよくしていきたいと考えているエンジニアを広く募集しているので、興味があればぜひお声がけください。

それでは本題に入っていきたいと思います。突然ですが、みなさんは何かしらのプロトコルで通信するサーバーをゼロから実装したことがあるでしょうか。私は以前、HTTP/2の仕様に興味をもって、HTTP/2のサーバーをゼロから実装していた時期があります。その時に感じた問題が今日のお話しするテーマの始まりです。

HTTP/2をゼロから実装するのはなかなか大変な作業でしたが、特に、自分が書いたサーバーが仕様どおりに動作しているかを確認するためのテストを用意するのがとても難しいと感じていました。なぜなら、仕様そのものをよく理解していることが必要で、理解をしていないときちんとしたテストも書けないからです。

当時、複数のHTTP/2サーバー実装がすでに存在していたのですが、これらの実装を見て、多くの実装者はみな似たようなテストを自分たちの実装のために書いていることがわかりました。しかしながら、プログラミング言語の違いや構造の違いといった実装方法自体の違いからテストそのものを共有して使うのは難しそうだなと思いました。

Goで実装したテストツール「h2spec」

そこで、もうちょっとうまくテストを共有する仕組みができないかと考えたのが、この「h2spec」という仕組みです。

h2specは、HTTP/2サーバーが仕様に準拠しているかを簡単にテストできるツールです。Goを使って、コマンドラインツールとして実装しています。スライドに表示しているように、HTTP/2サーバーのポートを指定してコマンドを実行すると、そのサーバーが仕様に準拠した挙動をしているのかをテストしてくれます。

おかげさまでこのツールは、HTTP/2サーバーの実装者たちの間で広く受け入れられました。MicrosoftのIIS(Internet Information Services)や、オープンソースでいうとApache Traffic Serverといったサーバー実装のテストにも使われていると聞いています。

「h2spec」をGoで開発した理由

では、仕様への準拠を確認するh2specのようなテストツールが、どのような動きをするのかを簡単に説明したいと思います。

まず、テストツールは、サーバーに対して仕様で定められているデータを何かしら送信します。するとサーバーは、それに対して何かしらの応答をします。テストツール側は、その応答の内容を確認して、それが仕様の通りの挙動かどうかをテストします。これが基本的な動きです。

h2specでは、HTTP/2のフレームと呼ばれるデータ形式でデータを送信して、それに応じたサーバーの動きをテストしています。ただ、このような仕組みも完璧ではありません。例えば応答を返さないプロトコルや、サーバーが複雑な内部状態をもっていてそこから挙動を行う場合に、内部状態に直接アクセスしないと結果が判断できないテストを書くのは難しいという弱点もあるので、注意が必要です。

h2specをなぜGoで開発したのかを簡単に説明します。まず、Goを使えば複数のOSで動作するコマンドラインツールが簡単に実装できることが挙げられます。これにより、Linux環境や、CIなどの環境でも簡単にテストの実行ができるようになりました。また、ビルドをすればシングルバイナリーが生成されるので、インストールを容易にできたのも大きなポイントです。

ほかにも、使いやすいインターフェイスをもった公式のHTTP/2パッケージが、仕様策定の途中から提供されていて、h2specを実装するにはちょうどよかったというのも大きな1つの理由です。

「h2spec」の課題

h2specは多くのサーバー実装者に使ってもらえたのですが、それでも課題はたくさんありました。1つは、テストケースがGoで実装されているため、テストケースの追加や修正をするのにGo実装のための知識が必要になってしまうという点です。

また、自分の実装に合わせてテストを少し修正して、実行してみたいなというケースがまれにあるのですが、そういった場合、Goのビルド環境を作って、そのうえで再ビルドが必要になってしまうのも課題の1つでした。

ほかにも、さまざまなHTTP/2サーバーがもつ細かい実装の差異のすべてに対応するのが難しかったです。例えば「あるHTTP/2サーバーは、ヘッダーフィールドのサイズが4KBまでしか受け付けられないから、特定のテストが通らない」というようなフィードバックをもらうケースもありました。こういった実装固有の要件にテストツール側を合わせていくのも、なかなか大変です。

また、「HTTP/2以外にも、こういったツールは非常に便利だからTLS1.3やQUICみたいな、他のプロトコルのテストもできるとうれしいんだけど、どう?」みたいなフィードバックをもらうこともありました。

こういった課題を踏まえて、よりテストの追加や変更が容易であり、かつ、他のプロトコルにも対応可能なツールがあるといいのではないかと考えるようになりました。

より汎用的なプロトコルの仕様準拠テストツールを目指す「protospec」

そこで、これらの課題を解決するために開発しているのが「protospec」というツールです。protospecもGoで実装していて、より汎用的なプロトコルの仕様準拠テストツールを目指しています。

h2specの、テストケースの追加や変更がしにくいという課題は、テストケースをYAML形式のファイルとして書けるようにすることで対応しています。また、作成したテストケースを他のユーザーにより簡単に共有するために、OCI Artifactsという仕組みにも対応しています。

ここからはこれらの仕組みについて、簡単に紹介をしていきたいと思います。なお、protospecのコードはスライドに記載のURLにアップロードしていますので、興味がある方はアクセスしてみてください。

これがprotospecでテストを実行した時の結果です。基本的なかたちは、先ほどのh2specと非常に似て、ほぼ変わりません。

今日は、個々のテストケースの記述方法をより具体的に説明していきたいと思います。

例えばHTTP/2の仕様では、通信を開始する際にこのような定義がされています。簡単に要約すると、「クライアントは最初にconnection prefaceを送信しなければならない」というものです。connection prefaceは「HTTP/2の通信を開始するよ」ということを示す文字列です。今回はHTTP/2サーバーにconnection prefaceを送信して、実際にハンドシェイクが完了するかを確認するテストケースを書いていこうと思います。

まずテストケースを定義したYAMLファイルを用意します。ファイル全体の内容はこのようなかたちです。先頭に名前などのメタデータがあり、testsというフィールドに複数のテストケースが書けるようになっています。

個々のテストケースは、タイトルやその説明をもちます。いわゆるメタデータのようなものを書けるところですね。個々のテストケースはstepsというフィールドをもち、ここにテストで実際にどういった処理を行うかを書いていきます。

今回は、connection prefaceを送信して、ハンドシェイクが完了するかをテストしたいので、仕様に従って、まずはconnection prefaceをサーバーに送信するという動作を書きます。これは仕様で定義された固定の文字列を送信するというかたちです。

次にHTTP/2のSETTINGSフレームと呼ばれるデータを送信します。これもハンドシェイクに必要なデータです。このレスポンスをサーバーに送信すると、あとはサーバー側の応答が来るのを待つことになります。

なので、次はackという、応答フラグ付きのSETTINGSフレームをHTTP/2サーバーから受信するのを待つための定義を書きます。もし一定期間内に、サーバーから応答フラグ付きのSETTINGSフレームによる応答がなかった場合は、テストが失敗するというかたちです。

定義したテストをprotospecを使って実行すると、このようになります。この例ではテストに成功した、つまり応答フラグ付きのSETTINGSフレームがサーバーから正しく送られてきたことを確認した、ということです。

(次回へつづく)