この記事は, CYBOZU SUMMER BLOG FES '24 Garoon Stage DAY 19 の記事です.
こんにちは!Garoon開発チーム(Tsukimiチーム)の森脇です.
この記事ではTsukimiチームで行ったジョブキューの切り替えについて, お話しします!
4行まとめ
- Garoonでは, 通知の送信などの非同期処理をジョブキューを利用して行っています.
- インフラ基盤の移行に伴い, ジョブキューを無停止で新しい仕組みに切り替えようとしていました.
- 切り替えに際して遭遇した3つの課題とその解決策を紹介します.
- 解決策を用いて実際にどのように切り替え・切り戻しを行うのか手順を紹介します.
はじめに
Garoonでは, 通知の送信などの非同期処理にジョブキューを利用しています. インフラ基盤移行プロジェクトの一環として, ジョブキューを新しいサービスに切り替えました. この切り替えでは, Garoonを無停止で切り替えることに挑戦しました.
この記事では, ジョブキューの無停止での切り替えにおいて遭遇した課題と解決策を紹介します.
Garoon開発チームの業務内容が気になっている方はもちろん, 非同期処理やジョブキューの切り替えをこれまで経験されたことがある方や今後このようなプロジェクトに携わる可能性のある方にとっても何かの参考になれば嬉しいです.
ジョブキューとは?
ジョブキューとは, 一連の処理(ジョブ)を非同期に実行してくれるサービスのことです. 内部にキューを持っており, 登録されたジョブを追加された順に実行していきます. 登録側はジョブをジョブキューに登録しておくだけで, ジョブキューが登録側の処理とは非同期的にジョブを処理します.
Garoonにおけるジョブキューの利用
Garoonにおけるジョブキューの利用について紹介します.
Garoonでは, スケジュールへの操作に対して各参加者に通知を送る場合など同期的に処理をする必要のない処理や, 時間がかかる処理にジョブキューを使用しています.
ジョブキューの利用の流れの簡単な模式図を図1に示します. まず, ユーザーから特定の画面操作があるとGaroonのApはジョブキューにジョブを登録します. この際, 事前にGaroonのBatch Apで非同期処理を実行するためのAPIを公開しておき, ジョブにはそのURLを持たせます. ジョブキューは, 一番古いジョブから順にデキューし, ジョブのURLをもとにBatch ApのAPIを叩きます.
今回の切り替えでは、このジョブキューを新しいものに切り替える作業を行いました.
ジョブキューの切り替えにおける挑戦
今回のジョブキューの切り替えでは, Garoonを無停止で切り替えることに挑戦しました.
弊社では基本的に数ヶ月に一度しか停止を伴うリリースを実施しません. そのため, 停止リリースのタイミングを待っていると, 効率的な開発ができないという問題がありました. また, 停止リリースを選択した場合は, 切り戻すにあたっても同様に停止させる可能性が高く, お客様への影響が大きくなります. 以上のことから, 無停止での切り替えを選択しました.
課題と解決策
無停止でのジョブキューの切り替えは一筋縄では行きませんでした. 具体的には, 以下の3つの課題がありました. それぞれの課題と解決策を紹介します.
- 切り戻しに対応する
- 切り替え時にジョブの重複実行を防止する
- ジョブの実行順序を保証する
切り戻しに対応する
課題
新規のジョブキューに切り替えた際に問題が発生する可能性は十分にありました. 例えば, 切り替えにより, 処理速度の低下やリソース使用量の増加が発生した場合です. もちろん, 非同期処理であるため, ある程度の処理速度の低下は許容できます. しかし, これらの問題は実際に切り替えてみないと影響がわからないため, 安定した動作が保証されている既存のジョブキューへ即座に切り戻しができるように準備しておく必要がありました.
解決策
新規のジョブキューから既存のジョブキューへ切り戻しをできるようにするためには, 新規のジョブキューのジョブが処理されることを確認するまで, 既存のジョブキューにバックアップ用のジョブを保持しておく必要があります. そうしておかないと, 新規のジョブキューのジョブが何らかの理由で処理が実行できず, 切り戻しを行った場合に, 既存のジョブキューから該当のジョブを受け取れないため, ジョブを処理できないまま失うことになります.
そのため, GaroonのApに対策を施しました. Apでは, ユーザから画面操作があった場合, 既存のジョブキューと新規のジョブキューの両方にジョブを登録するようにしました. 両方のジョブキューに登録したジョブは新規のジョブキューでのみ処理し, 既存のジョブキューではジョブを保持します. これにより, 既存のジョブキューにバックアップ用のジョブを用意しました.
しかし, この解決策により, 実行順序が乱れる問題やジョブが重複実行される問題が発生しました. それぞれの問題の詳細と, その解決策は後ほど言及します.
ジョブの実行順序を保証する
課題
切り戻しに対応するために既存と新規の両方のジョブキューにジョブを登録するようになったことで, ジョブの実行順序が乱れる可能性がありました.
例えば, 図2で示すようにJob3から両方のジョブキューにジョブを登録するようになった状況があるとします. このような状況では, Batch Apで適切にジョブの制御ができていないと既存のジョブキューにあるJob1やJob2が処理される前に新規のジョブキューにあるJob3が処理されてしまう可能性がありました.
解決策
ジョブの実行順序を保証するために, GaroonのBatch Apに対策を施しました.
ここで, Batch Apでは, 諸事情あって既存のジョブキュー用のエンドポイントとは別に, 新規のジョブキュー用のエンドポイントも用意するようにしています.
ジョブの実行順序の保証は, 新規のジョブキュー用のエンドポイントに手を加えることで実現しました. 具体的には, 新規のジョブキュー用のエンドポイントでジョブを処理する際, 以下の条件でジョブの処理方法を分岐しました.
- 既存のジョブキューの中に先に処理するべきジョブが存在するかどうか.
- 存在する → ジョブを処理しない.
- 存在しない → ジョブを処理する.
既存のジョブキューの中に先に処理するべきジョブが存在する場合, 新規のジョブキュー用のエンドポイントはジョブを処理しません. しかし, これはあくまで実行順序を保証するために行っているものなので, ジョブを処理をしなかった場合であっても, そのジョブを新規のジョブキューに残しておく必要があります. そのため, 今回は新規のジョブキューに対して一定時間後にリトライするように設定しました(図3).
既存のジョブキューの中に先に処理するべきジョブが存在しない場合, 新規のジョブキュー用のエンドポイントはジョブを処理します. この際, 新規のジョブキュー用のエンドポイントで処理が完了したジョブは不要であるため, 処理できた後に既存のジョブキューの中から該当のジョブを削除するようにしました(図4).
切り替え時にジョブの重複実行を防止する
課題
切り戻しに対応するために既存と新規の両方のジョブキューにジョブを登録するようになったことで, ジョブが重複実行される問題もありました.
前提として, Garoonのジョブには, 冪等性が保証されて作られていないものが存在します. 通知の送信処理で重複実行が起こるとお客様へ影響を与えることになるため, これらの重複実行は避けたいです. 今回の切り替え作業に際して冪等性を保証するようにリファクタリングする方法も考えられましたが, 現状動いている処理にリグレッションを発生させず, リファクタリングを行うコストは高かったため, 冪等性を持たせる方法は採用しませんでした.
解決策
ジョブの重複実行の防止は, 既存のジョブキュー用のエンドポイントに手を加えることで実現しました. 具体的には, 既存のジョブキューからのジョブを処理する際に, 以下の条件でジョブの処理方法を分岐しました.
- 新規のジョブキューに同一のジョブが存在するかどうか.
- 存在する → 処理しない.
- 存在しない → 処理する.
新規のジョブキューに同一のジョブが存在する場合は, 新規のジョブキュー用のエンドポイントに処理を委譲したいため, 既存のジョブキュー用のエンドポイントでは処理しません.
ここで, 「切り戻しに対応する」の節の「課題」で述べたように, 切り戻しに対応するためには, 新規のジョブキュー用のエンドポイントで処理が実行されるまで, 既存のジョブキューから該当のジョブを削除してはいけません. そのため, 処理しない場合には既存のジョブキューに対して一定時間後にリトライするように設定しました(図5). これにより, 新規のジョブキュー用のエンドポイントで処理が完了した場合は, エンドポイント側の処理で既存のジョブキューの中のジョブは削除され, 完了しなかった場合は, 既存のジョブキューの中にジョブが留まり続けます.
- 新規のジョブキュー用のエンドポイントで処理が完了したかどうか.
- 完了した → 既存のジョブキューからジョブは削除される.
- 完了していない → 既存のジョブキューにジョブが留まり続ける.
実際にジョブキューの切り替え・切り戻しを行ってみる
前章では課題とその解決方法について紹介しました. この章ではジョブキューの切り替え, 切り戻しの流れを見ていきます.
ジョブキューの切り替え
まず最初に切り替え前の状態です(図6). 既存のジョブキューだけにジョブが登録されます.
次にBatch Apの改修内容をリリースします. Batch Apに改修内容が適用されますが, 動作は適用前の状態と同じです. 新規のジョブキューにはジョブが存在しないため, 適用前の状態と変わらず, 既存のジョブキュー用のエンドポイントでのみジョブを処理します.
次にApの改修内容をリリースします. これにより, 既存と新規の両方のジョブキューにジョブが登録されます.
Apの改修内容をリリースした直後は, 既存のジョブキューの中に先に実行すべきジョブが登録されているため既存のジョブキュー用のエンドポイントでジョブを処理します(図7).
Apへの改修内容をリリースしてしばらく経過すると, 既存のジョブキューのジョブは処理されず, 新規のジョブキューのジョブだけが処理されます(図8). ここまで来ると, あとは既存のジョブキューを撤去するだけでジョブキューの切り替えが完了になります.
ジョブキューの切り戻し
ジョブキューの切り戻しの流れも見ていきます.
図9で示すように新規のジョブキュー用のエンドポイントで処理中に問題が発生した場合, ApとBatch Apに適用した改修内容を元の状態に戻します.
両方の改修内容を元の状態に戻すと, 既存のジョブキュー用のエンドポイントでのみジョブを処理する状態に戻るため, ジョブが失われることなく処理できます(図10).
やってみた感想
実際に解決策を用いてジョブキューの切り替えを行い無事, 完了することができました. しかし, 解決策として示した改修内容は複雑になってしまいました. 改修内容を正しく実装できていれば問題なく切り替えを行えますが, 万が一, 実装に不備があった場合, 重複実行や実行順序が乱れる可能性がありました. あくまで切り替え時に必要な一時的なロジックであり, 切り替えが完了した際には撤去するものでしたが, ユニットテストを充実させておいた方が実装する際には安心だったと思います.
今回はジョブに冪等性を持たせるのは工数の都合上諦めましたが, 冪等性を持たせた方がロジックをより単純にすることができたと思います. しかし, 歴史の長いプロダクトに対するリファクタリングが一筋縄ではいかない, と言うことはよくあることだと思うので, その代替案を示せたことは意味のあることだったと考えています.
まとめ
Garoon開発チームではジョブキューの切り替えを無停止で行いました. まず最初に, 無停止でのジョブキューの切り替えには3つの課題があったため, その課題の概要と解決策をそれぞれ紹介しました. 次に紹介した解決策を用いて, どのようにジョブキューの切り替えおよび切り戻しを行うのか説明しました. 最後にジョブキューの切り替えを行ってみた感想を述べました.
読んでいただき, ありがとうございました!
この記事が読んでくださった方にとって何かの参考になれば嬉しいです!