してみるテストロゴ
Apache 2.4系でHTTP/2サーバを構築してみるテスト。

HTTP/2のチューニング

現時点で、Apache httpdサーバのチューニングで、効果が高いのは、TCP接続の本数を減らすために、HTTP/2を利用すること、とTLSの鍵認証に使用するアルゴリズムをECDSAに変更することです。

HTTP/2に対応したインストールについてはコチラ、ECDSAに対応した証明書については、コチラ、をそれぞれご覧ください。

 

HTTP/2の細かな設定を行うディレクティブが用意されています。

このページでは、まず肝となる、HTTP/2プッシュについて説明します。

まだ書けていませんが、 このHTTP/2プッシュをうまく使うと、cssやjsファイルを優先的に配信できます。

HTTP/2のプッシュ機能

プッシュ機能は、HTTP/2の特徴の一つで、htmlファイルを取得する際に、サーバ側が把握している、依存関係にあるファイルも送ってしまおうというものです。これを使わずに「HTTP/2サーバです。」というのは、なんとも切ない話です。ということで早速使ってみましょう。

Apache 2.4.18で、H2Pushというディレクティブが追加されており、このバージョンからプッシュ機能が利用できます。H2PushはデフォルトでOnなので、以下のヘッダ情報さえつければ、利用できます。

Apache 2.4.20からは、H2PushDiarySizeによって、同一接続の間、一回プッシュしたファイルは、再度プッシュしないようになりました。

 

HTMLファイルの依存関係は、ヘッダ情報を元にサーバに伝えます。

まず、httpd.confを編集して、ヘッダモジュール(headers_module)を読み込むように設定します。

# vi /usr/local/apache2/conf/httpd.conf
/usr/local/apache2/conf/httpd.conf
…(省略)…
LoadModule headers_module modules/mod_headers.so
…(省略)…

続いて、以下のように、Headerディレクティブを使って、HTMLファイル単位で、依存関係にあるファイルを関連付けます。

# vi /usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
/usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
…(省略)…
<VirtualHost *:443>
…(省略)…
<Location /index.html>
       Header add Link "</common.cs.css>;rel=preload"
</Location>
…(省略)…
</VirtualHost>
…(省略)…

この場合、ドキュメントルートの/index.htmlが、おなじくドキュメントルートの/common.cs.cssファイルに依存していることを示しています。「;rel=preload」をつけるのをお忘れなく。

ここでは、「Header add Link…」を追加するファイルとして、httpd.confにインクルードされるファイルを編集していますが、実際には、サーバ管理者ではなく、コンテンツ運用者がいじりたい部分だと思います。その場合は、.htaccessを使う方が現実的かもしれません。

続いて、apachctl -tで、設定ファイルの間違いが無いことを確認の上、下記のように

# /usr/local/apache2/bin/apachctl -t
Syntax OK
# /usr/local/apache2/bin/apachctl stop
# /usr/local/apache2/bin/apachctl start

再起動します。これで、プッシュ機能を適用したので、nghttpでレスポンスヘッダを確認します。

-bash-4.1$ source ~/.profile_ssl
-bash-4.1$ nghttp -v https://http2.try-and-test.net/index.html
[  1.125] Connected
The negotiated protocol: h2
[  1.142] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
          (niv=1)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[  1.142] recv WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=2147418112)
[  1.142] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
…(省略)…
[  1.142] send HEADERS frame <length=44, flags=0x25, stream_id=13>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /index.html
          :scheme: https
          :authority: http2.try-and-test.net
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.5.0
[  1.143] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  1.143] recv (stream_id=13) :scheme: https
[  1.143] recv (stream_id=13) :authority: http2.try-and-test.net
[  1.143] recv (stream_id=13) :path: /common.cs.css
[  1.143] recv (stream_id=13) :method: GET
[  1.143] recv (stream_id=13) user-agent: nghttp2/1.5.0
[  1.143] recv (stream_id=13) host: http2.try-and-test.net
[  1.143] recv PUSH_PROMISE frame <length=66, flags=0x04, stream_id=13>)
          ; END_HEADERS
          (padlen=0,  promised_stream_id=2)
[  1.143] recv (stream_id=13) :status: 200
[  1.144] recv (stream_id=13) date: Sat, 12 Dec 2015 02:37:13 GMT
[  1.144] recv (stream_id=13) server: Apache/2.4.18 (Unix) OpenSSL/1.0.2e
[  1.144] recv (stream_id=13) last-modified: Fri, 11 Dec 2015 02:44:55 GMT
[  1.144] recv (stream_id=13) etag: "27e2-526964f006816"
[  1.144] recv (stream_id=13) accept-ranges: bytes
[  1.144] recv (stream_id=13) content-length: 10210
[  1.144] recv (stream_id=13) strict-transport-security: max-age=15552000
[  1.144] recv (stream_id=13) link: </common.cs.css>;rel=preload
[  1.144] recv (stream_id=13) content-type: text/html; charset=utf-8
[  1.144] recv HEADERS frame <length=168, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
<!doctype html>
<html>
…(省略)…
(HTMLファイル)
…(省略)…
</body>
</html>
[  1.144] recv DATA frame <length=10210, flags=0x01, stream_id=13>
          ; END_STREAM
[  1.144] recv (stream_id=2) :status: 200
[  1.144] recv (stream_id=2) date: Sat, 12 Dec 2015 02:37:13 GMT
[  1.144] recv (stream_id=2) server: Apache/2.4.18 (Unix) OpenSSL/1.0.2e
[  1.144] recv (stream_id=2) last-modified: Wed, 25 Nov 2015 06:24:28 GMT
[  1.144] recv (stream_id=2) etag: "7df-5255782b09281"
[  1.144] recv (stream_id=2) accept-ranges: bytes
[  1.144] recv (stream_id=2) content-length: 2015
[  1.144] recv (stream_id=2) strict-transport-security: max-age=15552000
[  1.144] recv (stream_id=2) content-type: text/css
[  1.144] recv HEADERS frame <length=61, flags=0x04, stream_id=2>
          ; END_HEADERS
          (padlen=0)
          ; First push response header
@charset "utf-8";
…(省略)…
(CSSファイル)
…(省略)…
[  1.145] recv DATA frame <length=2015, flags=0x01, stream_id=2>
          ; END_STREAM
[  1.145] send GOAWAY frame <length=8, flags=0x00, stream_id=0>

PUSH_PROMISEヘッダ情報が出て、promised_stream_id=2が割り当てられています。

これまでの動作確認では、HTMLファイルを読み込んで終了していたnghhttpコマンドの出力に、CSSファイルも含まれていることがわかります。

ログ上は、以下のようにGETしたようになります。

ww.xx.yy.zz "2015-MM-DD hh:mm:ss +0900" TLSv1.2 ECDHE-RSA-AES256-GCM-SHA384 26884 15 - GET "/index.html" "" HTTP/2 200 10210 "-"
 "nghttp2/1.5.0"
ww.xx.yy.zz "2015-MM-DD hh:mm:ss +0900" TLSv1.2 ECDHE-RSA-AES256-GCM-SHA384 26884 15 - GET "/common.cs.css" "" HTTP/2 200 2015 "-" "nghttp2/1.5.0"

なおプッシュすると、TCP通信のホップ数は削れますが、プッシュ設定したファイルは有無を言わさず送られるため、当然、ブラウザキャッシュの恩恵がなくなり、転送量としては増えてしまいます。

この対策が策定されるまでは、プッシュ機能で配信するファイルを、小さめのCSSやJavaSciptに限定するのがよいでしょう。

※2016年4月18日追記:Apache Httpd 2.4.20から、accept-push-policyリクエストヘッダへの暫定対応と、H2PushDiarySizeディレクティブの追加による、接続ごとのプッシュ履歴保持による、プッシュ抑制に対応しました。

ちなみに、プッシュ送信すると、ログ上、リファラが残りません。

リファラを出したいものは、プッシュしないようにしましょう。

 

ということで、プッシュ機能は、サーバ監理者の腕の見せ所かもしれません。

HTTP/2プッシュの優先順位

プッシュ機能は、うまく使えば、ユーザに素早くコンテンツを見せることできます。ところが闇雲に帯域を使ったのでは、ユーザの体感速度があまり変わらないこともありえます。

ここで、コンテンツタイプ毎に、送信順序や、割り当てる帯域を調整するのが、H2PushPriorityディレクティブです。つまり、依存元ファイルよりも、早めに送信した方が良いコンテンツタイプに、優先的に送信順序や、帯域を割り当てましょうというのが、このディレクティブです。

この機能を使うには、nghttp2のバージョンが1.5.0以上であることと、Apacheのバージョンが、2.4.18以上であることが必要なります。どちらもこちらでインストール方法を説明したとおりですので、まだの方は、参考にしてください。

H2PushPriorityには、MIMEタイプ、順序と、優先順位(ウェイト:whight)を設定します。MIMEタイプ毎に、順序と優先順位を決められます。順序は、after,before ,interleavedの3つがあります。

それぞれ、パターン1~3の例で説明します。

パターン1はデフォルトのプッシュ設定(after)です。依存元ファイル(この場合index.html)が送信された後に、依存先のindex.jsonとindex.cssのファイルをプッシュします。すべてのコンテンツタイプが、優先順位(ウェイト:weight)16ですので、json,cssともに、同じ16の優先順位です。ウェイトの数字は大きいほど、帯域が多く割り当てられます。最大値は256です。この場合、index.html送信後の帯域を、半分ずつ使います。

パターン2は、インターリーブ(間欠:interleaved)送信を行います。ここでは、cssファイルを256のウェイトで送信する設定になります。依存元のウェイトは明記されていませんが、index.htmlのウェイトは256として扱います。つまり、index.htmlとindex.cssに同じ帯域を割り当てて送信します。

この場合、json処理前なので、不完全ではありますが、cssファイルの情報を元に、画面を表示しますので、パターン1よりも、とりあえずの表示が速くなります。

パターン3は、依存先のjsonファイルをまず送信してから、依存元のindex.htmlを送信します。この場合、完全な表示が最も早くなります。json処理が重ければ重いほど、効果が期待できます。また、cgi/php等で、jsonを生成する場合も、beforeの方がjsonの生成の処理が優先されるため、表示が速くなることが期待できます。

個人的には、cgiやphpが絡む可能性の高いjsonは、before設定でプッシュ送信。

依存元のhtmlファイルと同時に解釈されるcssやjsはinterleavedでプッシュ送信。

判断できない物は、afterでプッシュ送信が良いと思います。

2016年4月19日追記:h2プッシュによる、ログの記録順への影響

H2PushPriorityに、interleavedもしくは、beforeの設定にすると、依存元のhtmlファイルよりも先にログファイルに記録されます。

ログ解析ソフトの中には、依存元のhtmlファイルのログが最初に来ないと挙動がおかしくなるものがあるので、ご注意ください。

もちろん、画像など大きめのファイルサイズで、ブラウザ側でキャッシュした方が効果的な物は、プッシュしないことも大切です。

なお、プッシュされるコンテンツを生成するcgi/phpには、H2PUSH環境変数が設定されます。この場合、重い処理を避けるなどの工夫をするといいかもしれません。

リクエストのHTTP/2プッシュの優先順位ポリシー

Apache2.4.20から、クライアントのブラウザは、accept-push-policyリクエストヘッダに、「none」もしくは、「default」を設定することができます。

このリクエストヘッダによって、クライアントは、ある程度H2プッシュの制御を行うことができます。

「default」は、通常のプッシュを行います。一方、「none」は、当該リクエストに限り、プッシュを停止することができます。

つまり、クライアント側が、キャッシュしている可能性の高いと判断しているhtmlコンテンツについては、accept-push-policy:noneを送ることで、帯域を節約することができるようになります。

Apache2.4.18以前は、accept-push-policy:noneをリクエストヘッダに含むんでも、以下の通り、プッシュされてしまいます。

$ nghttp -H'accept-push-policy:none' -v https://example.com
[  0.001] Connected
The negotiated protocol: h2
[  0.011] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
          (niv=1)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
 (…省略…)
[  0.011] send HEADERS frame <length=63, flags=0x25, stream_id=13>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /index.html
          :scheme: https
          :authority: http2.try-and-test.net
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.8.0
          accept-push-policy: none
[  0.011] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.012] recv (stream_id=13) :scheme: https
[  0.012] recv (stream_id=13) :authority: http2.try-and-test.net
[  0.012] recv (stream_id=13) :path: /common.cs.css
[  0.012] recv (stream_id=13) :method: GET
[  0.012] recv (stream_id=13) user-agent: nghttp2/1.8.0
[  0.012] recv (stream_id=13) host: http2.try-and-test.net
[  0.012] recv PUSH_PROMISE frame <length=66, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0, promised_stream_id=2)
[  0.012] recv (stream_id=13) :status: 200
[  0.012] recv (stream_id=13) date: Mon, 21 Mar 2016 23:40:22 GMT
[  0.012] recv (stream_id=13) server: Apache/2.4.18 (Unix) OpenSSL/1.0.2g
[  0.012] recv (stream_id=13) last-modified: Thu, 10 Mar 2016 00:30:09 GMT
[  0.012] recv (stream_id=13) etag: "8212-52da6eac16dad"
[  0.012] recv (stream_id=13) accept-ranges: bytes
[  0.012] recv (stream_id=13) content-length: 33298
[  0.012] recv (stream_id=13) strict-transport-security: max-age=15552000
[  0.012] recv (stream_id=13) link: </common.cs.css>;rel=preload
[  0.012] recv (stream_id=13) content-type: text/html; charset=utf-8
[  0.012] recv HEADERS frame <length=167, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
<!doctype html>

このように、クライアント側から、プッシュの有無を制御することはできませんでした。

Apache2.4.20からは、accept-push-policy:noneをリクエストヘッダに含むと、以下の通り、プッシュされません。

$ nghttp -H'accept-push-policy:none' -v https://example.com
[  1.729] Connected
The negotiated protocol: h2
[  1.744] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
          (niv=1)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
 (…省略…)
[  1.744] send HEADERS frame <length=63, flags=0x25, stream_id=13>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /index.html
          :scheme: https
          :authority: http2.try-and-test.net
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.9.2
          accept-push-policy: none
[  1.744] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  1.745] recv (stream_id=13) :status: 200
[  1.745] recv (stream_id=13) date: Sun, 10 Apr 2016 10:46:30 GMT
[  1.745] recv (stream_id=13) server: Apache/2.4.20 (Unix) OpenSSL/1.0.2g
[  1.745] recv (stream_id=13) last-modified: Sun, 10 Apr 2016 10:40:24 GMT
[  1.745] recv (stream_id=13) etag: "8616-5301f0e437675"
[  1.745] recv (stream_id=13) accept-ranges: bytes
[  1.745] recv (stream_id=13) content-length: 34326
[  1.745] recv (stream_id=13) strict-transport-security: max-age=15552000
[  1.745] recv (stream_id=13) link: </common.cs.css>;rel=preload
[  1.745] recv (stream_id=13) content-type: text/html; charset=utf-8
[  1.745] recv HEADERS frame <length=168, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
<!doctype html>

と、プッシュされないことがわかります。

なお、まだ対応しておりませんが、accept-push-policyには、「fast-load」と、「head」が設定できるようになる見込みです。

fast-loadは、読み込みが最短となるように、なるべくプッシュすることになります。

headは、ヘッダ情報のみをプッシュします。これによって、クライアントは、プッシュされたetagヘッダの情報などから、まだ読み込んでいないファイルを判断してリクエストを行うことができます。余計なストリームを消費しないで済みます。

fast-loadはともかく、headは待ち遠しいですね。

h2c Upgrade/Directを無効にする

非暗号のHTTP/2である、h2cについては、アップグレードモードと、ダイレクトモードがありますが、以下のディレクティブでそれぞれ無効にできます。

H2Upgrade

H2Direct

それぞれ、モジュールの互換性の問題が発生した場合に、いずれか、Offにする必要があります。

 

ダイレクトモードを無効にする方法

/usr/local/apache2/conf/httpd.confの一部
…(省略)…
LoadModule http2_module modules/mod_http2.so
<IfModule http2_module>
LogLevel http2:info
ProtocolsHonorOrder On
Protocols h2c http/1.1
H2Direct off
</IfModule>
…(省略)…

 

アップグレードモードを無効にする方法(H2UpgradeはApache2.4.18から利用できます。)

/usr/local/apache2/conf/httpd.confの一部
…(省略)…
LoadModule http2_module modules/mod_http2.so
<IfModule http2_module>
LogLevel http2:info
ProtocolsHonorOrder On
Protocols h2c http/1.1
H2Upgrade off
</IfModule>
…(省略)…

apachctl -tで、設定ファイルの間違いが無いことを確認の上、下記のように

# /usr/local/apache2/bin/apachctl -t
Syntax OK
# /usr/local/apache2/bin/apachctl stop
# /usr/local/apache2/bin/apachctl start

再起動します。

ちなみに、アップグレードとダイレクトモードを両方無効にする、

Protocols h2c http/1.1
H2Direct off
H2Upgrade off

といった設定は、結局は、h2cになれないので、

Protocols http/1.1

と同じ意味になります。

まとめ

このサイトは、当然、HTTP/2に対応していますが、立ち上げたときには、アクセスの半分も、HTTP/2は来ないかな?と何の根拠も無く予想していました。ところが、フタを開けてみれば、ロボットは、HTTP/1.1で、ほかのブラウザは、ほとんどがHTTP/2でのアクセスでした。

HTTP/2にとっては、最後の関門が、サーバ側の対応です。HTTP/2に対応したサーバが増えていけば、ユーザが気づかないうちに、HTTP/2の利用者が増えていく状況といって良いでしょう。

時を同じくしてサービスが開始された、Let's Encryptは、HTTP/2の普及に大きく貢献することは間違いありません。

当サイトも、HTTP/2の普及の一助になれば幸いと思います。

本稿では、なるべく成功例だけでなく、失敗例も記載してみました。これが、多くの人の助けになることを願っております。

HTTP/2は、プロトコルそのものはもちろん、TLS(SSL)についても十分な知識が求められる高度なプロトコルになっています。特にセキュリティー関連の情報は、今日の知識が、明日には陳腐化する恐ろしい分野です。HTTP/2のサーバを運用し始めたのであれば、セキュリティーの情報には、いっそうアンテナを張っていただければ幸いです。

2016年1月

管理人

NEXT >> チューニング[応用編]

©Copyrights 2015-2023, non-standard programmer

このサイトは、あくまでも私の個人的体験を、綴ったものです。 軽く参考程度にご利用ください。