73 lines
7.7 KiB
Markdown
73 lines
7.7 KiB
Markdown
# 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)。
|