1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ActiveStorageでアップロードしたファイルをsidekiqで遅延少なく処理する(sidekiqのコードリーディング)

Last updated at Posted at 2021-11-14

やりたいこと(背景)

フロントエンドからファイルをActiveStorageでアップロードし、そのファイルを利用した処理を行う必要がある。
該当のファイルへの処理は数秒かかるためweb apiはすぐに返し、該当の処理を非同期実行することにした。
該当処理のOK / NGの結果に応じてユーザーに再度アップロード要求する必要があり、ユーザーには解析中という表示で待たせている。(処理に時間がかかりすぎた時の対応は別途検討してますが今回は省略)
サーバー処理が終わったらActionCableを利用してフロントに判定結果を返す。

ファイルアップロード.png

解析job.png

※ 簡素化しているため一部違和感があるかもしれませんが全体の流れの把握が目的のためご容赦ください。

発生した事象

step1: ステージング環境で ActiveStorage::FileNotFoundError が発生

起きた事象

解析用のworkerを perform_async で呼び出した結果、最初の1回目に高確率で ActiveStorage::FileNotFoundError が発生。
ただし、2回目のリトライ時には当該エラーが発生せず、正常終了していた。

原因

ActiveStorageのファイルアップロードジョブは非同期で行われており、「ActiveStorageのファイルアップロードジョブ」と「ファイル解析用のworker」のレースコンディションが起きていた。

対応

解析用のworkerの呼び出しを perform_in に変更した上でファイルアップロード後に実行されやすいようにした。
=> これにより ActiveStorage::FileNotFoundError の発生が落ち着いた(可能性が0ではないがそれはsidekiqのretryに流すことにしました)

SampleWorker.perform_in(0.2.second, ~~_id)

step2: 想定したパフォーマンスが出ない

起きた事象

解析用のworker自体は2-3s程度で終了する見込みであったが、フロントの体験的に遅延が起こるケースが多かった。
=> 正確には予想していた2-3sで終わることもあれば10s以上かかるケースもあった

原因(ここの詳細がメインテーマなので後述します。)

sidekiqを perform_in で実行すると Sidekiq::Sucheduled の機構に乗ります。
その場合、workerの実行までにperform_inで指定した秒数以上のpending時間がかかる。

対応

perform_async に戻し、worker側で ActiveStorage::FileNotFoundError をハンドリングするようにしました。
(別のいい対処法があればご教授願いたいです)

class SampleWorker
  include Sidekiq::Worker

  def perform(~~_id)
    # ファイル解析処理
  rescue ActiveStorage::FileNotFoundError
    sleep(~) # ここの秒数は現在も調整中
    SampleWorker.perform_async(~~_id)
  end
end

実際の処理ではActiveStorageをattach指定るActiveRecordのcreated_atを見て直近にアップロードしたものでない時にはraiseするようにしました。
(ActiveStorageのファイルが別の理由で削除されていた時に無限にリトライされないように)

なぜ perform_in では指定した時間以上の時間がかかってしまったのか

わからないなりにコードを読んでみた。(全てを読んでるわけではないです)
参考: sidekiqコードリーディング

クラスについて確認

sidekiqのクラス関係(ざっくり)はこちら。

sidekiq概論.png

Jobをenqueueする

  • Sidekiq::Worker
    • Railsにmoduleとして提供している。perform_inやperform_asyncを呼んだ時にSidekiq::Clientを介してredisにデータをpushする
    • perform_inで指定された時間を計算してatオプションに変換する(参考)
  • Sidekiq::Client
    • redisへのコネクションの管理とpushを行う * atオプションが指定されている時には "schedule" に対してatの時間と共にzaddされる

雑感:
WorkerとClientの責務分担がちょっと把握しきれなかったです。

  • 何秒後に実施と指定した時にatオプションに変換するロジックがWorkerとClient両方にある。
  • 元々Client側で実装していて、moduleをincludeするだけで簡単に使えるようにWorkerを追加したような気がしました。

Jobをdequeueする

perform_inしたらなぜ遅くなったのか

perform_inで実行した結果"schedule"タグでenqueueされる。
その結果 Sidekiq::Scheduled::Poller のポーリング時間が課題となっていました。

対応方針の検討で以下のパターンがありましたが、今回は後者を選択しました。

  • average_scheduled_poll_intervalの感覚を短くする
  • perform_inを使わずperform_asyncにする

上で行った時の懸念として、プロセス数が増えたときに同時実行のリスクが高くなり、予期せぬバグや負荷を招く可能性があると思いました。

まぁsidekiqに流したのにパフォーマンス気にするなよというごもっともなツッコミはあるのですが、、
誰かの参考になれば、そしてこうした方がいいぞというコメントいただけるとありがたいです

参考

sidekiqコードリーディング

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?