駆け出しエンジニアはPMFの夢を見るか?

駆け出しエンジニアの記録

【備忘録】 Java + Spring + MySQLでジョブキューのシステムの実装方法

はじめに

当時やったことの概要

方針

  • HTTPでジョブを投げてRDBにジョブの実行に必要な情報をINSERTし、SpringBootの@Scheduled の機能を使ってRDBからジョブの情報を定期的に取得し、ジョブを実行するようにした。APIサーバとは別jarでデプロイしたかったが、チームの方針でAPIサーバの中に実装した。

実装概要

  • 2タイプ実装した。
    1. ワーカー側がSELECT FOR UPDATEを使って重複してジョブを取って実行しないように制御してジョブを実行する。
    2. APIサーバが自身がプライマリかどうか判断して、プライマリである方が通知の処理を実行する。*3*4

Fireworqのソースコードリーディング

Fireworqで気になったこと

  1. プライマリとバックアップの切り替えについて
  2. ディスパッチャの動きについて

調査結果

(備忘録)どうやってソースコードを読んだか?

  • まずは手元にソースコードをクローンして、ドキュメントを見ながら手元で動かしてみた。PUT /queue/{queue_name}を実行すると、ジョブキューのテーブルが生成され、 ジョブキューごとにノードの監視が開始されていたので、main.goやweb/application.goを見てPUT /queue/{queue_name}が何をしているか見た。
  • Goについては雰囲気で読む+ ChatGPTにソースコードを貼って何をしているか解説してもらった。
    • ログを追加して動きの概要を掴むことも合わせて行った。

プライマリとバックアップの切り替えについて

  • 1秒に一回、MySQLGET_LOCK()IS_USED_LOCK()を使って、名前付きロックを取れたものがプライマリーのノードになる
    • SELECT IS_USED_LOCK({ロック名}) = CONNECTION_ID(): 接続IDで名前付きロックを取れているか確認する
    • SELECT GET_LOCK({ロック名}, {タイムアウト値}): 名前付きロックを取得する
    • ノードの監視部分
  • コネクションプールのコネクションの最大値を1にすることで、CONNECTION_ID()が変わらないようにしている

ディスパッチャの動きについて

ルフレビュー

  • ワーカー側でSELECT FOR UPDATEを使うと、ワーカーの並列数を増やすと詰まってしまうのでスケールしない
    • 実践ハイパフォーマンスMySQL 第3版の「6.8.1 MySQLでキューテーブルを作成する」があり、SELECT FOR UPDATEを使わないで同等の処理を実施するアイデアが書かれている
    • (アイデア)ジョブのカラムにownerstatusを追加して、UPDATEを使ってownerMySQLCONNECTION_ID()の値とstatusにジョブを掴んだことを示す値を入れマークする。その後マークしたものをSELECTすればよい。
  • プライマリーとバックアップのノードの判定は名前付きロックを使って実現できる

Java + Spring + MySQLでプライマリーのノードかどうか判定をするには?(案)

  • APIサーバとは別jarでデプロイしたかったが、チームの方針でAPIサーバの中に実装した。」の縛りは入れた状態で、プライマリーのノードか判定するクラスを作ってみた
    • 定期時刻はSpring@Scheduledを使う。
    • コネクションプールのコネクション数の最大値を1にできない。hasLock()の引数に接続IDを加えて、バックアップノードと誤判定されないようにした。
      • 別のSQLクライアントから接続IDをKILLすると、直ちに新しい接続IDでプライマリーノードに切り替わったので問題ないはず。
    • ジョブキューのクラスはActivatorとジョブキューのマッパーをDIして、ディスパッチャーはジョブキューのクラスをDIして、@Scheduledを使って定期的にジョブをディスパッチするようにすれば良さそう。

    実装案(クリックすると展開されます)

終わりに

  • ジョブキューの動きを保証するテストコードがどのようなものか気になるので、テストコードがどうなっているかは改めて追いたい。
  • MySQLキューというキーワードで検索していれば、それなりに記事が出てくるのに当時の自分は全く調べなかったので反省。
  • 名前付きロックというものを知った。調べる中でトランザクションの分離レベルなどなあなあにしているところに気づけたので、改めて復習したい。

参考情報

*1:確か2020年4月ごろ、Webエンジニア歴1.5年目くらいのとき。

*2:Goはほぼ触ったことがないのでChatGPTの助けを借りた。理解の浅い部分があるかもしれない。

*3:自分の担当ではなかったが、冗長化されている環境にデプロイしたら冗長化が考慮されておらず、二重で通知されていた。

*4:GET_LOCKは使っていなかっはずだが、プライマリーとバックアップの切り替え処理はそれでいいんだっけ?という感覚があり、自分の中でもやもやしていた。