SUUMOスマホサイトでHTTP/2対応
こんにちは、SUUMOスマホサイトの開発チームに所属しているエンジニアの上野です。
今回、SUUMOスマホサイトがHTTP/2に対応しました!
PCのChromeにてhttp2-and-spdy-indicatorを追加して、SUUMOスマホサイトを開くと「青いイナズママーク()」にて確認できます。
効果 / 結果
まず結果ですが、全体的に100ms〜200ms程度、速度が改善されました。
また、ブラウザ別で結果は大きく異なり、
- FirefoxはHTTP/2の優先度制御実装が良いと言われいてるからか、Speed Indexが約半分程度に改善
- 残念ながら、スマホでのFirefoxのシェアは低い…
- Chromeではそこまでの改善効果が見られなかった
- HTTP/1.1時代のテクニックのドメインシャーディングが優先度制御に悪影響を与えている?
- 優先度制御自体、Firefoxに劣る部分がある
- Safariではそこそこの改善効果が見られた
という結果になりました。
当初はHTTP/1.1のテクニックを使用しているサイトに対してHTTP/2を導入してもそこまで効果が出ないのではないかと懸念していましたが、意外と効果がありました。
実際の構成
実際の構成が以下となります。(直接関係しない部分については省略しています)
変更前 | 変更後 |
ポイントとしては以下になります。
ELBをL4として使用
AWSが提供しているELBはHTTP/2に対応していません。(2016/07 現在)
そのためHTTP/2で通信するためには、こちらで用意したHTTP/2に対応させたプロキシサーバにてHTTPリクエストを処理する必要があります。
Proxy Protocolを使用
ELBをL4として使用すると、基本的にはクライアントのIPを取得することができなくなります。
L4ロードバランサーを使用してもクライアントのIPを取得できる仕組みがHAProxyによって策定されたProxy Protocolとなります。
こちらにProxy Protocolの説明とELBでProxy Protocolを有効にする手順が書いてあります。
Proxy – App間はHTTP/1.1通信
プロキシサーバとアプリサーバ間の通信ではHTTPのHOL Blocking問題がないため、HTTP/1.1で通信させても、
前段のHTTP/2のパフォーマンスには影響を与えないため、HTTP/1.1での通信を行わせています。
変更にあたっての検証
検証した中でも主にパフォーマンスとその他機能の2つを紹介します。
パフォーマンスに関して
こちらに検証結果とまとめてくれている素晴らしいページがあったため、参考にしました。
また自らの手でも計測を行い、
- h2oが一番早そう
- Nginxである程度チューニングを行えばh2oに匹敵する
という結論に至りました。
(負荷テストツールとしては、Golang製のVegetaがオススメです)
この検証を行っていく中で、導入することである程度速度改善されることがわかってきました。
HTTP/2 | HTTP/1.1 |
Page Load Time:1.80 [s] | Page Load Time:2.55 [s] |
しかし、このテックブログにてh2oのリバースプロキシを使用してHTTP/2の検証を行う中、静的コンテンツの読み込みが高速化されず、むしろ悪化していることが判明しました。
原因を調べたところ、容量が大きいページにおいてはHTTPのHOL Blockingの制約よりもTCPのHOL Blockingの制約の方が大きく影響し、HTTPの方が有利になる場合があることがわかりました。
(これを調べるためにwiresharkと日々戦っておりました…)
以下は容量の大きな画像を複数表示させたページに対しての測定結果です。
容量の大きなページに対しては、HTTP/1.1の方が有利であることがわかります。
今回、HTTP/2対応を行ったSUUMOスマホサイトでは、容量が大きなページはほとんどなかったため、HTTP/2導入に踏み切りました。
その他機能に関して
プロキシサーバを選定するにあたって、様々な項目を確認する必要がありました。
その中で簡単にまとめたのが以下になります。
Proxy Protocol | Reverse Proxy | Server Push | IP Restriction | mruby | HTTP/2 | Upstream | |
◯ | ◯ | ☓ | ◯ | ◯ | 1.9.5〜 | http / https | |
◯ | ◯ | ◯ | △(mruby使用) | ◯ | 0.9.0〜 | httpのみ |
これらの調査のもと、HTTP/2のメリットの一つであるserver pushなどに関しては妥協してNginxを採用することにしました。
はまったところ
NginxのupstreamとしてELBを設定
Nginxは基本的に起動時にしか名前解決を行わない仕様になっています。
なので、ELBのPublic DNSをproxy_passに設定すると、起動時に解決したIPを使用し続けます。
一方、ELBのIPは変わるため、変わったタイミングでつながらなくなる現象が発生しましたが、
Nginxのconfファイルに以下の記述することでこれを解決しました。
# ★ELBの定期的な名前解決のために設定 resolver 8.8.8.8 valid=XXs; resolver_timeout XXs; server { ...
アプリサーバでX_Forwarded_Forが上手く取得できない
前述の通り、以下のような構成にしています。
ここで、プロキシサーバにproxy_set_header X-Forwarded-For $remote_addr;と記述していたのですが、
アプリサーバのログを見るとX_Forwarded_ForにProxyのEIPが記録されていました。
Proxy Protocolを使用したとき、Nginxがよしなに$remote_addrにクライアントのIPを入れてくれると思ってたのが間違いでした。
Proxy Protocolを使用している場合、クライアントIPが格納されるのは$proxy_protocol_addrとなります。
そのため、Nginxの設定は以下のようになります。
... server { ... # X-Forwarded-ForにクライアントのIPを追加する proxy_set_header X-Forwarded-For $proxy_protocol_addr; ...
またこの時、アプリサーバ側ではX_Forwarded_ForにクライアントのIPとプロキシサーバのIPが記録されます。
一般的にX_Forwarded_Forは経由したサーバ分のIPをもつようになっているためです。
※ ちなみに、似たような項目であるX_Client_IPはクライアントのIP(終端のIP)だけしか保持させないみたいな使い分けがされるようです。
※ 使用しているFWによってはX_Forwarded_Forに複数のIPがカンマ区切りで入ることが考慮されていないものがあるので、場合によっては対応が必要かもしれません。
頻発するSegmentation Fault
Nginxで突然、エラーログにSegmentation Faultが大量に発生しました。
Nginxのリリースノートを見るとHTTP/2通信が増えるとSegmentation Faultを吐きだすというngx_http_v2_moduleのバグが原因でした。
※実際に、2,000アクセス(HTTP/2) / minで1回のSegmentation Faultが出力されていました。
HTTP/2はNginxの1.9.5からサポートされているのですが、比較的新しいものであるためなのかngx_http_v2_moduleにはバグが多いようで、
結局、1.10系を使用することでこの問題を解消しました。(1.10系以降は比較的、HTTP/2関連が安定してきているようです)
HTTP/2のブラウザサポート
検証時にある日突然、ChromeでHTTP/2通信が行われなくなってしまいめちゃくちゃ焦りました。
調べたところ、原因はTLSネゴシエーションの仕組みにありました。
そもそも、TLSネゴシエーションの仕組みには以下の2つがあります。
- ALPN(Application Layer Protocol Negotiation)
- NPN(Next Protocol Negotiation)
(※両者の違いについては、こちらを参照)
HTTP/2自体はNPN / ALPNをサポートしているのですが、どちらで通信するかはサーバとブラウザの両方に依存します。
サーバ側でこのネゴシエーションの仕組みは、opensslで実現されており、ALPNはopenssl1.0.2以降にサポートされています。
そして今回構築していたプロキシサーバでは、openssl1.0.1を使用していました…
一方、Chromeは以前までNPN / ALPNをサポートしていたのですがVersion 51以降、NPNを捨ててALPNに一本化を図りました。
そのため、バージョン51への自動アップデートが行われたタイミングでHTTP/2が無効になってしまいました…(泣)
(調べてみるとNginx本家のサイトでも話題にされていました → Supporting HTTP/2 for Google Chrome Users)
この問題の解決のためにopenssl1.0.2のビルドインストールが必要かと思われたのですが、
Nginxには以下のビルドオプションがあり、これを与えてビルドすることでなんとかなりました。
--with-openssl=/path/to/openssl-1.0.2h
(※以外にも本家のドキュメントにこのオプションは記載されていませんでした…)
最後に
HTTP/2は比較的新しい技術でもあり、検討段階でも多くの考慮事項がありましたが、結果としてはある程度の改善効果が得られました。
現状、速度改善効果がブラウザ依存しますが、今後はChromeを始めHTTP/2の対応を進めていくと思われ、ますますHTTP/2による恩恵は大きくなってくるものと思われます。
みなさまもHTTP/2対応を検討してみてはどうでしょうか?
参考
- Head of Line Blocking – High Performance Web 2015
- ウェブを速くするためにDeNAがやっていること – HTTP/2と、さらにその先
- OpenSSL の ALPN/NPN API の使い方