このエントリは GREE Advent Calendar 2014 15日目の記事になります。
初めてのHTTP/2サーバプッシュ
はじめに
こんにちは、インフラストラクチャ本部の後藤です。
前回はWebサイトをHTTP/2に対応するためにリバースプロキシを検証した記事を書かせていただきました(HTTP2を試してみる)。
あれから幾つかの議論を経てHTTP/2の仕様も大分安定してきており、HTTP/2を実装したクライアントや実験的にHTTP/2を有効にしているサービスもあるので実際に試すことも出来ます。
そこで今回は応用編としてHTTP/2のサーバプッシュについて、その仕組と実際に試したことについて書かせていただきます。
余談ですが、 現在の仕様では "HTTP2.0" ではなく "HTTP/2" もしくは "HTTP2" が正しい名称になります。
HTTP/2概要
まず、軽くHTTP/2の概要に触れておきます。
HTTP/2は2012年の末頃より、HTTP/1のセマンティクスを維持したままパフォーマンスを改善する目的で議論が開始されました。
Googleの考案したSPDYと言うプロトコルをベースにしており、一つのTCPコネクション上でHTTPリクエストやHTTPレスポンスをフレームというメッセージとして多重化して送受信します。
単にメッセージを多重化するだけでなく、以下の機能を持っています
- ヘッダ圧縮:ハフマン符号や辞書データを用いることで、HTTPヘッダを少ないデータで表現する(h2-14でかなり簡略化されました)
- 優先付け:クライアントがリクエストに優先度を付加することが出来る
- フロー制御:各通信単位でフロー制御が行える
- サーバプッシュ:サーバはリクエストが無くてもレスポンスを返せる
どの機能も非常に面白いのですが、今回はその中のサーバプッシュについて詳しく説明していきます。
サーバプッシュの概要
HTTP/1では、サーバはクライアントからのリクエストがあって初めてレスポンスを返すことが出来ました。HTTP/2では、サーバはクライアントからのリクエストが無くてもレスポンスを返すことが出来ます。
HTTPのレスポンスをプッシュするというのが少々イメージが湧きづらいかと思いますが、どういう事か以下の図で説明します。
まずクライアントはサーバにindex.htmlをリクエストします。この時リクエストされたindex.htmlはhoge.jsを読み込むhtmlだったとします。
リクエストを受信したサーバはindex.htmlの中身を知っているので、次にクライアントからhoge.jsがリクエストされる事が予想できます。そのような場合にサーバプッシュを使います。
サーバプッシュをすることでクライアントからの要求なしにhoge.jsのレスポンスを返すことが出来ます。
こうすることで、クライアントはhtmlをパースしてから改めてリクエストを送信するよりも早くhoge.jsを受け取れる事になり、Webページを表示するのに待たされる時間も短くなります。
サーバプッシュを体感する
以上の説明は概要になります。では、実際にサーバプッシュするサーバを構築し、どのようなメッセージがやりとりされているか見てい行きましょう。
今回は、nghttp2のpythonバインドを使用し、簡単なサーバを作ってみることにします。
まず、環境を構築します。今回利用した環境は Ubuntu 14.04になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#パッケージ入れる sudo apt-get update sudo apt-get install git vim tcpdump sudo apt-get install make binutils autoconf automake autotools-dev libtool pkg-config zlib1g-dev \ libcunit1-dev libssl-dev libxml2-dev libevent-dev libjansson-dev libjemalloc-dev \ cython python3.4-dev #ビルドする git clone https://github.com/tatsuhiro-t/nghttp2.git cd ./nghttp2 autoreconf -i automake autoconf ./configure PYTHON=/usr/bin/python3.4 make sudo make install /sbin/ldconfig #python側のセットアップ cd ./python sudo python3.4 ./setup.py install |
環境構築が出来たところで、サーバプッシュを体感するために簡単なサーバを作成します。
リクエストが来たら、index.htmlを返し、さらにhoge.jsをプッシュする簡単なサーバです。
比較のために、プッシュを無効化する(do_push = False)にも出来る仕組みになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import nghttp2 class Handler(nghttp2.BaseRequestHandler): self.do_push = True def on_headers(self): js_file = open('./hoge.js', encoding='utf-8') js = js_file.read() #hoge.jsへのリクエストに対しhoge.jsを返す(do_push = Falseの時用) if self.path.decode("utf-8") == "/hoge.js": self.send_response(status=200, headers = [('content-length', str(len(self.js)))], body=js) #index.htmlへのリクエストに対しindex.htmlをレスポンスし、hoge.jsをpushする else: if self.do_push: self.push(path='/hoge.js', status=200, headers = [('content-length', str(len(self.js)))], body=js) html_file = open('./index.html', encoding='utf-8') html = html_file.read() self.send_response(status=200, headers = [('content-length', str(len(html)))], body=html) #ただし、ブラウザでアクセスする際はsslを有効にする必要があります server = nghttp2.HTTP2Server(('10.0.2.15', 80), Handler, ssl=None) server.serve_forever() |
ちなみに使用するindex.htmlの中身は以下のようにhoge.jsを読み込むだけのものになります。
1 2 3 4 |
<!DOCTYPE html> Let's enjoy http/2! <script src="./hoge.js"></script> </html> |
ネットワーク遅延のある環境下でGoogle Chromeのデベロッパーツールでサーバプッシュをする場合としない場合の様子を見て行きましょう。
ロード時間の縦軸合ってないことに注意して下さい。
do_push = True | ||
---|---|---|
do_push = False |
プッシュあり:最初のindex.htmlとともにhoge.jsも受け取っており、実際にリクエストが行われるときはキャッシュから読み込まれています。そのため、一瞬でhoge.jsを読み込めています。
プッシュなし:index.htmlを受け取ってから改めてhoge.jsをリクエストしているため遅くなっています。
このように、サーバプッシュを行うことでラウンドトリップ回数が減り、待ち時間が短くなっていることが分かります。
サーバプッシュの中身を覗く
サーバプッシュ出来たので、実際にどのようなメッセージがやりとりされているか気になる方も居ると思います。
そのような人のために、どのような仕組みでサーバプッシュが実現されているか少々深堀りしてみましょう。
HTTP/2のフレームレベルの話しになりますので、簡単に説明しておきます。冒頭でも触れたとおり、HTTP/2ではHTTP/1のセマンティクスを維持しておりますが、そのメッセージの送信方法はバイナリ形式のフレームで送受信されています。
各フレームはストリームという単位で管理されており、HTTP/1でいう一対のHTTPリクエスト・HTTPレスポンスが一つのストリーム上で行われるイメージです。HTTPリクエストをするたびに新しいストリームが生成されます。
ストリーム上で送受信されるフレームには幾つか種類があり、そのうち一部を紹介しておきます
- HEADERSフレーム : HTTPヘッダの情報が格納されるフレーム。実際はヘッダ圧縮された形式で表現される。
- DATAフレーム : HTTPボディの情報が格納されるフレーム。
- SETTINGSフレーム : 通信する上でのパラメータを通知するフレーム。
- PUSH_PROMISE : サーバプッシュをするために想定リクエストヘッダと、レスポンスのためのストリームを予約する。
以上を踏まえて、サーバプッシュを行う際にどのようなフレームが送受信されているのかを説明していきます。(HTTP/2を利用する上での接続で手順は省略しています)
- 最初のメッセージになります。このフレームはサーバプッシュとは関係なく、通常のリクエストになります。
HEADERSフレームはHTTPリクエストのヘッダが格納されているフレームになります。HTTP/2ではHTTP/1で使用していたヘッダがそのまま使われていますが、一部「:」で始まる擬似ヘッダが定義されています。
例えば以下の様なメッセージが送信されています(ただし、実際はヘッダ圧縮されています)12345:authority localhost:method GET:path /index.html:scheame httpsuser-agent test-client - サーバから、サーバプッシュのためにPUSH_PROMISEフレームが送信されます。このフレームではレスポンスを送信するために使用するストリームの予約及び、想定するHTTPリクエストに関する情報が含まれています。
上記例では、サーバプッシュ用にstream id = 2を予約しています。
また、実際は行われていませんが、どのようなリクエストに対するレスポンスなのか明示するために以下のようなヘッダ情報が付加されています(実際はヘッダ圧縮されています) 。
このヘッダ情報を見てクライアントは、キャッシュを行い、以下の様なリクエストを行う際はプッシュされたリソースを使うことが出来ます。1234:authority localhost:method GET:scheme https:path hoge.js - (1)でクライアントからのリクエストに対するレスポンスになります。HTTP/1.1のレスポンスヘッダはHEADERSフレームで表現されます。
例えば以下の様なメッセージが送信されています(ただし、実際はヘッダ圧縮されています)12:status 200server simple-push-serverDATAフレームは、HTTPレスポンスボディ相当の情報が格納されています
1234<!DOCTYPE html>Let's enjoy http/2!<script src="./hoge.js"></script></html>> - (2)で確保されたストリームでサーバプッシュで送るデータのレスポンスヘッダ・レスポンスボディ情報が格納されています。
12:status 200server simple-push-server12//hoge.jsの中身<em>console</em>.<em>log</em>("hoge");
以上のようにサーバからレスポンスがプッシュされています。
また、今回は説明しませんでしたが、フレームには制御用のフラグや、プライオリティ・パディングなども設定される場合があります。
さらに詳しく知りたい方は、Wiresharkでは既にHTTP/2に対応しています(開発版だとdraft14対応)ので、パケットをキャプチャすることでフレームのフラグやヘッダがどのように圧縮されているかなどが確認できより理解が深まりますので、是非試してみてください。
実際のサイトで試す
上記で簡単に試したpythonのコードを多少改変し、Proxy機能とプッシュのルール設定を出来るようにしまいた。
このPush+Proxyサーバを介して、普段使用している開発環境にプロキシする構成を作りました。
プッシュのルールは今回はシンプルに以下のように、リクエストされたhost名 + pathから、プッシュすべきファイルを対応付けたyamlを読み込む簡単なものです。
以下のようなシンプルなyamlファイルで、「https://localhost/」にアクセスした場合に「/css/portal.css」,「/js/jquery.min.js」をpushする事を意味しています。
|
PC版GREEのトップページで、css,jsファイルをプッシュされる様子をChromeのデベロッパーツールで見てみましょう。
pushあり | |
---|---|
pushなし |
最初の実験と同様、cssとjsが一瞬でロードされていることが確認できると思います。
index.htmlを中継しながら、pushの準備を開始しているわけですが、遅くなることもなくラウンドトリップ回数を減らすことができています。
おわりに
今回はサーバプッシュで少し遊んでみました。
簡単な例ではありましたが他にも、htmlを動的に解釈しプッシュする、プライオリティを加味する、はたまたHTTPライブストリーミングと合わせるといったことも出来るでしょう。
ただ、やはり実際のサービスに導入するのには考えなければならないことも沢山あります。
- アプリケーション側でのHTTPS対応
- 負荷分散装置・ファイアウォールといったネットワーク中間装置の問題
- 1つのTCPコネクションの生存時間が長くなることによる、gracefull restart時や、DNSレコードを用いたサーバ切り替えへの影響
- モニタリング・監視項目の検討・追加
- ログの粒度と可読性の問題
- 仕様で指定された暗号スイートのサポート、ALPNのサポート
興味を持っている方がいれば是非お話したいですし、試してみたという話があれば是非情報交換させてい頂きたいです。
このブログでもそういった話が出来るようにこれからも頑張っていきますので、よろしくお願いします。
最後になりましたが、日本では有志のコミュニティがものすごく活発であり、IETFのミーティングでも 「HTTP/2 Local Activities Report in Japan」として報告されています。
イベントも行っており、先日行われたHTTP2Conferenceも大盛況でした。定期的に勉強会も行われておりますので興味ある方は参加してみるのもいいかと思います。
関連リンク
HTTP/2 仕様 : http://http2.github.io/
nghttp2 : https://nghttp2.org/
日本コミュニティ:http://http2.info