株式会社リゾーム 業務ソリューション事業グループの原です。
弊社ではショッピングセンター向けの製品の自社開発を行っており、私はBOND GATEという製品の開発に携わっています。 こちらはRuby on Railsで開発しており、その中の管理者用の機能の一つとして、ログの内容をCSVファイルにして出力する機能があります。
今回は、この機能を一部の環境で使用した際に、処理が重くなり動作しなくなる……という事象が発生しました。 そのときの対応・検証についてまとめたので、似たような問題が起きた際の解決策となると思い、記事にしています。
経緯
ログ内容のCSVファイルを出力するActiveJobを実行した際に、ある環境だけメモリ使用率が100%まで達し、処理ができなくなるという状態が発生しました。
原因を調べたところ、大量のActiveRecordをeachで処理しており、環境によっては100万件以上になってしまうためメモリが足りなくなってしまう……という状態でした。
これに関してはfind_eachを使うことでメモリ使用率は抑えられる……のですが、今度はCPU使用率が90%前後になっていました。
BOND GATEはAPサーバとワーカーを1台サーバーで動かしているので、ワーカーがCPUを90%も使っていたらAPサーバが動けなくなってしまいます。
この負荷への対処方法を検討していた際、当初はActiveJobのCPU使用率が高いことから、リソースの制限を加える方向で考えていました。
チームで調査を進める中で、「プロセスの優先度を下げれば、他の処理に影響を与えずに済むのでは?」というアイデアが出ました。調べてみると、Linuxには「Nice値」というプロセスの優先度を調整する仕組みがあることがわかりました。これを使えば、他の処理に影響を与えずに負荷をコントロールできそうです。
本当に効果があるのかを確かめるため、検証を行うことにしました。
Nice値の設定
0を標準として、-20~19の範囲で数字を大きくするほど優先度は低く、下げると高くなります。逆にしないように注意。
ActiveJobアダプタにはsidekiqなどがありますが、BOND GATEではqueというgemを使用しています。
queのプロセスはsystemdで設定しているので、Nice値もここから設定できました。
[Service]
Type=forking
WorkingDirectory=#{current_path}
ExecStart=/opt/bondgate/bin/que_#{File.basename(deploy_to)}.sh
PIDFile=#{shared_path}/pids/que.pid
User=********
Group=********
Nice=19
これでデプロイするとqueのNice値が19になったので、ここからstress-ngコマンドを使って負荷をかけていきます。
まずはstress-ngの負荷を色々変えてみて、topコマンドでジョブ実行中のqueがどのくらいのCPU使用率になるのかを見てみます。
(COMMAND列は長くなり見づらくなったため書き換えています)
stress-ng100%- queはほとんど使われなくなる
stress-ng --cpu 0 --cpu-load 100
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2703937 ******** 20 0 84508 7736 4016 R 93.4 0.2 1:43.14 stress-ng 100 2703938 ******** 20 0 84508 6220 4020 R 92.1 0.2 1:42.42 stress-ng 100 2424664 ******** 39 19 1745624 483748 11368 S 1.0 12.3 38:38.20 que
stress-ng50%- queの使用率は70~80%
- 負荷なしのときより多少使用率は減るが、あまり変わらない印象
stress-ng --cpu 0 --cpu-load 50
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2424664 ******** 39 19 1745624 483752 11368 R 78.7 12.3 40:13.35 que 2704125 ******** 20 0 84508 6424 4232 S 46.8 0.2 0:52.49 stress-ng 50 2704126 ******** 20 0 84508 6424 4236 R 45.5 0.2 0:52.48 stress-ng 50
stress-ng80%- queの使用率は45%前後
stress-ng --cpu 0 --cpu-load 80
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2704529 ******** 20 0 84508 7612 3884 R 76.7 0.2 0:54.29 stress-ng 80 2704530 ******** 20 0 84508 7612 3888 R 75.1 0.2 0:54.32 stress-ng 80 2424664 ******** 39 19 1745624 483740 11368 S 42.5 12.3 43:41.81 que
stress-ng80% +abコマンドabコマンド(Apache Bench)を使って並列で100アクセスさせる負荷を追加でかけてみました- queの使用率は20~30%程度
stress-ng --cpu 0 --cpu-load 80 ab -c 100 -n 1000 https://********
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2706435 ******** 20 0 84508 6300 4100 R 56.1 0.2 1:35.51 stress-ng 80 2706434 ******** 20 0 84508 6300 4096 R 54.5 0.2 1:35.82 stress-ng 80 2424664 ******** 39 19 1745624 484084 11368 S 22.3 12.3 45:59.07 que
その後Nice値を変えて結果が変わるのかも見てみました。reniceコマンドで実行中のプロセスのNice値を変更できます。
renice 0 -p <PID>
queの優先度が高くなるのだから、先程よりCPU使用率は上がるはずです。
しかしqueのNice値を0にしてみても特に結果が変わりませんでした。
stress-ngの負荷が高すぎてこうなるのかわかりませんが、Nice値の設定は本当に効いてるの?と少し不安になります。
そこで逆にqueのNice値を0、stress-ngのNice値を上げてみました。
こちらはreniceではなく、実行時にNice値指定すればOKです。
- Nice値10
nice -n 10 stress-ng --cpu 0 --cpu-load 100
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3138372 ******** 30 10 84508 7808 4096 R 93.0 0.2 0:25.72 stress-ng 100 3138371 ******** 30 10 84508 7808 4092 R 90.7 0.2 0:25.48 stress-ng 100 2424664 ******** 20 0 1703336 383344 10660 S 14.6 9.7 91:41.96 que
- Nice値19
nice -n 19 stress-ng --cpu 0 --cpu-load 100
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3138439 ******** 39 19 84508 8140 4164 R 82.0 0.2 0:17.98 stress-ng 100 3138440 ******** 39 19 84508 6360 4168 R 76.3 0.2 0:18.31 stress-ng 100 2424664 ******** 20 0 1703336 383304 10660 S 40.0 9.7 92:03.43 que
10、19と変えてみるとちゃんとstress-ngのCPU使用率が減り、queは増えました。
それでも大半はstress-ngが占めているので、元々の処理の重さで変わりそうですが、ちゃんと効いてそうです。
ちなみにNice値を1上げるだけだと特に変わらないように見えたので、ある程度大きく変更したほうが良いかもしれません。
queue_with_priority を設定する
これでOK……とはいかず、そもそも原因となったジョブが7~8分かけてCSV出力していたので、そこに他のジョブが入ってくるとその間は処理を待つことになります。 可能性は低いとは思いますが、CSV出力処理を連打して複数個queに登録されてしまったら、30分ほど他のジョブが捌けない、という状態になってしまいます。
幸い、このジョブは裏でCSV生成・出力処理を行い、後ほどダウンロードできるようになったらお知らせします、というものなので後回しになっても良いものです。 ということでqueに登録するジョブそのものの優先度を設定することにしました。
queue_with_priorityでジョブの優先度を設定することができます。
既に設定されているところはないかとコードを確認してみると、どうやらメール送信のジョブでqueue_with_priority(200)に設定されていたので、同様に設定してみました。
class CreateCsvJob < ApplicationJob # CSV出力は優先度を下げるため、200に設定 queue_with_priority(200)
これで他のジョブより後に実行されるようになるはずです。
確認方法としてはまずこのジョブを数回queに登録しておきます。 その間に別のジョブ(今回は別のデータのzipファイル出力処理)を実行します。 優先度が同じならばCSV出力がすべて終わった後にzip出力処理が行われるはずですが、優先度を下げてるので現在のCSV出力が終わったら、残っているCSVの前にzip出力処理が実行されるはずです。 この方法で無事優先度が下がっていることを確認しました。今度こそ問題なさそうです。
まとめ
今回は優先度周りの設定をすることで処理の重いジョブを上手く捌くことができました。
私自身はこういった対応方法に疎く、「ジョブが重いなら処理をどうにかして軽くするしか無い」……という方向に行きがちなのですが、チームメンバーから対応方法についてのアプローチを頂き、今回の対応をしました。
今回の対応のように、別の視点からの解決方法がないか、探せるように知識をつけていければ良いなと思う出来事でした。