跳至内容

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 与批处理

先按状态变化分类

同一句“数据没了”,恢复语义可能完全不同:

类型典型例子首要边界常见恢复方向
单笔 DMLUPDATE 漏写 WHERE一笔 commit 前后逻辑修补或对象提取
多笔 DML客户端 autocommit 循环删除第一笔与最后一笔坏提交多候选、批量对账
事务型 DDLDROP TABLEDROP SCHEMADDL 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 ids

UPDATE 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 logsession、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+0810: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 语义。


返回本章目录 · 下一节:恢复目标与时间线 · 查看全书目录 · 查看索引中心

最后更新于