リクエストを受け付けてレスポンスを返すようなシステムには、アプリケーション・サーバーというミドルウェアが必要になります。どんなシステムも完全放置して良いものはありませんが、こいつも放置されると機嫌を損ないやすいので、ちょいちょい面倒を見てあげるとよいです。
どんなポイントをどのように調べて、どのように調整してあげると喜ぶか、というのを初級編的にまとめていきたいと思います。
アプリケーション・サーバーの要所
昔は Apache + module という形で、WEBサーバーと同居する形で動かすことがありましたが、今は Nginx の80番ポートが受けて、後ろに控えているアプリケーション・サーバーに socket なり HTTP なりで流す。というのが主流だと思います。この20年で流行り廃りはありましたが、基本的な設定項目──というか注視すべきポイントというのはそう変わっていません。その要所について振り返ってみます。
同時接続数
まずは、接続数です。リクエストを同時に受け付けられる数を表します。固定の場合は、その数値より多いリクエストを受けた場合、待機またはエラーになります。可変の場合は、起動時の最小接続数から始まり、必要に応じて最大接続数まで受け付けられるようにし、それを超えれば固定の場合と同様です。
これだけだと可変の方が良くねって見えるかもですが、固定だと先に待受準備をしておけるので、暖機運転的にできることや、ある程度使用リソースをバシッと確定できるというメリットがあります。可変だと、不要な消費リソースを抑える効果がありますが、新たな接続リソースの準備や初回利用に時間を要するかもしれません。
どちらがよいかというよりは、ちゃんと理解して試算できていれば、どちらでも問題なく運用できるので、選ばれたミドルウェアをモノにする姿勢が大事です。
メモリ容量
デーモンを起動すると、たいていは master 的(最近だと main とか parent とか言えばいいの?)な親プロセスが立ち上がり、その下に worker 的な子プロセスがぶら下がって、worker プロセスが実際のアプリ処理を行います。このプロセスは目安として数百MBといったメモリ容量を必要としますが、アプリケーションに含まれるコードやライブラリの量、または取り扱うデータ容量などによって、必要とするメモリ容量が変わります。例えば、1万行のデータを扱うにしても、1行1行取り出してループ処理するのか、いったん1万行を配列変数に格納してからループ処理するのか、では必要とする最大容量に大きな違いがでる、とかそういう感じです。
また、起動後一定期間は増加傾向になるけど、何回も処理していれば大体の最大値が確定していく値であり、でもたまに、いわゆるメモリリーク的に際限なく容量が拡大していくこともある。というちょっとクセのあるリソースだったりもします。
そんな1プロセスあたりに必要なメモリ容量があり、それがリクエストを同時・並列に捌くために複数用意するため、必要な容量は
子プロセスあたりの容量 * 最大同時接続数 + 親プロセス
となるので、接続数とは密接に関わっているということになります。
メモリが足りなくなると適切な処理ができなくなる、というかOOM Killer先生に殺られる可能性が高いため、とても重要なリソースなのですが、徐々に増える傾向にあったり、メモリリークが怖かったりするので、ミドルウェアによっては最大メモリ容量を設定して、超えたら作り直す、という機能があったりします。
リクエスト数
そんな並列に並んだ子プロセスは、ラウンドロビンなど決められたルールで分散されて処理を担当します。もし順番に処理した場合、全プロセスがほぼ同数のリクエストを捌くことになります。何か悪いことがない限り、そのプロセスは半永久的に処理し続けられるはずですが、メモリリークやバグといった可能性はゼロではないので、いつか異常な状態になるかもしれないと想定して運用するとします。
それに対しては、1プロセスあたりが、一定回数のリクエストを捌いたら自動的に作り直す、という仕組みもあります。
さきほどのメモリ容量上限による機能と似ていますが、こちらはオマジナイ的な意味合いを含むと考えます。なぜなら、回数によって確実に何かが起きるという確証もないからです。ある程度の回数を処理したプロセスは、慢性的に容量が大きくなっているかもしれないし、異常な肥大化をするかもしれない、だから定期的に作り直したほうが少しは健康的に運用できるでしょう。という希望的な機能であると推測できます。
プロセスの作り直しと初期アクセスには多少のコストが発生することになりますが、例えば1日~1週間に1回程度の作り直しならば、そのコストよりも安定運用になりそう、というメリットが上回るという考え方ができます。
ミドルウェアの作者とかに言わせれば、もっと重要な項目があるかもですが、今回はこの3つを調整していく流れでいきます。
同時接続数の計算
復習が終わったところで、まずは接続数について、どう計算して設定するかですが、前にこんな記事を書きました。vCPU数・想定処理時間・想定メモリ容量などから適した接続数を算出するというものでしたが、今回は稼働中のサーバーという視点からの算出となります。
監視データ
稼働中ということは監視データがあるということで、算出には以下2つのデータを使うものとします。できればどちらも、前段WEBサーバーとしてのデータではなく、アプリケーションサーバーとしてのデータが望ましいです。前段はどうしても、アプリケーションに回さないリクエストや、ゴミアクセスがくるため、リクエスト数としては多く、処理時間としては短くなる傾向にあるからです。
簡単な例から式を考える
では例えば、1秒に1回のリクエストがきて、処理に平均1秒間かかるとします。当然ですが、これだと平均1ユーザー=1接続だけが常に利用される状況となります。リクエスト数が増えた場合。1秒に2回きて、処理は1秒とします。これだと平均2接続が常に利用されることになります。
処理時間が速くなった場合。1秒に1回きて、処理は500ms とします。これだと平均0.5接続が利用されることになります。
これを式にすると、こうなります。
平均接続数 = リクエスト/秒 * 平均処理時間/req
現実的な例で範囲を算出する
現実的な数値で考えてみると、監視データを見て、ピークタイムに 20req/s のリクエストと
200ms の平均レスポンスタイム と観測できたとすると、
20req/s * 0.2s = 4conn
となり、ユーザーの出入りが多いように見えて、平均的には同時 4接続 が一日の中で利用されている最大値ということになります。
これはあくまで平均なので、処理時間が長い処理が多い瞬間があったり、日によってピークトラフィックが変わったり、を考えると最大4接続の設定は不足なので、それよりは多く設定することになります。
また、さきほどの過去記事より、vCPU からもある程度の算出ができるので、超大雑把に vCPU * 2.0 以上にするメリットはないとしたら、vCPU = 8 ならば 16 以下でいいでしょうと判断でき、
2つの試算から、16 >= 接続数設定値 > 4 と見積もることができます。
で、これはいったん仮の姿として、次にメモリを見ていきます。
メモリ容量の計算
現在の使用量を確認する
今回は既に稼働中ということで、親/子プロセスの使用メモリ容量を確認することができる、という前提です。今回はミドルウェアが Unicorn ということで調査したいつかのメモですが
1 2 3 4 5 6 |
$ ps axuw | | grep unicorn_rails USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 51271 6.9 1.7 849536 290836 ? Sl 18:21 0:19 unicorn_rails master -c config/unicorn.rb -E production www-data 51348 9.7 2.3 955812 384756 ? Sl 18:21 0:24 unicorn_rails worker[0] -c config/unicorn.rb -E production www-data 51358 10.7 2.3 954516 381688 ? Sl 18:21 0:27 unicorn_rails worker[1] -c config/unicorn.rb -E production ... |
こんな感じでプロセスを表示すると、RSS列を確認できます。これが実際に使用中のメモリ容量(KB) になります。
もし健全に稼働していて、プロセスがある程度長時間、動いているならば、この中で最大になっている数値が必要としているメモリ容量である、と仮確定してよいでしょう。
だいたい、親プロセスが300MB、子プロセスの最大が400MB とすると、
使用量(MB) = 300 + 400 * worker数
が、現時点でアプリケーション・サーバーとして必要な容量、ということになります。
アプリケーション以外の使用量を確認する
アプリ以外にも、OSの大事なプロセスや、Nginx などがいっぱい動いているので、それらがどれくらいの容量を必要としているのかも確認しておきます。
1 2 |
$ ps axuw | grep -v unicorn_rails | awk '{print $6}' | grep "^[0-9]" | python -c "import sys; print(sum(int(l) for l in sys.stdin))" 2053232 |
パイプを順番に増やしていけばわかりますが、アプリケーション以外のRSS列を全部足しただけです。今回はだいたい 2,000MB だったということにしましょう。
これでアプリと、それ以外のプロセスで必要な容量がわかりましたが、それ以外にもOSはファイルキャッシュなどでメモリを消費します。これは空いている分だけ使用して、他が必要とすれば開放するような性質だったりするので、どれだけ必要と確定するのは難しいです。なので、今回は1,000~2,000MB は欲しいということにして、2,000MB としておきましょう。
メモリ量から接続数を計算する
今回はインスタンスのリソースが 8vCPU , Mem 16,000MB とします。まず、他プロセスと、ファイルキャッシュなどOS用を確保する意味で引き算します。
アプリ用容量(MB) = 16,000 – 2,000 – 2,000 = 12,000
12,000MB 使ってよいことがわかったので、次は最大どのくらいの接続数を待機させられるかを計算してみます。親プロセス分を引いて、子の必要量で割ると
同時接続数の最大 = (12,000 – 300) / 400 = 29.25 ≒ 29
さっきの接続数のところでは 16 >= conn > 4 と仮確定したので、これだとだいぶ大きいのと、400MB ギリで計算するとプロセスが肥大してきた時に余力が全然ないことになるので、あくまでこれ以上にはできない、という最大値になります。
接続数からメモリ容量を計算する
仮の接続数から計算してみましょう。16接続なら、どのくらいのプロセスあたりの容量上限になるかというと子プロセスの上限容量(MB) = (12,000 – 300) / 16 = 731.25 ≒ 730
稼働中400MBに対して、300MB 以上の余力がある設定になります。もし 8接続 とした場合、
子プロセスの上限容量(MB) = (12,000 – 300) / 8 ≒ 1460
これだと余力がありあまる形になります。どうせ使わない可能性が高い容量を確保するよりも、同時接続数を多く捌けたほうが有用なので、では 24接続 ならというと
子プロセスの上限容量(MB) = (12,000 – 300) / 24 ≒ 480
余力が 80MB しかないことになり、もしかしたら開発が進みつつ運用が数ヶ月経過したときには、不足してしまうかもしれません。
と、考えると今回のインスタンス・リソース量と、稼働中プロセスの状況からすると、ちょうどよい数字としては 16接続 かつメモリ上限設定を 600~700MB にしておくと、長期的にみて余裕、かつOS全体としても余力が十分、リクエスト数が倍以上になっても大丈夫!ということになりそうです。
リクエスト数の計算
一定の処理回数に到達したら、プロセスを作り直す仕組みを入れるとしたら、その回数を決める必要があります。監視データから、24時間範囲でのリクエスト/秒 の平均値を確認し、今回は 10 req/s だったとします。その場合、秒分時日週でどのような処理回数になるかというと、
のように計算できます。
これを見て、例えば1日に1回は作り直したいならば、864,000 に近い── まぁ 1,000,000 とかにしておけばよさそうですし、週に1回なら 6,000,000 あたりでよさげと判断できます。
ただこの設定値は、日常の平均req/s が変われば到達期間も変わってしまうので、あまりシビアに考えるとよくありません。平均10req/s だったのが 20req/s になることは普通にありますし、CPUリソースや台数の都合でインスタンスタイプを1つ上げて台数を半減させただけでも、流入する req/s は倍になるからです。
環境条件が変わるたびに調整するのは現実的じゃないので、これはあくまでオマジナイ要素が半分以上だと思って、気持ち緩和的な数値で大きめに設定しつつ、忘れ去られない程度には調整する、くらいに思っておいたほうがよさげです。
Unicorn での例
本調査をした時の Rails + Unicorn を例にします。設定項目
子プロセス数の数はここで設定しています。
1 2 |
$ cat config/unicorn.rb | grep ^worker_process worker_processes 16 |
メモリ・リクエスト数の上限による作り直しは、unicorn-worker-killer で実現していて、
1 2 3 4 5 6 |
$ cat config/initializers/unicorn_worker_killer.rb | grep -v "#" | grep -v ^$ if Rails.env.production? require "unicorn/worker_killer" Rails.application.middleware.use Unicorn::WorkerKiller::MaxRequests, 900000, 1000000 Rails.application.middleware.use Unicorn::WorkerKiller::Oom, (650 * (1024**2)), (700 * (1024**2)) end |
設定数値に前後がありますが、前の値を超えたらその後にランダム気味に再起動対象となり(複数同時の再起動を回避するため)、後ろの値はズバリ上限として超えたらアウトな絶対的存在になっています。
通常メモリ容量は超急激に上昇することはないので、この仕組みだとほぼ前の数値を超えた時点で再起動対象ということになります。
事故例
リリース当初はプロセスあたり 300MB だったため、上限値を 400MB にしていたとしましょう。それが長期間の開発と運用によって、徐々に 360~401MB の範囲で消費するようになってしまったとします。どうなるかというと、当然 400MB を超えた時点でほぼプロセスが再起動するのですが、起動後に 400MB に到達するのが実は数十秒~数百秒 程度だったとすると、各プロセスが数分間隔で再起動を繰り返す、という事象になります。
幸いなことに、バラバラのタイミングでの再起動になるので障害にはならないのですが、再起動が頻発するとどうなるかというと、
といった現象を観測できます。パフォーマンス的には、やたらと秒単位のレスポンス速度が記録されたり、SHOWクエリの数と割合がエライことになったりします。
それを見つけると、他のグラフも確認し、Unicorn の再起動回数が記録されていれば原因にたどり着くことは早いですが、無いと結構頭をひねることになるかもしれません。
原因をプロセス再起動と推測したら、すでに記述した通り、実際に数値を取得してきて試算し、設定の変更を反映して茶でもしばきます。
本来、プロセスの安定のために入れた仕組みが、管理不足で逆に事故の原因になる、というのは皮肉なものです。が、より根本的な原因はなにかと言うと、定期的な管理不足もしくは自動監視不足と言ってよいでしょう。
インスタンスタイプの調整
少し話の方向を変えて、リソース計算での効率の話にします。さきほどは、8vCPU , Mem 16GB で、監視データから接続数 16 , メモリ上限 650~700MB , リクエスト上限 90~100万 としました。
これなら総合的にほどよいバランスですが、このリソース量は AWS EC2 でいうところの C系 のタイプの割当量です。これがもし、M系 で運用されていたとしたら、どうなるでしょうか。
M系で同CPU数だと、8vCPU , Mem 32GB になります。メモリが多いと、より接続数を多くできますが、今回の試算上は少なくとも 16以上 はいらないと判断しています。ということは、メモリ過剰になっているということです。
これをメモリ量で最適化しようとすると、1つタイプを下げて 4vCPU , Mem 16GB にすることになりますが、これだと vCPU が変わって接続数の試算が変わるだけじゃなく、捌ける処理量自体が減少するのでよくありません。
この場合、m5.2xlarge (8vCPU , Mem 32GB) だったものを、c5.2xlarge (8vCPU , Mem 16GB) にすることで最適化メリットを得られます。
逆に、メモリ割合が高いタイプにしなくちゃいけなくなる試算結果の場合もあるかもしれませんが、よほどの内容でない限りは基本的にCPUが強いタイプを選んで台数でカバーする方が、サービス全体のパフォーマンスとしては良くなることが多いので、うまい具合にコストとパフォーマンスのバランスを考えてみましょう。
接続数の全体調整
最後におまけ的に、接続数を軸にした全体の調整の話です。接続数はミドルウェアの構成の中で、結構重要な要素になっていて、調整を適当にやるとよろしくないことになります。システムの構成は大雑把には、
LoadBalancer → WEB/AP → DB | KVS
という多段構成になっています。
この中で接続数は、後ろに行くほど受け入れ可能な接続数を 多く しておくことが健康的だということです。わかりやすい箇所としては、APs > DB です。
DBサーバーの最大接続数が 500 に設定されているとします。
APサーバーの1台あたりの最大接続数を 16 とし、Autoscalingによる最大台数が 50 だとしましょう。普段はピークタイムでも 20台 程度だとしたら、APサーバー全体の接続数としては 16 * 20 = 320 なので、全APプロセスがDBに接続することができます。
これが高負荷により最大台数に到達したとき、APサーバー全体の最大接続数としては、16 * 50 = 800 となります。このとき、当然あぶれたAP300プロセス分はDB接続エラーが発生します。これを解決するには、APサーバーの最大台数を減らすか、AP1台あたりの接続数を減らすか、DB接続数を増やして対処することになります。
リクエストの処理としては、前から順に正常に受け入れられているのに、バックエンドの都合で接続エラーになってしまうのは、勿体ないというか不適切な設定構成であるということです。つまり、接続数の関係としては、
LoadBalancer < WEB/AP < DB | KVS
となり、もっと具体的に表現すると
最大同時ユーザー接続数 < LB接続数 < WEB/AP 群の合計接続数 < DB接続数
となります。こうすると少なくとも接続エラーは起きませんが、CPUリソース不足やストレージ性能上限での処理遅延が障害のメインになります。でも、リソース原因でも接続数でも障害要因となる設計よりも、リソース要因だけにしておいた方が対処は確実にシンプルです。
接続数を多くするということは、特にDBでは スレッドメモリ * 接続数 でのメモリ計算があるので、さらなる試算が待っているのですが、それはデータベースの勉強をするということで……
とにかく、接続数を調整するということは、システム全体の各接続数も調整対象になる、ということです。
この辺は小規模では適当気味で動いていても、中規模以降では台数の増加により途端に重要性が高くなるので、油断大敵です。
できれば設定値も自動化とかしたいところですが、自動化はOSのリソース量から計算できるものはしやすくても、不確定な変化や他サーバーとの関連があると、完全な自動化は難しく部分的になるかと思います。
なので、可能な限り関連項目を監視データ・アラートにすることと、定期検査が大切です。というなんとも普通すぎて締まらない感じでFINISHです:-)