【備忘録】 Java + Spring + MySQLでジョブキューのシステムの実装方法
はじめに
- YAPC2023で、「ジョブキューシステムFireworqのアーキテクチャ設計と運用時のベストプラクティス」という発表があった。Fireworqは、Go + MySQLで実装された軽量かつハイパフォーマンスなジョブキューシステムである。
- 過去にJava + Spring + MySQLでアプリケーションの1機能としてジョブキューを実装した経験がある*1。しかし、その時の設計や実装のレビューがほぼなかったし、これでよかったのかという感覚が自分の中に残った。Fireworqのソースコードリーディング*2や関連項目の調査結果をまとめることで、自分の実装方法が正しかったのかをセルフレビューしたいと思った。
当時やったことの概要
方針
- HTTPでジョブを投げてRDBにジョブの実行に必要な情報をINSERTし、SpringBootの@Scheduled の機能を使ってRDBからジョブの情報を定期的に取得し、ジョブを実行するようにした。APIサーバとは別jarでデプロイしたかったが、チームの方針でAPIサーバの中に実装した。
実装概要
- 2タイプ実装した。
Fireworqのソースコードリーディング
Fireworqで気になったこと
- プライマリとバックアップの切り替えについて
- ディスパッチャの動きについて
調査結果
(備忘録)どうやってソースコードを読んだか?
- まずは手元にソースコードをクローンして、ドキュメントを見ながら手元で動かしてみた。PUT /queue/{queue_name}を実行すると、ジョブキューのテーブルが生成され、 ジョブキューごとにノードの監視が開始されていたので、main.goやweb/application.goを見て
PUT /queue/{queue_name}
が何をしているか見た。 - Goについては雰囲気で読む+ ChatGPTにソースコードを貼って何をしているか解説してもらった。
- ログを追加して動きの概要を掴むことも合わせて行った。
プライマリとバックアップの切り替えについて
- 1秒に一回、MySQLの
GET_LOCK()
とIS_USED_LOCK()
を使って、名前付きロックを取れたものがプライマリーのノードになるSELECT IS_USED_LOCK({ロック名}) = CONNECTION_ID()
: 接続IDで名前付きロックを取れているか確認するSELECT GET_LOCK({ロック名}, {タイムアウト値})
: 名前付きロックを取得する- ノードの監視部分
- コネクションプールのコネクションの最大値を1にすることで、CONNECTION_ID()が変わらないようにしている
ディスパッチャの動きについて
- プライマリとバックアップともに定期実行されていて、バックアップのノード側はジョブをポップするとnilとInactiveErrorが返されるようになっている
セルフレビュー
- ワーカー側で
SELECT FOR UPDATE
を使うと、ワーカーの並列数を増やすと詰まってしまうのでスケールしない- 実践ハイパフォーマンスMySQL 第3版の「6.8.1 MySQLでキューテーブルを作成する」があり、
SELECT FOR UPDATE
を使わないで同等の処理を実施するアイデアが書かれている - (アイデア)ジョブのカラムに
owner
とstatus
を追加して、UPDATEを使ってowner
にMySQLのCONNECTION_ID()
の値とstatus
にジョブを掴んだことを示す値を入れマークする。その後マークしたものをSELECT
すればよい。
- 実践ハイパフォーマンスMySQL 第3版の「6.8.1 MySQLでキューテーブルを作成する」があり、
- プライマリーとバックアップのノードの判定は名前付きロックを使って実現できる
Java + Spring + MySQLでプライマリーのノードかどうか判定をするには?(案)
- 「
APIサーバとは別jarでデプロイしたかったが、チームの方針でAPIサーバの中に実装した。
」の縛りは入れた状態で、プライマリーのノードか判定するクラスを作ってみた- 定期時刻は
Spring
の@Scheduled
を使う。 - コネクションプールのコネクション数の最大値を1にできない。
hasLock()
の引数に接続IDを加えて、バックアップノードと誤判定されないようにした。- 別のSQLクライアントから接続IDをKILLすると、直ちに新しい接続IDでプライマリーノードに切り替わったので問題ないはず。
- ジョブキューのクラスはActivatorとジョブキューのマッパーをDIして、ディスパッチャーはジョブキューのクラスをDIして、
@Scheduled
を使って定期的にジョブをディスパッチするようにすれば良さそう。
実装案(クリックすると展開されます)
- 定期時刻は
終わりに
- ジョブキューの動きを保証するテストコードがどのようなものか気になるので、テストコードがどうなっているかは改めて追いたい。
MySQL
とキュー
というキーワードで検索していれば、それなりに記事が出てくるのに当時の自分は全く調べなかったので反省。- 名前付きロックというものを知った。調べる中でトランザクションの分離レベルなどなあなあにしているところに気づけたので、改めて復習したい。
参考情報
- ジョブキューシステムFireworqのアーキテクチャ設計と運用時のベストプラクティス - Speaker Deck
- セルフレビューしてみるきっかけになったスライド。
- GoとMySQLを用いたジョブキューシステムを作るときに考えたこと - ゆううきブログ
- Fireworqの設計思想やジョブキューの実装のOSSなどの情報が書かれていて、スライドの補足や自分で調査する時の足がかりになった。
- O'Reilly Japan - 実践ハイパフォーマンスMySQL 第3版
- 「6.8.1 MySQLでキューテーブルを作成する」