監視・スケーリング戦略

Mahjong Vision 本番運用の観測対象 / メトリクス収集 / アラート / スケーリング / 障害対応 Runbook 一式
作成日: 2026-05-14 対象 Phase: クラファン公開 〜 量産運用 対象オペレータ: SRE / 開発者 / 現地担当 関連: production-deployment-guide.html / docs/runbook-incident-response.md
← INDEX 本番デプロイ運用ガイド 設計ボトルネック

1. 観測対象 (SLI / SLO)

1.1 主要 SLI / SLO 一覧

カテゴリ指標 (SLI)目標 (SLO)悪化判定 (アラート)
推論レイテンシP50 / 1 frame 推論≤ 50 ms (Jetson) / ≤ 150 ms (Cloud)P50 が SLO を 2 倍超過
P95 / 1 frame 推論≤ 100 ms (Jetson) / ≤ 300 ms (Cloud)P95 > SLO で 5 分継続
P99 / 1 frame 推論≤ 200 ms (Jetson) / ≤ 600 ms (Cloud)P99 > SLO で 5 分継続
品質false positive 率 (1 動画 60s あたり FP 件数)≤ 3 件 / 60s日次 3 倍 (≥ 9 件)
マスク fallback 率 (HSV マスク失敗で生フレーム推論に降格)≤ 5%15% 超過
リソースGPU 使用率 (Jetson / RTX4090)≤ 85% (平均 5 分)95% 超過 10 分
CPU 使用率≤ 70%90% 超過 10 分
メモリ使用率 (Jetson 8GB)≤ 75%90% 超過
外部 APIRoboflow Inference API クォータ消費月 ≤ 80%残 10% 以下
サイトmahjong-scorekeeping-docs.pages.dev HTTP 5xx 比率≤ 0.1%5 分平均 1% 超過
モデル品質mAP@50 (val 500 枚 / 日次自動評価)≥ 0.80前日比 −5% 以上 (絶対値で 0.04 ダウン)

1.2 取得タイミング

2. メトリクス収集アーキテクチャ

2.1 全体構成図

[Jetson Orin Nano] -- node_exporter ----+ | [Jetson Orin Nano] -- mahjong-edge ------+ (Prometheus pull or remote_write) (structured log / FastAPI /metrics) | v [RTX4090 dev box] -- python /metrics ---+--> [Prometheus] <----+ | | [Cloudflare Workers] --(workers-analytics)+ | - Analytics Engine -- API pull | | v | [cAdvisor / containers] -------+ | [Grafana dashboard] <----------+ | v [Alertmanager] -- Slack #mj-alerts -- LINE Bot (LineClaude 経由) -- PagerDuty (SEV1 only)

2.2 各環境からの収集方式

環境方式エンドポイント / 仕組み
Jetson Orin NanoPrometheus pull:9100/metrics (node_exporter) + :8000/metrics (mahjong-edge FastAPI)
RTX4090 devpush (任意)バッチ評価時のみ python -m src.metrics.push --gateway ...
Cloudflare Workers AIAPI pullCloudflare GraphQL Analytics API を 60 秒間隔で pull するエクスポータ
Roboflow APIAPI pullRoboflow Workspace usage API を 15 分間隔で pull
Cloudflare PagesCloudflare LogpushR2 にログ → Loki / Grafana で 5xx を集計

2.3 Prometheus 例 (scrape config)

scrape_configs:
  - job_name: mahjong-edge
    scrape_interval: 10s
    static_configs:
      - targets:
          - jetson-01.local:9100
          - jetson-01.local:8000
          - jetson-02.local:9100
          - jetson-02.local:8000
        labels:
          env: prod
          role: edge

  - job_name: cadvisor
    static_configs:
      - targets: ['jetson-01.local:8080', 'jetson-02.local:8080']

  - job_name: cloudflare-workers-ai
    metrics_path: /probe
    static_configs:
      - targets: ['cf-exporter:9101']

2.4 各スクリプトでの structured logging

既存スクリプト (scripts/render-3d-overlay-cf.py, scripts/evaluate-agari-transition-detector.py, scripts/filter-roboflow-frames.py 等) に Python 標準 logging モジュールの JSON フォーマッタを統一導入する。

import logging, json, sys, time

class JsonFormatter(logging.Formatter):
    def format(self, record):
        payload = {
            "ts":       int(time.time() * 1000),
            "level":    record.levelname,
            "logger":   record.name,
            "msg":      record.getMessage(),
            "script":   getattr(record, "script", None),
            "model":    getattr(record, "model", None),
            "frame":    getattr(record, "frame", None),
            "latency_ms": getattr(record, "latency_ms", None),
            "fp_count":   getattr(record, "fp_count", None),
        }
        return json.dumps({k: v for k, v in payload.items() if v is not None}, ensure_ascii=False)

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
log = logging.getLogger("mahjong.edge")

log.info("inference done", extra={"script": "render-3d-overlay-cf",
                                  "model": "v2-7", "frame": 1234, "latency_ms": 28.4})
journald / docker logs に出した JSON ログを Promtail で Loki に集約し、Grafana で latency_ms / fp_count をクエリ可能にする。 Python 3.14 では logging 標準 + 追加依存ゼロで動作する。

3. アラート設定 (閾値・通知先)

3.1 アラート一覧 (Alertmanager 想定)

アラート名条件 (PromQL ベース)通知先SEV
InferenceLatencyP95Highhistogram_quantile(0.95, rate(mj_inference_latency_ms_bucket[5m])) > 100 for 5m (Jetson)Slack #mj-alertsSEV2
InferenceLatencyP99CriticalP99 > 600 ms for 5m (Cloud)Slack + LINESEV1
FalsePositiveSpikerate(mj_fp_count[10m]) > 3 * avg_over_time(mj_fp_count[24h])SlackSEV2
MaskFallbackHighmj_mask_fallback_ratio > 0.15 for 10mSlackSEV3
MapDailyDrop日次バッチ: mj_val_map50 < (yesterday - 0.04)Slack + LINESEV1
RoboflowQuotaLowroboflow_quota_remaining_ratio < 0.10Slack + LINESEV2
WorkersAIErrorsrate(cf_workers_ai_errors_total[5m]) > 5SlackSEV2
PagesHttp5xxSpikerate(cf_pages_http_5xx_total{site="mahjong-scorekeeping-docs"}[5m]) > 0.01 * rate(total[5m])Slack + LINESEV1
JetsonGpuThermaljetson_gpu_temp_c > 85 for 5mSlackSEV2
EdgeServiceDownup{job="mahjong-edge"} == 0 for 2mSlack + LINE + PagerDutySEV1

3.2 通知チャネル設計

API キー監視で .env を出力しない: Roboflow / Cloudflare の残クォータ取得時に、レスポンス全文をログ出力するとキーや内部 ID が漏れる可能性がある。 エクスポータは quota 数値のみを Prometheus に書き、レスポンス本文はログしない こと。

4. スケーリング戦略 (1 → 10 → 100 ユーザ)

4.1 段階別構成

段階同時推論構成月額目安ボトルネック
α (社内検証)1Jetson 1 台 / RTX4090 (dev)電気代のみなし
β (10 ユーザ)10Workers AI + Jetson 1 台$10〜30 / 月Workers AI cold start
GA1 (100 ユーザ)100Workers AI + Cloudflare Queues + Jetson 3 台$80〜200 / 月キュー深さ・Jetson 熱
GA2 (1,000 ユーザ)1,000Workers AI multi-region + Jetson 10 台分散$500〜1,500 / 月モデル更新の rolling

4.2 Cloudflare Queues + Workers AI でのリクエストキュー

// Worker (TypeScript)
export default {
  async fetch(req: Request, env: Env) {
    const body = await req.arrayBuffer();
    await env.MJ_QUEUE.send({ image: body, ts: Date.now() });
    return new Response(JSON.stringify({ status: "queued" }), { status: 202 });
  },
  async queue(batch: MessageBatch<Job>, env: Env) {
    for (const msg of batch.messages) {
      const result = await env.AI.run("@cf/mahjong-vision/mleague-tiles-v2-7", {
        image: [...new Uint8Array(msg.body.image)],
      });
      // 結果は KV / R2 に格納
      await env.RESULTS.put(`result/${msg.body.ts}`, JSON.stringify(result), {
        expirationTtl: 3600,
      });
      msg.ack();
    }
  },
} satisfies ExportedHandler<Env>;

4.3 Jetson クラスタ (3 台で 100 ユーザ想定)

1 Jetson Orin Nano = 30 FPS (1280×720, INT8) ≈ 30 同時ストリーム (1 FPS / ユーザ前提) を捌ける。 100 ユーザ同時を見込む場合は 3 台構成。

[クライアント x 100] --> [Nginx LB] --> [Jetson-01] --> [Jetson-02] --> [Jetson-03] | +--> 共有 NFS (モデル engine) +--> Prometheus / Grafana
項目1 台3 台
同時ユーザ収容 (1 FPS 想定)30100 (余裕 20%)
初期費用 (本体 + 周辺)¥105,300¥315,900
月額電気代 (24h 連続)¥250〜400¥750〜1,200
停止許容 (1 台落ちても 67 ユーザ収容可)不可

4.4 スケーリング発動条件 (自動 / 半自動)

条件アクション担当
Workers AI P95 > 300 ms かつ Queue 深さ > 50 が 10 分継続Workers consumer 並列度を 100 → 200 に増やすSRE 半自動 (PR ベース)
同時ユーザ > 70 (Jetson 1 台運用時)Jetson 2 台目を冷予備から起動SRE (15 分以内)
クラファン公開当日 (予測ピーク)当日 09:00 から Jetson 全 3 台 + Workers AI 並列 200 で稼働SRE (事前計画)

5. 障害対応 Runbook (3 ケース)

詳細な markdown 版は docs/runbook-incident-response.md に。本セクションは Web で即参照できる短縮版。

R-1. 検出が突然 0 件になった (アガリ検出 / 牌検出が止まる)

SEV: SEV1 (ユーザ体験が完全停止)

典型原因: モデル engine 破損 / カメラ USB 切断 / マスク前処理が画面全体を黒く塗っている / Roboflow API 401

診断 (60 秒以内):

# 1. サービス起動確認
systemctl status mahjong-edge

# 2. 直近 log で例外を確認
journalctl -u mahjong-edge -n 200 --no-pager | grep -E '(ERROR|Traceback)'

# 3. カメラ device 認識
ls /dev/video* && v4l2-ctl --list-devices

# 4. マスクが全黒でないか (前処理) → 直近 frame をダンプ
ls -la mleague/output/_native_bbox_check.png

復旧: モデル engine が原因なら B-6 ロールバック 実行。カメラなら USB 再差し + systemctl restart

R-2. Cloudflare Pages デプロイ停止 (mahjong-scorekeeping-docs.pages.dev)

SEV: SEV1 (クラファン期間中) / SEV2 (それ以外)

背景: mercari-text-copy で 2026-04-11 〜 04-29 に発生した 「git push しても auto-deploy が完全停止 / 18 日間 404」 インシデントと同型。 Cloudflare ダッシュボードから Source / Build configurations が消える UI バグが既知。

5 分診断:

# 既存ページは生きているはず
curl -s -o /dev/null -w "%{http_code}\n" https://mahjong-scorekeeping-docs.pages.dev/index.html

# 最新追加した HTML が 404 なら auto-deploy 停止確定
curl -s -o /dev/null -w "%{http_code}\n" https://mahjong-scorekeeping-docs.pages.dev/production-deployment-guide.html

復旧 (wrangler CLI 経由 / 5〜10 秒で反映):

npx wrangler login                  # 初回のみ
npx wrangler pages deploy docs-site/ \
    --project-name mahjong-scorekeeping-docs \
    --commit-dirty=true \
    --branch main
恒久対策: package.json"deploy:docs" script 追加 + .git/hooks/pre-push で自動 wrangler deploy fallback。 ¥20,833 のインシデントコスト計上 (mercari-text-copy 事例) を踏まえ、本プロジェクトでも同等の予防策を必須化する。
R-3. Roboflow API レート制限 / クォータ枯渇

SEV: SEV2

典型原因: 月次 quota (Hosted Inference) を消費し切った / 短時間に多数バッチ呼び出し / API キーが他環境と共有で重複消費

診断:

# 1. エラー判定
journalctl -u mahjong-edge -n 200 --no-pager | grep -E '(429|quota|rate)'

# 2. 残クォータ (web ダッシュボード) を確認
# https://app.roboflow.com//settings/usage

復旧:

  1. 短期: Workers AI または Jetson ローカル推論へ --backend 切替で迂回 (4.2 モデル切替の典型コマンド)
  2. 中期: Roboflow Workspace プラン引き上げ (Starter → Growth)
  3. 長期: Hosted Inference 依存を減らし、TensorRT engine をエッジに常駐させる方針 (本ガイド A→B/C への漸進移行)
API キー取扱: 復旧時に .env.local から ROBOFLOW_API_KEY を読み直す場合も、 値をコンソール出力・ログ出力しないこと。[ -n "$ROBOFLOW_API_KEY" ] && echo "key set" 等で存在確認のみ行う。

上記 3 ケース以外の障害分類 (SEV1/SEV2/SEV3 全体) と SLA 定義は docs/runbook-incident-response.md を参照。