Files
crearte-monorepo/docs/specs/2026-09-30-backup-observability-design.md
T

73 lines
7.7 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.
# P3 备份 + 可观测设计 / Backup & Observability Design
- **状态**:设计已定(2026-09-30;三个选型向维护者提问未被应答,按推荐默认拍板并记录 §2,维护者可事后推翻)。
- **路线图**:P3(第一波末项),依赖 P1 拓扑已定型。
- **涉及仓库**:crearte-server(0.11.1 → **0.12.0**)、crearte-deploy(0.5.1 → **0.6.0**);**crearte 前端零改动**。
## 1. 背景与现状(2026-09-30 实地盘点)
- prod 栈(`db-prod` postgres:17-alpine / `minio-prod` pgsty/minio / `api-prod` / `web-prod`)数据落命名卷 `pgdata-prod`、`minio-data-prod`,**没有任何备份面**;`api-prod` 无宿主端口映射(仅 `web-prod` 的 8080 反代 `/api/` 与静态),MinIO console 宿主端口 `MINIO_PROD_PORT:-9001`。
- server 日志 = 26 处 stdlib `log.Printf` 散在 12 个文件(handler×7、service×2、api 中间件×2、cmd/serve.go×1 等);无请求日志中间件;无 `/metrics`。`gin.New()` 仅挂 `Recovery` + CORS。
- 限流器 `internal/api/ratelimit.go`:进程内 `map[ip]*windowCounter` 固定窗口(bundle-key 60/min、login 10/min、register 5/h、upload 10/min、submission 20/h、reaction 30/min),`maxTrackedIPs=10000` 惰性清理——纯单实例语义。
- 依赖基线:gin 1.11 / pgx 5.8 / aws-sdk-go-v2;**无 prometheus 依赖**。go.mod `go 1.24.1`。
## 2. 选型拍板(默认=推荐项)
| # | 岔路 | 决定 | 理由 / 放弃项 |
|---|---|---|---|
| D1 | `/metrics` 实现 | **手写极简 exposition**(零新依赖,Prometheus 文本格式 v0.0.4) | 单实例小站够用;代码可控可测;延续本仓最小依赖风格。放弃 `prometheus/client_golang`(依赖链重,Grafana 生态是未来事)与"不做"(ROADMAP 明列交付物)。 |
| D2 | 备份调度 | **deploy 仓 `backup.sh` + 主机 crontab**(runbook 给一行安装示例) | 脚本可手跑、可 CI `--dry-run` 验证、不绑 systemd。放弃栈内 cron sidecar(挂 docker.sock = 攻击面);systemd timer 留作 runbook 备选写法。 |
| D3 | 备份落点 | **主机目录 `BACKUP_DIR`(默认 `./backups/prod/<UTC 时间戳>/`)+ 保留最新 `BACKUP_KEEP_DAYS=7` 份** | 防误删/防配置炸;同盘不防磁盘坏——3-2-1 与 rclone 异地钩子写进 runbook 作可选步骤,不配不生效(本轮不引入外部 S3 凭据依赖)。 |
## 3. server 设计(0.12.0)
### 3.1 结构化日志(slog)
- `config.Load()` 新增 `LOG_FORMAT=text|json`(默认 `text`)、`LOG_LEVEL=debug|info|warn|error`(默认 `info`;非法值报错)。`serve.go` 启动时据此配置 `slog.Default`(输出 stderr;JSON 供 prod/容器采集)。
- 26 处 `log.Printf` 全量迁移到 `slog`,**机械映射规则**:原 `msg k1=%s k2=%v` → `slog.<lvl>("<msg 前缀短语>", "k1", v1, "k2", v2)`;携带 `error=%v` 的一律 `slog.Error(..., "error", err)`;启动/生命周期一行 `slog.Info`。文案短语与键名不变。
- 新增**请求日志中间件**(`internal/observability`):每请求一行 `http_request`,attrs = `method`、`route`(`c.FullPath()`,未匹配路由记 `nomatch`,基数有界)、`status`、`duration_ms`、`ip`;`/healthz` 整体跳过(不记日志**也不计指标**,防 scrape 噪声);`status >= 500` 用 Error 级。
- `gin.Recovery()` 保留(panic 兜底),不动。
### 3.2 `/metrics`(手写)
- `internal/observability`:并发安全 Registry(mutex + `map[method|route|status]→{count,sumSeconds}`)。中间件一次计时同时喂日志与指标。
- 指标族:
- `crearte_http_requests_total{method,route,status}`(counter)
- `crearte_http_request_duration_seconds_sum/_count{method,route,status}`(counter 语义,够算均值)
- `go_goroutines`、`go_memstats_alloc_bytes`(`runtime` 现值 gauge)
- `crearte_pgpool_*`(`pgxpool.Stat()`:`max_conns/total_conns/acquired_conns/idle_conns/acquire_count/empty_acquire_count` gauge;pool 为 nil 时省略——纯 mock 测试路由无池)
- `GET /metrics` 与 `/healthz` 同口注册(`RouteMetrics="/metrics"`),无 auth、无 CORS 例外。
- **暴露边界**:prod 形态 api-prod 宿主零端口 → `/metrics` 仅容器网内可达,抓取配方 `docker compose exec -T api-prod wget -qO- http://127.0.0.1:8080/metrics`。外网暴露 + allowlist/basic-auth 明确不做(未来接 Prometheus 抓取器再开 nginx 转发段)。
- 测试路径无池、无真栈:httptest 经真 router 打请求,断言文本行、计数增长、`nomatch` 桶存在。
### 3.3 限流单实例权衡(记录,不改造)
server `docs/README.md` 新增「已知权衡」节:多实例部署时每实例独立计数(有效限额 ≈ N×、重启清零、跨实例不共享);`maxTrackedIPs` 上限的内存数量级;扩多实例的前置条件是引入 Redis/网关层共享计数。**只记录现状语义,不改限流器。**
## 4. deploy 设计(0.6.0)
- **compose 结构零改动**:备份全走 `docker compose exec` / `run --no-deps` 一次性容器(`db-prod` 自带 pg_dump/pg_restore/psql,unix socket trust 免口令;`minio-prod` 镜像自带 `mc`,凭据经容器 env 注入而非命令行字面量)。
- `scripts/backup.sh`:
1. `--dry-run`:不碰 docker,仅校验 `.env` 存在、`POSTGRES_PASSWORD`/`MINIO_ROOT_*` 非空(`:?` 守卫)并打印计划——CI 步骤用它。
2. 预检:docker 可用、`compose --profile prod config -q`、`db-prod` 存活(`pg_isready`)。
3. `pg_dump --format=custom` 流式 → `$BACKUP_DIR/prod/<UTC 时间戳>/database.dump`。
4. `mc mirror --overwrite --remove` 桶(`MINIO_BUCKET:-crearte`)→ 同目录 `objects/`。
5. 完整性:`pg_restore --list` 读 dump 头。
6. manifest:全表**精确行数**(循环 `pg_tables` 逐表 `count(*)`)+ 产物体积。
7. `prod/latest` 软链指向本次目录;按目录名排序保留最新 `BACKUP_KEEP_DAYS` 份,只删 `^\d{8}T\d{6}Z$` 白名单形态目录。
- `scripts/restore-drill.sh`:latest dump 恢复到 **debug 栈 `db-test`**(drop/recreate 仅限该测试库),逐表行数与 manifest 对比,输出 PASS/FAIL。绝不连 prod 库。
- `docs/BACKUP-RUNBOOK.md`:定时约定(crontab 一行 + systemd timer 备选写法);恢复步骤(恢复到现有 prod 库 / 恢复到全新栈两条路径);对象侧 `mc mirror` 反向推送;异地 3-2-1 与 rclone 可选钩子;已知风险(同盘单拷贝、`mc mirror` 非快照一致性——写侧对象上传少可接受、备份窗口内行数漂移 manifest 与 dump 快照的微小差)。
- `.env.example` 增 `BACKUP_DIR`/`BACKUP_KEEP_DAYS` 注释示例;README prod 节链 runbook;`validate.yml` 增 `backup.sh --dry-run` 步骤(CI 里先 `cp .env.example .env` 再 append 假值过 `:?` 守卫)。
- CHANGELOG 0.6.0(英/中连续行)。
## 5. 验收标准(本机任务 4 实测)
1. server:容器四连(gofmt/vet/build/test)+ `TEST_DATABASE_URL` 集成层全绿、0 skip;新单测覆盖指标格式、中间件计数、`nomatch` 桶、日志 JSON 合法(jq)、`LOG_*` 解析。
2. 实栈冒烟(prod profile 演练栈):打一轮真实请求 → `docker compose exec` 抓 `/metrics` 计数吻合、JSON 日志行可读;`backup.sh` 全绿产出 dump + objects + manifest;`restore-drill.sh` 对 db-test **PASS**;连跑两次 + `BACKUP_KEEP_DAYS=1` 验证 latest 软链与修剪。
3. `backup.sh --dry-run` 无栈可跑(CI 等价);deploy 新 workflow 步骤本机过。
4. crearte e2e:stack full-loop 不回归(中间件动过路由)。
## 6. 非目标
Prometheus/Grafana 服务端搭建、`/metrics` 外网暴露与鉴权、异地同步落地实现、PITR/WAL 归档、卷快照级备份、限流器改造、dev/debug 栈备份(仅 prod profile)。