第1話: HTTP/2 の雑多なメモ 〜 REAL WORLD HTTP第7章より 〜
yagisuke.hatenadiary.com の第1話です。
HTTP/2って
ワーキンググループにおいて HTTP/2.0
で制定作業をしていたが、
後に HTTP/2
に名称を置き換えることになりました。
参考: https://ja.wikipedia.org/wiki/HTTP/2#.E5.90.8D.E7.A7.B0
マイナーアップデートなんてしてられないぜってことかな。
大型アップデートだがこれまで通り
HTTP/2はHTTP1.1から16年ぶりの大きなアップデートでした。
HTTP/2においても「メソッド」、「ヘッダー」、「ステータスコード」、「ボディ」という
HTTPの提供する4つの基本要素は変わらない。
基本要素を忘れちゃった方はこちら: http://www.tohoho-web.com/ex/http.htm
目的は高速化
HTTP/2の目的は高速化に尽きます。
HTTP/1.0からHTTP/1.1にかけて、TCPソケットレベルで見ると次のような改善が行われてきました。
機能 | 効果 |
---|---|
キャッシュ(max-age) | 通信そのものをキャンセル |
キャッシュ(ETag, Date) | 変更がなければボディ送信をキャンセル |
Keep-Alive | アクセスごとに接続にかかる時間(1.5TTL)を削減 |
圧縮 | レスポンスのボディサイズの削減 |
チャンク | レスポンス送信開始を早める |
パイプライニング | 通信の多重化 |
1回の通信では、接続の確立リクエスト送信、受信と何往復もパケットが飛び交います。 また、データサイズを通信速度で割った時間だけ通信時間がかかります。 通信待ちがあれば、その分通信完了までの時間は長くなります。
これまで行われた高速化はこの通信のあらゆる箇所での高速化に寄与してきました。
HTTP/2ではこれまで手がつけられてこなかった「ヘッダー部の圧縮」や、
規格化されたもののいまひとつ活用されていなかった「パイプライニングの代替実装」が追加されました。
端的にまとめてくれている記事: http://blog.redbox.ne.jp/http2-cdn.html#HTTP11
ストリームによる通信の高速化
バイナリフレームの採用
HTTP/2の一番の大きな変化は、テキストベースのプロトコルから バイナリベースへのプロトコルに変化したという点です。
コンピューターは、バイナリしか理解できないので、 テキストで送られてきたらデータを解析する必要があり、 データを解析するオーバーヘッドが解消されたという訳です。
画像: http://webcommu.net/http2/
各データは「フレーム」と呼ばれる単位で送受信が行われます。
TYPE | FRAME TYPE | ROLE |
---|---|---|
0 | DATA | リクエスト/レスポンスのボディ部分に相当 |
1 | HEADERS | リクエスト/レスポンスのヘッダー部分に相当 |
2 | PRIORITY | ストリームの優先順位を指定(クライアントのみ送信可能) |
3 | RST_STREAM | エラーなどの理由でストリームを終了するために用いる |
4 | SETTINGS | 接続設定を変更する |
5 | PUSH_PROMISE | サーバプッシュを予告します(サーバのみ送信可能) |
6 | PING | 接続の生存状態を調べる |
7 | GOAWAY | エラーなどの理由で接続を終了するために用いる |
8 | WINDOW_UPDATE | ウインドウサイズを変更する |
9 | CONTINUATION | サイズの大きなHEADERS/PUSH_PROMISEフレームの断片 |
ストリームの多重化
これまでの問題
HTTPの問題
従来のHTTPでは、リクエストとレスポンスの組を1つずつしか同時に送受信できないことが、 パフォーマンス上のボトルネックになっています。
パイプライニングの欠点
このような制約のなか、HTTP/1.1では前回のリクエストの完了を待たなくてもいい「パイプライニング」 という仕組みがありました。パイプライニングを使うとクライアント側は前回のリクエストのレスポンスが 返ってくる前に余裕があれば、次のリクエストを送ることができます。
しかし、サーバー側はクライアントからのリクエストの順番通りに返さなくてはいけなく、 どこかで重いリクエストが来たらその処理以降のレスポンスがブロックされてしまいます。 これを「ヘッドオブライン・ブロッキング」問題と言います。
そして残念なことに、現在ほとんどのブラウザはパイプライニング機能を全く実装していないか、 デフォルトでオフにしています。 これには、実装の困難さから、 パイプライニングを正しく実装したサーバが少ないという事情があるようです。
画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.20
更に、ブラウザから同時接続できるTCP接続数は最大6本であるため、 Webサイトに画像が100枚あっても、同時にリクエストとレスポンスできる画像は、たった6枚。 非常にリソースの無駄と考えたGOOGLE先生はHTTP/1.1を進化させるために頑張ることになります。
ストリームの多重化をすることで
リクエストとレスポンスの組を1つずつしか同時に送受信できないという問題に対して、 HTTP/2では1本のTCP接続の内部に、ストリームという仮想のTCPソケットを作って通信を行うことで対応しています。
ストリームはフレームに付随するフラグで簡単に作ったり閉じたりできるルールになっており、 通常のTCPソケットのようなハンドシェイクは必要ありません。 そのため、IDの数値とTCPの通信容量が許す限り、簡単に数万接続でも並列化できます。
HTTP/1.1
画像: http://webcommu.net/http2/
HTTP/2
画像: http://webcommu.net/http2/
要するに、あるストリーム上ではリクエストにあたるフレームが送信中だとしても、 別のストリームではレスポンスにあたるフレームを受信するといったことが可能になります。 これにより、全体的なパフォーマンスが向上し以下のようなリクエストとレスポンスが可能となりました。
画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.20
ストリームの優先度
フロントエンドとしてはファイルの読み込む順番はきになるもの。
ストリームの多重化によりヘッドオブライン・ブロッキングの問題は解決しましたが、 あまり重要でないストリームが先にリソースを占有してしまい、 重要なストリームが返却を待たされてしまう可能性があります。
これを解決するために、HTTP/2ではクライアントがPRIORITYフレームを用いて、 ストリーム間に優先順位を付けることが可能となりました。 これはあくまでもリソースに余裕がないときの動作で、 後続のストリームをブロックするものではありません。
(ちなみに、PRIORITYフレームの送信が許されているのはクライアント側のみ)
参考: https://qiita.com/Jxck_/items/16a5a9e9983e9ea1129f
HTTP/2のアプリケーション層
ヘッダーとボディだけ
HTTP/2においても「メソッド」、「ヘッダー」、「ステータスコード」、「ボディ」という基本要素が 存在するのは変わりありませんが、メソッドとパス、ステータスコードとプロトコルバージョンは すべて擬似ヘッダーフィールド化され、ヘッダーの中に組み込まれました。 上位のアプリケーションから見た機能性には変更ありませんが、 実装上は「ヘッダー」「ボディ」しかありません。
リクエストメッセージ
画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.16
レスポンスメッセージ
画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.17
高度な並列化もどんとこい
HTTP/1.1はテキストプロトコルでした。 ヘッダーの終端を探すには空行を見つけるまで1バイトずつ先読みして発見する必要がありました。 エラー処理などもあるため、サーバーとしてはパースまで込みで逐次処理をせざるを得ず、 高度な並列化は難しかったようです。
HTTP/2はバイナリ化され、最初にフレームサイズがはいっています。 TCPソケットのレイヤーでは、データをフレーム単位へと簡単に切り分けられるため、 受信側のTCPソケットのバッファを素早く空にでき、通信相手に対して、 次のデータを高速にリクエストできるようになっています。
フローコントロール
フローコントロールとは、ひとつのストリームがリソースを占有してしまうことで、 他のストリームがブロックしてしまうことを防ぐ仕組みです。
具体的な状況はいくつか考えられます。 - 大きなファイルの通信が帯域を食いつぶし、他の通信を妨害する。 - あるリクエストの処理にサーバが占有され、他のリクエストをサーバが処理できなくなる。 - 高速なアップロードを行うクライアントと、低速な書き込みをしているサーバとの間に挟まった プロキシが、調整のためにデータを貯めているバッファが溢れる。
こうした状況を防ぐために、HTTP/2ではTCPと、その上に多重化されたストリームの両者について、 フローコントロールする仕組みを備えています。
実際にはウィンドウサイズという閾値を設定し、この値の範囲内であればデータを送ることができ、 その値を使い切った場合、送信者はデータ送信を停止します。 受信者は、リソースが回復したことをWINDOW_UPDATEというフレームで通知し、 そこに設定された分の値だけウィンドウサイズを回復されることで、 送信者はデータの送信を再開するというものです。
詳細はこちらがオススメ: https://qiita.com/Jxck_/items/622162ad8bcb69fa043d
サーバープッシュ
HTTP/1.1では、ブラウザーがページを要求するとサーバーはHTMLを応答として送信しますが、 サーバーがHTMLに埋め込まれているJavaScript、画像、CSSのアセットを送信するには、 ブラウザーがHTMLを解析し、それらのリクエストを送信するまで待たなければなりません。
HTTP/2では、クライアントからのリクエスト内容をもとに、 サーバー側で必要ファイルを判別して事前にクライアントに送信できます。 例えば、リクエストしたHTMLにJavaScriptファイルの読み込み記載があった場合、 そのJavaScriptファイルを最初のHTMLリクエストの段階で送信しておくといった具合になります。
画像: https://www.digicert.ne.jp/ssl/http2.html
HPACKによるヘッダーの圧縮
gzip圧縮の予定だった
HTTP1.xでは、CookieやUserAgentといったHTTPヘッダーがなんども同じ内容で送信されており、 オーバーヘッドが生じています。
そこでHTTP/2の前進であるSPDYでは当初、ヘッダーの圧縮にgzipを用いていましたが、 後に「CRIME」と呼ばれる攻撃手法が発見され、 圧縮率をさげざるをえなくなってしまいました。
CRIMEについて: https://www.scutum.jp/information/waf_tech_blog/2012/09/waf-blog-014.html
HPACKの誕生
そこでHTTP/2では、ヘッダーの差分だけを送るアルゴリズムを使ったHPACKという 圧縮機構を採用しました。
ただ肝心のCRIMEの脆弱性対策ですが、gzipよりも困難になりましたが、 完全に対策できているものにはなっていないそうです。 結局フレームにパディングを入れてサイズをごまかしたり、 機密度の高いヘッダー情報は圧縮用にインデックスしないなどHTTP/2時代でも CRIME攻撃を意識してHPACKを使わないといけない状況なんだそうです。
参照: http://d.hatena.ne.jp/jovi0608/20150527/1432723310
HPACKで使われる用語
Reference Set
前回送信したヘッダー名/値ペアのセット。クライアントやサーバーは、 ヘッダーの差分を計算するために接続毎にリファレンスセットを保持します。
Static Table
よく送信されるヘッダー名/値ペアにIDを設定したものを管理するテーブルです。 クライアントとサーバーの両方で保持されます。 送信するヘッダーがこのテーブルに含まれていた場合は、 設定されたIDを使用してヘッダーを送信することができます。
フロー
画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.24
画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.25
SPDYとQUIC
SPDY
SPDYはGOOGLE先生が開発していたHTTPの代替プロトコルでこれがそのままHTTP/2となりました。 2010年からCHROMEにSPDYを搭載し、2014年にHTTP/2にバトンタッチしました。
GOOGLE先生がSPDYを開発したのは、これまでのHTTPが改善してきた転送速度を一段と向上させるためです。 ウェブサイトの構成によって効果が大きく変わるため、導入の効果としては30%から、3倍以上まで、 色々な数値があげられましたが、並列アクセスでのブロッキングが減るため、 小さなファイルをたくさん転送するほど高速化します。
HTTP/1.x時代ではJavaScriptやCSS、画像などなるべく少ないファイル数にまとめることで 高速化させるというのが常でした。
SPDYとHTTP/2時代では、ファイルをまとめる効果は小さくなります。 圧縮されているとはいえ、サイズは0ではないので、結合した方が通信量は減りますが、 細かい方が変更があったときもキャッシュが生き残る確率は上がります。 なので、結合してもしなくてもどちらも一長一短で差はあまりないかもしれません。
QUIC
SPDYなりQUICなり、早くして〜って感じ。
QUICが登場した背景
HTTP/2はTCP上で転送されるプロトコルであることに起因した問題が残されています。
このTCP接続を開始するためには、3wayハンドシェイクが行われています。 つまりこれは、接続を開始するたびにラウンドトリップ(ネットワークパケットの往復)が 追加されるということであり、新たな接続先に対し大幅な遅延が発生します。
それに加えて、暗号化された安全なHTTP接続を行うために、 TLSとのネゴシエーションも必要ということになると、 さらに多くのパケットをネットワーク間でやり取りしなければなりません。
一方、UDPはFire and Forget(撃ちっ放しの)プロトコルだと言えます。 メッセージがUDPで送信されると、後は宛先に到達するものだと想定されています。 なので、ネットワークでパケットを検証するために費やされる時間が少なくなります。 ただし欠点もあり、信頼性を確保するには、パケットのデリバリ確認ができるよう、 UDPの上に何かを構築しなければいけません。
そこで、登場するのがQUICプロトコルです。 QUICプロトコルは、接続を開始すると、全てのTLS(HTTPS)パラメータを1つか2つのパケットで ネゴシエートすることができます。 QUICにおけるGoogleの狙いは、UDPとTCPの良いところを取り、 最新のセキュリティー技術と組み合わせることということです。
画像: http://jp.techcrunch.com/2015/04/20/20150418google-wants-to-speed-up-the-web-with-its-quic-protocol/
以下はHTTP/2 over TLS/TCPと、HTTP/2 over QUICの構成です。
画像: http://internet.watch.impress.co.jp/docs/column/ietf2017/1060156.html
左側がHTTP/2 over TLS/TCP、右側がHTTP/2 over QUICです。 QUICは、TCPの機能に相当する輻輳(ふくそう)制御、ロスリカバリー、 およびHTTP/2の機能の一部であるストリーム制御、フローコントロールなどを提供します。 暗号方式のネゴシエーションや鍵交換にはTLS1.3の仕組みを使うそうです。
QUICの進捗はこちら
QUIC WGでは、2018年中にQUICベースプロトコルおよびHTTP/2へのマッピングを RFCにするというマイルストーンで動いているので現在絶賛稼働中。 - git: https://github.com/quicwg/base-drafts - IETF: https://datatracker.ietf.org/wg/quic/about/
引用元や参考情報
- https://techblog.yahoo.co.jp/infrastructure/http2/introduction_to_http2/
- https://www.slideshare.net/techblogyahoo/http2-35029629
- http://www.websuppli.com/http-2/551/
- http://webcommu.net/http2/
- http://labs.gree.jp/blog/2014/12/11987/
- https://developers.google.com/web/fundamentals/performance/http2/?hl=ja
- https://summerwind.jp/docs/rfc7540/#section4-2
- http://d.hatena.ne.jp/jovi0608/20150527/1432723310
- http://postd.cc/googles-quic-protocol-moving-web-tcp-udp/
- http://internet.watch.impress.co.jp/docs/column/ietf2017/1060156.html
- http://jp.techcrunch.com/2015/04/20/20150418google-wants-to-speed-up-the-web-with-its-quic-protocol/