Neinvalli

研究テーマは ”IT 環境でのリソース最適化” です

インフラ視点で見た時のリバースプロキシの必要性について

インフラ視点で見た時のリバースプロキシの必要性について

3年前の記事ですが、ふとしたことがきっかけでこちらの記事が目に入ってきました。

Reverse Proxy がなぜ必要か
http://d.hatena.ne.jp/naoya/20140826/1409024573

ウェブのインフラの経験がある私としてはとても共感できました。 その中で自分なりに掘り下げることができる箇所があったので、私の考えを述べたいと思います。

この「重い」アプリケーションサーバーと「軽い」Reverse Proxy を組み合わせてそれぞれ自分が得意なものだけ担当することで、システム全体の系でみたときにリソース効率を全体最適させましょう・・・というのがインフラ視点で Reverse Proxy を導入したい一番の理由である。

上のブログで言われている通り、インフラ視点で見た場合にリバースプロキシを導入し、リソース効率を最適化する場合、

  1. 画像や CSS のような静的なファイルを返すだけの処理はリバースプロキシで返す
  2. アプリケーションの処理が必要なものだけをアプリケーションサーバープロセスへ流す

という考え方になるかと思います。

さらに下記のことも言われています。

Reverse Proxy はネットワーク的に遅いクライアントや、KeepAlive リクエスト、あるいは大きなファイルをアップロードしてくるクライアントなど「あまり仕事はしないけどネットワーク接続の維持は要求される」されるような側面でも役に立つ。それらは Reverse Proxy 側で処理しておき、アプリケーションサーバーには本当に必要なときだけにしかリクエストを転送しない・・・ たとえば遅いクライアントのHTTPリクエストがすべて着信してから転送、KeepAlive はフロントだけ有効にしておきバックエンドは応答が終わったらそく切断、アップロードファイルは終わるまでフロント側でバッファリング・・・などである。

これもそのとおりで、前述の部分も含めて同感なのですが、大量にアクセスが来る環境だとこの部分が非常に大きいと思われますのでもうちょっと掘り下げてみたいと思います。

 

今回考える環境について

ここでは掘り下げる箇所に特化するため、下記のような条件を考えます

静的ファイルは別サーバーから配信されるため、このサーバーは完全にアプリケーションサーバーとしての役割しかない状態ですが、こういった場合でもリバースプロキシは配置したほうがリソース面でのメリットがあります。

この環境でリバースプロキシの有無でのサーバーリソース消費量の差を見ていきます。

なお、通信や処理時間については、下記のとおりとします。(夜間 22時ころに有名な国内サイトへアクセスしたものの計測結果を平均したものです)

  • HTTP レスポンスサイズ 36KB (gzip 圧縮)
  • TCP コネクト SYN 送信開始から、SYN/ACK を経て、ACK を送信直後までが 51msec
  • HTTP リクエストの送信に 25msec
  • サーバーでの HTTP レスポンス結果生成が 150msec
  • HTTP レスポンスの送信にかかる時間が 312msec

 

メモリ消費量の差が大きい

結論から言うと、大量アクセスをさばく環境では、画像や CSS 等の静的ファイルを別のサーバーから配信していた場合であっても、 アプリケーションサーバープロセスの前段にリバースプロキシを導入するのとしないのとではメモリ消費量に差が出ます。
そして、現状のウェブシステムのサーバーリソース(CPU、メモリ、HDD、通信帯域)の使用具合では、そのメモリの差の部分がリソース面で一番最初にボトルネックとなる可能性が非常に高いです。 そのため、リバースプロキシを導入しボトルネックを解消しましょう、というのがインフラ視点で見た時の考え方だと思われます。

 

リバースプロキシ有り無しでのシーケンス図

まずはこの部分のシーケンス図を見ます。 リバースプロキシを導入しない場合はクライアントとの通信の最初から最後までアプリケーションサーバープロセスが拘束されることになります。

図中には手元で計測した各処理の大体の所要時間を記載してあります。
特に図中の「Apache PHP プロセスの拘束時間: 約○○msec」という箇所にご注目ください。

リバースプロキシ無し

f:id:neinvalli:20171009160135p:plain

リバースプロキシ有り

f:id:neinvalli:20171009161427p:plain

同じアクセス数を処理する場合であっても、リバースプロキシを利用しない場合はアプリケーションサーバープロセスの拘束時間が長くなり、プロセスの並列稼働数が多くなってしまいます。 リバースプロキシを導入することで、この拘束時間を最小化しプロセスの同時実行数を減らし、メモリ消費量を抑えることができます。

 

VM 環境にて検証

まずは下記環境での実験結果を見ていただきたいと思います。

f:id:neinvalli:20171009172456p:plain

  • リバースプロキシとして、nginx が 80番ポートで稼働。受けたアクセスはすべて、localhost の 81番ポートへプロキシ
  • アプリケーションサーバープロセスとして、Apache PHP(Prefork) が 81番ポートで稼働。試験用に test.php を用意。今回のテストのため、81番ポートで nginx 経由なしでアクセスを受け付けるようになっている。
  • test.php 内部では 144 ミリ秒 sleep した後に 150KB(mod_deflate で圧縮して 36KB) ほどの HTML を返す usleep(1000 * 144); echo file_get_contents('./contents.html');
  • Linux ルーター(st-router001)を間に配置し、tc を使って片道 25msec, 往復 50msec の遅延を発生させる
  • Apache, nginx でそれぞれ、コネクションごとの帯域制限をかける(800Kbps 程度に絞ってあります)

アプリケーションの処理と結果の HTML のサイズをある程度実環境に近づけるため、test.php には上記の処理を入れました。 国内のラウンドトリップタイムをある程度実環境に近づけるために tc で通信遅延を発生させています。 また、tc での遅延 25msec を考慮した上で HTTP レスポンス転送時間を 312msec に近づけるために、コネクションごとに帯域制限をかけています。

この構成でクライアントから、サーバーに対して大量に HTTP アクセスを投げた場合にどういった結果となるのか、確認してみましょう。

負荷の生成には httperf を使うことにします。 秒間同時接続数 100 で負荷をかけます。 同時に大量にアクセスを投げられるツールであれば何でも構いません。

テストは VM 環境で 1~2CPU, 1GB Ram 等の環境で行うため、大量アクセスとはいきませんが、リバースプロキシ有無の違いを見ることがテストの目的ですので、問題ないと考えています。

 

負荷をかけつつ、サーバーリソース情報を Munin でグラフ化

テストのパターンは3つ

  1. Apache PHP へ直接アクセスする (Apache プロセス数は 100, Apache の設定でコネクションごとの帯域制限 800Kbps)
  2. nginx 経由で Apache PHP へアクセス (Apache プロセス数は 100, Apache 帯域制限 Off, nginx の設定でコネクションごとの帯域制限 800Kbps)
  3. nginx 経由で Apache PHP へアクセス (Apache プロセス数は 40, Apache 帯域制限 Off, nginx の設定でコネクションごとの帯域制限 800Kbps)

この3つのパターンのテストを実施し、その間の下記の項目の状態を確認します。

  • CPU 使用率
  • メモリ使用状態
  • nginx のコネクション数(stub_status)
  • Apache status(mod_status)

これとは別に負荷をかけつつ、Apache mod_status のページ状態をコマンドで確認しておきます。 サーバー状態をより正確に把握するため、Munin は設定を変更し、1分毎のサンプリングを元にグラフ化しています。 負荷をかける前後で下記コマンドでメモリやプロセスの状態をクリアしています。
systemctl restart apache2; systemctl restart nginx; swapoff -a && swapon -a; sync; echo 3 > /proc/sys/vm/drop_caches

 

テストのコマンドとその結果 (テスト1~3 ともにエラーや大きな遅延はなし)

Test 1 (直接 Apache へアクセス、Apache プロセス数 100、Apache 帯域制限 800Kbps)
[root @st-webclient001 ~]# /usr/local/bin/httperf --add-header 'accept-encoding:gzip, deflate, br\n' --server 10.1.0.2 --port 81 --uri /test.php --rate 100 --num-conn 30000 --num-call 1 --timeout 30
httperf --timeout=30 --client=0/1 --server=10.1.0.2 --port=81 --uri=/test.php --rate=100 --send-buffer=4096 --recv-buffer=16384 --add-header='accept-encoding:gzip, deflate, br\n' --num-conns=30000 --num-calls=1
Maximum connect burst length: 5

Total: connections 30000 requests 30000 replies 30000 test-duration 300.492 s

Connection rate: 99.8 conn/s (10.0 ms/conn, <=61 concurrent connections)
Connection time [ms]: min 498.3 avg 503.2 max 689.1 median 501.5 stddev 6.5
Connection time [ms]: connect 50.9
Connection length [replies/conn]: 1.000

Request rate: 99.8 req/s (10.0 ms/req)
Request size [B]: 104.0

Reply rate [replies/s]: min 90.0 avg 99.8 max 101.0 stddev 1.3 (60 samples)
Reply time [ms]: response 200.5 transfer 251.8
Reply size [B]: header 217.0 content 36263.0 footer 0.0 (total 36480.0)
Reply status: 1xx=0 2xx=30000 3xx=0 4xx=0 5xx=0

CPU time [s]: user 112.24 system 186.98 (user 37.4% system 62.2% total 99.6%)
Net I/O: 3566.8 KB/s (29.2*10^6 bps)

Errors: total 0 client-timo 0 socket-timo 0 connrefused 0 connreset 0
Errors: fd-unavail 0 addrunavail 0 ftab-full 0 other 0
[root @st-webapp001 ~]# curl -s 'http://localhost/server-status?auto'
~省略~
BusyWorkers: 46
IdleWorkers: 54
Scoreboard: CC_CWWW__W_____WW_WWW_______W_W_W_WW____W_WCCW__C_C___C__C__WWW_W_W__W_WWWW_WWW____W_WW____WW__C_W__
Test 2 (nginx 経由でアクセス、Apache プロセス数 100、Apache 帯域制限 Off、nginx 帯域制限 800Kbps)
[root @st-webclient001 ~]# /usr/local/bin/httperf --add-header 'accept-encoding:gzip, deflate, br\n' --server 10.1.0.2 --port 80 --uri /test.php --rate 100 --num-conn 30000 --num-call 1 --timeout 30
httperf --timeout=30 --client=0/1 --server=10.1.0.2 --port=80 --uri=/test.php --rate=100 --send-buffer=4096 --recv-buffer=16384 --add-header='accept-encoding:gzip, deflate, br\n' --num-conns=30000 --num-calls=1
Maximum connect burst length: 3

Total: connections 30000 requests 30000 replies 30000 test-duration 300.524 s

Connection rate: 99.8 conn/s (10.0 ms/conn, <=61 concurrent connections)
Connection time [ms]: min 379.5 avg 529.5 max 661.6 median 532.5 stddev 20.9
Connection time [ms]: connect 50.7
Connection length [replies/conn]: 1.000

Request rate: 99.8 req/s (10.0 ms/req)
Request size [B]: 104.0

Reply rate [replies/s]: min 89.6 avg 99.8 max 101.0 stddev 1.4 (60 samples)
Reply time [ms]: response 200.9 transfer 277.9
Reply size [B]: header 221.0 content 36263.0 footer 0.0 (total 36484.0)
Reply status: 1xx=0 2xx=30000 3xx=0 4xx=0 5xx=0

CPU time [s]: user 99.83 system 199.25 (user 33.2% system 66.3% total 99.5%)
Net I/O: 3566.8 KB/s (29.2*10^6 bps)

Errors: total 0 client-timo 0 socket-timo 0 connrefused 0 connreset 0
Errors: fd-unavail 0 addrunavail 0 ftab-full 0 other 0
[root @st-webapp001 ~]# curl -s 'http://localhost/server-status?auto'
~省略~
BusyWorkers: 16
IdleWorkers: 84
Scoreboard: _____________________________________W_____________W_______W_WW_W_WW_W__WW_________W_W___W_W__W_____
Test 3 (nginx 経由でアクセス、Apache プロセス数 40、Apache 帯域制限 Off、nginx 帯域制限 800Kbps)
[root @st-webclient001 ~]# /usr/local/bin/httperf --add-header 'accept-encoding:gzip, deflate, br\n' --server 10.1.0.2 --port 80 --uri /test.php --rate 100 --num-conn 30000 --num-call 1 --timeout 30
httperf --timeout=30 --client=0/1 --server=10.1.0.2 --port=80 --uri=/test.php --rate=100 --send-buffer=4096 --recv-buffer=16384 --add-header='accept-encoding:gzip, deflate, br\n' --num-conns=30000 --num-calls=1
Maximum connect burst length: 2

Total: connections 30000 requests 30000 replies 30000 test-duration 300.523 s

Connection rate: 99.8 conn/s (10.0 ms/conn, <=61 concurrent connections)
Connection time [ms]: min 380.1 avg 529.7 max 698.0 median 532.5 stddev 20.3
Connection time [ms]: connect 50.7
Connection length [replies/conn]: 1.000

Request rate: 99.8 req/s (10.0 ms/req)
Request size [B]: 104.0

Reply rate [replies/s]: min 89.4 avg 99.8 max 101.2 stddev 1.4 (60 samples)
Reply time [ms]: response 200.8 transfer 278.2
Reply size [B]: header 221.0 content 36263.0 footer 0.0 (total 36484.0)
Reply status: 1xx=0 2xx=30000 3xx=0 4xx=0 5xx=0

CPU time [s]: user 110.14 system 188.94 (user 36.6% system 62.9% total 99.5%)
Net I/O: 3566.8 KB/s (29.2*10^6 bps)

Errors: total 0 client-timo 0 socket-timo 0 connrefused 0 connreset 0
Errors: fd-unavail 0 addrunavail 0 ftab-full 0 other 0
[root @st-webapp001 ~]# curl -s 'http://localhost/server-status?auto'
~省略~
BusyWorkers: 16
IdleWorkers: 24
Scoreboard: W_WWWW_WWWW__W________WW___W___W______WW

Test 1~3 の区間を図中に書き入れています。

CPU 使用状態

f:id:neinvalli:20171009161443p:plain

メモリ使用状態

f:id:neinvalli:20171009161448p:plain

Apache プロセスのステータス

f:id:neinvalli:20171009161439p:plain

nginx コネクション数

f:id:neinvalli:20171009161452p:plain

 

リバースプロキシ有りの時は Apache の同時稼働プロセス数が少ない

Test 1 の nginx コネクション数がないのは nginx を経由していないからで当然なのですが、Test 2, 3 の Apache プロセス数が少ないことに気がつくと思います。 Test 1 では 50 弱程度のプロセスが常時仕事をしているのに対し、Test 2, 3 では 20 弱です。

Test 1 の Apache プロセスのステータス別に確認すると、Closing, Sending reply がほぼすべてを占めています。 明確に調査したわけではないですが、Sending reply は PHP 処理+クライアントへレスポンスを送信中。Closing は接続のクローズ処理中だと思われます。

ローカルの通信と違い、クライアントとの間にはインターネットがあるため、パケットのやりとりに時間がかかります。(今回のテストでは tc とコネクション事の帯域制限で再現) リバースプロキシを挟まない場合は Apache がクライアントとの通信を担当するため、その間 Apache PHP のプロセスが拘束されてしまいます。 リバースプロキシを挟んだ場合は nginx がこの部分の処理をすべて担当し、Apache PHP はローカルの nginx との通信だけで済むため、プロセスの拘束時間が短くなります。

プロセスの同時稼働数は下記の計算式で大体近い数が出るかと思います。
1 リクエストを処理するためのプロセスの拘束時間(msec) / 1000 * 秒間リクエスト数

今回のテストの結果を当てはめてみると下記のようになります。
Test 1 462(msec) / 1000 * 100(req/sec) -> 46プロセスが同時稼働
Test 2, 3 151(msec) / 1000 * 100(req/sec) -> 15プロセスが同時稼働

 

Apache 同時稼働プロセスが少ないためプロセス数を少なくすることが可能

見てきたとおり、リバースプロキシを挟んだ場合は Apache PHP のプロセス数が少なくて済みます。 Test 3 は Test 2 の結果を確認し、Apache PHP のプロセス数を 40 まで下げています。 その結果、メモリの消費量も抑えることができました。

Apache PHP は HTTP 通信をしたらいいだけの nginx プロセスと違い、メモリを多く消費します。 イメージとしては、Apache PHP が 1プロセス(1コネクション)あたり数MB〜数十MBに対して、nginx で 1コネクション担当するのに必要なメモリはせいぜい数十KB〜数百KB程度もしくはもっと少ないと思われます。

これについても上のブログで言われているとおりです。

ご存知のようにアプリケーションサーバーはメモリを大量に消費する。メモリ使用量という文脈で「重い」と言える。そのため、32GB とかどんなに潤沢にメモリが確保できる状況でも、せいぜい最大プロセス数は、すなわち並行処理性能は 50 とか 100 とかそのぐらいである。

 

大量アクセスをさばくシステムだとこのメモリの差が大きく響いてくる

今回のテストだと test.php が内部で処理をほとんどしていないためメモリ消費量が少なかったのと、負荷の秒間接続数が 100 程度であったため、それほど大きな差は出ていませんでした。

これが大規模なアプリだと、PHP 1 プロセスあたりメモリ 20~40MB消費、秒間接続数 500 というようなことになり、Apache のプロセス数を 350 でいくのか、150 に下げれるのかで、20MB(~40MB) * 200プロセス = 4GB(~8GB) ほどの差が出ます。

環境によりますが、カーネルやシステム部分に必要なメモリ、nginx 動作に必要なメモリ、ページキャッシュ用に必要なメモリ、安全マージン、をそれぞれ考慮すると2, 3GB 程度のメモリが必要だと思われます。 これに加えて上記の Apache PHP に必要なメモリを考慮すると、下記のようなメモリの違いになるかと考えられます。

  • 3GB(システム、nginx、その他) + 6GB(Apache PHP 40MB * 150 プロセス) + 安全マージン 2GB= 11GB
  • 3GB(システム、nginx、その他) + 14GB(Apache PHP 40MB * 350 プロセス) + 安全マージン 5GB = 22GB

リバースプロキシ導入で、メモリが 16GB で足りるのか、32GB 必要なのかという違いです。メモリが潤沢にある環境であればいいのですが、そうでない環境だとこの違いは大きいのではないでしょうか。
アプリケーションサーバープロセスのメモリ消費量がもっと多い場合や、通信に時間がかかる場合、秒間リクエスト数が多い場合は、メモリ消費量の差はもっと広がります。

 

リバースプロキシ導入のデメリット

インフラ視点で見た時に、逆にリバースプロキシを導入することで発生するデメリットもあります。

  • リバースプロキシのプロセスが増える分、管理コストが増加する(パフォーマンス、死活監視、セキュリティアップデート等)
  • リバースプロキシとアプリケーションサーバープロセス間の通信のためのリソースが消費される

2点目のリソース部分が注意点です。特に TCP/IP で通信している場合は、リソースの枯渇に気をつける必要があります。 これについてはまたの機会があれば言及したいと思います。

 

まとめ

リバースプロキシを導入し アプリケーションサーバープロセスが仕事をしている時間を最小化する ことでサーバーリソース(主にメモリ)を最適化しましょう、ということでした。