2014年6月20日

HTTP/2

Yahoo! JAPAN における HTTP/2 への取り組み

  • このエントリーをはてなブックマークに追加

はじめに

こんにちは。システム統括本部 プラットフォーム開発本部 エンジニアの大久保諒です。 Yahoo! JAPAN のサービス利用者の皆様に、より高速にコンテンツをお届けするためのキャッシュ・プロキシサーバー関係の開発業務に携わっております。

本記事ではオープンソースの HTTP キャッシュ・プロキシサーバー Apache Traffic Server で HTTP/2 通信を可能とする為の機能追加を行いましたので簡単にご報告致します。 HTTP/2 の基本的な解説に関しては 前回の記事 にまとまっています。ぜひそちらをご覧の上で本記事をご覧ください。 本件の実装は Apache Software Foundation にパッチを投稿する形で公開 しておりますので、もし実装に興味がある、または動かしてみたいという方がいらっしゃればすぐに試していただけます。:)

Apache Traffic Server について

Apache Traffic Server (以下、ATS)は、オープンソースの高性能 HTTP キャッシュプロキシサーバーです。( Apache HTTP Server とは別のソフトウエアです!) 類似する機能を持つソフトウエアに、 Squid, Varnish などが存在します。 ATS は Yahoo! Inc. が自社で使用していたキャッシュサーバーがオープンソース化されたもので、現在は Apache のトップレベルプロジェクトの一つとして開発が進められています。 使用している企業として、Yahoo! Inc. や Taobao 、LinkedIn が挙げられます。 Yahoo! JAPAN でもこの ATS をさまざまなサービスのアプリケーションサーバーの前に HTTP アクセラレータとして設置し、活用しています。

ATS はイベント駆動による I/O 多重化方式を採用しており、多量のリクエストを捌くことができます。 また RAM やディスクを組み合わせて効率的にキャッシュシステムを構築したり、さらに ATS を立ち上げたサーバーを複数台用意しそれらで階層的なキャッシュサーバーを構築したりすることも可能です。 近々リリースされる 5.0.0 では SPDY プロトコルをプロキシすることが可能になる など、新機能の追加も積極的に行われています。

もし ATS についてさらに詳しく知りたい方がいらっしゃれば、有志による翻訳活動が進められている 公式ドキュメントの日本語訳ページ を参照してみてください。主要な情報は日本語で参照できる状態になっています。

ATS の HTTP/2 対応に向けた活動

前回の記事 で紹介しました通り、HTTP/2 は HTTP/1.1 のセマンティクスを維持しつつパフォーマンス上の懸念点を解消するため通信の多重化やヘッダの圧縮を行います。 昨今 Yahoo! JAPAN のサービスをスマートフォンやタブレット端末を用いてモバイル回線から利用するユーザーが多く、 HTTP/2 の通信の多重化などの恩恵によりページの表示が高速化される、スマートフォンアプリケーションの通信処理待ちの時間が短縮されるなどの効果が期待できます。

下記では HTTP/2 をプロキシサーバーでサポートするメリットとそれを実現する為にわれわれが行った実装、そして ATS で HTTP/2 通信を試す方法について簡単に説明します。

プロキシサーバーで HTTP/2 サポートをするメリット

HTTP/2 を利用するためには Web サーバーが HTTP/2 を解釈できなければなりません。 通常、複数のアプリケーションサーバーがある場合は各々で HTTP/2 対応を行わなければならず導入コストが大きくなりがちですが、クライアントとアプリケーションサーバーの間に HTTP/2 でリクエストを受けることのできるプロキシサーバーを立てることによりこれを低減できます。

下図のように HTTP/2 でリクエストを受けて HTTP/1.1 で各アプリケーションサーバーにプロキシすれば、アプリケーションサーバー側で各々 HTTP/2 対応せずとも HTTP/2 を使用することによる恩恵を受けられます。 さらに HTTP/2 は HTTP/1.1 と同様 http, https スキームを使用して TLS のプロトコルネゴシエーションのための拡張機能を用いてプロトコル判別を行うため、クライアントが HTTP/2 を解釈できればそれを用い、そうでなければ従来通り HTTP/1.1 を使用するという使い分けも容易です。

図1. 各プロトコルでリクエストを受けるプロキシサーバー

HTTP/2 対応実装の概要

今回行った HTTP/2 対応の実装は、 SPDY サポート のコードを参考にしています。 これは HTTP/2 の仕様が SPDY ベースになっているため、実装を参考にできる部分が多々あったためです。 もちろん SPDY と HTTP/2 の間で扱うバイナリフレームの種類やフォーマット、プロトコルネゴシエーションの方法など差異も多くあるため、自前で HTTP/2 特有の処理を実装していく必要がありました。

2014年4月の時点で SPDY 対応の実装は下記のようになっていました。

  • SPDY のプロトコルとしての主要な処理(バイナリフレームやストリームによる多重化など)は tatsuhiro-t 氏が開発している SPDY ライブラリ spdylay に任せる。
    • ネットワークにフレームを流したりプロキシする処理はATSで担う。
  • https スキームでアクセスした際は、 TLS-NPN によって SPDY 通信を判定。
  • http スキームの場合はリクエスト開始時の TCP セグメントの先頭 1 バイトが ASCII 文字でない場合 SPDY 通信であると判定

これを参考にし、かつ SPDY と HTTP/2 の差異を吸収するため下記のような方針で実装を進めました。

  • HTTP/2 の主要な処理に、 spdylay の作者である tatsuhiro-t 氏が開発している nghttp2 を利用。
  • HTTP/2 通信を行う為の最低限のフレーム ( DATA, HEADERS, RST_STREAM, SETTINTGS, GOAWAY, WINDOW_UPDATE ) 処理を記述。
  • https スキームでアクセスした際、 TLS-NPN または TLS-ALPN でプロトコル判別する。
  • http スキームの場合はコネクションプリフェイスと呼ばれる HTTP/2 で付与される、 HTTP/1.1 におけるリクエストラインに似た文字列の内容で判別する。

以上の方針に基づき、 ATS で HTTP/2 通信を行うための機能追加を完了しました。 本記事執筆時点(2014年6月現在)で HTTP/2 の仕様の 12 番目のドラフト に基づいた最低限のやり取りができる機能を実装し、動作確認ができています。

Apache Software Foundation にパッチを投稿した話

今回行った HTTP/2 サポートの為の実装の成果をパッチとして ATS の開発を管理する JIRA に投稿 しました。 もし実装に興味がある、もしくは実際に動作させてみたいという方がいらっしゃれば、この JIRA のチケットのページに添付されている http2-0004.patch というファイルをご参照ください。

パッチを投稿した結果ですが、 2014年6月現在で順調にいけば ATS 5.1.0 でマージされるであろう状態にあります。 コミッターの James Peach 氏と Yunkai Zhang 氏にフィードバックをいただいていており、今後は改善を進めていこうと計画しています。

ATS with HTTP/2 クイックスタート

簡単ながら HTTP/2 による通信が可能な ATS をビルドし、動作確認を行う手順を示します。

  1. OpenSSL 1.0.1 以上をインストールする

    TLS-NPN を利用する為に OpenSSL 1.0,1 以上をインストールしてください。 TLS-ALPN も利用したい場合は OpenSSL 1.0.2 が必要になります。

  2. nghttp2 をインストールする

    https://github.com/tatsuhiro-t/nghttp2 から nghttp2 のソースコードを取得し、インストールしてください。

  3. ATS のソースコードを取得する

    https://github.com/apache/trafficserver から ATS のソースコードを取得してください。

  4. HTTP/2 サポートパッチを適用する

    下記ページに添付されている http2-XXXX.patch というファイルを取得して ATS のソースコードに適用してください。 ファイル名の XXXX の部分の数値が一番高いパッチが、最新の ATS のコードに適用できるものとなっています。 2014年06月現在、最新のパッチは http2-0004.patch です。ATS の git リポジトリの master ブランチの commit: 43be06849b560b62ae16d1c3f450c860ac1bbe0d に対応します。

  5. https://issues.apache.org/jira/browse/TS-2729

  1. HTTP/2 を有効化し ATS をビルド、インストールする

    HTTP/2 を有効にしてビルドする際には configure に --enable-http2 オプションを渡す必要があります。下記のようにすればビルドできます。 (事前に ATS のビルドに必要なパッケージをインストールしておいてください。)

$ autoreconf -if
$ ./configure --enable-http2
$ make all && make install
  1. ATS の設定ファイルを編集する

    ATS 全体の設定や秘密鍵、証明書のパス、リクエストのマッピングに関する設定などを適切に記述します。 詳細についてはここでは触れません。 ドキュメント を参考にしてください。 動作確認をするだけであれば records.config , remap.config , ssl_multicert.config を編集すれば OK です。

    ちなみに records.config に下記のような設定値を記述することよって HTTP/2 に関連する ATS のデバッグログ(やり取りされる HTTP/2 フレームの内容など)を出力させられます。

CONFIG proxy.config.diags.debug.enabled INT 1
CONFIG proxy.config.diags.debug.tags STRING http2
  1. ATS を起動する

    設定が適切にされていれば、下記コマンドで ATS が起動できるはずです。

$ trafficserver start
  1. HTTP/2 に対応したクライアントで通信してみる

    HTTP/2 に対応したクライアントの実装として、2014年06月現在 Google Chrome CanaryFirefox Nightly Builds 、本手順の 2. で登場した nghttp2 などがあります。 Google Chrome Canary を使用する場合は SPDY 対応状況をアドレスバー上のアイコンで通知する SPDY indicator という拡張が HTTP/2 通信しているかの判別に便利です。 HTTP/2 通信できている場合、下の図2.のように青色の稲妻アイコンが表示されます。(下図の動作確認時は試験のため www.yahoo.co.jp ドメインを ATS を動作させているサーバーの IP アドレスに割り当てるよう hosts の設定を書き換えています。)

図2. Chrome Canary を利用して HTTP/2 で Yahoo! JAPAN トップページを閲覧した際の画面

この画面で青色の稲妻アイコンをクリックすると、図3. のようなSPDY, HTTP/2 のセッション情報を閲覧できる画面に遷移します。 Protocol Negotiated の項目が h2-12 となっており、 HTTP/2 のドラフト番号12で通信できていることが示されています。

図3. トップページにアクセスした際の HTTP/2 セッション情報

nghttp2 クライアントを用いて HTTP/2 通信を行う場合、 -v オプションを付けることで図4. の通りにやり取りされている HTTP/2 バイナリフレームの内容を確認できます。

図4. nghttp2 クライアントを用いた HTTP/2 通信のテスト

手順の 6. で ATS のデバッグ出力を有効にしている場合、 ATS のログ出力先ディレクトリの traffic.out というファイルに下記のようなログが出力されます。 RECV HEADERS frame や SEND SETTINGS frame などの出力から、送受信している HTTP/2 フレームの種類やその内容が分かります。

[Jun 12 17:55:21.605] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:181 (state_session_readwrite)> (http2) ----[WRITE EVENT]
[Jun 12 17:55:21.605] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:223 (http2_process_write)> (http2) ----TOTAL SEND (sm_id:0, total_size:0, total_send:0)
[Jun 12 17:55:21.605] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:193 (state_session_readwrite)> (http2-event) ++++Http2ClientSession[0], EVENT:101, ret:0, nr_pending:1
[Jun 12 17:55:21.605] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:174 (state_session_readwrite)> (http2) ++++[READ EVENT]
[Jun 12 17:55:21.607] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:193 (state_session_readwrite)> (http2-event) ++++Http2ClientSession[0], EVENT:100, ret:0, nr_pending:1
[Jun 12 17:55:21.607] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:174 (state_session_readwrite)> (http2) ++++[READ EVENT]
[Jun 12 17:55:21.607] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:100 (http2_show_ctl_frame)> (http2) ++++RECV SETTINGS frame (sm_id:0, flag:0, length:10, niv:2)
[Jun 12 17:55:21.607] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:102 (http2_show_ctl_frame)> (http2)     (3:100)
[Jun 12 17:55:21.607] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:102 (http2_show_ctl_frame)> (http2)     (4:65535)
[Jun 12 17:55:21.607] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:109 (http2_show_ctl_frame)> (http2) ++++RECV HEADERS frame (sm_id:0, stream_id:1, flag:5, length:50)
[Jun 12 17:55:21.607] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:141 (http2_fetcher_launch)> (http2) ++++Request[0:1] https://www.yahoo.co.jp/
[Jun 12 17:55:21.608] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:193 (state_session_readwrite)> (http2-event) ++++Http2ClientSession[0], EVENT:100, ret:0, nr_pending:1
[Jun 12 17:55:21.608] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:181 (state_session_readwrite)> (http2) ----[WRITE EVENT]
[Jun 12 17:55:21.608] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:176 (http2_send_callback)> (http2) ----http2_send_callback, length:8
[Jun 12 17:55:21.609] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:100 (http2_show_ctl_frame)> (http2) ----SEND SETTINGS frame (sm_id:0, flag:1, length:0, niv:0)
[Jun 12 17:55:21.609] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:193 (state_session_readwrite)> (http2-event) ++++Http2ClientSession[0], EVENT:101, ret:0, nr_pending:1
[Jun 12 17:55:21.683] Server {0x7ffff5317700} DEBUG: <Http2ClientSession.cc:246 (http2_process_fetch)> (http2) ----[FETCH HEADER DONE]
[Jun 12 17:55:21.683] Server {0x7ffff5317700} DEBUG: <Http2ClientSession.cc:287 (http2_process_fetch_header)> (http2) ----nghttp2_submit_headers
[Jun 12 17:55:21.683] Server {0x7ffff5317700} DEBUG: <Http2ClientSession.cc:193 (state_session_readwrite)> (http2-event) ++++Http2ClientSession[0], EVENT:-2, ret:0, nr_pending:1
[Jun 12 17:55:21.683] Server {0x7ffff5317700} DEBUG: <Http2ClientSession.cc:256 (http2_process_fetch)> (http2) ----[FETCH BODY DONE]
[Jun 12 17:55:21.683] Server {0x7ffff5317700} DEBUG: <Http2ClientSession.cc:366 (http2_process_fetch_body)> (http2) ----nghttp2_submit_data
[Jun 12 17:55:21.684] Server {0x7ffff5317700} DEBUG: <Http2ClientSession.cc:193 (state_session_readwrite)> (http2-event) ++++Http2ClientSession[0], EVENT:-4, ret:0, nr_pending:1
[Jun 12 17:55:21.684] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:181 (state_session_readwrite)> (http2) ----[WRITE EVENT]
[Jun 12 17:55:21.684] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:176 (http2_send_callback)> (http2) ----http2_send_callback, length:391
[Jun 12 17:55:21.684] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:109 (http2_show_ctl_frame)> (http2) ----SEND HEADERS frame (sm_id:0, stream_id:1, flag:4, length:383)
[Jun 12 17:55:21.684] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:318 (http2_read_fetch_body_callback)> (http2)     stream_id:1, call:0, length:4096, already:4008
[Jun 12 17:55:21.684] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:330 (http2_read_fetch_body_callback)> (http2) ----Request[0:1] https://www.yahoo.co.jp/ 78 4008
[Jun 12 17:55:21.685] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:54 (clear)> (http2) ****Delete Request[0:1]
[Jun 12 17:55:21.685] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:176 (http2_send_callback)> (http2) ----http2_send_callback, length:4016
[Jun 12 17:55:21.685] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:193 (state_session_readwrite)> (http2-event) ++++Http2ClientSession[0], EVENT:101, ret:0, nr_pending:1
[Jun 12 17:55:21.687] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:174 (state_session_readwrite)> (http2) ++++[READ EVENT]
[Jun 12 17:55:21.687] Server {0x7ffff2f26700} DEBUG: <Http2Callbacks.cc:121 (http2_show_ctl_frame)> (http2) ++++RECV GOAWAY frame (sm_id:0, last_stream_id:0, flag:0, length:8
[Jun 12 17:55:21.687] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:193 (state_session_readwrite)> (http2-event) ++++Http2ClientSession[0], EVENT:100, ret:0, nr_pending:1
[Jun 12 17:55:21.687] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:174 (state_session_readwrite)> (http2) ++++[READ EVENT]
[Jun 12 17:55:21.687] Server {0x7ffff2f26700} DEBUG: <Http2ClientSession.cc:129 (clear)> (http2-free) ****Delete Http2ClientSession[0], last event:104, nr_pending:0

現在の進捗と今後の展望

最低限の HTTP/2 通信を行うための実装は行えましたが、 HTTP/2 特有の機能で未実装な部分がまだまだあります。 例えばストリームの優先度の扱いやフロー制御、サーバー側から自発的にコンテンツを配信するサーバープッシュ機能などがまだサポートできていません。 HTTP/2 というプロトコル自体、まだ仕様がドラフトの段階なので今後追加で実装が必要になる仕様が追加される可能性があります。 また今回ご紹介した実装についても今後の改善方針により中身が大きく変わる可能性もあります。

今後は HTTP/2 特有の機能の実装を進めつつ、 ATS コミュニティの方々と議論しつつこの HTTP/2 サポート作業を進めていこうと考えております。 また、仕様が固まってくればより多種のクライアント実装が現れてくるものと思われるので、それらを用いてうまく通信できるかなど接続実験を実施していこうと思います。

HTTP/2 は twitter.com で既に導入されていたり、 GREE Engineers' Blog で言及されるなど注目度の高いプロトコルです。 Yahoo! JAPAN でも今後各種サービスで HTTP/2 を利用可能にしたいと考えております。 なにか進展がありましたら、またこのブログで紹介致します。

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加