| カテゴリ | 指標 (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% 超過 | |
| 外部 API | Roboflow Inference API クォータ消費 | 月 ≤ 80% | 残 10% 以下 |
| サイト | mahjong-scorekeeping-docs.pages.dev HTTP 5xx 比率 | ≤ 0.1% | 5 分平均 1% 超過 |
| モデル品質 | mAP@50 (val 500 枚 / 日次自動評価) | ≥ 0.80 | 前日比 −5% 以上 (絶対値で 0.04 ダウン) |
| 環境 | 方式 | エンドポイント / 仕組み |
|---|---|---|
| Jetson Orin Nano | Prometheus pull | :9100/metrics (node_exporter) + :8000/metrics (mahjong-edge FastAPI) |
| RTX4090 dev | push (任意) | バッチ評価時のみ python -m src.metrics.push --gateway ... |
| Cloudflare Workers AI | API pull | Cloudflare GraphQL Analytics API を 60 秒間隔で pull するエクスポータ |
| Roboflow API | API pull | Roboflow Workspace usage API を 15 分間隔で pull |
| Cloudflare Pages | Cloudflare Logpush | R2 にログ → Loki / Grafana で 5xx を集計 |
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']
既存スクリプト (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})
latency_ms / fp_count をクエリ可能にする。
Python 3.14 では logging 標準 + 追加依存ゼロで動作する。
| アラート名 | 条件 (PromQL ベース) | 通知先 | SEV |
|---|---|---|---|
| InferenceLatencyP95High | histogram_quantile(0.95, rate(mj_inference_latency_ms_bucket[5m])) > 100 for 5m (Jetson) | Slack #mj-alerts | SEV2 |
| InferenceLatencyP99Critical | P99 > 600 ms for 5m (Cloud) | Slack + LINE | SEV1 |
| FalsePositiveSpike | rate(mj_fp_count[10m]) > 3 * avg_over_time(mj_fp_count[24h]) | Slack | SEV2 |
| MaskFallbackHigh | mj_mask_fallback_ratio > 0.15 for 10m | Slack | SEV3 |
| MapDailyDrop | 日次バッチ: mj_val_map50 < (yesterday - 0.04) | Slack + LINE | SEV1 |
| RoboflowQuotaLow | roboflow_quota_remaining_ratio < 0.10 | Slack + LINE | SEV2 |
| WorkersAIErrors | rate(cf_workers_ai_errors_total[5m]) > 5 | Slack | SEV2 |
| PagesHttp5xxSpike | rate(cf_pages_http_5xx_total{site="mahjong-scorekeeping-docs"}[5m]) > 0.01 * rate(total[5m]) | Slack + LINE | SEV1 |
| JetsonGpuThermal | jetson_gpu_temp_c > 85 for 5m | Slack | SEV2 |
| EdgeServiceDown | up{job="mahjong-edge"} == 0 for 2m | Slack + LINE + PagerDuty | SEV1 |
#mj-alerts: 全 SEV を集約。SEV1 は @hereEdgeServiceDown, PagesHttp5xxSpike) のみ on-call ローテcron 09:00 JST)| 段階 | 同時推論 | 構成 | 月額目安 | ボトルネック |
|---|---|---|---|---|
| α (社内検証) | 1 | Jetson 1 台 / RTX4090 (dev) | 電気代のみ | なし |
| β (10 ユーザ) | 10 | Workers AI + Jetson 1 台 | $10〜30 / 月 | Workers AI cold start |
| GA1 (100 ユーザ) | 100 | Workers AI + Cloudflare Queues + Jetson 3 台 | $80〜200 / 月 | キュー深さ・Jetson 熱 |
| GA2 (1,000 ユーザ) | 1,000 | Workers AI multi-region + Jetson 10 台分散 | $500〜1,500 / 月 | モデル更新の rolling |
// 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>;
GET /result/:ts で polling、または Durable Objects で push 通知1 Jetson Orin Nano = 30 FPS (1280×720, INT8) ≈ 30 同時ストリーム (1 FPS / ユーザ前提) を捌ける。 100 ユーザ同時を見込む場合は 3 台構成。
| 項目 | 1 台 | 3 台 |
|---|---|---|
| 同時ユーザ収容 (1 FPS 想定) | 30 | 100 (余裕 20%) |
| 初期費用 (本体 + 周辺) | ¥105,300 | ¥315,900 |
| 月額電気代 (24h 連続) | ¥250〜400 | ¥750〜1,200 |
| 停止許容 (1 台落ちても 67 ユーザ収容可) | 不可 | 可 |
| 条件 | アクション | 担当 |
|---|---|---|
| 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 (事前計画) |
詳細な markdown 版は docs/runbook-incident-response.md に。本セクションは Web で即参照できる短縮版。
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。
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 事例) を踏まえ、本プロジェクトでも同等の予防策を必須化する。
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
復旧:
--backend 切替で迂回 (4.2 モデル切替の典型コマンド).env.local から ROBOFLOW_API_KEY を読み直す場合も、
値をコンソール出力・ログ出力しないこと。[ -n "$ROBOFLOW_API_KEY" ] && echo "key set" 等で存在確認のみ行う。
上記 3 ケース以外の障害分類 (SEV1/SEV2/SEV3 全体) と SLA 定義は
docs/runbook-incident-response.md を参照。