やっぱ非同期ジョブと排他制御って難しいね
きっかけ
最近、ユーザーが作成した複数のデータを一括で処理する非同期ジョブ機能をリリースしました。特に月末などの繁忙期には多くのユーザーに利用されることが想定されています。
しかし、このジョブには大きな課題がありました。それは、同一のテナント内で他の特定の非同期ジョブが実行されていると、ジョブを開始できないというものです。一方でプロダクトマネージャーとしてはユーザーにガンガン並行実行してほしい、というのが本来の意図だったことがわかり、データ上も特に並行で操作しても問題のない処理内容でした。
さらに、このジョブが利用している共通の非同期処理モジュールを使って、後続の別プロジェクトを計画しています。そちらのプロジェクトでは、今回のジョブとは関係なく、高頻度での並行実行が求められる仕様でした。
現状のアーキテクチャのままでは、本来この機能が持つべきポテンシャルを発揮できないだけでなく、次のプロジェクトの足かせにもなってしまう。こうした背景から、改善を始めることになったのです。
なぜこんなことが起きていたのか?
調査を進めると、課題の根っこには排他制御の実装がありました。大きく分けて2つの難しさに直面しました。
1. ジョブレベルでの排他制御の難しさ
このシステムの他の機能では、データの一貫性を保つため、テナント単位で一度に実行できる非同期ジョブを一つに制限する、シンプルなロック機構が採用されていました。
今回リリースした機能は、同じ非同期処理を並行実行できないように(ここもプロダクトマネージャーと擦り合わせできていなかったポイント)実装したのですが、その際に共通のモジュールを利用したことによって全く関連性のないジョブ同士まで互いにブロックし合うという副作用を生んでしまったのです。
仕様も含めて「とりあえずロックしておけば安全」という初期設計が、システムの拡張性や利便性を損なう典型的な例ですね。どの範囲のジョブを、どの粒度で制御するべきか、というのは非常に悩ましい問題です。
2. データベースレベルでの排他制御の難しさ
さらに、もう一つの難関がデータベースのロックです。
この一括処理ジョブは、処理を開始する前に、対象となるすべてのドキュメントを一括でロック(悲観的ロック)する仕様でした。これもデータ整合性を担保する上では安全な方法です。
しかし、この「一括でロック」方式には大きなデメリットがありました。
ロック待ちとタイムアウトの多発
前述の「ジョブレベルでの排他制御の難しさ」をクリアできたとしても、複数のユーザーがほぼ同時に処理を実行しようとすると、ロックの奪い合いが発生し、タイムアウトでジョブが失敗するリスクがありました。
1件でも失敗すると全体が失敗
たった1件でもロックに失敗すると、ジョブ全体が開始できずにエラーとなる実装になっていました。
おわりに
今回の件を通して、非同期処理における排他制御は、システムの初期段階ではシンプルに考えがちですが、サービスの成長とともに必ず見直しが必要になる重要なテーマだと改めて感じました。
「安全」と「便利」と「パフォーマンス」、これらのバランスをいかに取るか。単純な正解はなく、アプリケーションの特性やユーザーの利用シーンを深く理解した上での設計が必要だなと思いました。