『Sidekiq in Practice』読書メモ・感想

「Sidekiq in Practice」を読みました。

nateberk.gumroad.com

実践的な観点でSidekiqの仕組みや設定、留意点などが解説されており、読んだ後の今は、すぐにでも業務のプロダクトに活かしたい!という気持ちになっています。
既存のSidkiqの設定の変更の見直しのきっかけにもなりつつ、開発チームのレビューの観点にも新しく含められそうなところがたくさんあり、大変勉強になりました。

各章ごとに印象に残ったポイントと感想を簡単にまとめました。

1: How Sidekiq Works, And Why

要約・印象に残った内容

  • Sidekiqのクライアントは単なるRubyオブジェクト
  • 実行されるRedisコマンドの数を意識するべき
    • Redisは直列実行なので、一般的にコマンド数が2倍になると実行時間も2倍になる。
Sidekiqの流れの要約
  1. クライアントは、[キュー、クラス、引数]をJSONに変換し、2つのRedisコマンド(SADDとLPUSH)を使ってプッシュ
  2. プロセッサオブジェクトはBRPOPを使ってジョブを取得し、デシリアライズされた引数を使って、ワーカークラスのperformメソッドを実際に呼び出す

感想

  • SidekiqはRedisを使って実現している、くらいまでしか知らなかったので、内部の仕組みまで知れたのは勉強になった

2. Understanding Queueing Systems

要約・印象に残った内容

  • 待ち時間は、サーバーの利用率によって指数関数的に増える(not直線的)
  • 逆に言えば、サーバーを増加することによる効果が大きいとも言える
  • 稼働率を監視する際には、OSレベルのCPU利用率だけに注目しない
    • CPU利用率が20%でもSidekiqが1スレッドのみで処理をずっとしていたら実質100%
  • 飽和状態の解決は、1. Sidekiqのプロセスを増やす 2. 並列性を上げる の手段がある

感想

  • キューイングシステムが持つ特徴と落とし穴、スケールする方法の概要が説明された章
  • CPUの利用率で安心していたこともあったので、まさに、、という感想。

3. Setting Concurrency

要約・印象に残った内容

  • 並列性を上げる方法(concurrency数を上げる)は、思ったよりも高コストになる
    • メモリの問題(glibcとCRubyの相性)
    • GVL(同時にRubyコードを実行できるスレッドは1本だけ)でキューイング(待ち時間)が発生
    • DBコネクションプールとの関係(DBの接続数 < Sidekiqのスレッドだと、待ち時間が爆増する恐れ)
  • GVLにより、Rubyコードは1本のスレッドでしか動けない。
    • APMなどでI/O待ち時間の割合を計算し、concurrencyを決めると良い

感想

  • GVLについて、なんとなく知っていた、くらいだったが、具体的に並列実行を考えたときにどのような動きになるのか、という点を理解できるようになったのはとても良かった

4. Why Is My Queue So Long?

要約・印象に残った内容

  • 以下の計算でサーバー(1プロセス)利用率を50%程度にするのが適切
    1. 提供トラフィックを計算(リトルの法則)
    2. 1秒あたりにどれだけのjobが到着するか × jobのサービスタイム(実行時間)の平均
    3. 並列度の計算
    4. プロセス数 × Amdahlの法則での換算値(I/O割合とconcurrencyから)
    5. 利用率
    6. 提供トラフィック ÷ 並列度
  • 制御しやすくするため、飽和が起こる場所はRedisのキューに限定する
    • CPUやメモリが閾値を超えないようにする
  • Redisの処理がボトルネックにならないよう、引数はなるべく小さくする

感想

  • これは実際にプロダクトの状況を計測して改善できるところがないかさっそく探すことにする
  • 引数を小さくする話は、開発チームとしてのレビュー観点などに入れるのも良さそう

5: How Many Queues Should I Have?

要約・印象に残った内容

  • キューには、criticalなどより、いっそ具体的に in_5_minutesなどの実行開始までの時間を書いておき、このキューごとにオートスケール戦略を立てると良い
  • DB高負荷のジョブやレート制限のあるAPIを叩くジョブなどは、concurrencyを下げたキューを設定して制御する
    • 1プロセス運用でも、CapsuleというProから使える機能を使うことで制御可能

tech.smarthr.jp

  • キューには重みづけを設定し、優先度の高いキューに大量のジョブが積まれて優先度の低いジョブが実行できない、という状態を避ける
  • find_in_batchedを使うなどし、ジョブ数が爆発しないようにする
  • push_bulkを使い、redis操作を1回で済むように工夫できる
  • 並列実行すると何が起こるかわからないジョブは並列数1で隔離キューに置いておく

感想

  • かなり具体的に色々な対応方法が示されていた章
  • 知らないノウハウもあったので非常に有益な章だった

6: Maximizing Servers Without Running Out Of Resources

要約・印象に残った内容

  • リソースを使い切れるかという基準にCPUの使用率を用いないこと
    • I/O比率が高いジョブをたくさん処理していたらもうリソースはフルで使っていてもCPU上は低かったりする
  • 余計な費用がかかることを避けるため、オートスケーリングの単位は小さく(平均トラフィックの25%以下に)する
  • Redisはシングルスレッド + イベントループ設計なので、1スレッドの処理能力が重要
  • Sidekiq Enterprise Swarmも1つの手段

感想

  • Sidekiq Enterprise Swarmを使うとオートスケーリングの単位は大きくなるので、そこはバランスを取る必要がありそう

7: My Jobs Are Running Twice!

要約・印象に残った内容

  • ジョブの重複実行を避けるよりも冪等性を徹底することが大切
    • ジョブの重複実行防止を実現しようとするプラグインはRedisに大きな負荷をかける
    • 厳密に冪等性を管理するなら悲観的ロックという方法もあるが、当然使い方には注意

感想

  • これは重要な割り切りだと思った
  • 重複実行を避ける方法を考えると沼にハマるので、冪等性にして重複実行前提にする、という割り切りが必要
  • これも開発チームの重要なレビュー観点にできそう

8: How Do I Set Database Pool Sizes?

要約・印象に残った内容

  • 総DB接続 = min(接続が要るスレッド数, AR pool) × プロセス数/サーバー × サーバー数
    • 1プロセスあたりのコネクションプールとSidekiqのconcurrencyにも気を配りつつ、DB全体の接続数の上限を超えないようにする
    • pgbouncerを使ったりするのも手段

感想

  • これを機に、(正直今まであまり考えられていなかった)プロセスごとのコネクション数と、DB全体のコネクション数について考えることができた

9: Is My App Thread-Safe?

要約・印象に残った内容

  • 同じ並列度を達成するなら1スレッドでプロセスを増やすより、マルチスレッドにした方がより少ないメモリで実現できる
  • スレッドバグを避けるには、Sidekiqアプリケーションのどこに共有状態が存在するのかを理解することが重要
  • Sidekiqジョブにおける「共有」状態は、複数の Sidekiq::Processor インスタンスからアクセス可能なあらゆるオブジェクト
    • ただし、Sidekiqはジョブごとにジョブクラスの新しいインスタンスを生成する
  • Minitestで並列実行を有効にすれば並列環境でテスト可能

感想

  • 並列実行のテストしたらいいのに、は本当にそうだね、となった
  • 共有についてはRubyの基本的な使い方をしていれば概ね問題はなさそうだなという印象

10. Making Sidekiq Use Less Memory

要約・印象に残った内容

  • CRubyではメモリが一度膨らむと戻りづらい
    1. find_eachでバッチ化、2. mapmap!でコピーによるメモリ爆増を防ぐ といった回避方法
  • メモリは100%に近いでもスループットにほとんど影響がない一方で、101%になるとOOMで終わる(崖)性質を持つことを理解する

感想

  • メモリが思ったより高いところで張り付いてしまっているぞ?の経験は今までもあったので共感した
  • 根が深い問題のようなので、とにかくメモリが爆増しない実装に注意し続ける必要がある