Files
photo-live/infra/deploy/monitoring.md
T
浅念 d01cecd2f6 docs: p5 preparation complete and implementation draft
- docs/plans/2026-09-21-P5-真实环境接入.md 草案(头部标注待 G5 凭证评审
  转正):T1-T12 任务序(每任务 产出/依赖用户输入/关口)——G1 OSS 冒烟→
  国内起栈→ov 部署+bootstrap 对账→域名证书→CDN(含图片加速三选一)→
  跨境 MySQL 通道定案(白名单+SSL/隧道/HTTP 化三选项,含改造量评估)→
  G4 用户源→故障演练→压测 D05→G2 真机→G6 试运行→发布交接;用户输入
  汇总表 + 决策 D03-D13 状态速查 + 风险表(跨境链路/双层签名/MySQL
  8.0.30 门槛/镜像仓库通道等)
- 总纲 P5 行注记:准备件完成 2026-09-21,实装待 G5 + G4
- P5-prep 计划全项勾选回填(评审项注明自审已修 + 独立 reviewer 由编排方追加)
- 自审修正:monitoring 统计副本表名(无 stat_snapshots 表,副本位在
  albums/photos 行)与 outbox_events.occurredAt/photo_jobs.updatedAt 列名、
  jq 管道括号;cdn 失效多 URL 换行分隔;k6 helpers 请求体用 ArrayBuffer
  原生形态、read-feed 身份串延迟生成(init 上下文 VU 编号恒 0)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 01:56:56 +08:00

155 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 监控告警方案(P5-prep Task 3)
> 适用对象:photo-live 双地域生产栈(编排见 `docker-compose.cn.yml` /
> `docker-compose-ov.yml`)。**治理决策(登记):P5 起步不引入额外监控系统**
>(Prometheus/Grafana/ELK 均不上)——1Panel 应用监控 + 云监控 + 结构化日志
> 检索三件套覆盖本阶段全部指标面;业务量或团队规模变化时再评估升级,
> 届时优先补「日志聚合 + 指标导出」,而非替换现有日志结构。
## 1. 指标 → 实现方式映射总表
| 指标 | 实现方式 | 位置 |
| --- | --- | --- |
| 服务存活 / health | 容器 healthcheck(api-cn/api-ov/web 有 `/v1/health` 探针;worker/sync 无 HTTP 面,以进程存活 + restart 策略承担)+ 1Panel 容器状态页 | 两台 1Panel |
| 容器重启次数 | 1Panel 容器监控(重启计数突增 = crash loop) | 两台 1Panel |
| CPU / 内存 / 磁盘容量 | 1Panel 应用监控 + 主机概览;云监控(ECS 自带)做兜底告警 | 两台 |
| MySQL / Redis 连接与资源 | 1Panel 数据库监控(连接数、内存);连接池上限已在连接串 `connection_limit=10` 固化 | 两台 |
| 同步失败 / 同步水位延迟 | sync 容器结构化日志检索(§3 检索语句)+ `outbox_events` 未投递计数(SQL) | cn 服务器 |
| 上传失败率 | api-cn 访问日志(pino-http)按 `req.url` + `res.statusCode` 计数(§3) | cn 服务器 |
| 计数差异(点赞/浏览对不上) | sync `stat return cycle failed` 日志 + `albums`/`photos` 副本 `statsVersion` 前进检查(§3) | cn 服务器 |
| 队列积压(图片处理) | worker `sweep 完成` 日志的 `claimed` 持续 >0 + `photo_jobs` pending 行龄 SQL(§3) | cn 服务器 |
| 磁盘 / 流量(OSS) | OSS 控制台用量 + 云监控;CDN 接入后加 CDN 控制台流量看板 | 阿里云控制台 |
| 证书到期 / 域名解析 | 1Panel 证书管理提示 + 云监控站点监控 | 各自 |
要点:**业务指标不带独立监控系统,全部落在结构化日志检索**。三端(api /
worker / sync)日志均为 pino JSON 单行 stdout,`docker logs` 直接可查;字段
口径见 §2(如实列自代码,非设想)。
## 2. pino 结构化日志字段表
日志器装配(代码事实):api = `nestjs-pino` + `pino-http`(`services/api/src/
shared/logger-options.ts`,每请求自动访问日志);worker / sync = 原生 pino
(`services/worker/src/main.ts` / `services/sync/src/main.ts`)。级别映射 pino
数值:20=debug 30=info 40=warn 50=error。凭据打码:`req.headers.authorization`
与 `req.headers.cookie` 已按 `logger-options.ts` redact 为 `[REDACTED]`。
### 2.1 通用字段(每行日志必有)
| 字段 | 来源 | 示例 / 说明 |
| --- | --- | --- |
| `level` | pino | `30`(数值;人读用 `docker logs … \| jq -r '.level'` 映射) |
| `time` | pino | Unix 毫秒时间戳 |
| `pid` / `hostname` | pino | 进程 / 容器主机名(排障定位容器) |
| `msg` | 各调用点 | 人类可读消息(检索主键,见表 2.3) |
| `context` | nestjs-pino(仅 api) | Nest Logger 上下文名,如 `AllExceptionsFilter`、`UserSyncService` |
### 2.2 HTTP 访问日志字段(api 每请求一行,pino-http 自动打点)
| 字段 | 说明 |
| --- | --- |
| `req.id` | 请求 ID:透传上游 `x-request-id` 或现生成 UUID;**同时回写响应头 `x-request-id`**,全链路排障锚点(`request-id.middleware.ts`) |
| `req.method` / `req.url` | 方法与含 query 的路径(如 `GET /v1/public/albums/tok/photos?limit=30`) |
| `req.headers` | 已 redact 授权头/Cookie |
| `req.remoteAddress` / `req.remotePort` | 客户端地址(反代后是网关地址,trust proxy 登记为 P5 项) |
| `res.statusCode` | 响应状态码(错误率计数的取数字段) |
| `responseTime` | 服务端耗时毫秒(粗粒度性能观测;精测用压测框架 `loadtest/`) |
| `msg` | 固定 `request completed` |
### 2.3 业务/后台进程自定义字段(msg 为检索主键,如实列自调用点)
| 进程 | `msg`(检索用) | 级别 | 自定义字段 | 语义 |
| --- | --- | --- | --- | --- |
| sync | `outbox batch pushed` | info | `picked`(取数)`acceptedEventIds[]`(ov 确认)`applied`(实际应用) | 推送健康;`picked≥batchSize` 连续出现 = 排积压 |
| sync | `outbox push failed(整批未标记,下轮重推)` | error | `err` `failureStreak` `backoffMs` | **同步失败告警主信号**;连续出现 = cn→ov 中断 |
| sync | `reconcile cycle failed(幂等安全,下轮再试)` | error | `err` | 对账循环失败(5min 一轮自愈) |
| sync | `stat snapshots returned` | info | `picked` `snapshots` `accepted` | 统计回传健康;长期不出现 = ov 聚合停滞 |
| sync | `stat return cycle failed(未确认行留待下轮重发,幂等安全)` | error | `err` | **计数差异告警信号**(回传断流) |
| sync | `sync sender + reconciler + stat-return started` | info | `cnApiUrl` `ovApiUrl` `intervalMs` `batchSize` `reconcileIntervalMs` `statReturnIntervalMs` | 启动自描述(排障先看这行核对 env) |
| sync | `env 非正整数,回退契约默认值` / `缺少内部同步密钥,发送器退出` | warn / error | `name,raw` / `env` | 配置异常 |
| worker | `process job 完成` | info | `photoId` `jobId` `outcome` | 单照片流水线完成 |
| worker | `process job 终败(重试耗尽)` | error | `photoId` `jobId` `errorCode` | **图片处理失败告警**(errorCode 对照契约错误码) |
| worker | `sweep 完成` | info | `claimed`(本轮认领孤儿)`enqueueFailures` `staleReEnqueued` | **队列积压观测锚**:稳态应恒为 `claimed:0` |
| worker | `sweep 轮次异常` / `photo-process 入队失败(sweep 将补偿)` | error/warn | `err` / `photoId` | 补偿通道异常 |
| worker | `redis 连接异常(ioredis 自动重连)` | warn | `label` `err` | Redis 闪断(自愈,频发则查 Redis) |
| api | `ApiException <status> -> <code> (requestId=…)` | warn | 消息内嵌 code 与 requestId | 业务异常计数(含上传 complete 失败码) |
| api | `HttpException …` / `Unhandled exception …` | warn/error | 同上 + stack | 非业务异常(5xx 必查 requestId 关联访问日志行) |
## 3. 每类告警的检索语句样例(docker logs + jq,两台服务器上直接执行)
容器名约定(compose 项目名前缀):`photo-live-cn-sync-1` /
`photo-live-cn-worker-1` / `photo-live-cn-api-cn-1`(ov:`photo-live-ov-api-ov-1`)。
```bash
# ① 同步失败(告警口径:近 10 分钟出现 error 级 outbox push failed)
docker logs --since 10m photo-live-cn-sync-1 2>&1 \
| jq -c 'select(.msg | contains("outbox push failed"))'
# ①' 同步水位延迟(未投递账本堆积——发布前快照同款 SQL,阈值见 §4)
docker exec <cn-mysql容器> mysql -uphoto -p -e \
"SELECT COUNT(*) AS undelivered, MIN(occurredAt) AS oldest FROM photo_cn.outbox_events WHERE deliveredAt IS NULL;"
# ② 上传失败率(api 访问日志:uploads 路径非 2xx 占比;complete 失败码单独数)
docker logs --since 10m photo-live-cn-api-cn-1 2>&1 \
| jq -c 'select(.msg=="request completed" and (.req.url | contains("/v1/uploads")) and (.res.statusCode < 200 or .res.statusCode > 299))'
# 业务失败码(ALBUM_UPLOAD_DISABLED / UPLOAD_SESSION_CONFLICT / UPLOAD_VERIFY_FAILED 等):
docker logs --since 10m photo-live-cn-api-cn-1 2>&1 \
| jq -r 'select(.msg | startswith("ApiException")) | .msg' | sort | uniq -c | sort -rn
# ③ 计数差异(stat 回传断流 + 版本不前进)
docker logs --since 30m photo-live-cn-sync-1 2>&1 \
| jq -c 'select(.msg | contains("stat return cycle failed"))'
# 版本前进检查(statsVersion 应随互动增长单调;长期不变即聚合停滞——
# 统计副本位内联在 albums/photos 行上:viewCountReplica/likeCount + statsVersion):
docker exec <cn-mysql容器> mysql -uphoto -p -e \
"SELECT id, statsVersion, viewCountReplica, statsSyncedAt FROM photo_cn.albums ORDER BY statsSyncedAt DESC LIMIT 10;"
# ④ 队列积压(sweep claimed 持续 >0 = 孤儿持续被重新认领 = 消费不及或 worker 停)
docker logs --since 10m photo-live-cn-worker-1 2>&1 \
| jq -c 'select(.msg == "sweep 完成")' | jq -s 'map(.claimed) | {samples: length, nonzero: ([.[] | select(. > 0)] | length)}'
# 兜底 SQL:pending 行龄(photo_jobs 无 createdAt,以 updatedAt 近似行龄;
# oldest 超过 5 分钟即真积压——注意行被认领/重试会刷新 updatedAt)
docker exec <cn-mysql容器> mysql -uphoto -p -e \
"SELECT COUNT(*) AS pending, MIN(updatedAt) AS oldest, MIN(nextRetryAt) AS nextRetry FROM photo_cn.photo_jobs WHERE state='pending';"
# ⑤ 图片处理终败(重试耗尽的照片,需人工补处理)
docker logs --since 1h photo-live-cn-worker-1 2>&1 \
| jq -c 'select(.msg | contains("process job 终败"))'
# ⑥ 5xx 洪峰(api 全量非 5xx 校验;冒烟/压测期配合使用)
docker logs --since 10m photo-live-cn-api-cn-1 2>&1 \
| jq -c 'select(.msg=="request completed" and .res.statusCode >= 500)'
```
## 4. 告警项总表(阈值 = P5 起步建议值,试运行后按观测校准)
| 告警项 | 阈值建议 | 检测方式 | 接收人 |
| --- | --- | --- | --- |
| sync 推送失败 | 10 分钟内 `outbox push failed` ≥ 3 次(对应退避 3 轮未自愈) | 日志检索 ①(人工/计划任务) | 【占位:运维负责人】 |
| 同步水位延迟 | `outbox_events` 未投递 > 1000 行,或 oldest > 10min | SQL ①'(发布检查单同款,可入 1Panel 计划任务) | 【占位】 |
| 上传失败率 | uploads 路径非 2xx 占比 > 5%(10 分钟窗,样本 ≥ 20) | 日志检索 ② | 【占位】 |
| 计数差异 | `stat return cycle failed` 连续 30 分钟,或活跃相册 `statsSyncedAt` > 30min 前且互动非零 | 日志/SQL ③ | 【占位】 |
| 队列积压 | sweep `claimed` 连续 6 轮(约 3min)>0,或 photo_jobs pending oldest > 5min | 日志/SQL ④ | 【占位】 |
| 图片处理终败 | 1h 内 `process job 终败` ≥ 5 张 | 日志检索 ⑤ | 【占位】 |
| API 5xx 洪峰 | 10 分钟 5xx ≥ 10 次 | 日志检索 ⑥ / 云监控站点监控 | 【占位】 |
| 容器重启 | 1Panel 重启计数 1h 内 +3(crash loop) | 1Panel 应用监控 | 【占位】 |
| 磁盘容量 | 1Panel 磁盘 > 80% 告警 / > 90% 严重 | 1Panel 主机监控(自带阈值告警) | 【占位】 |
| ECS / 网络可用性 | ping / 站点探测失败 | 阿里云监控(站点监控免费档即可) | 【占位】 |
| OSS 流量异常 | 月度流量突增(对比基线 ×3) | OSS 控制台 + 云监控账单告警 | 【占位】 |
| 证书到期 | 剩余 < 14 天 | 1Panel 证书管理 / 云监控 | 【占位】 |
> 1Panel 计划任务可承载 SQL 类巡检(①'/③/④ 的 SQL + 失败时发通知),把
> 「人工检索」升级为「定时巡检」零新增组件;通知通道用 1Panel 的通知设置。
## 5. 日志保留策略
- **已固化**(compose 两栈统一 `x-logging`):docker json-file 轮转
`max-size=10m × max-file=3`,单容器上限约 30MB,六容器合计 ~180MB 上限,
磁盘不会被日志撑爆。轮转之外无中心化留存——重启/删除容器丢历史,属已知取舍。
- **建议归档(P5 试运行起执行)**:1Panel 计划任务每日低峰
`docker logs <容器> --since 24h > /opt/photo-live/logs/<容器>-$(date +%F).json`
后 gzip 存本地 30 天(约每天几 MB),异常事件窗口按 RUNBOOK §1 的「水位与
账本快照登记」随发布记录另存。归档目录加入 1Panel 备份计划即可,不建独立
日志系统。
- 排障现场优先用 `--since` 缩窗 + §3 jq 语句;跨容器串事件用 `req.id` /
`requestId=`(响应头同值,用户报障时让前端从 DevTools 复制即可定位)。