DB接続プールを50へ増やした後、次のテーマとしてMatcherに取り組んでいる。しかし、設定を変えてベンチの数字を見るだけでは、自分の中に理解が残っていない感覚があった。
そこで今回は、Matcherが「いつ動くのか」から考え直すことにした。一定間隔で待ち行列を確認する方式を、ライドの作成や椅子が空いたことをきっかけに動かす方式へ変えてみる。
今日のゴールは、イベントで起きるMatcherを実装し、定期巡回との違いを自分の言葉で説明できる状態で、比較ベンチを通すことだ。点数が上がることだけを成功条件にはしない。
今のMatcherは、1回に何件割り当てるのか
初期実装は1回に最大1件だったが、現在のNode.js版は未割当ライドを古い順に最大10件取得する。その後、ライドごとに空き椅子を検索し、乗車地点に近い椅子を割り当てる。
つまり、今も依頼をDBにためて巡回時にまとめて拾っている。ただし、候補の検索と割り当ては1件ずつ行う。10件を割り当てる場合、ハンドラー内の問い合わせはライド取得1回、椅子検索10回、更新10回の計21回となる。
現在の空き判定では、その椅子に割り当てられたライドに「COMPLETEDがない」または「椅子側への未通知状態がある」場合、使用中として扱う。完了した瞬間と、現在の実装で次の割り当てが可能になる瞬間は、必ずしも同じではない。
0.1秒間隔でも、毎秒10回動くとは限らない
現在のループは「Matcherを呼ぶ→処理完了を待つ→0.1秒休む」という順序だ。処理に0.3秒かかれば、次の開始まで約0.4秒かかる。
この巡回待ちを減らすことが、イベント駆動を試す動機になる。ただし、Matcherが現在の主なボトルネックだと確定したわけではない。空き椅子が戻らないことが原因なら、起動だけ速めても割り当ては増えない。
「まとめる」と「イベントで起こす」は別の変更
会話の中で、三つの設計を混同していることに気づいた。
| 観点 | 現状 | 考えられる変更 |
|---|---|---|
| 起動のタイミング | 定期巡回 | ライド作成や椅子が空いたときに起動 |
| DBへの問い合わせ | ライドごとに椅子を再検索 | 空き椅子と位置をまとめて取得 |
| 割り当て方 | 古いライドから近い椅子を選択 | 複数のライドと椅子の組み合わせを比較 |
今回、最初に試すのは起動タイミングの変更だ。近傍選択と最大10件の処理を基準として、イベント駆動によって何が変わるかを見る。
今回の設計案
ライドの作成がDBに確定したら、Matcherへ実行を要求する。椅子が割り当て可能になったときにも、同じ要求を出す。具体的にどの処理の完了を起点にするかは、状態更新と通知のコードを読んで決める。
Matcherが実行中なら、別のMatcherを並行して起動するのではなく、「もう一度確認する」という要求を残す。複数のイベントをまとめて受け取り、実行中に届いた要求を取りこぼさない仕組みを考える。
イベントは仕事が増えたことを知らせる合図で、未割当ライドはDBに残る。低頻度の巡回も残し、合図の取りこぼしや再起動から復旧できるようにする。
最大10件を処理した後にも仕事が残っている場合の再実行と、空き椅子がない場合に無駄なループを続けない条件も決める必要がある。また、単一プロセス内の実行制御だけでは複数プロセス間の二重割り当てを防げないため、今回の実行構成と保護範囲を明記する。
仕様として変更してよいのか
公式のアプリケーションマニュアルでは、内部エンドポイントの /api/internal/matching と isuride-matcher.service は、利用方法・仕様を自由に変更してよいとされている。イベント駆動化自体はその範囲に入る。一方、ライドの状態遷移や通知順序など、外部に提供する振る舞いは維持する。
参照:ISURIDE アプリケーションマニュアル:ライドのマッチング
実装しながら理解したいこと
- DBへの保存が確定してからMatcherを起こすのはなぜか。
- ライド作成時だけでなく、椅子が空いたときにも起こす必要があるのはなぜか。
- 同時にイベントが来ても、二重割り当てや実行要求の取りこぼしを防ぐにはどうするか。
- 即座に動かすことで、待ち時間とDB負荷はそれぞれどう変化するか。
比較ベンチの計画と結果
今回のベンチでは、ライド作成から椅子割り当てまでの時間と、椅子が受理してENROUTEになるまでの時間を区別して確認する。定期巡回版との比較値はまだ取れていないため、未測定の比較表は掲載しない。
| 指標 | イベント駆動版 |
|---|---|
| スコア・pass | 7,171 / true |
| マッチ待ち不満率 | 24.0% |
| 乗車地点到着不満率 | 52.9% |
| 実移動時間不満率 | 99.5% |
| Matcherの起動 | 内部HTTP巡回なし、ライド作成イベント経由 |
| DB負荷 | 2.65k QPS、COMMIT 111.35秒 / 19,294回 |
| 評価APIのエラー | 5xx 6件、要切り分け |
イベント駆動版では内部HTTPエンドポイントを経由しない可能性があるため、アクセスログの件数だけを実行回数として比較しない。共通の計測位置を設ける。
前回の参考結果はスコア4,780、pass=true、評価APIの502が4件だった。今回のイベント駆動版ではスコア7,171、pass=trueだったが、評価APIの5xxが6件残っている。ベンチごとの揺れもあるため、7,171点をイベント駆動化の効果とは断定しない。Matcherの内部HTTP巡回は発生しておらず、ライド作成後のイベントからマッチング処理が進んだことは確認できた。
最終的な不満率は、マッチ待ち24.0%、乗車地点到着52.9%、実移動99.5%だった。マッチ待ちだけを見ると極端に詰まってはいない。一方で実移動時間の不満はほぼ残っており、次に見るべき場所がMatcherの起動方式だけではないことも分かった。
実装した最小構成
既存のマッチング処理を matchOnce(dbConn) として切り出し、HTTPの内部エンドポイントとイベント駆動の両方から呼べるようにした。ライド作成のトランザクションが COMMIT した後に requestMatching() を呼ぶ。バックグラウンド処理では共有PoolからDB接続を借り、処理後に返却する。
同時に複数のライドが作成された場合は、running と requested で実行を制御する。実行中に新しい要求が来たら別のMatcherを並行起動せず、処理完了後にもう一度 matchOnce を実行する。今回の実装ではライド作成時のイベントだけを扱い、椅子が空いたときのイベントはまだ追加していない。
そのため、作成時点で空き椅子がないライドを、椅子が空いた直後に再試行する仕組みは未完成である。低頻度巡回を復旧用に残す案も、今回の比較では使っていない。
次に確認すること
- 椅子の状態が
COMPLETEDになり、椅子側への未通知状態がなくなったタイミングでMatcherを起こす。 - 評価APIの5xxがイベント駆動のDB接続と関係するか切り分ける。
- 定期巡回版とイベント駆動版を同じ条件で複数回実行する。
matchOnceのDB問い合わせ回数と、マッチ処理のp95を記録する。
今回持ち帰りたいのは、呼ばれた回数だけ処理を起動する必要はなく、要求をまとめながら仕事を取りこぼさず処理する設計ができる、という理解だ。その設計が実際に成立するかを、コードとベンチで確かめる。