Files
photo-live/infra/deploy/RUNBOOK.md
T
浅念 c26d6b9a73 fix(deploy): review fixes — smoke photoId field, web healthcheck wget, outbox table name, cdn key shape
Reviewer findings on p5-prep batch:
1. BLOCKING: prod-smoke.sh visibility grep used "id" but API items
   serialize photoId (cn public.service / ov public-ov.service) — every
   healthy deployment would false-FAIL release gate ④. 4 sites fixed.
2. web/web-ov healthchecks used curl on nginx:alpine (absent) → perpetual
   false-unhealthy; switched to busybox wget.
3. RUNBOOK rollback rule cited nonexistent sync_outbox → outbox_events.
4. cdn-checklist invalidation sample key shape aligned to actual
   variantsObjectKey layout (.../albums/{albumId}/variants/{photoId}/{version}/{variant}.jpg).

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

187 lines
11 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.
# RUNBOOK —— 发布 / 迁移 / 回滚 / 紧急通道运行手册(P5-prep Task 2)
> 适用对象:photo-live 双地域生产栈(编排见 `docker-compose.cn.yml` /
> `docker-compose-ov.yml`,环境说明见 `README-1Panel.md`)。
> 原则:**一切发布向前兼容**(迁移只前滚不回滚;回滚=旧镜像跑新 schema);
> **同步账本(outbox / sync_inbox / sync_state 水位 / outbox_stat_events)任何
> 情况下不清理不重置**——幂等与补漏对账都建立在水位连续之上。
## 0. 角色与容器速查
| 地域 | 容器 | 角色 | 端口(容器内) |
| --- | --- | --- | --- |
| cn | api-cn | 写路径 API + cn 公开读 + user-sync | 8080(/v1/health) |
| cn | worker | sharp 变体流水线 + 发布事务 + outbox | 无 HTTP |
| cn | sync | outbox→ov 推送 + 对账 + 统计回传(双库直连) | 无 HTTP |
| cn | web | nginx:viewer + admin + /v1 反代 | 宿主机 127.0.0.1:8088 |
| ov | api-ov | ov 公开读 + 互动 + WS + inbox + emergency | 8081(/v1/health) |
| ov | web-ov | nginx 网关 | 宿主机 127.0.0.1:8089 |
## 1. 发布前检查单(每次发布,逐项打勾)
- [ ] **变更范围确认**:本次代码是否含 Prisma 迁移(`packages/db-*/prisma/migrations`
新增目录)?含则必须先走 §2 迁移步骤,且确认迁移向前兼容(新增列可空/带默认、
不删旧列不收窄类型——旧镜像需能跑在新 schema 上)。
- [ ] **数据库备份**(两地域各自):
`docker exec <mysql容器> mysqldump -uroot -p<密码> --single-transaction photo_cn | gzip > photo_cn_$(date +%Y%m%d%H%M).sql.gz`
(ov 用 photo_ov;备份文件留存 ≥ 2 份历史)。
- [ ] **水位与账本快照登记**(回滚后比对用,只记不删):
cn:`SELECT count(*) FROM outbox_events WHERE deliveredAt IS NULL;`(未投递账本)
ov:`SELECT entityType, entityId, appliedVersion FROM sync_state;`(行内水位)
+ sync 容器最近 20 行日志截图/粘贴。
- [ ] **镜像 tag 登记**:新 tag = `git短sha-日期`(server 与 web 同 tag 配套),登记到
发布记录;两台服务器均已导入该 tag(`docker images | grep photo-live`)。
- [ ] **env 变更 diff**:`.env.production-*` 与仓库 example 对比,确认无缺键、
`SYNC_INTERNAL_KEY` 两端一致、无真实密钥入 git。
- [ ] **磁盘与负载水位**:1Panel 概览确认磁盘 < 80%、MySQL/Redis 内存正常;
worker 队列无积压(api-cn 日志无 process job 超时)。
- [ ] **冷备窗口**:选择业务低峰;ov 端确认无正在进行的展示位活动(嵌入轮播依赖
ov 公开读可用性)。
## 2. 发布步骤(cn 与 ov 各自执行;顺序:迁移 → 服务 → 冒烟)
1. **迁移先行**(若本次含迁移;无迁移跳过):
```bash
# cn 服务器
docker run --rm --env-file /opt/photo-live/.env.production-cn --network 1panel-network \
photo-live-server:<新tag> migrate-deploy cn
# ov 服务器
docker run --rm --env-file /opt/photo-live/.env.production-ov --network 1panel-network \
photo-live-server:<新tag> migrate-deploy ov
```
Prisma `migrate deploy` 只应用未执行过的迁移,可安全重跑。
2. **切镜像起服务**(编排目录内;`SERVER_IMAGE`/`WEB_IMAGE` 指新 tag):
```bash
# ov 先(cn sync 依赖 ov 可达)
SERVER_IMAGE=photo-live-server:<新tag> WEB_IMAGE=photo-live-web:<新tag> \
docker compose -f docker-compose-ov.yml up -d
docker compose -f docker-compose-ov.yml ps # 等 api-ov healthy
# cn 后,逐容器替换(单实例"滚动":起稳一个再下一个)
SERVER_IMAGE=photo-live-server:<新tag> WEB_IMAGE=photo-live-web:<新tag> \
docker compose -f docker-compose.cn.yml up -d api-cn
docker compose -f docker-compose.cn.yml ps # api-cn healthy 后
SERVER_IMAGE=photo-live-server:<新tag> docker compose -f docker-compose.cn.yml up -d worker sync web
```
3. **四点冒烟**(发布完成的判定口径):
```bash
scripts/prod-smoke.sh https://photo.example.cn https://photo.example.com <账号> <密码>
```
覆盖:①两地域 /v1/health ②登录 ③建册→presign 上传→complete→公开可见
(worker 处理 + cn 公开读,轮询 ≤ 60s)④ov 公开读确认。脚本输出 PASS/FAIL
摘要并软删清理测试照片。任一 FAIL → 停在原地排查,必要时按 §3 回滚。
4. **观察 10 分钟**:sync 日志有 `outbox batch pushed`、无连续 `push failed`;
api 无 5xx 洪峰;1Panel 容器监控正常。之后发布记录归档(tag、时间、检查单)。
## 3. 回滚步骤(镜像回旧 tag;数据与同步状态保留)
1. **决策口径**:新版本故障且冒烟/观察发现 → 回滚镜像。**迁移不回滚**(发布前
检查单已保证旧代码兼容新 schema;反向迁移会破坏账本与水位)。
2. 执行(与发布 §2.2 相同的命令,tag 换回上一个登记过的稳定 tag,顺序 ov→cn、
api→worker/sync→web):
```bash
SERVER_IMAGE=photo-live-server:<旧tag> WEB_IMAGE=photo-live-web:<旧tag> \
docker compose -f docker-compose-ov.yml up -d
SERVER_IMAGE=photo-live-server:<旧tag> WEB_IMAGE=photo-live-web:<旧tag> \
docker compose -f docker-compose.cn.yml up -d
```
3. **数据与同步状态保留原则**(硬性):
- 不清 `outbox_events` / `sync_inbox` / `sync_state` / `outbox_stat_events`;
未确认事件在新一轮推送中按 eventId 幂等重放,水位断层才需要人工补漏
(对账循环 5min 自动按水位拉 reconcile,无需干预)。
- 不 `migrate resolve`/手工改 `_prisma_migrations`;异常迁移记录单独记录并
上报,由开发者出修正迁移。
- 回滚后必须重跑 §2.3 冒烟 + 比对 §1 水位快照(outbox 计数只允许持平或下降
到「已确认」值,不允许突降为 0)。
4. 若故障来自**数据本身**(坏行导致 sync/apI 循环报错):先 `docker compose stop
sync`(冻结推送,cn 本地功能不受影响),保留现场日志与 outbox 行,联系开发
处理;恢复后 `up -d sync` 从冻结水位继续。
## 4. 紧急停用通道(ov 侧 emergency,独立于同步链路)
用途:跨境同步中断/内容事故时,**直接调用 ov 的 emergency 端点**下架单册内容;
语义:仅允许更严屏蔽(disable),不修改 cn 权威数据,不因普通同步事件自动解除,
恢复需显式 enable(技术方案 §2A;鉴权=与 sync 同一把 `SYNC_INTERNAL_KEY` 的
HMAC-SHA256 原始请求体签名 + ±300s 时间戳)。
**可复制样例**(bash + openssl + curl;OV_BASE=ov 域名,KEY=SYNC_INTERNAL_KEY):
```bash
#!/usr/bin/env bash
OV_BASE="https://photo.example.com"
KEY="你的SYNC_INTERNAL_KEY"
ALBUM_TOKEN="目标相册publicToken" # 与 albumId 恰好提供其一
REASON="2026-09-21 事故下架:演示"
BODY=$(printf '{"albumToken":"%s","reason":"%s"}' "$ALBUM_TOKEN" "$REASON")
TS=$(date +%s)
# 签名 = hex(HMAC-SHA256(原始请求体字节, KEY));curl -d 发送的必须是同一字符串
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$KEY" | awk '{print $NF}')
curl -sS -X POST "$OV_BASE/v1/internal/emergency/disable" \
-H "Content-Type: application/json" \
-H "x-sync-timestamp: $TS" \
-H "x-sync-signature: $SIG" \
-d "$BODY"
# 恢复:body 换 {"albumToken":"..."}(无 reason),端点换 /enable,签名同样重算
```
注意:
- 401 = 签名/时间戳问题(两端正则:`X-Sync-Signature = hex(HMAC-SHA256(rawBody, KEY))`,
`X-Sync-Timestamp` Unix 秒且与 ov 服务器时差 < 300s——先 `date +%s` 对时)。
- 若 ov 整域不可达(DNS/断网),本通道同样不可达——这是它设计上依赖 ov API 存活
的边界;整域故障直接在 1Panel 停 web-ov/api-ov 容器(服务面下线,数据面无恙)。
- 调用成功后在 cn 管理端补记事件(audit 无自动跨域记录,凭 reason 字符串回溯)。
## 5. 故障速查(三类典型)
### 5.1 同步积压(cn 相册更新后 ov 长时间不出现)
1. `docker logs --tail 100 photo-live-cn-sync-1`:连续 `outbox push failed` →
看 err:`401/403` = SYNC_INTERNAL_KEY 两端不一致或时钟漂移;`timeout/ECONNREFUSED`
= cn 服务器到 OV_API_URL(公网域名)不通;`P1001` = 到 ov MySQL 3306 不通
(防火墙/白名单/SSL)。
2. 修复连通后无需人工干预:失败退避封顶 60s 自动重推;对账循环每 5min 按水位
补漏;满批(100/批)连续追推排空积压。
3. 单事件卡死(如 ov 库坏行):`docker compose stop sync` 冻结 → 定位
`sync_inbox` 对应 eventId 行与错误日志 → 上报开发 → 恢复 `up -d sync`。
### 5.2 计数差异(ov 显示的点赞/浏览与 cn 统计对不上)
1. 链路:ov `like_batches`/`visit_sessions` 聚合 → `outbox_stat_events` →
sync 统计回传循环(60s)→ cn `stat-snapshots` 版本替换。差异先看回传:
sync 日志 `stat snapshots returned` 是否持续推进。
2. `docker logs --tail 50 photo-live-cn-sync-1` 有 `stat return cycle failed` →
多为 cn 库连接闪断,幂等重试自愈;持续失败查 `outbox_stat_events` 未确认行
(ov 库)与 cn 库可达性。
3. 版本不前进:cn `stat-snapshots` 的 statVersion 单调,若 ov 侧聚合停滞
(互动未入账)→ 查 api-ov 日志 likes/visits 429 与 Redis ov 连接。
4. 窗口语义提醒:互动流水超窗口清理(like_batches 24h/visit 30min)只清流水,
累计总数永不清——对账以 `stat-snapshots` 权威,勿手工 UPDATE 副本计数。
### 5.3 CDN 缓存(隐藏照片仍可见 / 旧图不刷新)
1. 图片 URL 两类缓存语义:**variants 内容寻址键**(/variants/…按 photoId+规格,
长缓存 immutable)与**签名 URL 短缓存**(OSS 现签 GET,TTL=SIGNED_URL_TTL_
SECONDS=900s)。CDN 接入清单(P5-prep Task 3 `cdn-checklist.md`)落地前,
本节按「无 CDN」基线排查。
2. 隐藏/删除照片后旧链接仍出图:先确认语义——公开 API 立即 404(PHOTO_NOT_
PUBLIC),但**已签发的旧 URL 在 TTL 内仍可取到对象**(设计允许的失效窗口,
上限 900s;P4a 契约决策)。超过 900s 仍可取 → 查 OSS 桶是否误配公共读。
3. 缩略图/浏览图更新不刷新:variants 键含内容版本——同一 photoId 重新处理
会生成新键;旧键对象留存(禁止覆盖语义),前端拿到新签名 URL 即新键。若
页面仍显示旧图 → 浏览器本地缓存,强刷;CDN 接入后另见 cdn-checklist 的
失效 API 样例与隐藏照片失效窗口验证步骤(待凭证执行)。
---
*本手册全部命令以 1Panel 目录约定 `/opt/photo-live/` 与网络 `1panel-network` 为例;
实际路径/网络名不同处按 README-1Panel.md §4 的说明替换。*