Slide 1

Slide 1 text

Sidekiqで実現する 長時間非同期処理の中断と再開 2024.10.25 Fri Kaigi on Rails 2024@有明セントラルタワーホール & カンファレンス(東京) @hypermkt SmartHR プロダクトエンジニア

Slide 2

Slide 2 text

ばーちー 自己紹介 株式会社SmartHR バックエンドエンジニア @hypermkt 2

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

SmartHRでは 非同期処理 を活用しています 5

Slide 6

Slide 6 text

なぜ私は今日発表するのか ● SmartHRの本体機能の開発の過程で、一部の非同期処理が抱える課題解決を チームで担当 ● 非同期処理の中断と再開に関する情報・事例が社外であまりない ● 同じような悩みを抱えている企業やエンジニアに向けて解決方法や設計方針を共有 したい 6

Slide 7

Slide 7 text

想定する聞き手 ● Sidekiqを利用しているエンジニア ● 長時間のジョブによりデプロイの影響を受けるシステムを運用している企業 ● 中断・再開機能の実装に興味のある方 7

Slide 8

Slide 8 text

● 長時間実行のジョブが抱える課題 ● ジョブの中断・再開の技術 ● ユースケース別実装パターン ● 中断・再開処理のテスト ● Sidekiq Iterationについて 本日お話すること 8

Slide 9

Slide 9 text

● インフラ: Google Cloud ● バックエンド: Rails ○ 非同期処理にSidekiqを利用 SmartHRのシステム構成 9

Slide 10

Slide 10 text

Sidekiqとは 10 ● Rubyで動作する非同期処理のライブラリ ○ https://github.com/sidekiq/sidekiq ○ ジョブ管理のRedisを利用 ● 大量データ処理や時間がかかるタスクを非同期で処理 ● 無償版(OSS), 有償版(PRO, ENTERPRISE)があり、プランによって提供される機能に 違いがある ○ 有償版ではジョブが失われずに再取得できるsuper_fetch機能などがある ○ SmartHRではENTERPRISE版を利用

Slide 11

Slide 11 text

長時間実行のジョブが 抱える課題 11

Slide 12

Slide 12 text

デプロイ担当者が 抱える悩み …… 12

Slide 13

Slide 13 text

長時間実行中のジョブを 止めるのが怖い …… 😰 13

Slide 14

Slide 14 text

● データや処理の不整合 ○ 想定外の箇所でジョブが強制停止されてしまい、再実行時にメー ル通知などが二重送信されてしまうリスクがある ● 実行時間の増加 (= ユーザー体験の低下 ) ○ 処理を最初から再実行する必要があるため、実行時間が大幅に 延びることがある ジョブを停止することの懸念点 14

Slide 15

Slide 15 text

● 以下を目視確認してからSidekiqを手動停止 ○ 長時間実行されているジョブがない! 👈 ヨシ! ○ ジョブを停止してもデータ登録に影響がなさそう! 👈 ヨシ! ● イマダ!! Sidekiq停止! デプロイ開始ダ!! ウオー!! 暫定的な回避策 ... 15

Slide 16

Slide 16 text

いやー辛い …… 😭 16

Slide 17

Slide 17 text

これを解決するのが 中断・再開処理 17

Slide 18

Slide 18 text

ジョブの中断・再開処理の 技術 18

Slide 19

Slide 19 text

中断・再開処理とは ● 中断処理 ○ Sidekiqのプロセス停止を検知して任意のタイミング で 非同期処理を停止させること ● 再開処理 ○ 再開した非同期処理を中断した箇所 から継続させること 19

Slide 20

Slide 20 text

メリット ● デプロイ担当者 ○ 安心かつ安全 に非同期処理を停止できるようになり、デプロイがスムーズに進 められる ● 利用ユーザー ○ 実行した非同期処理を最低限の実行時間 で終えることができる 20

Slide 21

Slide 21 text

長時間非同期処理において 中断・再開処理は 必須の技術 21

Slide 22

Slide 22 text

中断・再開処理の全体像 Sidekiq プロセス Redis 1 ジョブは進捗を保存しながら 処理を進行 22

Slide 23

Slide 23 text

中断・再開処理の全体像 Sidekiq プロセス Redis 1 ⚡すべて処理終了! Sidekiq プロセス Redis 2 ジョブは進捗を保存しながら 処理を進行 23

Slide 24

Slide 24 text

中断・再開処理の全体像 Sidekiq プロセス Redis 1 中断処理 ジョブはSidekiqの処理終了を検知して 任意のタイミングで処理を停止 Sidekiq プロセス Redis 2 Sidekiq プロセス Redis ジョブは進捗を保存しながら 処理を進行 3 24 ⚡すべて処理終了!

Slide 25

Slide 25 text

中断・再開処理の全体像 Sidekiq プロセス Redis 1 中断処理 ジョブはSidekiqの処理終了を検知して 任意のタイミングで処理を停止 Sidekiq再起動... Redis Sidekiq プロセス Redis 2 Sidekiq プロセス Redis ジョブは進捗を保存しながら 処理を進行 3 Sidekiq プロセス 4 25 ⚡すべて処理終了!

Slide 26

Slide 26 text

中断・再開処理の全体像 Sidekiq プロセス Redis 1 中断処理 ジョブはSidekiqの処理終了を検知して 任意のタイミングで処理を停止 Sidekiq再起動... Redis Redis 再開処理 ジョブは進捗を取得し処理を継続 Sidekiq プロセス Redis 2 Sidekiq プロセス Redis ジョブは進捗を保存しながら 処理を進行 3 Sidekiq プロセス 4 Sidekiq プロセス 5 26 ⚡すべて処理終了!

Slide 27

Slide 27 text

中断処理 27

Slide 28

Slide 28 text

中断処理 28 ● 概要 ○ Sidekiqのプロセス停止を検知して任意のタイミングで非同期処 理を停止させること

Slide 29

Slide 29 text

Sidekiq設定ファイルにイベントハンドリングを設定 config/initializers/sidekiq.rb Sample 29 ● Sidekiqの処理終了(quiet)・停止 (stop)を管理する変数を定義、フラグ として利用する ● イベントハンドリングを設定し、 Sidekiqの処理終了と停止を検知 したらフラグをあげる ● このフラグで中断を行うかの 判定条件に利用する 参考: https://github.com/sidekiq/sidekiq/wiki/Signals https://github.com/sidekiq/sidekiq/wiki/Deployment

Slide 30

Slide 30 text

Sidekiqの停止を検知したら例外を送出するメソッドを定義 app/workers/application_worker.rb 30 Sample app/workers/application_worker.rb

Slide 31

Slide 31 text

中断処理の組み込み 31 ● performメソッド内で raise_if_shutting_downメソッドを呼ぶ ● Sidekiqプロセスが処理終了シグナルを受 け取った場合、ジョブは例外を発生させて 処理を終了。受け取っていない場合はそ のまま処理を継続します。 Sample DB更新、メール送信などの 様々な処理

Slide 32

Slide 32 text

● データ登録や各種処理の後、区切 りの良いタイミングで中断処理を入 れる ● 例えば、ループ処理の場合はルー プ内の各処理が完了した後に中断 を確認することで、データや処理の 整合性を保つことができる 大事なポイント : 中断処理を入れるタイミング 32 Sample DB更新、メール送信などの 様々な処理

Slide 33

Slide 33 text

再開処理 33

Slide 34

Slide 34 text

● 概要 ○ 再開した非同期処理を中断した箇所から処理を継続させること 再開処理 34

Slide 35

Slide 35 text

再開処理の全体像 35 Redis 行番号 1 2 3 4 5 ⚡中断 ● 例) CSVで5件のデータをインポートする 1回目の実行 1行目 終わり! 2行目 終わり! 3行目 終わり! 進捗管理 DB データ登録

Slide 36

Slide 36 text

再開処理の全体像 36 Redis 行番号 1 2 3 4 5 ⚡中断 ● 例) CSVで5件のデータをインポートする 1回目の実行 1行目 終わり! 2行目 終わり! 3行目 終わり! 進捗管理 DB データ登録 Redis 行番号 1 2 3 4 5 2回目の実行 1行目 終わり! 2行目 終わり! 3行目 終わり! 4行目 終わり! 5行目 終わり! スキップ 進捗管理 DB データ登録

Slide 37

Slide 37 text

① ID番号保持方式 ② 行番号保持方式 進捗管理の方式 37

Slide 38

Slide 38 text

● 用途 ○ IDなどテーブルの主キーを用いたデータベースへの一括処 理 ■ 例) データの一括削除 ● ポイント ○ RedisのキーはSidekiqのジョブID + IDの種類名とする ■ IDの種類名: item_id などデータの種類を示すもの ■ 1回のジョブで複数データの進捗を管理する 場合もあるため ○ 処理済みのIDをRedisに追加 ID番号保持方式 38 キー:#{ジョブID}/#{IDの種類名} Redis aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb cccccccc-cccc-cccc-cccc-cccccccccccc …

Slide 39

Slide 39 text

39 Sample Redisに指定IDの存在確認をする RedisにIDを追加する RedisからIDを削除する 一意なキーを生成する

Slide 40

Slide 40 text

40 Sample Redisでデータを管理するための一意なキーを生成する

Slide 41

Slide 41 text

41 Sample 指定IDの存在確認をする

Slide 42

Slide 42 text

42 Sample RedisにIDを追加して進捗を記録する

Slide 43

Slide 43 text

43 Sample RedisからIDをすべて削除する

Slide 44

Slide 44 text

44 Sample 処理済みか判定しスキップをする 処理がすべて完了したらRedisから 進捗データをすべて削除する 進捗を登録する ID番号保持方式の組み込み例

Slide 45

Slide 45 text

● 用途 ○ 主にCSVファイルからのデータインポート系 ● ポイント ○ RedisのキーはSidekiqのジョブIDとする ○ 処理済みの行番号とエラーメッセージ をRedisに追加 行番号保持方式 45

Slide 46

Slide 46 text

CSVインポート系の課題: 行ごとに複数個のエラーが発生する可能性がある 46 社員番号 姓 名 部署1 部署 sample-001 須磨 英知 システム部 2行目:登録済みの社員番号です。 2行目 部署1 部署:存在しない部署名です。

Slide 47

Slide 47 text

種類毎にキーを分けて Redisで進捗管理する 47 キー:#{jid} Redis 1行目 2行目 3行目 処理済みの行番号 失敗した行番号 3行目 … 3行目:登録済みの社員番号です。 3行目:XXXXXは存在しない部署です。 ... 行ごとのエラーメッセージ errors/#{jid} error-logs/#{jid}

Slide 48

Slide 48 text

48 Redisで種類別の進捗管理するた めのキーを生成する Sample Redisに行番号を追加する

Slide 49

Slide 49 text

49 Sample Redisで種類別の進捗管理するためのキーを生成する

Slide 50

Slide 50 text

50 Sample Redisに処理済みの行番号、失敗した行番号、エラーメッセージを追加する

Slide 51

Slide 51 text

Sample Redisに処理済みの行番号、失敗した行番号、エラーメッセージを追加する 51 処理済みの行番号を保存する

Slide 52

Slide 52 text

Sample Redisに処理済みの行番号、失敗した行番号、エラーメッセージを追加する 52 失敗した行番号を保存する

Slide 53

Slide 53 text

Sample Redisに処理済みの行番号、失敗した行番号、エラーメッセージを追加する 53 行に紐づく複数個のエラーメッセージを Redis に登録する

Slide 54

Slide 54 text

Sample Redisに処理済みの行番号、失敗した行番号、エラーメッセージを追加する 54 エラーメッセージ毎にスコア付けをすることで 順番に並ぶようにする

Slide 55

Slide 55 text

55 Sample 処理済みか判定しスキップする 行番号とエラーメッセージの 進捗を追加する 行番号保持方式の組み込み例

Slide 56

Slide 56 text

56 Sample エラーメッセージの進捗を登 録する 行番号保持方式の組み込み例

Slide 57

Slide 57 text

ユースケース別 実装パターン 57

Slide 58

Slide 58 text

● 実行時間が短い & ファイル容量が小さいファイルエクスポート ● 実行時間が長い & ファイル容量が大きいファイルエクスポート 実際に遭遇した開発例 58

Slide 59

Slide 59 text

実行時間が 短い & ファイル容量が 小さい ファイルエクスポート 59

Slide 60

Slide 60 text

● 想定される実行時間 ○ 数分 ● 想定されるファイル容量 ○ 数MB以下 ● 処理内容 ○ DBからデータ抽出したものをファイル書き出しするのみ 実行時間が短い & ファイル容量が小さいファイルエクスポート 60

Slide 61

Slide 61 text

実行時間が短い & ファイル容量が小さいファイルエクスポート 61 ● 実装方針 ○ 処理が冪等であるため中断処理のみで十分 ○ 実行時間が数分程度と短く、再実行してもユーザー体験に影響が少ない場合、再開処理は不要 ○ 中断箇所からの再開処理は必ずしも必須ではない Sample

Slide 62

Slide 62 text

実行時間が 長い & ファイル容量が 大きい ファイルエクスポート 62

Slide 63

Slide 63 text

● 想定される実行時間 ○ 数時間以上 ● 想定されるファイルサイズ ○ 数百MB ● 処理内容 ○ 従業員の履歴情報のダウンロード 実行時間が長い & ファイル容量が大きいファイルエクスポート 63

Slide 64

Slide 64 text

運用上の問題 ● デプロイに影響 ○ SmartHRでは毎日デプロイを行っている ○ 長時間のジョブが実行中の場合は内容に応じてデプロイを スキップする可能性がある ■ 中断は可能だが最初から再実行に伴いお客様への業務影響が大きい ■ 重要なリリースがある場合に、終わるまで粘るか、延期するか苦しい判断 をしなくてはならない......😭 64

Slide 65

Slide 65 text

技術的な問題 ● ファイルサイズが大きい ○ 非同期処理の過程で生成される大きい一時ファイルを中断時にどこに保存 するか🤔 65

Slide 66

Slide 66 text

ファイルの保存先候補 66 ● Redis ● オブジェクトストレージ (GCS)

Slide 67

Slide 67 text

● Redis ○ 巨大なファイルを保存する際のRedisにかかる負荷影響が不明 ■ ネット上では負荷がかかるという記事あり ○ Redisのメモリー容量を超える危険性 ■ 万が一メモリー上限に達してしまった場合に Sidekiqの運用に影響がでる ファイルの保存先候補 67

Slide 68

Slide 68 text

ファイルの保存先候補 68 ● オブジェクトストレージ (GCS) ○ 大容量のファイルの保存が可能 ○ ライブラリを使って簡単にアップロード可能 ○ 一時ファイルに有効期限を設けて削除することも可能 ○ セキュリティが強固(暗号化、アクセス制御機能) ○ GCSへの転送速度も速い 今回の用途としては最適 !!! 😆

Slide 69

Slide 69 text

中断時に一時ファイルをオブジェクトストレージに保存する 69 Sample 中断が発生したら例外をキャッチし 作成中の一時ファイルを GCSにアップロードする

Slide 70

Slide 70 text

再開可能かを判 Sample 初回は一時ファイルを新規作成し、再開時は一時ファイルを復元する 70 resumable?: 進捗データが保存されているか && GCSに一時ファイル が保存されているか restore_csv_from_gcs: GCSから一時ファイルを取得し 指定の場所に配置をする create_csv_file: CSVファイルを新規作成する

Slide 71

Slide 71 text

中断・再開処理のテスト 71

Slide 72

Slide 72 text

● 中断処理 ○ 意図した通りに停止するか ○ 中断時にデータが適切に保存され、一貫性が保たれているか ● 再開処理 ○ 再開後に正しく途中から処理を再開するか ○ 処理済みのデータがスキップされ、再度処理されていないか 中断・再開処理のテスト観点 72

Slide 73

Slide 73 text

中断のみの RSpec実装例 73

Slide 74

Slide 74 text

74 Sample 中断のみの実装例

Slide 75

Slide 75 text

75 Sample and_wrap_originalを利用して中断を再現 ● and_wrap_original は元のメソッドをそのまま呼び出しつつ、追加の処理を行いたいときに使う RSpecのメソッド ● このテストでは中断処理が正しく動くかを確認するために、任意のタイミングで中断が発生することを再現して いる 中断のトリガーとなる例外を 3 件目で発生させる

Slide 76

Slide 76 text

76 Sample 中断が発生したことで意図した通りの状態になっているかを確認 # 3件目までが処理済み # 3件目以降は未処理

Slide 77

Slide 77 text

中断 + 再開処理の RSpec実装例 77

Slide 78

Slide 78 text

78 Sample 中断 + 再開処理の実装例

Slide 79

Slide 79 text

79 Sample and_wrap_originalを利用して中断を再現 中断のトリガーとなる 例外を3件目で発生させる Sample

Slide 80

Slide 80 text

80 Sample worker.performを2回呼び 出すことで再開処理を再現 し、期待する挙動になるかを 確認する 再開処理の確認

Slide 81

Slide 81 text

Sidekiq Iterationについて 81

Slide 82

Slide 82 text

Sidekiq Iterationとは ● 2024/07/03 Sidekiq v7.3.0で Sidekiq Iterationという新機能が導入 ○ https://github.com/sidekiq/sidekiq/wiki/Iteration ○ This feature should be considered BETA until the next minor release. ● 機能 ○ 中断時に進行状況(カーソル)を保存し、再開時には保存したポイントから処理を再 開 ○ on_start, on_resume, on_stop, on_completeなどのコールバック ○ Active Record, CSV, 配列用にEnumeratorを生成するヘルパーメソッド 82

Slide 83

Slide 83 text

● Sidekiq Iterationは進捗管理は自動化されており、我々の実装よりも手間を減らし、 簡単に中断・再開が導入できる ● コールバックを活用することで、中断・再開時の柔軟な個別処理が実装しやすくなる ○ 例) 中断時の一時ファイルのGCSへの退避など ● (2024/10/20時点) ベータ状態なので本番導入が注意が必要 ● 今後 Sidekiq Iterationの導入を検討する価値がある Sidekiq Iterationの所感 83

Slide 84

Slide 84 text

まとめ 84

Slide 85

Slide 85 text

まとめ 85 ● 長時間の非同期処理には中断・再開処理が重要で、導入すること で安心・安全な運用が可能になります。 ● SmartHRで実際に導入している中断・再開処理の実装・設計方針 を紹介しました ● 同様の課題を抱えている場合、参考にしていただけると幸いです

Slide 86

Slide 86 text

ありがとうございました ! 86