Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SideKiqでジョブが二重起動した事象を深堀りしました

 SideKiqでジョブが二重起動した事象を深堀りしました

Fukuoka.rb #397 〜RubyKaigi 2025の機運〜 登壇資料
https://fukuokarb.connpass.com/event/345164/

はたちとものり

March 20, 2025
Tweet

More Decks by はたちとものり

Other Decks in Programming

Transcript

  1. https://findy-code.io/companies/2086/jobs/B9yw9L141p8aA 
 • [Done] Rails 6.0 -> 6.1 
 •

    [Done] Rails 6.1 -> 7.0 ※今月 
 • [Todo] Rails 7.0 -> 8.1 
 売上前年比200%で成長中! 
 • 100万ユーザーを突破しました! 
 • データ量が多くなってきました 
 • 初期に開発したところ、データ量的に厳しさが 
 • 拡張し辛いコードもときどき。 
 • 積極的に片付けながら開発してます! 
 エンジニア募集中

  2. 自己紹介
 廿千 智紀(ハタチ トモノリ) @t_hatachi
 株式会社トイポ シニアエンジニア
 略歴
 • 2006

    - 2020 医薬業界SIer
 • 2020 - 2023 SES
 • 2023/09〜株式会社トイポ(=Ruby歴)
 Rails楽しいです。SQL好きです。
 趣味
 • 釣り
 • DIYなんでも(3Dプリンタ歴10年くらい)

  3. 最近になって急に2回発生しました。
 • 2024/12/23 18:00
 ◦ ChargeXXXXXJob
 • 2025/02/09 00:07
 ◦

    PublishCouponXXXXXXXJob 
 
 SideKiqでジョブが二重起動してしまった
 直接お金に関わる
 クーポン増殖

  4. 最近になって急に2回発生しました。
 • 2024/12/23 18:00
 ◦ ChargeXXXXXJob
 • 2025/02/09 00:07
 ◦

    PublishCouponXXXXXXXJob 
 背景
 • 32種類の定期ジョブ
 • 2台のtoypo-worker
 ◦ ジョブキューにRedis。 
 ◦ 同時にジョブをキューイングしたことで、 
 ジョブが重複して実行されてしまった模様。 
 ◦ キューイングの起点までは(私は)追えませんでした。 
 SideKiqでジョブが二重起動してしまった
 直接お金に関わる
 クーポン増殖

  5. SideKiqでジョブが二重起動してしまった
 最近になって急に2回発生しました。
 • 2024/12/23 18:00
 ◦ ChargeXXXXXJob
 • 2025/02/09 00:07


    ◦ PublishCouponXXXXXXXJob 
 背景
 • 32種類の定期ジョブ
 • 2台のtoypo-worker
 ◦ ジョブキューにRedis。 
 ◦ 同時にジョブをキューイングしたことで、 
 ジョブが重複して実行されてしまった模様。 
 ◦ キューイングの起点までは(私は)追えませんでした。 
 直接お金に関わる
 クーポン増殖
 toypo-worker-1
 toypo-worker-2
 job queue
 queueing
 queueing

  6. 二重起動の原因を調べる
 # Removes a queued job instance # # @param

    [String] job_name The name of the job # @param [Time] time The time at which the job was cleared by the scheduler # # @return [Boolean] true if the job was registered, false otherwise def self.register_job_instance(job_name, time) job_key = pushed_job_key(job_name) registered, _ = Sidekiq.redis do |r| r.pipelined do r.zadd(job_key, time.to_i, time.to_i) r.expire(job_key, REGISTERED_JOBS_THRESHOLD_IN_SECONDS) end end registered end
 ruby/2.7.0/gems/sidekiq-scheduler-3.1.0/lib/sidekiq-scheduler/redis_manager.rb
 ジョブが重複しない制御があるのでは?
 • Sidekiq-scheduler(gem)
 ◦ Redisに一意なキーを作成(zadd) 
 ◦ 成功したらキューイング 
 

  7. ジョブが重複しない制御があるのでは?
 • Sidekiq-scheduler(gem)
 ◦ Redisに一意なキーを作成(zadd) 
 ◦ 成功したらキューイング 
 •

    Sidekiq(gem)
 ◦ エンキュー
 ◦ 成功したらRedisからキーを削除(zrem) 
 
 二重起動の原因を調べる
 def enqueue_jobs(now=Time.now.to_f.to_s, sorted_sets=SETS) Sidekiq.redis do |conn| sorted_sets.each do |sorted_set| while job = conn.zrangebyscore(sorted_set, '-inf', now, :limit => [0, 1]).first do if conn.zrem(sorted_set, job) Sidekiq::Client.push(Sidekiq.load_json(job)) Sidekiq::Logging.logger.debug { "enqueued #{sorted_set }: #{job}" } end end end end end ruby/2.7.0/gems/sidekiq-5.2.10/lib/sidekiq/scheduled.rb

  8. ジョブが重複しない制御があるのでは?
 • Sidekiq-scheduler(gem)
 ◦ Redisに一意なキーを作成(zadd) 
 ◦ 成功したらキューイング 
 •

    Sidekiq(gem)
 ◦ エンキュー
 ◦ 成功したらRedisからキーを削除(zrem) 
 これらを踏まえると。
 ➔エンキュー後重複ジョブをキューイング可能。
  (ジョブの起動とか完了は関係なかった)
 二重起動の原因を調べる
 def enqueue_jobs(now=Time.now.to_f.to_s, sorted_sets=SETS) Sidekiq.redis do |conn| sorted_sets.each do |sorted_set| while job = conn.zrangebyscore(sorted_set, '-inf', now, :limit => [0, 1]).first do if conn.zrem(sorted_set, job) Sidekiq::Client.push(Sidekiq.load_json(job)) Sidekiq::Logging.logger.debug { "enqueued #{sorted_set }: #{job}" } end end end end end ruby/2.7.0/gems/sidekiq-5.2.10/lib/sidekiq/scheduled.rb

  9. ジョブが重複しない制御があるのでは?
 • Sidekiq-scheduler(gem)
 ◦ Redisに一意なキーを作成(zadd) 
 ◦ 成功したらキューイング 
 •

    Sidekiq(gem)
 ◦ エンキュー
 ◦ 成功したらRedisからキーを削除(zrem) 
 これらを踏まえると。
 ➔エンキュー後重複ジョブをキューイング可能。
  (ジョブの起動とか完了は関係なかった)
 二重起動の原因を調べる
 toypo-worker-1
 toypo-worker-2
 job queue
 ②queueing
 ③enqueue
 ①zadd(key1)
 ④zrem
 ⑤zadd(key1) => true
 ⑥queueing

  10. ジョブの二重起動を防ぐ
 
 エンキュー後も一定期間はジョブのキューイングをロックしたい。
 ➔activejob-uniquenessを使う。
 二重起動したくないジョブに適用していく
 
 
 class UniqueApplicationJob <

    ApplicationJob unique :until_expired end app/jobs/unique_application_job.rb(NEW!) 
 config.lock_ttl = 1.minute config/initializers/active_job_uniqueness.rb(NEW!) 
 - class ChargexxxxxJob < ApplicationJob + class ChargexxxxxJob < UniqueApplicationJob queue_as :default app/jobs/charge_xxxxx_job.rb(もう二重起動したくない) 
 継承

  11. ジョブの二重起動を防ぐ
 activejob-uniqueness
 • キューイング後Redisに一意なキーを作成
 ◦ sidekiq-schedulerがzaddするkeyとは別モノ 
 ◦ キーが存在する間は同じジョブをロック 


    ◦ 設定により期限は60秒間 
 • ロックされた状態でジョブをキューイング
 するとエラー
 ActiveJob::Uniqueness::JobNotUnique: Not unique ChargexxxxxJob (Job ID: dc0af9b7-d849-4744-925e-658afea66494) (Lock key: activejob_uniqueness:charge_xxxxx_job:no_arguments) []
  12. ジョブの二重起動を防ぐ
 activejob-uniqueness
 • キューイング後Redisに一意なキーを作成
 ◦ sidekiq-schedulerがzaddするkeyとは別モノ 
 ◦ キーが存在する間は同じジョブをロック 


    ◦ 設定により期限は60秒間 
 • ロックされた状態でジョブをキューイング
 するとエラー
 toypo-worker-1
 toypo-worker-2
 job queue
 ②queueing
 + zadd(unique)
 ③enqueue
 ①zadd(key1)
 
 ④zrem(key1)
 (uniqueは残る)
 ⑤zadd(key1) => true
 ⑥queueing
 uniqueがある ので✕
 ActiveJob::Uniqueness::JobNotUnique: Not unique ChargexxxxxJob (Job ID: dc0af9b7-d849-4744-925e-658afea66494) (Lock key: activejob_uniqueness:charge_xxxxx_job:no_arguments) []
  13. ジョブの二重起動を防ぐ
 他の案だったもの
 • sidekiq enterprise → 💲223/mo
 • ジョブの実行タイミングによらず処理の一貫性が確保されるようにプロダクトコード を修正する。

    → 💲??? * 32ジョブ
 テスト
 • 同じジョブを何度も実行する場合があった
 • uniqueを無効にした
 ActiveJob::Uniqueness.test_mode! spec/rails_helper.rb 

  14. まとめ
 • Sidekiqでworker複数台構成だとジョブが二重起動する可能性があります。
 ◦ トイポではこの対応を2/21にリリース後、再発していません。 
 ◦ 今日の話がどこかで誰かの役に立てたら嬉しいです。 
 ◦

    トイポのSREはよいSREです。 
 
 • 今回の対応で分かったことと、分からなかったこと。
 ◦ Sidekiqのジョブのキューイングの挙動がよく理解できました。 
 ◦ worker2台のキューイングのタイミングはどうなっている…? 
 ◦ 機会があったらgemのコードを追いかけてみます。