yagisukeのWebなブログ

フロントエンドとサーバーサイドをさまようエンジニア

第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の一番の大きな変化は、テキストベースのプロトコルから バイナリベースへのプロトコルに変化したという点です。

コンピューターは、バイナリしか理解できないので、 テキストで送られてきたらデータを解析する必要があり、 データを解析するオーバーヘッドが解消されたという訳です。

f:id:yagi_suke:20180121232213p:plain

画像: 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では前回のリクエストの完了を待たなくてもいい「パイプライニング」 という仕組みがありました。パイプライニングを使うとクライアント側は前回のリクエストのレスポンスが 返ってくる前に余裕があれば、次のリクエストを送ることができます。

しかし、サーバー側はクライアントからのリクエストの順番通りに返さなくてはいけなく、 どこかで重いリクエストが来たらその処理以降のレスポンスがブロックされてしまいます。 これを「ヘッドオブライン・ブロッキング」問題と言います。

そして残念なことに、現在ほとんどのブラウザはパイプライニング機能を全く実装していないか、 デフォルトでオフにしています。 これには、実装の困難さから、 パイプライニングを正しく実装したサーバが少ないという事情があるようです。

f:id:yagi_suke:20180121232214p:plain

画像: 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
f:id:yagi_suke:20180121232221p:plain

画像: http://webcommu.net/http2/

HTTP/2
f:id:yagi_suke:20180121232252p:plain

画像: http://webcommu.net/http2/

要するに、あるストリーム上ではリクエストにあたるフレームが送信中だとしても、 別のストリームではレスポンスにあたるフレームを受信するといったことが可能になります。 これにより、全体的なパフォーマンスが向上し以下のようなリクエストとレスポンスが可能となりました。

f:id:yagi_suke:20180121232229p:plain

画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.20

ストリームの優先度

フロントエンドとしてはファイルの読み込む順番はきになるもの。

ストリームの多重化によりヘッドオブライン・ブロッキングの問題は解決しましたが、 あまり重要でないストリームが先にリソースを占有してしまい、 重要なストリームが返却を待たされてしまう可能性があります。

これを解決するために、HTTP/2ではクライアントがPRIORITYフレームを用いて、 ストリーム間に優先順位を付けることが可能となりました。 これはあくまでもリソースに余裕がないときの動作で、 後続のストリームをブロックするものではありません。

(ちなみに、PRIORITYフレームの送信が許されているのはクライアント側のみ)

参考: https://qiita.com/Jxck_/items/16a5a9e9983e9ea1129f

HTTP/2のアプリケーション層

ヘッダーとボディだけ

HTTP/2においても「メソッド」、「ヘッダー」、「ステータスコード」、「ボディ」という基本要素が 存在するのは変わりありませんが、メソッドとパス、ステータスコードプロトコルバージョンは すべて擬似ヘッダーフィールド化され、ヘッダーの中に組み込まれました。 上位のアプリケーションから見た機能性には変更ありませんが、 実装上は「ヘッダー」「ボディ」しかありません。

リクエストメッセージ
f:id:yagi_suke:20180121232205p:plain

画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.16

レスポンスメッセージ
f:id:yagi_suke:20180121232204p:plain

画像: 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リクエストの段階で送信しておくといった具合になります。

f:id:yagi_suke:20180121232242p:plain

画像: 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を使用してヘッダーを送信することができます。

フロー

f:id:yagi_suke:20180121232233p:plain

画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.24

f:id:yagi_suke:20180121232237p:plain

画像: 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時代ではJavaScriptCSS、画像などなるべく少ないファイル数にまとめることで 高速化させるというのが常でした。

SPDYとHTTP/2時代では、ファイルをまとめる効果は小さくなります。 圧縮されているとはいえ、サイズは0ではないので、結合した方が通信量は減りますが、 細かい方が変更があったときもキャッシュが生き残る確率は上がります。 なので、結合してもしなくてもどちらも一長一短で差はあまりないかもしれません。

QUIC

SPDYなりQUICなり、早くして〜って感じ。

QUICが登場した背景

HTTP/2はTCP上で転送されるプロトコルであることに起因した問題が残されています。

このTCP接続を開始するためには、3wayハンドシェイクが行われています。 つまりこれは、接続を開始するたびにラウンドトリップ(ネットワークパケットの往復)が 追加されるということであり、新たな接続先に対し大幅な遅延が発生します。

それに加えて、暗号化された安全なHTTP接続を行うために、 TLSとのネゴシエーションも必要ということになると、 さらに多くのパケットをネットワーク間でやり取りしなければなりません。

一方、UDPはFire and Forget(撃ちっ放しの)プロトコルだと言えます。 メッセージがUDPで送信されると、後は宛先に到達するものだと想定されています。 なので、ネットワークでパケットを検証するために費やされる時間が少なくなります。 ただし欠点もあり、信頼性を確保するには、パケットのデリバリ確認ができるよう、 UDPの上に何かを構築しなければいけません。

そこで、登場するのがQUICプロトコルです。 QUICプロトコルは、接続を開始すると、全てのTLSHTTPS)パラメータを1つか2つのパケットで ネゴシエートすることができます。 QUICにおけるGoogleの狙いは、UDPTCPの良いところを取り、 最新のセキュリティー技術と組み合わせることということです。

f:id:yagi_suke:20180121232257p:plain

画像: 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の構成です。

f:id:yagi_suke:20180121232301p:plain

画像: 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/

引用元や参考情報