プロジェクト一覧

サウンドマインド

オディヤ位置収集サーバー

30秒間隔でポーリングする端末が一斉に位置を送り込みます。1件も失わず、1件のために止まりもしない保存経路を作る仕事でした。

役割
位置収集パイプラインのバックエンド
期間
2025.07 ~ 現在
使用技術
Spring BootJava 17RedisMariaDBFlywayShedLock
アプリ / クライアントネイティブサーバーワーカーストレージ外部
端末が上げた位置はまずキューに積まれ、ドレインが分割保存します。破損分は隔離され自動で再投入されます。ブロックにカーソルを合わせると、つながるフローだけが強調されます。ドラッグで移動できます。

改善結果

  • 0件

    アトミックpop移行後の負荷テストでのデータ損失

  • 60秒 / 5,000件

    ドレイン周期とティックあたりの保存上限

01課題

端末が送ってくる位置をリクエストごとにそのままDBへ書き込むと、書き込み負荷が集中したときにサーバーごと崩れます。そこでRedisキューに溜めてから分割保存する構成にしたのですが、ここに2つの落とし穴がありました。キューから取り出す方式がアトミックでないと、2つのプロセスが同じデータを分け合って失ってしまい、壊れたデータが1件でもバッチに混ざると、まとまり全体がロールバックされて全ユーザーの位置が蓄積されなくなります。

02判断

取り出す経路をアトミックな一括popの1本に統一しました。範囲取得後に切り取る方式とスクリプトベースの方式を両方試しましたが、負荷テストで損失が実測されてしまいました。保存に失敗した場合は取り出した順序を反転してキューの先頭に戻し、再保存時の重複は取り除きます。パースと検証は保存前に済ませて、壊れた件だけを別に隔離し、自動で再処理します。

  • アトミックな一括pop専用、60秒ごとに最大5,000件ずつ分割保存
  • 失敗時は逆順復元と重複排除で最低1回の保存を保証
  • 破損した件は隔離テーブルへ退避して定期的に再投入、再処理不能な件は除外
  • サーバー命令はアップロード応答に載せて届ける単方向チャネルとして設計
  • 全スケジューラーに単一実行ロックをかけ、無停止デプロイの重なり区間での二重実行を遮断

03レガシーTomcatから無停止デプロイへ

引き継いだデプロイ方式は、外部Tomcatにwarを載せる構成でした。デプロイはサーバーを落とし、ファイルを差し替え、再び上げる順序です。その間、収集はまるごと止まり、そのとき子ども端末が送った位置は再送キューに溜まるか捨てられます。位置サービスにおいてデプロイ時間とは、保護者が子どもを見られない時間そのものです。しかもビルドをサーバー上で回していたため、ビルドが失敗すればサーバーが半分死んだまま残りました。

デプロイ中も収集は止まらない

チェックに失敗すればトラフィックはblueのままです

  • 成果物を実行可能な単一jarに統一しつつ、既存のwar経路もしばらく併存させました。デプロイ方式とアプリ構造を同時に作り替えないためです
  • サーバーでは絶対にビルドしないを原則としました。ローカルでイメージを作って圧縮し、SSHでストリーム転送して、サーバーでは受け取って起動するだけにします。ビルド失敗が本番サーバーに届くことがなくなります
  • スロットを二つに分け、非稼働側に新バージョンを先に立ち上げます。DBとRedisの接続まで確認する準備状態チェックを通過して初めて前段のプロキシがトラフィックを渡し、その後に旧スロットを落とします。利用者から見て途切れる瞬間がありません
  • 本番環境にはチェックをもう一段置きました。ログイン画面が正常応答するか、本人認証のリダイレクトがhttpsで出るかまで確認します。以前リダイレクトがhttpで出てアプリのWebViewに遮断された事故があったためです
  • イメージにロールバックタグを付け、問題が見えたら以前のバージョンへ即座に戻します。戻すのに再ビルドは要りません
  • 二つのスロットが一時的に同時に動く区間が生まれるため、すべての定期処理に単一実行ロックをかけ、バッチが二重に走らないようにしました。無停止デプロイが生んだ新しいリスクも併せて塞いだということです

04結果

負荷テストで実測されていた損失がなくなり、壊れたデータが入ってきても該当の件が隔離されるだけで、全体の蓄積は止まりません。DB障害が起きてもコネクション待機を短く区切ってサーバー全体へ波及させず、デプロイは3つの環境すべてでトラフィック切り替え方式のため、収集が途切れません。