ActiveJobの優先度制御にNice値を使用する

株式会社リゾーム 業務ソリューション事業グループの原です。

弊社ではショッピングセンター向けの製品の自社開発を行っており、私はBOND GATEという製品の開発に携わっています。 こちらはRuby on Railsで開発しており、その中の管理者用の機能の一つとして、ログの内容をCSVファイルにして出力する機能があります。

今回は、この機能を一部の環境で使用した際に、処理が重くなり動作しなくなる……という事象が発生しました。 そのときの対応・検証についてまとめたので、似たような問題が起きた際の解決策となると思い、記事にしています。

経緯

ログ内容のCSVファイルを出力するActiveJobを実行した際に、ある環境だけメモリ使用率が100%まで達し、処理ができなくなるという状態が発生しました。

原因を調べたところ、大量のActiveRecordeachで処理しており、環境によっては100万件以上になってしまうためメモリが足りなくなってしまう……という状態でした。

これに関してはfind_eachを使うことでメモリ使用率は抑えられる……のですが、今度はCPU使用率が90%前後になっていました。 BOND GATEはAPサーバとワーカーを1台サーバーで動かしているので、ワーカーがCPUを90%も使っていたらAPサーバが動けなくなってしまいます。

この負荷への対処方法を検討していた際、当初はActiveJobのCPU使用率が高いことから、リソースの制限を加える方向で考えていました。

チームで調査を進める中で、「プロセスの優先度を下げれば、他の処理に影響を与えずに済むのでは?」というアイデアが出ました。調べてみると、Linuxには「Nice値」というプロセスの優先度を調整する仕組みがあることがわかりました。これを使えば、他の処理に影響を与えずに負荷をコントロールできそうです。

本当に効果があるのかを確かめるため、検証を行うことにしました。

Nice値の設定

webkaru.net

0を標準として、-20~19の範囲で数字を大きくするほど優先度は低く、下げると高くなります。逆にしないように注意。

ActiveJobアダプタにはsidekiqなどがありますが、BOND GATEではqueというgemを使用しています。

github.com

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-ng 100%
    • 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-ng 50%
    • 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-ng 80%
    • 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-ng 80% + 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に登録するジョブそのものの優先度を設定することにしました。

railsguides.jp

queue_with_priorityでジョブの優先度を設定することができます。 既に設定されているところはないかとコードを確認してみると、どうやらメール送信のジョブでqueue_with_priority(200)に設定されていたので、同様に設定してみました。

class CreateCsvJob < ApplicationJob
  # CSV出力は優先度を下げるため、200に設定
  queue_with_priority(200)

これで他のジョブより後に実行されるようになるはずです。

確認方法としてはまずこのジョブを数回queに登録しておきます。 その間に別のジョブ(今回は別のデータのzipファイル出力処理)を実行します。 優先度が同じならばCSV出力がすべて終わった後にzip出力処理が行われるはずですが、優先度を下げてるので現在のCSV出力が終わったら、残っているCSVの前にzip出力処理が実行されるはずです。 この方法で無事優先度が下がっていることを確認しました。今度こそ問題なさそうです。

まとめ

今回は優先度周りの設定をすることで処理の重いジョブを上手く捌くことができました。

私自身はこういった対応方法に疎く、「ジョブが重いなら処理をどうにかして軽くするしか無い」……という方向に行きがちなのですが、チームメンバーから対応方法についてのアプローチを頂き、今回の対応をしました。

今回の対応のように、別の視点からの解決方法がないか、探せるように知識をつけていければ良いなと思う出来事でした。