📖 はじめに — 性能の負債は、データ量が増えるまで沈黙する
性能負債の厄介さは、開発環境では正しく動いてしまうことだ。100 件のテストデータでは何も起きない。AI が書いたコードはレビューでも「ロジックは正しい」と通る。そして本番相当のデータ量が入った瞬間、別の顔を見せる。
本章は 2 つの実録を扱う。前半は「件数を数えるだけの処理」が分散処理全体を止めた障害の解剖。後半は、性能改善として導入したキャッシュが別の障害の元凶になり、アーキテクチャ契約で正しい場所に戻されるまで — 実践編でおそらく一番「因果が一周する」章である。
💥 障害の解剖 — COUNT のつもりが、全件運んでいた
ステージング環境で、AI マッピング SAGA が Queue = Running のまま永久停止した。処理は進まず、エラーも見えない。調査で判明した因果連鎖はこうだ。
根本原因は 1 行に凝縮できる。件数が欲しいだけなのに、Where(...).ToListAsync() で全レコードをアプリ側に運んでから数えていた。ORM / LINQ の古典的な罠で、DB 側で COUNT(*) に落とせば済む処理だ。是正後の実測は 50 秒超 → 100ms 未満、およそ 500 倍の改善だった。
この障害から持ち帰るべきは「COUNT を使え」ではない。3 つある。
- 性能負債はデータ量というディメンションで顕在化する。 ロジックレビューでは捕まらない。「この処理は N に対してどうスケールするか」は、独立した確認観点として明示的に持つ必要がある
- 分散系では、1 つの遅いクエリが障害に増幅される。 タイムアウト → リトライ → 毎回同じ理由で失敗 → DLQ という連鎖は、リトライ機構が「回復」ではなく「同じ失敗の反復実行」になる典型。リトライ設計は「時間で失敗する処理」と相性が最悪だ
- 教訓は NFR として明文化する。 このステアリングは是正後、「DB のステートメントタイムアウトを超えない」を非機能要件として文書に固定した。障害の記憶は風化するが、要件は残る
🧠 キャッシュ刷新 — 完全一致から「意味の近さ」へ
後半の主役は、AI マッピングのコスト最適化だ。カテゴリマッピングで LLM 呼び出しを減らすため、当初は「大分類・中分類・小分類・性別・検索ワードの 5 属性完全一致」をキーにしたキャッシュがあった。だが完全一致キーは表記が 1 文字揺れれば別キーになり、実運用のヒット効率と再利用品質に限界があった。
刷新の設計は「文字列の一致」から「意味の近さ」への転換だ。
- カテゴリ情報を埋め込みベクトル化し、コサイン類似度 ≥ 0.95 でキャッシュヒットと判定(pgvector で近傍検索)
- 埋め込みモデルと次元数は、既存の別ジョブが生成済みの資産と同一(1536 次元)に統一 — 新しい埋め込み空間を増やさない
- 成功指標を先に定義: AI 呼び出し 50% 以上削減、キャッシュヒット時のマッピング精度 90% 超
型 3(定量が意思決定を支える)はここでは「導入判断」に効いている。「キャッシュを賢くしました」ではなく、削減率と精度という 2 軸の数値目標を立ててから作る。片方だけなら簡単なのだ — 閾値を緩めれば削減率は上がるが精度が落ちる。2 軸で縛るから、閾値 0.95 という設計点に意味が出る。
なお埋め込みベクトルの扱いには本シリーズ外でも落とし穴を踏んでおり、それはベクター検索の task_type 落とし穴として別記事にまとめてある。
🔄 伏線回収 — その最適化が、障害の元凶と名指しされる
ここからが本章の核心だ。第4章で解剖した SAGA の障害リストに、こういう項目があったのを覚えているだろうか — 「Create Chunk ハンドラに独自の埋め込みキャッシュ類似検索を実装し、statement_timeout を構造的に超過」。
それは、この類似度キャッシュだった。
キャッシュ自体の設計は正しい。問題は置き場所だ。SAGA の Create Chunk ハンドラは「対象 ID を取得してチャンクに割るだけ」の軽量処理であるべき場所(キュー排他制御の中で走るため、ここが重いと SAGA 全体が詰まる)。そこに「N 件の対象それぞれに埋め込み生成 + 3N 回の類似検索」というキャッシュフィルタリングが実装され、タイムアウトを構造的に超過した。前半の障害が「不注意な 1 行」だったのに対し、こちらは正しい機能の間違った配置である。
帰結が SAGA 同型性契約への明文化だ。
- C-1: Create Chunk ハンドラでの外部 API 呼出・埋め込み生成・類似検索・キャッシュフィルタを禁止行為として列挙
- C-8: キャッシュの保存は SAGA チャンク完了後の非同期 bulk 処理のみ — 重い処理は、重くてよい場所へ
「性能改善」と「アーキテクチャ契約」は独立に評価してはならない、というのが一周した因果の結論だ。最適化は局所では常に正しく見える。だが分散処理の排他制御の中では、速くするための機能が全体を止める。パフォーマンス改善の設計レビューには、「その処理はどの実行コンテキストで走るか」という配置の観点が要る — そして配置の規約は、第4章の検査スクリプトが機械で守っている。
🧾 まとめ
- 「数えるだけ」の全件マテリアライズが 52.7 秒 → timeout → リトライ → DLQ と増幅して SAGA を止めた。COUNT 化で 500 倍。性能はロジックレビューと別の観点(N へのスケール)で確認する
- 障害の教訓は NFR として文書に固定する(statement_timeout を超えない、等)
- キャッシュ刷新は完全一致から類似度 0.95 へ。削減率 50% と精度 90% の 2 軸で縛るから設計点に意味が出る
- そしてその最適化が Create Chunk 重量化の元凶に — 正しい機能でも、置き場所を間違えると障害になる。配置の規約(C-1 / C-8)まで含めて初めて性能改善は完成する
次章は実践編の最終章。「ローカルで緑でも本番で 400」という .NET の実挙動の罠を含む、セキュリティ編へ。
📚 シリーズ記事(ステアリング駆動開発・実践編)
序章
ドメイン別の実録(全 9 章)
- リファクタリング総点検ガイド — 7 つの観点と進め方
- DDD/SOLID/BC 編 — god class 一掃と境界の機械ガード
- SAGA 編 — 新アーキテクチャ挑戦と再発ゲート
- Atomic Design リファクタ編 — 47 page 新規移植
- Storybook × a11y 実機編
- テスト品質・網羅性編
- Python ジョブ群編
- パフォーマンス編
- セキュリティ編
続編
関連記事
- ベクター検索の落とし穴 — 埋め込み利用の別の罠
- SAGA 編 — C-1 / C-8 契約の全体像