本节摘要:本节给「最坏的一天」准备剧本。备份部分的三要素:频率(全量与增量怎么配)、保留(留几份、留多久、存哪里)、恢复演练——核心立场是「没做过恢复演练的备份等于没有备份」,并给出演练的固定步骤与记录表。升级部分遵循第 2 章架构决定的固定次序:先跑 migration 进程完成数据库结构迁移,确认成功后再滚动重启其余服务;回滚方案要在升级前就位,而且和数据备份绑在一起——升级前的备份就是回滚的锚点。PG 18 与 Redis 8 的版本约束(官方技术栈口径)在升级检查单里占一行:组件版本与主程序版本的兼容矩阵要同时核对。
| 要素 | 决策内容 | 示例方案(示意) |
|---|---|---|
| 频率 | 全量与增量的组合 | 每日全量 + 每小时增量(交易时段) |
| 保留 | 留几份、留多久、放哪 | 7 天滚动 + 4 周末快照,异地/异机存放 |
| 恢复演练 | 定期真恢复到隔离环境 | 每月一次,计时并记录差异 |
要备份的东西按第 2 章架构盘点:PostgreSQL 里的策略配置、订单与成交记录、账户状态;策略代码与参数文件(含第 5 章 params 的版本历史);配置文件与环境清单。Redis 里的队列与缓存可以重建,明确「不备份、靠重建」反而减少恢复时的混乱。
# bash —— PostgreSQL 备份与校验(写法示意,凭据与路径按部署实际调整) # 每日全量(cron 调度),输出带日期与校验和 pg_dump -Fc -d quantdinger -f /backup/qd_$(date +%F).dump sha256sum /backup/qd_$(date +%F).dump > /backup/qd_$(date +%F).sha256 # 备份三问自检:能列出内容吗?校验和过吗?异地有副本吗? pg_restore --list /backup/qd_$(date +%F).dump > /dev/null && echo "结构可读" sha256sum -c /backup/qd_$(date +%F).sha256
备份的完整性还可以再配一段自检脚本,把「三问」变成每日机器检查(纯标准库,路径与命名按部署实际调整):
# backup_verify.py —— 备份完整性自检(纯标准库,路径与命名按部署实际调整) import hashlib import os from datetime import date BACKUP_DIR = "/backup" # 示意路径 MAX_AGE_DAYS = 2 # 最新备份距今超过该天数即黄灯(示意值) def sha256(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def verify(): dumps = sorted(f for f in os.listdir(BACKUP_DIR) if f.endswith(".dump")) if not dumps: return "红:备份目录为空" newest = dumps[-1] stem = newest[:-len(".dump")] sum_path = os.path.join(BACKUP_DIR, stem + ".sha256") if not os.path.exists(sum_path): return f"红:{newest} 缺校验和文件" with open(sum_path, encoding="utf-8") as f: recorded = f.read().split()[0] if recorded != sha256(os.path.join(BACKUP_DIR, newest)): return f"红:{newest} 校验和不匹配" age = (date.today() - date.fromisoformat(stem[-10:])).days if age > MAX_AGE_DAYS: return f"黄:最新备份已 {age} 天" return f"绿:{newest} 完整且新鲜"
这段脚本与 bash 三问的分工:bash 那段在备份完成的当下跑,这段在每天固定时间独立跑——「备份那一刻是好的」与「现在仍然有一份好的」是两个命题,后者才会暴露「备份任务悄悄停了三天」这类慢性故障。它的输出可以直接当作 11.1 红绿灯之外的第 4 格。
恢复演练的固定步骤:在隔离环境起一套空实例;用最近一次备份恢复;跑三条验证——最新成交记录数与生产对得上、第 5.3 节的对账逻辑能通过、策略 on_start 预热后指标出值正常;全程计时并记录卡点。下表是演练记录的最小格式,每次演练填一行:
| 日期 | 备份点 | 恢复耗时 | 验证三项 | 发现的问题 |
|---|---|---|---|---|
| (示例)第 1 次 | 当日全量 | 18 分钟 | 通过/通过/通过 | 环境变量未入备份清单,补 |
演练发现的典型问题与修复:恢复慢(提示备份粒度太粗)、缺文件(把配置纳入备份范围)、没人会做(把本节步骤写进值班手册)。演练的价值恰恰在于把这些问题暴露在事故之前。
第 2 章的进程分工决定了升级次序:数据库结构变更由 migration 进程独立完成,其余服务重启后才按新结构工作。固定次序四步:
升级四步(次序固定,不可颠倒) 1. 预检:读版本发布说明;核对组件兼容矩阵(PG 18 / Redis 8,官方技术栈口径) 与当前部署版本;确认回滚锚点(本次升级前的备份)已就位 2. 冻结与备份:暂停开新仓(联动第 10 章应急开关)、等在途订单结算、做升级前备份 3. 迁移:先只启动 migration 进程 → 观察迁移日志与退出码 → 成功后才继续 4. 滚动重启:backend → 数据/scheduler/celery → trading-worker 最后恢复交易 → 跑 11.1 的三类关键指标红绿灯 → 解除冻结
第 3 步是唯一的硬闸门:migration 失败就停在原地,不要带着「也许没影响」的侥幸去重启服务——新旧代码对表结构的假设不一致,是最常见的升级事故来源。
预检环节展开成可打勾的子清单(升级会前过一遍):
| 序 | 预检项 | 通过标准 |
|---|---|---|
| 1 | 发布说明通读 | 破坏性变更条目全部定位到自己的部署 |
| 2 | 组件兼容矩阵 | PG 18 / Redis 8 与目标版本逐一核对(官方技术栈口径) |
| 3 | 回滚锚点 | 升级前备份完成且通过 backup_verify 自检 |
| 4 | 冻结通知 | 停新单联动确认,相关方知晓升级窗口 |
| 5 | migration 预估 | 迁移脚本已审读,耗时与锁表风险心里有数 |
第 5 项的「审读」不是走过场:迁移脚本里新增索引、改列类型这类操作的耗时与锁行为,决定了冻结窗口要留多长——低估了它,冻结期内交易恢复时间就会跟着漂移,第 10 章的开关解除时点也就不可信了。
回滚方案的三件事:锚点(升级前备份 + 升级前版本号记入值班手册)、触发条件(升级后三类关键指标任一 critical,或对账不一致,10 分钟内不解决即回滚)、动作(停服、恢复备份、回退代码版本、按 11.2 一次完整重启)。回滚后要写事件记录:升级为什么失败、migration 是否需要手工清理——这是下次升级预检的输入。回滚剧本每半年至少演练一次,与恢复演练同场做。
| 问题 | 排查方向 | 要点 |
|---|---|---|
| 恢复演练卡在权限 | 隔离环境的角色与凭据未备 | 把凭据清单纳入演练物料 |
| migration 跑到一半失败 | 迁移日志与退出码 | 停在原地评估,不带侥幸重启服务 |
| 备份校验耗时过长 | 校验时段安排 | 放低峰跑,别与备份任务抢 IO |
| 回滚后发现丢了一段成交 | 锚点时间点之后仍有交易 | 冻结纪律就是为此:升级窗口内不留交易 |
| Redis 数据要不要备份 | 可重建性判断 | 明确「不备份、靠重建」,写进手册 |
第四行是最疼的一类问题,根因几乎总是冻结不彻底:升级开始前停了新单,但已在途的订单回执落在备份点之后,回滚把它们带走了。这也解释了升级四步里「等在途订单结算」为什么是显式的一步而不是附带动作——回滚锚点的时间点,就是它必须罩住的时间点。
单机自营到此已经完整。如果你的目标不止自用——把平台开放给多个用户或小团队——下一节看多租户 SaaS 能力要补哪些课。