Apache 2.4系でHTTP/2サーバを構築してみるテスト。
HTTP/2のチューニング[応用編]基本編のチューニングで書いたとおり、HTTP/2のチューニングは、H2Pushの設定を行うとろこから始まります。 このページの応用編のチューニングは、基本編のチューニングに相当する設定が、済んでいる前提です。 その上で、細かいパラメータを調整していくことになりますが、応用編では、これらを調整していきます。 なおmpmモジュールは、event mpmを想定しています。 [参考]>>mod_http2のリファレンス
なにはさておき、ログ解析HTTP/2のストリームの数を決めるにしても、KeepAliveTimeoutの必要時間を検討するにしても、まずは、アクセスログを解析する必要があります。 たいしてアクセス数もない弱小サイトではありますが、当サイトのログから、チューニングの方向性を探ってみたいと思います。 以下のログを取得するためには、コチラの設定を行ってください。 ここでは、このサイトのinstall.htmlのログで確認します。 Internet Explorer 11のHTTP/1.1(TLS)のログですが、以下のようになります。
また、Chrome 50のHTTP/2のログは以下のようになります。
比べてみると一目瞭然ですが、Internet ExplorerのHTTP/1.1は、リモートポート番号がリクエスト毎に異なっています。つまり、4回TLS接続を行っています。 一方、ChromeのHTTP/2は、リモートポート番号が一つだけです。つまりTLS接続を1回しか行っていません。これだけでもかなりサーバに優しいですね。 HTTP/2を使えるようにするだけで、相当、TLS接続回数を減らせることが判ります。 つまり、HTTPS接続のチューニングとして最も有効な設定は、HTTP/2を利用できるようにすることです。 以下、このログの情報を元に、パラメータの調整を行います。
KeepAliveTimeoutの秒数上記のログでは、いずれのブラウザも、最後に、favicon.icoをリクエストしています。これは、install.htmlのログが記録されてから、3~5秒後にリクエストされていることが判ります。 たとえば、KeepAliveTimeoutが3秒に設定されていると、favicon.icoがリクエストされる前に接続が切れてしまい、再度、TLS接続することになります。 これは避けたい事態です。 そこで、当サイトのログを調べてみました。 Operaは0秒から1秒とほぼ同時。 WindowsのIE、Chrome、Firfoxは2秒~10秒。 スマホのChromeは、15秒といった感じでした。 ブラウザのソフトウエアの差だけではなく、クライアントの帯域なども影響しているのでしょうか? とにかく、余裕をみて、KeepAliveTimeoutは、最低でも20秒程度は必要ということが判ります。 以前は、Prefork mpmに、PHPモジュールを組み合わせて使うと、PHPが多くのメモリを消費するため、すぐに他の処理を行えるよう、KeepAliveTimeoutを1秒とかにしていましたが、今回のチューニングでは、event mpmを想定しているので、PHPモジュールは無い前提です。そこで長めのKeepAliveTimeoutとします。 このように、TLS接続の負荷を減らす観点から、KeepAliveTimeoutを長めにするパラダイムがあるとすると、動的コンテンツは、PHPによる実装から、徐々に、直接モジュール化となるのかもしれません。 (世の中、リバースProxy全盛といった感じですが、チューニングとか言っておいて、余計なホップが追加されるリバースProxyを安易に使うのは、なんだか負けた気がするので、リバースProxyは最後の手段ということで。) このほか、サイト内の他のページへ飛ぶことがある程度期待できるコンテンツの場合、実際にログから、サイト内の他のページに飛ぶまでの時間を調べ、その多くが再接続しないような秒数を設定します。 たとえば、当サイトでは、そこそこ文字数があるので、180秒を設定します。 # cd /usr/local/apache2/conf/extra # vi extra/httpd-default.conf
/usr/local/apache2/conf/extra/httpd-default.conf
…(省略)… KeepAlive On KeepAliveTimeout 180 …(省略)…
HTTP/2接続あたりのストリーム数次に、HTTP/2接続あたりのストリーム本数を検討します。 つまり、1本のTCP接続に対して、いくつのリクエストを同時に多重処理できるようにするか決めます。 1つのストリームが、1つのリクエストを処理します。 HTTP/2のストリームには、ストリームIDと呼ばれるIDで管理されます。 Chromeでは奇数番号に、通常のリクエスト、偶数番号にプッシュを割り当てるようです。 たとえば、上記ログで、common.cs.css は、ストリームIDとして2が割り当てられています。プッシュされたストリームのIDは、2始まる偶数を順番に使うようです。 なお、Firefoxとnghttp2も、奇数番号に、通常のリクエスト、偶数番号にプッシュを割り当てるのは、Chromeと同じようですが、通常のリクエストには、13番から割り当てられます。 また、プッシュはストリームIDが、2から割り当てられるのは同じです。 いずれにしてもストリームの本数としては、リクエスト+プッシュの数だけ使用します。 上記のログの場合4本あれば、十分と言うことになります。 これを設定するディレクティブはH2MaxSessionStreamsです。 他のHTMLファイルについても調べて、最もリクエスト数が多くなるHTMLファイルを基準にストリームの本数を決めます。 ただ、同時リクエスト数(=ストリーム本数)が、10を超える場合は、10で良いかもしれません。 調べてみたわけではありませんが、10を超えるリクエストの応答を多重送信していれば、帯域を無駄に使う状況には無いと思われます。 上記のログの場合4本あれば、十分としましたが、4だとあまりに応用範囲が狭いので、ここでは、あえて10本とします。 とはいえ、ストリームは、ファイルをバッファリングするためのメモリ容量を確保しますので、少なければ少ないほど、メモリに余裕ができます。 デフォルトは100なので、減らしておきます。 後述のバッファの最大サイズが大きい場合は、H2MaxSessionStreamsを3程度まで抑えても良いでしょう。 # cd /usr/local/apache2/conf/extra # vi httpd-ssl-vhost.conf
/usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
…(省略)… H2MaxSessionStreams 10 …(省略)…
バッファの最大サイズとファイルハンドル数ストリームあたりのバッファの最大サイズは、HTML及び、依存ファイルのファイルサイズから決定します。 これは、H2StreamMaxMemSizeディレクティブによって設定します。 先ほどのログから、favicon.icoが最も大きく99Kバイトが最大のファイルサイズだと判ります。 とすれば、ストリームのバッファサイズが100Kバイトあれば、ファイル全体を一気にバッファに読み込めるので、ファイルハンドルをすぐに解放できます。 ストリームのバッファがデフォルトの64Kしかなければ、まず、ファイルハンドルを開き、64Kバイト分読み込んで、64Kバイトをクライアントに送信します。その後、残りの35Kバイト分を読み込んで、ここで初めてファイルハンドルを解放できます。 大きなサイズのファイルを送信する場合は、その数だけファイルハンドルが必要になります。 ところが、1プロセス当たりのファイルハンドル(ファイルディスクリプタ)の数は上限があります。 OSにもよりますが、32ビットの1プロセスあたり、2Gないしは、4Gバイトのメモリ空間と、65536このファイルディスクリプタが利用できるリソースになります。 H2SessionExtraFilesディレクティブは、ファイルハンドルの数が増えすぎて、1プロセスあたりのファイルディスクリプタ数が、65536を超えないようにするための設定です。 従って、送信ファイルのサイズに対して、十分なバッファサイズがあれば、H2SessionExtraFilesの数は、数個で十分です。逆に、何回もバッファリングを繰り返して送信するサイズのファイル、たとえば映像などのファイルがあれば、その分をプラスします。 どんなに多くても、ストリームと同じ本数だけファイルハンドルがあればそれ以上は必要ありません。つまり、H2SessionExtraFilesの数を、H2MaxSessionStreamsと一致した数値にしておけば、十分と言えます。 ここでの例のように、最大100Kのストリームバッファに、ストリーム数が10個なら、1TCP接続あたり、最大1Mバイトのストリームバッファが確保されることになります。この場合、単純にプロセスに割り当てられた2Gバイトのメモリを、接続あたりの最大ストリームバッファサイズの1Mバイトで割ると、2000接続分のメモリを利用できる計算になります。(実際は、もう少し余裕を持たせると思います…) HTTP/2が利用するファイルハンドル数は、 (h2_connections × extra_files) + (h2_max_worker) と求められますので、この例のH2SessionExtraFilesが3の場合、2000接続×3+αとなります。 つまり、6000+α個のファイルハンドルを利用することになります。 H2MaxSessionStreamsが10で、H2SessionExtraFilesを10としたとしても、20000個+αのファイルハンドルとなります。 これは、最大ファイルディスクリプタ数からみれば、問題ない数値です。 ということで、32ビットのバイナリを利用している場合は、メモリーの方が先に消費され尽くされてしまうため、H2SessionExtraFilesの数を、H2MaxSessionStreamsと一致するようにしても、H2MaxSessionStreamsが10以下なら、限界に達することはまれでしょう。 64ビットのバイナリだと、2Gバイトないしは4Gバイトを超えてメモリを利用できるので、H2SessionExtraFilesの数は慎重に決めた方が良いでしょう。大きすぎると、ファイルディスクリプタ数が足りなくなります。 なお1TCP接続あたりの、最大のストリームバッファサイズは、この後、コネクション数の決定に利用しますので,覚えておきます。 H2StreamMaxMemSizeが100Kバイト、H2MaxSessionStreamsが10本なら、1TCP接続あたり、 (H2StreamMaxMemSize:100K)×(H2MaxSessionStreams:10)=最大1Mバイト となります。 この設定は、バーチャルホスト毎に設定できます。 # cd /usr/local/apache2/conf/extra # vi httpd-ssl-vhost.conf
/usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
…(省略)… H2SessionExtraFiles 3 H2StreamMaxMemSize 102400 …(省略)…
接続毎のH2Push履歴の記憶数H2PushDiarySizeディレクティブは、同一接続で、一回プッシュしたファイルを再度プッシュしないように、履歴を記憶しておくための記憶数を設定します。 プッシュするファイルは、 Header add Link "<[push file path]>;rel=preload" といった設定で指定しますので、この[push file path]の数だけ、記憶しておけば大丈夫ということになります。 たとえば、基本編のチューニングでは、以下のように設定しました。
/usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
…(省略)… <VirtualHost *:443> …(省略)… <Location /index.html> Header add Link "</common.cs.css>;rel=preload" </Location> …(省略)… </VirtualHost> …(省略)… プッシュ対象のファイルが、上記の例のように、/common.cs.cssだけならば、極端な話し、1エントリで良いことになります。 下記の例のように、common.cs.cssが共通で、button.jsとdownload.jsがそれぞれ、別のHTMLからプッシュされる場合、エントリ数は、3となります。
/usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
…(省略)… <VirtualHost *:443> …(省略)… <Location /index.html> Header add Link "</common.cs.css>;rel=preload" Header add Link "</button.js>;rel=preload" </Location> <Location /download.html> Header add Link "</common.cs.css>;rel=preload" Header add Link "</download.js>;rel=preload" </Location> …(省略)… </VirtualHost> …(省略)… デフォルトでは、256エントリですが、1エントリあたり、8バイト消費するので、1接続毎に2Kバイトを消費することになります。少なければ少ないほど良いので、上記の例であれば、以下のようになります。 # 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" Header add Link "</button.js>;rel=preload" </Location> <Location /download.html> Header add Link "</common.cs.css>;rel=preload" Header add Link "</download.js>;rel=preload" </Location> H2PushDiarySize 3 …(省略)… </VirtualHost> …(省略)…
アップロード方向のTCP Windowサイズクライアントから見て、アップロード方向のフローコントロールのWindowサイズは、H2WindowSizeディレクティブで設定します。 デフォルトで65535バイトです。 1024以下の値を設定すると、1024になります。 クライアントの送信したデータがこのサイズに到達すると、サーバがこのデータを処理できる領域に移動したとアナウンスするまで、クライアントは送信を一旦止めます。 リクエストのサイズは、GETメソッドのパラメータや、その他リクエストヘッダの合計です。 HTMLファイル等のファイルを、単純にダウンロードするだけの用途であれば、1リクエスト512バイトを超えることはそうないと思われます。 もちろん、ストリームの本数以上は、必要無いので、 H2MaxSessionStreams×512バイト といった計算式で求めた値で、メモリを節約するのもいいでしょう。 POSTメソッドで、ファイルなど大きめのデータをアップロード送信する場合は、デフォルトの数倍を確保すると良いでしょう。 ここまでの設定例では、10本のストリーム(H2MaxSessionStreams)ですので、以下のようになります。 # vi /usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
/usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
…(省略)… H2WindowSize 5120 …(省略)…
プロセス数とスレッド数ここまで設定した内容から、1つのTCP接続当たりのメモリ消費量が決まります。 おおまかには、以下の計算式で求めます。 TCP接続当たりのメモリ消費量=((H2StreamMaxMemSize × H2MaxSessionStreams + H2PushDiarySize × 8+ H2WindowSize ) × ( VirtualHost数 + 1 ) あとは、サーバマシンのメモリ量と相談です。 サーバの搭載メモリの7割くらいを、上記の用途で利用する想定でいくと、16Gバイトのメモリを搭載しているマシンなら、10Gバイト程度を利用する想定でしょうか。 モジュールなどによっては、5割、DBも一緒に動かすならさらに小さい割合にします。 10Gバイト程度利用できるなら、1接続当たり1Mバイト利用するとすれば、1万接続分のメモリを確保できそうです。 あとは、1万接続が最大となる設定を入れます。 もちろん1万接続に到達する前にCPUがアップアップなら、それまでです。その場合はメモリを減らして、サーバ台数を増やし、負荷分散装置の出番です。 重要な設定ディレクティブは以下のとおりです。 AsyncRequestWorkerFactorに、 [number of idle workers]を掛けた数に、ThreadsPerChildを足したものが、プロセス毎の接続可能数になります。これを超えると、新たに接続を受け入れません。 これは、諸説ありますが、デフォルトの2でいいかと思います。 [number of idle workers]は、ディレクティブで設定するものではなく、処理を行っていないスレッドの本数で、httpdの実行中に、随時変動する値です。 最も接続数が多くなるのは、[number of idle workers]とThreadsPerChildが等しい時、つまり、プロセスのスレッドが全部アイドル状態なら、接続数が最大になります。
また、全プロセスのワーカスレッドの総数は、MaxRequestWokersディレクティブの制限を受けます。 そこで、MaxRequestWokersは以下の数値を入れます。 MaxRequestWokers=ThreadsPerChild×ServerLimit
全スレッドがアイドル状態だと、 [全プロセスの最大同時接続数]=(AsyncRequestWorkerFactor + 1) * MaxRequestWorkers となります。
これを逆算していきます。 これまでの例のとおり、[全プロセスの最大同時接続数]が1万接続を想定します。 AsyncRequestWorkerFactor=2なら、リクエストワーカスレッド(MaxRequestWorkers)の数は、3333となります。
CPUやOSのスレッド切り替え性能によりますが、1プロセス当たりのスレッド数は、256前後が無難です。 1プロセスあたり、256スレッドとすれば、13プロセスあれば、3328スレッドとなります。 5スレッドほど足りませんが、ご愛嬌ということで。
この設定は、全スレッドがアイドル状態にならないと、最大接続数に到達しません。 そこで、実際に運用して、CPU負荷に余裕があって、接続数が上がらなければ、AsyncRequestWorkerFactorの値を増やします。 逆に接続数が増えすぎてCPU負荷がアップアップなら、AsyncRequestWorkerFactorや、場合によっては、ThreadsPerChildの数を減らして、接続数を抑えます。(当然この場合、台数を増やして、DNSラウンドロビン、または、負荷分散装置のお世話になります。) AsyncRequestWorkerFactorの値は、少数点を含む数値でもOKです。たとえば、2.5といった設定もできます。 また、MaxConnectionsPerChildは同時接続数の設定ではなく、プロセスの寿命の設定です。 MaxConnectionsPerChildの数だけ接続を処理すると、プロセスが終了します。0なら接続数による終了を行いません。 なお、mod_http2には、ThreadsPerChildの値を使用せず、H2MaxWorkersとH2MinWorkersの範囲のスレッド数で稼働させることもできます。この場合、必要に応じてスレッド数を増減できるので、暇なときはメモリに余裕を持たせると言った運用ができますが、アクセス集中などで、いきなりスレッド数が増えると、比較的重そうな、スレッド生成処理も集中してしまう恐れがあるため、今回は省いております。 なお蛇足ですが、H2MaxWorkersを利用する場合、MaxRequestWokersは、 MaxRequestWokers=H2MaxWorkers×ServerLimit で計算した値を使います。
これらのディレクティブは、サーバ毎の設定(server config)に設定します。 まとめると、以下の設定になります。 # vi /usr/local/apache2/conf/extra/httpd-mpm.conf
/usr/local/apache2/conf/extrahttpd-mpm.conf
…(省略)… ThreadsPerChild = 256 ServerLimit = 13 AsyncRequestWorkerFactor 2 MaxRequestWorkers = 3328 ThreadLimit 256 ThreadsPerChild 256 MaxConnectionsPerChild 0 …(省略)…
|