Skip to content

Application Map

bianchiidentity edited this page Oct 10, 2026 · 1 revision

アプリ全体の地図

Home · アプリの仕組み · 負荷試験の観点 · 計測記録

最初に押さえるのは「CSVを更新する主催者」と「ランキングを見る参加者」が同時に動き、その閲覧が請求にも影響すること。 ランキングは読み取り専用の処理ではない。ここを理解すると、DB・ロック・キャッシュの改善を考えやすくなる。

このページは仕様書・アプリの初期実装・DBスキーマを使った実装の地図。 負荷試験の内部実装は参照しない。未計測の改善案は仮説として扱う。

15〜30分で把握する順番

目安 見るもの 答えられるようにする問い
5分 このページの構成図・データ表 誰が、何を更新し、誰がそれを読むか?
5分 アプリの仕組み と 負荷試験の観点 正しく動くとは何か?更新前後に何が変わるか?
10分 Goの Run → 認証・テナント判定 → ランキング 1リクエストがどのDB・ロックを通るか?
10分 CSV入稿 → 請求 → 初期化 書き込みと読み取りの整合性をどこで守っているか?

全言語の実装を順に読む必要はない。現在稼働しているGoで、重要な1リクエストを入口から返却まで追う。 読んだ結果は「SQL何回・何行読む・どのロック・どんな副作用」の4項目でメモするとよい。

システムとデータの流れ

flowchart LR
    B[ブラウザー・APIクライアント] --> N[nginx: ホスト名とパスで転送]
    N -->|認証| A[blackauth: JWT発行]
    N -->|API| G[Goアプリ: JWT・役割・テナント判定]
    N -->|静的ファイル| S[public]
    G --> M[MySQL: テナント・ID・閲覧履歴]
    G --> T[テナント別SQLite: 参加者・大会・スコア]
    G --> L[テナント単位のファイルロック]
    I[初期SQLiteデータ] -->|initializeで復元| T
Loading

MySQLとSQLiteが分かれていても、ランキングと請求は両方を使う。 Redisはサーバーに導入されているが、このGo初期実装の中心経路には登場しない。

6つのデータを覚える

保存先 データ 役割 更新する操作
MySQL tenant ホスト名から利用団体を特定 テナント作成
MySQL id_generator ID発行 参加者・大会・CSV行などの追加
MySQL visit_history 誰がどの大会のランキングを見たか ランキング閲覧
テナント別SQLite player 参加者名・失格状態 追加・失格
テナント別SQLite competition 大会名・終了時刻 作成・終了
テナント別SQLite player_score 入稿スコアとCSV行順 CSV再入稿で置き換え

DBスキーマは 管理DB と テナントDB。 初期の player_score は主キー以外の索引がない。検索条件に合う索引は候補になるが、書き込みコストと実際の実行計画も確認する。

重要な4経路

操作 初期実装の処理の流れ 最初に疑う点
ランキング閲覧 認証・失格確認 → 大会確認 → MySQLへ閲覧履歴追加 → テナントロック → 大会のスコア取得 → 参加者を個別取得 → Goで重複除去・ソート → 最大100件 閲覧ごとの書き込み、全件取得、参加者取得の繰り返し、ロック待ち
CSV入稿 大会状態確認 → ロック → CSVを読む → 行ごとに参加者確認・MySQLでID発行 → 既存スコア削除 → 行ごとにSQLiteへINSERT 行数に比例するDB往復、長いロック区間、トランザクション・一括処理の余地
参加者の戦績 参加者確認 → 大会一覧 → ロック → 大会ごとに当人の最新CSV行取得 → 大会情報取得 大会数に比例する問い合わせ、適切な索引
請求 大会ごとにMySQLの閲覧履歴を集約 → ロック → SQLiteのスコア登録者取得 → 重複を除いて課金区分を計算 DBを跨ぐ集約の繰り返し、終了済み大会の再計算

これはコードから読み取れる処理量の候補。まだCPU・SQL・待ち時間の順位は計測していない。 ファイルロック待ちはCPUプロファイルだけでは見落とし得るため、HTTP時間とSQL時間も合わせて見る。

改善するときに壊しやすい関係

関係 保つべきこと
CSV入稿 → ランキング・戦績 最新のCSVを反映。同一参加者の重複行は最後の行を採用。入れ替え途中の不整合を出さない
ランキング → 請求 閲覧履歴の副作用を維持。レスポンスをキャッシュしても、必要な閲覧記録は省かない
同点 → 順位 CSVの行順で順序が決まる。ページ境界まで同じ順序を保つ
大会終了 → 入稿・請求 終了後の入稿を拒否し、請求に終了状態を反映する
失格 → 参加者API 当人の参加者向けアクセスを拒否。表示用のキャッシュだけで認可を済ませない
initialize → 新しい保存形式・キャッシュ 自分が追加した保存先も初期状態に戻す。古い値を次の実行へ持ち越さない

請求の初期実装は、終了時刻より後に初めて閲覧した参加者を閲覧課金から除外し、 スコア登録者を閲覧者と二重に数えない。キャッシュ対象はこうした状態境界を確認して決める。

specification.md のランキング節には「一番大きい値」という記述もあるが、同じ仕様書の入稿節は重複行の最後を採用すると定め、 初期実装も最後の行を採用する。ここは再実装前に重複・同点の例で挙動を確認する。最大値への単純な置き換えは避ける。

コードを読むための入口

対象は 初期Go実装。関数名で検索すると辿りやすい。

問い 読む関数
どのURLがどの処理へ行くか? Run
JWT・ホスト名・失格をどう判定するか? parseViewer, retrieveTenantRowFromHeader, authorizePlayer
SQLiteの開き方とロック単位は? connectToTenantDB, flockByTenantID
ランキングと書き込みの関係は? competitionRankingHandler, competitionScoreHandler
戦績は何回DBを読むか? playerHandler
請求をどこで繰り返し計算するか? billingReportByCompetition, billingHandler, tenantsBillingHandler
ID発行と初期化は? dispenseID, initializeHandler, webapp/sql/init.sh

まずどう攻略するか

次の1回は変更せず計測し、ランキング・CSV入稿・請求のどこに時間が使われるかを決める。 その後は1つの仮説につき1つの変更を行い、同じ条件で再計測する。

仮説 確かめる観測 最初に試す変更の方向
スコア検索が大量の行を読む 該当APIの累積時間、SQLiteの実行計画・読取行数 検索条件と並び順に合う索引
CSVの行単位処理が重い 入稿の所要時間、MySQLのID発行回数、SQLiteのINSERT処理 検証を保った一括処理・トランザクション化
ランキングのN+1とソートが重い CPUプロファイル、参加者参照の回数、HTTP累積時間 一括取得・必要な列の取得・計算結果の再利用
終了済み請求を繰り返し計算している 請求APIの時間、同じ大会の集約の繰り返し 終了時の確定値保存と初期化対応
ロックが複数APIをまとめて止める CPU使用率に比べHTTP待ちが長い、入稿中だけ閲覧が遅い 整合性を守りつつロック範囲・更新方法を見直す

最初から全DB移行や全面キャッシュを行うと、効果と不具合の原因が分かりにくい。 小さい変更で根拠を作り、伸びない・正しさが崩れる変更は戻す。 実験の根拠・実装・判定はIssueへ、時刻・変更・スコアは 計測記録 へ残す。