32.1 先界定误操作
误操作事故最容易犯的第一个错误,是把“有人说删错了”直接翻译成一个 PITR 时间。事故 描述是业务语言,恢复目标是 WAL 历史上的精确边界,中间至少还缺对象、事务、提交、 后续写入和外部副作用五层证据。
先写事故合同,再碰恢复命令:
affected truth 哪些业务事实错了
bad transaction 哪一笔或哪一组提交制造错误
first/last bad 错误开始和结束边界
continuing mutation 错误作业是否仍在运行
good after 错误之后哪些合法事实必须保留
external effects 数据库之外已经发生了什么
authority 谁能判定最终业务真相若其中关键项仍是 unknown,正确动作通常是围住继续伤害、保存证据和并行恢复候选,而不是 在原集群上赌一个时间点。
32.1.1 错误 UPDATE、DELETE、DROP 与批处理
先按状态变化分类
同一句“数据没了”,恢复语义可能完全不同:
| 类型 | 典型例子 | 首要边界 | 常见恢复方向 |
|---|---|---|---|
| 单笔 DML | UPDATE 漏写 WHERE | 一笔 commit 前后 | 逻辑修补或对象提取 |
| 多笔 DML | 客户端 autocommit 循环删除 | 第一笔与最后一笔坏提交 | 多候选、批量对账 |
| 事务型 DDL | DROP TABLE、DROP SCHEMA | DDL commit 与依赖范围 | 从隔离 PITR 提取对象 |
| 粗粒度操作 | TRUNCATE、分区 detach/drop | 对象与关联表 | 对象提取或整库恢复 |
| 后台批处理 | 调度器持续重算错误价格 | 作业开始、每次 commit、停止时刻 | 先停作业,再找完整坏窗 |
| 权限/配置 | 错误 GRANT、参数或路由变更 | 数据是否真的变化 | 配置回退,不一定 PITR |
| 外部副作用 | outbox 已发、支付已扣 | 数据库 commit 与外部确认 | 数据恢复 + 业务补偿 |
PostgreSQL 的许多 DDL 具有事务性,未提交时可以 ROLLBACK;一旦已经提交,就不能回到
原会话补发 ROLLBACK。PITR 的作用不是“撤销 SQL”,而是从较早基础备份重放 WAL,
在指定提交边界之前或之后构造另一份完整历史。
这一区别会直接改变调查方式:
single transaction
-> 找到 commit identity
-> 验证 before / after 两个候选
autocommit batch
-> 找到 first bad commit
-> 找到 last bad commit
-> 判断中间是否夹杂合法事务
external side effect
-> 找数据库提交
-> 找 message/payment idempotency identity
-> 恢复数据后仍要外部对账“执行成功”不说明影响正确
应用需要把下面这些信息当作审计事件,而不是只记录 SQL 文本:
change_id / job_id / request_id
application actor and database role
transaction id or commit-correlated identity
object and business-key range
expected rows / actual rows
old/new aggregate or manifest
started_at / committed_at in UTC
idempotency key and external event idsUPDATE 1000000 的 row count 能提示范围异常,却不能说明哪一百万行本来应该改变;
statement log 能说明服务端收到什么文本,却可能没有 bind 参数;WAL 是物理恢复事实,
不是自动生成的业务审计表。恢复能力必须在事故前就布置可关联的应用、数据库和外部系统
身份。
批处理不是“一笔大事务”的同义词
考虑一个每 1,000 行提交一次的脚本:
10:00:01 batch 1 commit bad
10:00:03 user order good
10:00:04 batch 2 commit bad
10:00:06 refund good
10:00:07 batch 3 commit bad恢复到 batch 1 之前会同时丢掉两笔合法写;恢复到 batch 3 之后又保留全部错误。此时没有 一个单独 PITR 点能直接得到最终正确业务状态。需要先取得历史正确对象,再按业务身份重放 或合并合法变化。
因此发现坏批次后,第一步是阻断相同 mutation source:
- 禁用精确的 scheduler job,而不是停掉所有 scheduler;
- 吊销或冻结产生错误的 service role,而不是随意改全局 HBA;
- 对受影响业务 key 加写围栏,而不是默认让全站停写;
- 记录围栏生效前最后一笔提交,不能把“点击停止”当成“已经停止”。
停止动作、实际停止证据和最后坏提交是三件事。
复制不会替你保留“正确过去”
流复制和同步复制的目标是复制已经提交的 WAL。误更新只要正常提交,副本也会正确地把 它重放。以下判断都不成立:
replica lag = 0 -> 副本数据一定正确
three replicas -> 一定存在错误之前的副本
fail over -> 误更新会消失
backup job succeeded -> 一定能定位到正确边界若团队希望保留延迟副本,必须把延迟窗口、监控、promotion 防误触和 WAL 保留纳入独立 设计;它也不能替代基础备份、连续归档和恢复演练。
32.1.2 影响对象、开始时间、结束时间和持续写入
建立事故包络
把事故范围写成一个六维包络:
$$ I=(O,K,[T_f,T_l],X,A,E) $$
- $O$:database、schema、table、partition、sequence、large object 等对象集合;
- $K$:受影响业务 key 或谓词;
- $[T_f,T_l]$:第一笔到最后一笔坏提交的区间;
- $X$:已确认或候选 transaction identity;
- $A$:坏提交之后必须保留的合法写集合;
- $E$:消息、支付、邮件、搜索索引、缓存等外部副作用。
不要把“发现时间”写进 $T_l$。四个时刻应分开:
t_start 错误动作开始
t_commit 某笔错误事务真正提交
t_detect 人或监控发现异常
t_fence 错误来源已被证明停止若一个作业持续运行,则 t_fence 之前仍可能出现新坏提交,事故窗口是移动的。先建立写
围栏并确认它生效,才有稳定的恢复问题。
从多层证据收敛,而不是信一个时间戳
| 证据层 | 能回答 | 不能单独回答 |
|---|---|---|
| 工单/聊天 | 人声称何时做了什么 | 实际 commit 边界 |
| 应用审计 | request、actor、业务 key、idempotency | 数据库是否提交 |
| PostgreSQL log | session、statement、error、duration | 未记录的参数与业务正确性 |
| 审计表 | run、stage、XID、LSN、对象摘要 | 外部系统是否消费 |
| 当前表状态 | 现在受影响多少 | 历史旧值 |
| backup/WAL catalog | 可恢复血缘与覆盖范围 | 哪个业务状态正确 |
| 外部账本 | 支付、消息、邮件结果 | 数据库内部完整状态 |
pg_stat_activity 适合回答“现在什么还在运行”,不是历史审计;累计统计也不能还原某笔
事务。对关键批处理,最好在同一事务里写入最小 incident_audit:
INSERT INTO ops.change_audit (
change_id, stage, xid, observed_lsn, details
)
VALUES (
:'change_id',
'before-dangerous-change',
pg_current_xact_id(),
pg_current_wal_lsn(),
jsonb_build_object(
'predicate', :'reviewed_predicate',
'expected_rows', :expected_rows
)
);这不是让 DBA 在事故后凭空制造 XID,而是展示可恢复性如何进入变更协议。若事前没有 审计,必须用日志、应用请求、业务 key、WAL 工具和候选恢复交叉定位,并扩大不确定区间。
把事后合法写入作为一等对象
很多恢复票只写“删除 1,000 行”,没有写错误之后发生的 100 笔合法订单。应单独建立
good-after manifest:
identity order_id / ledger_id / event_id
commit evidence audit row / immutable ledger / application event
ordering depends-on or sequence
payload digest canonical fields, not secret-bearing raw request
external state not-sent / sent / acknowledged / compensated
replay method idempotent insert / conditional update / manual review
owner business authority这个集合为空也必须有证据:例如错误后立即完成全局写围栏,且所有 writer、job 与外部 ingress 都被证明停止。“没看到新行”不等于没有合法写。
用上下界表达不确定性
若只能确认错误发生在 10:00:00+08 到 10:00:05+08 之间,不要假装存在一个精确
10:00:03。更安全的候选搜索是:
candidate A upper bound before suspected error
candidate B first candidate where damage appears
candidate C immediately before independently identified commit每个候选都运行同一组业务探针。目标选择是一个可证伪的搜索过程,而不是一次性猜值。 恢复副本越容易创建,越应该多恢复一个候选,少在原集群上做一次不可逆猜测。
事故包络的完成条件
进入 restore 前至少要能填写:
incident:
affected_objects: [...]
affected_business_keys: [...]
first_bad_commit: known | bounded | unknown
last_bad_commit: known | bounded | unknown
mutation_source_fenced: true
good_after_manifest: evidence-reference
external_effects: none | bounded | unknown
business_truth_owner: named-person-or-role
unresolved_unknowns: [...]若 mutation_source_fenced=false,先回第 31 章处理现场;若
good_after_manifest=unknown,可以并行恢复候选,但不能批准覆盖或切流。
32.1.3 逻辑补偿、对象恢复与整库 PITR 的选择
三种路线解决不同问题
PostgreSQL 连续归档的物理恢复只能恢复整个 database cluster,不是按 database 或 table 选择性恢复 (官方说明)。 “只恢复一张表”通常意味着先把整套物理副本恢复到隔离环境,再从中逻辑提取对象。
| 路线 | 适合条件 | 主要风险 | 验收重点 |
|---|---|---|---|
| 原地逻辑补偿 | 影响 key 精确、逆变换明确、当前值可条件匹配 | 覆盖并发合法写 | affected-row preimage 与业务不变量 |
| 隔离 PITR 后对象提取 | 只需部分对象的历史值,原服务需继续 | FK、sequence、触发器与跨表遗漏 | 完整依赖与合并 manifest |
| 隔离 PITR 后整库切换 | 影响广、catalog 级破坏、无法可靠逐项修补 | 丢失目标后合法写与外部状态 | 写围栏、delta replay、路由与回退 |
选择依据不是“哪条命令最熟”,而是:
$$ \text{total risk} = \text{unknown blast radius}
- \text{good-after loss}
- \text{merge error}
- \text{external inconsistency}
- \text{cutover risk} $$
什么时候优先逻辑补偿
例如误把一组账户余额减去固定值,且满足:
- 受影响 account ID 由不可变 change ID 精确列出;
- 每行当前版本仍等于错误后的预期值;
- 目标之后的合法变化有独立 ledger;
- 没有不可逆外部副作用,或已经有补偿协议;
- 修补能放在一笔受控事务中,并在 row count 不符时回滚。
应使用条件更新,不要盲目写回:
UPDATE app.account AS a
SET balance_cents = r.recovered_balance_cents
+ r.audited_post_delta_cents,
status = 'active'
FROM recovery_candidate AS r
WHERE a.account_id = r.account_id
AND a.balance_cents = r.expected_current_balance_cents
AND a.status = 'mispriced';随后检查实际 row count 必须等于 manifest;少一行说明当前状态已被其他事务改变,整笔 修补应回滚并重新调查。
什么时候从恢复副本提取对象
误 DROP TABLE、误删一个租户或一个时间分区时,常见流程是:
physical PITR full cluster into isolation
-> validate historical object and dependencies
-> export selected schema/data
-> import into quarantine schema
-> compare current and recovered identities
-> merge under write fence导出前要连同以下对象评审:
- sequence 当前值与 identity;
- foreign key 的父子行;
- partition、index、constraint、trigger、policy;
- large object 与外部文件;
- extension 类型、collation 与函数依赖;
- logical replication identity 和 downstream 消费位置。
单独 pg_dump -t target 能生成表数据,不代表它已经捕获业务闭包。先定义 closure,再
导出。
什么时候考虑整库切换
下面情况更可能需要整库路线:
- 大范围 schema/catalog 破坏,受影响对象无法可靠枚举;
- 大量相互依赖的数据都被错误转换;
- 目标后写入已被严格围住,或能从独立日志完整重放;
- 业务明确接受目标后的数据损失;
- 原集群已不可信,但恢复候选通过完整引擎与业务验证。
即便如此,也先做 side restore。直接覆盖 managed PGDATA 会同时销毁现场、当前合法 写和比较基准;空间不足不是自动授权覆盖,而是需要事故指挥人明确选择保存哪些证据并 承担什么损失。
五个常见伪方案
在 replica 上查旧值
错误 WAL 通常已经重放;lag 不是受控历史边界。
对已提交事务执行 ROLLBACK
ROLLBACK 只能影响当前未提交事务。
从最近 full backup 直接启动
缺少目标前 WAL 时,它只代表 backup 一致点,不代表事故边界。
恢复到“报警前一分钟”
报警时间不是 commit 时间,时钟与采集链还有误差。
在原 PGDATA 上反复试 target
每次都覆盖当前现场,丧失比较和回退能力。交给下一节的恢复假设
完成本节后,应该得到而不是猜到:
route compensate | extract | full-cutover
bad commit exact identity or bounded interval
inclusive expectation damage must be present or absent
exclusive expectation safe facts present, damage absent
good-after manifest plus replay/merge owner
external effects fenced and reconciled plan
production cutover still not authorized下一节把这些业务边界映射到 PostgreSQL 的 time、XID、LSN、name、inclusive 与 timeline 语义。
返回本章目录 · 下一节:恢复目标与时间线 · 查看全书目录 · 查看索引中心