数据库基础体系 · 第 139/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
数据库故障演练:延迟、断网、磁盘满、主库切换和恢复验证
数据库故障演练不是“把数据库弄坏,再看服务是否还能访问”。一次合格的演练需要回答五个问题:
- 故障发生在哪一层,影响了哪些数据流?
- 数据库和业务分别进入了什么状态?
- 监控是否能区分正常变慢、连接失败、复制中断和数据不可用?
- 故障消除后,系统是否自动恢复,还是留下了复制、连接池或数据一致性问题?
- 恢复结果是否满足既定的 RPO、RTO 和数据正确性要求?
本文以 PostgreSQL 和 MySQL 8.4 的公开稳定语义为边界。命令会明确引擎、事务和部署假设。自动故障转移、半同步复制、备份软件、云厂商托管服务和代理层的具体行为并不由数据库 SQL 标准统一规定,必须结合实际部署验证。
一、先定义演练对象和成功条件
1. 故障演练中的几个基本概念
故障注入是有意改变系统条件,例如增加网络延迟、丢弃数据包、阻断主库连接或填满测试磁盘。
故障表现是组件对故障的可见反应,例如:
- SQL 请求延迟升高;
- 新连接超时;
- 事务回滚;
- 复制延迟增加;
- 备用库进入无法追赶的状态;
- 主库无法写入;
- 自动切换失败;
- 恢复后出现缺少事务或重复执行。
恢复至少有三种不同含义:
- 故障条件消失:例如删除
tc netem规则或恢复网络路由; - 组件恢复服务:数据库进程能够接受连接;
- 数据恢复正确:数据、复制位点、约束、业务不变量都符合预期。
第三种才是恢复验证的最终目标。进程能启动,不等于数据库已经恢复。
2. 示例部署边界
后文使用两类示例:
- PostgreSQL:一个主库和一个物理流复制备用库,客户端通过应用或连接池访问;
- MySQL 8.4:一个 source 和一个 replica,使用二进制日志复制。MySQL 8.4 文档仍使用
SOURCE、REPLICA术语;较旧版本常见的MASTER、SLAVE命令属于旧命名。
除非特别说明:
- 演练在隔离环境进行;
- 业务使用显式事务;
- 连接池、负载均衡器和故障转移控制器都属于演练范围;
- 数据库节点之间的网络故障按双向故障考虑;
- 不把“客户端重试成功”直接当成“原事务成功”。
3. 需要记录的基线
演练开始前,先记录正常状态,而不是故障后才寻找比较对象。
至少记录:
- 数据库版本、操作系统和部署拓扑;
- 主库、备用库、代理、应用实例的地址;
- 当前主库身份;
- 连接数、活跃事务、锁等待、提交延迟;
- 复制状态和复制延迟;
- 磁盘容量、inode、WAL 或 binlog 占用;
- 最近一次备份的时间、类型和校验结果;
- 当前业务写入速率;
- 当前 SLO、RPO、RTO。
对于 PostgreSQL,可查看基础状态:
SELECT
version(),
current_database(),
pg_is_in_recovery(),
now();
主库上的 pg_is_in_recovery() 应返回 false,备用库上应返回 true。
查看 PostgreSQL 主库复制状态:
SELECT
application_name,
client_addr,
state,
sync_state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
这里要区分:
sent_lsn:主库已经发送到备用库的 WAL 位置;write_lsn:备用库已经写入接收端的 WAL 位置;flush_lsn:备用库已经持久化的 WAL 位置;replay_lsn:备用库已经重放并对查询可见的 WAL 位置。
发送、持久化和可见不是同一个时间点。
MySQL 8.4 的复制状态可以在 replica 上查看:
SHOW REPLICA STATUS\G
重点关注:
Replica_IO_Running:复制 I/O 线程是否运行;Replica_SQL_Running:复制 SQL 线程是否运行;Seconds_Behind_Source:一个近似延迟指标,不是严格的提交时间差;Relay_Source_Log_File和Exec_Source_Log_Pos:中继日志接收与执行位置;- GTID 相关字段:如果部署启用了 GTID,应同时记录已接收和已执行的 GTID 集合。
Seconds_Behind_Source = 0 不能证明复制无延迟。例如复制线程可能刚好追上,也可能源库没有新的事务;网络、磁盘和提交确认延迟还需要单独观测。
二、用 RPO、RTO 和数据不变量定义“成功”
1. RPO 和 RTO
**RPO(Recovery Point Objective,恢复点目标)**表示故障后最多允许丢失多长时间范围内的数据。
如果某次恢复得到的数据状态对应于时刻 ,故障发生时刻为 ,则实际数据丢失窗口可近似写为:
若要求:
则恢复点满足 RPO。
**RTO(Recovery Time Objective,恢复时间目标)**表示从故障被确认到服务恢复到约定可用状态的最长时间。实际恢复时间通常应拆成:
其中:
- :发现故障;
- :确认故障并决定切换或恢复;
- :提升备用库或启动恢复;
- :让客户端访问新主库;
- :完成最低限度的数据和服务验证。
如果只测“数据库进程启动耗时”,而不测 DNS、连接池、应用重试和数据验证,就没有测到完整 RTO。
2. 事务边界比 SQL 行数更重要
一次业务操作可能包含多个 SQL:
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;
故障发生在两个 UPDATE 之间时:
- 数据库事务应整体回滚;
- 客户端可能只收到连接断开;
- 客户端无法仅凭“请求失败”判断数据库是否已经提交;
- 盲目重试可能造成重复扣款或重复创建订单。
因此,演练必须给每个测试事务设置唯一业务标识,例如 order_id 或 request_id,并在恢复后核对:
- 事务是否提交;
- 是否重复提交;
- 是否部分提交;
- 是否违反余额、库存或状态机约束。
数据库连接断开不是事务语义的替代品。客户端得到异常时,提交结果可能是“未提交”,也可能是“数据库已经提交但响应在网络中丢失”。
三、延迟故障:慢不等于挂,复制确认会改变故障形态
1. 延迟故障影响哪些路径
网络延迟可以发生在不同链路:
- 客户端到数据库;
- 应用到连接池;
- 主库到备用库;
- 数据库到存储系统;
- 数据库到备份仓库。
这些延迟的结果不同。
客户端到数据库增加 200 ms,主要表现为请求耗时上升;主库到备用库增加 200 ms,则可能导致复制落后、同步提交变慢或主备切换后丢失更多最近事务。
如果事务包含 次必须等待响应的网络往返,每次往返延迟增加 ,则仅网络部分的额外耗时近似为:
这是近似关系,不包含服务器排队、锁等待、批量发送和并行执行。
2. 使用 Linux tc netem 注入延迟
以下命令在 Linux 测试节点的 eth0 上增加 200 ms 延迟和 20 ms 抖动:
sudo tc qdisc add dev eth0 root netem delay 200ms 20ms
查看规则:
tc qdisc show dev eth0
删除规则:
sudo tc qdisc del dev eth0 root
命令的语义是:
delay 200ms:为匹配到的数据包增加约 200 ms 延迟;20ms:增加随机抖动;- 未指定过滤器时,可能影响该网卡上的所有流量,包括 SSH、监控、复制和业务流量。
因此,生产演练不能直接对承载管理流量的物理网卡执行这条命令。更安全的做法是在隔离的网络命名空间、测试容器、专用网卡或明确的端口过滤器中注入。tc 的实际作用点是操作系统网络栈,不是数据库内部;它无法模拟数据库进程停止、存储延迟或 SQL 锁等待。
3. PostgreSQL 同步复制下的延迟
PostgreSQL 流复制可以是异步或同步。同步复制的关键语义是:提交是否需要等待某个同步备用库达到规定的确认状态,取决于 synchronous_commit 和同步备用库配置。
因此,增加主库到同步备用库的网络延迟,可能直接增加提交延迟:
SHOW synchronous_commit;
SHOW synchronous_standby_names;
如果 synchronous_commit 为 on,提交通常要等待规定级别的远端确认;如果为 remote_apply,要求更强,远端不仅要持久化,还要完成重放。具体确认点由配置和版本语义决定,不能简单等同于“所有查询都已经可见”。
测试事务:
CREATE TABLE IF NOT EXISTS chaos_probe (
id bigint PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
payload text NOT NULL
);
BEGIN;
INSERT INTO chaos_probe(id, payload)
VALUES (1001, 'latency-test');
COMMIT;
演练时同时观察:
SELECT
pid,
wait_event_type,
wait_event,
state,
query
FROM pg_stat_activity
WHERE state <> 'idle';
可能看到的结果包括:
- 客户端提交耗时上升,但事务最终成功;
- 连接池活跃连接数增加;
- 应用超时,数据库中事务随后成功提交;
- 备用库
replay_lag增大; - 同步复制场景下,提交等待远端确认;
- 异步复制场景下,主库提交较快,但切换时可能缺少尚未到达备用库的事务。
这说明“业务超时”不一定表示“事务未提交”。
4. MySQL 复制和半同步的边界
MySQL 异步复制中,source 提交成功与 replica 已经接收、持久化、执行之间没有同步提交保证。增加复制链路延迟通常会使 replica 的接收或执行落后。
如果启用了半同步复制,提交确认还受半同步插件、超时和降级配置影响。半同步不是“绝不丢数据”的保证:
- 达到配置的确认条件后,source 才向客户端确认;
- 超时或插件状态变化时,系统可能退回异步行为;
- 确认“收到”与“已经执行并对外可读”仍可能不同。
MySQL 8.4 replica 上可执行:
SHOW REPLICA STATUS\G
主库上可结合:
SHOW BINARY LOG STATUS\G
查看当前二进制日志位置。具体字段和复制过滤配置必须结合实际拓扑判断,不能只看一个延迟字段。
5. 延迟演练的验证顺序
应使用一个带唯一标识的事务,并记录四个时间:
- 客户端发起时间;
- 数据库收到或开始执行的时间;
- 客户端收到响应的时间;
- 备用库能够读到该标识的时间。
例如在 PostgreSQL 中:
SELECT id, created_at, payload
FROM chaos_probe
WHERE id = 1001;
如果主库已提交但备用库查询不到,不能立即判定数据丢失,可能只是备用库尚未重放。应继续记录 WAL 位点和重放时间,区分:
- 延迟;
- 复制中断;
- 事务回滚;
- 已提交但查询路由到了旧备用库。
四、断网故障:必须区分客户端断网、复制断网和脑裂风险
1. 断网的三种不同实验
“断网”不是单一故障。
客户端到主库断网
数据库可能仍在正常提交事务,只是客户端收不到响应。此时最危险的是客户端重试。
主库到备用库断网
主库可能继续写入,备用库停止接收 WAL 或 binlog。异步复制下,主库通常仍可用;同步复制下,主库提交可能阻塞或超时,取决于同步配置。
主库与故障转移控制面断网
数据库节点可能都认为对方不可达。若没有仲裁、租约或 fencing(隔离旧主),直接提升备用库会产生双主写入,也就是脑裂。
2. PostgreSQL 复制断网后的状态
备用库通过流复制接收 WAL。网络中断后,主库上的复制连接可能消失,备用库停止接收新 WAL。主库可查看:
SELECT
application_name,
client_addr,
state,
sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind
FROM pg_stat_replication;
pg_wal_lsn_diff 返回两个 WAL 位点之间的字节差,是积压量的近似表示,不是时间延迟。
如果主库继续产生 WAL,必须关注 WAL 保留能力。备用库断开时间过长时:
- 保留的 WAL 可能足够,备用库重新连接后可以继续追赶;
- 如果所需 WAL 已被回收,备用库可能无法直接追赶,通常需要重新建立基线;
wal_keep_size、复制槽和归档配置会影响结果;- 复制槽可以防止仍需要的 WAL 被过早回收,但也可能使主库磁盘持续增长。
查看复制槽:
SELECT
slot_name,
slot_type,
active,
restart_lsn,
confirmed_flush_lsn,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
) AS retained_wal
FROM pg_replication_slots;
复制槽不是无限存储。网络故障与磁盘满可能形成连锁故障:备用库断开导致 WAL 保留,WAL 保留又填满主库磁盘,最终主库无法写入。
3. MySQL 复制断网后的状态
MySQL replica 断网后,I/O 线程可能无法从 source 获取新的 binlog;SQL 线程可能继续执行已经写入 relay log 的内容。
因此,I/O 线程停止不一定意味着 SQL 线程停止。要分别查看:
SHOW REPLICA STATUS\G
典型状态组合包括:
Replica_IO_Running: No,Replica_SQL_Running: Yes:暂时无法拉取,但已有中继日志仍在执行;- 两者都是
No:复制完全停止或发生错误; - I/O 恢复后,SQL 线程仍需一段时间追赶积压。
如果使用 GTID,应记录 source 和 replica 的 GTID 集合,恢复后验证 replica 是否已经执行了故障期间应保留的事务。只使用文件名和位置时,切换和重新指向 source 更容易因位置判断错误而产生遗漏或重复。
4. 安全的断网验证顺序
断网实验应按以下逻辑执行:
- 先确认当前唯一写主库;
- 记录主库身份、复制位点和测试事务标识;
- 只阻断一条明确链路;
- 观察数据库、复制、应用和告警;
- 恢复网络;
- 确认复制重新建立;
- 确认备用库追平;
- 再进行主库切换。
如果计划在断网期间提升备用库,必须先确保旧主库已被隔离。仅仅“旧主库目前连不上”不等于它已经停止写入。旧主库可能继续接受来自其他客户端的写请求,恢复网络后就可能出现双向写入。
五、磁盘满:容量、inode、WAL、临时空间不是同一个问题
1. “磁盘满”的四种含义
文件系统块耗尽
df -h
显示可用空间为 0,普通数据文件、日志或 WAL 无法继续扩展。
inode 耗尽
df -i
即使还有容量,只要 inode 用完,也无法创建新文件。大量小文件、临时文件或日志切片可能导致这种情况。
某个挂载点耗尽
数据库数据目录、WAL 目录、binlog 目录、临时目录和备份目录可能位于不同文件系统。根分区有空间,不代表数据库所在挂载点有空间。
配额或容器限制耗尽
容器、云盘、项目配额可能在操作系统看似还有空间时拒绝写入。数据库只能看到写入失败。
2. PostgreSQL 磁盘满的故障路径
PostgreSQL 写入通常需要:
- 生成 WAL;
- 将数据页和相关元数据写入;
- 在提交时满足相应持久化语义;
- 临时排序、哈希或创建索引时可能使用临时文件。
磁盘不足可能出现在:
pg_wal所在文件系统;- 数据目录;
- 表空间;
temp_tablespaces指定的临时空间;- 日志目录。
先查看文件系统:
df -hT
df -i
du -xhd1 /var/lib/postgresql
du 只能帮助定位已存在文件,不能完整解释已删除但仍被进程打开的文件。此时需要结合:
sudo lsof +L1
不要直接删除 PostgreSQL 数据目录中的文件,也不要手工删除 WAL 文件。WAL 的保留和回收由数据库、归档、复制槽等机制共同决定。
查看复制槽保留量:
SELECT
slot_name,
active,
restart_lsn,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
) AS retained_wal
FROM pg_replication_slots;
常见错误表现包括:
could not extend file;No space left on device;- 事务写入失败;
- 建索引或排序时失败;
- 归档失败导致 WAL 持续积压;
- 备用库因无法写入而停止重放。
3. MySQL 磁盘满的故障路径
MySQL 可能需要写入:
- InnoDB 数据文件;
- redo log;
- undo 空间;
- binary log;
- relay log;
tmpdir临时文件;- 错误日志、慢查询日志;
- 表空间文件。
可执行:
df -hT
df -i
du -xhd1 /var/lib/mysql
MySQL 侧查看日志和临时目录等配置:
SHOW VARIABLES WHERE Variable_name IN
(
'datadir',
'log_bin',
'log_bin_basename',
'relay_log',
'tmpdir',
'innodb_data_file_path'
);
磁盘满时,可能出现:
- InnoDB 事务提交失败;
- 复制线程因无法写 relay log 或数据页而停止;
- binlog 无法追加;
- 临时查询失败;
- 数据库可以连接但不能执行写入;
- 连接建立成功,实际 SQL 失败。
“能登录”只能说明部分控制路径还可用,不能说明业务可写。
4. 安全的磁盘满演练
不要在数据库数据目录中创建填充文件。应在独立的测试挂载点进行,例如:
mkdir -p /srv/chaos-fill
fallocate -l 8G /srv/chaos-fill/fill.bin
演练前必须知道该文件系统的剩余空间,并设置自动清理和硬性停止条件。恢复:
rm -f /srv/chaos-fill/fill.bin
sync
df -hT /srv/chaos-fill
如果要测试数据库所在文件系统,至少应预留一个不被填充的紧急空间,并提前确认:
- 谁有权限删除测试文件;
- 如何保留 SSH、监控和数据库日志通道;
- 复制槽或 binlog 保留不会无限增长;
- 备份进程不会因空间不足留下大量临时文件。
5. 空间恢复后的验证
删除文件后,不代表数据库立即恢复:
- 文件系统需要重新显示可用空间;
- 数据库可能仍有失败事务;
- 复制线程可能已经停止,需要人工恢复;
- 归档或备份任务可能进入失败状态;
- 连接池可能保留已失效连接;
- 之前超时的业务请求可能正在被重试。
PostgreSQL 可重新检查:
SELECT
application_name,
state,
sync_state,
replay_lag
FROM pg_stat_replication;
MySQL 重新执行:
SHOW REPLICA STATUS\G
并确认 I/O、SQL 线程、错误字段和延迟都恢复,而不是只看到磁盘空间增加。
六、主库切换:提升、路由、隔离和数据损失是四件事
1. 主库切换的两个方向
**计划内切换(switchover)**是在旧主库仍然可控时进行:
- 停止或冻结业务写入;
- 等待备用库追平;
- 确认旧主库不再接受写入;
- 提升备用库;
- 更新路由;
- 验证新主库;
- 将旧主库重建为新的备用库。
**故障切换(failover)**是在旧主库不可用或无法确认状态时进行。此时不一定有机会等复制追平,可能发生 RPO 范围内的数据丢失。更重要的是,必须防止旧主库恢复后继续写入。
2. PostgreSQL 手工提升示例
在确认备用库已经停止接收主库流复制,并且旧主库已被隔离后,可以在 PostgreSQL 备用库执行:
SELECT pg_is_in_recovery();
SELECT pg_promote();
先确认第一次查询返回 true。pg_promote() 请求备用库退出恢复并成为可写主库;它不是完整的故障转移编排器,不负责:
- 关闭旧主库;
- 修改 DNS 或负载均衡;
- 更新应用连接池;
- 确认客户端不再访问旧主库;
- 自动重建旧主库为备用库。
提升后验证:
SELECT
pg_is_in_recovery(),
current_wal_lsn();
应返回 false,且能够执行受控写入:
CREATE TABLE IF NOT EXISTS failover_probe (
probe_id bigint PRIMARY KEY,
promoted_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
INSERT INTO failover_probe(probe_id)
VALUES (2001);
如果旧主库没有被隔离,不能因为备用库已经提升就认为切换成功。切换成功的必要条件是:新主库是唯一写入口。
3. MySQL 8.4 手工提升示例
在 MySQL replica 上,先停止复制:
STOP REPLICA;
SHOW REPLICA STATUS\G
确认复制线程已停止,并记录最后执行位置或 GTID 状态。随后根据部署的只读策略解除只读。例如:
SET GLOBAL super_read_only = OFF;
SET GLOBAL read_only = OFF;
这些设置需要足够权限,并且可能被配置管理系统重新覆盖。执行前应确认该实例确实已被选为新主库,且旧 source 已隔离。
验证:
SELECT @@read_only, @@super_read_only;
然后执行唯一标识写入:
CREATE TABLE IF NOT EXISTS failover_probe (
probe_id BIGINT PRIMARY KEY,
promoted_at TIMESTAMP(6) NOT NULL
);
INSERT INTO failover_probe(probe_id, promoted_at)
VALUES (2001, CURRENT_TIMESTAMP(6));
MySQL 本身不会因为执行 STOP REPLICA 就自动完成完整的拓扑切换。路由更新、旧主隔离、GTID 关系、复制用户、过滤规则和新 replica 重建都属于运维编排。
如果重新配置旧 source 作为 replica,不能凭经验直接执行 RESET REPLICA ALL。该操作会清理复制连接配置和 relay log 信息,使用前必须已经保存所需的 GTID 或位点,并确认不会破坏恢复依据。
4. 为什么“备用库可写”不是切换成功
至少还要验证以下路径:
- 旧主库无法接受业务写入;
- 读写路由指向新主库;
- DNS、VIP、代理和连接池已经刷新;
- 新连接和已有连接的行为符合预期;
- 应用不会把只读查询继续发到旧主库;
- 复制监控已经切换监控对象;
- 业务事务在新主库提交;
- 新主库的备份、日志和复制任务已重新建立。
还要测试连接池生命周期。许多连接池不会因为 DNS 变化立即断开已有连接,导致部分请求继续访问旧主库。切换验证应包含“已有连接”和“新建连接”两类客户端。
七、恢复验证:恢复的是数据库状态,不只是数据库进程
1. 物理恢复、逻辑恢复和 PITR
物理备份保存数据库的数据文件或物理页,恢复时通常需要匹配的数据库版本、架构和备份工具语义。PostgreSQL 的物理基础备份结合 WAL 归档可以支持时间点恢复;MySQL 的物理备份能力取决于具体工具和发行版。
逻辑备份保存表、对象和数据的逻辑表示,例如:
pg_dump -Fc -d appdb -f appdb.dump
createdb appdb_restore
pg_restore -d appdb_restore appdb.dump
这是 PostgreSQL 的逻辑导出和恢复示例。它可以验证对象和数据是否能被逻辑恢复,但不能证明物理备份、WAL 归档或完整 PITR 链路可用。
MySQL 的逻辑示例:
mysqldump \
--single-transaction \
--routines \
--events \
--triggers \
appdb > appdb.sql
mysql appdb_restore < appdb.sql
--single-transaction 对事务型表可以在一致性读语义下减少锁表影响,但不等价于对所有存储引擎、非事务表和所有对象都提供同样的一致性保证。生产数据还应考虑字符集、用户权限、触发器、事件、生成列和外部对象。
**PITR(Point-in-Time Recovery,时间点恢复)**的目标不是恢复到“最近一个备份”,而是:
PostgreSQL 中,这通常意味着基础备份加连续 WAL 归档。MySQL 中,通常意味着全量或物理备份加连续 binlog;具体恢复步骤依赖使用的备份工具和备份格式。
2. PostgreSQL 备份完整性验证
如果使用 PostgreSQL 的物理基础备份,pg_verifybackup 可以验证备份目录的校验和和备份清单:
pg_verifybackup /backup/base_20250301
它能发现备份文件缺失、校验不匹配等问题,但不能证明:
- WAL 归档链完整;
- 备份能在目标主机启动;
- 应用连接配置正确;
- 恢复后的业务数据满足不变量。
所以仍必须执行一次隔离恢复:
- 创建干净的恢复目录;
- 恢复基础备份;
- 提供所需 WAL;
- 指定恢复目标时间;
- 启动实例;
- 确认恢复完成;
- 执行数据和业务验证。
恢复目标应明确到时间点或事务边界。若目标时间落在事务中间,数据库恢复到的是符合数据库恢复语义的某个一致状态,不是把一个事务拆成半提交状态。
3. 恢复后的三层验证
第一层:服务验证
SELECT 1;
SELECT version();
确认:
- 进程运行;
- 端口可连接;
- 用户认证正常;
- 目标数据库可打开;
- 应用所需扩展、字符集和参数存在。
第二层:数据库一致性验证
检查主键、外键、唯一约束和关键表行数。不要只依赖行数,因为删除一行再插入一行可能保持行数不变。
例如:
SELECT COUNT(*) FROM orders;
SELECT COUNT(*) FROM order_items;
SELECT order_id
FROM order_items
GROUP BY order_id
HAVING COUNT(*) = 0;
更有价值的是使用业务不变量:
SELECT COUNT(*)
FROM orders o
LEFT JOIN order_items i ON i.order_id = o.id
WHERE i.order_id IS NULL
AND o.status IN ('PAID', 'SHIPPED');
该查询只是示例,真实不变量必须来自业务模型。
第三层:业务一致性验证
设计一组恢复前写入的探针数据:
演练批次:20250301-01
订单:100001
支付流水:pay-100001
库存扣减:sku-A, -1
恢复后验证:
- 订单是否存在且状态正确;
- 支付流水是否最多一次;
- 库存扣减是否与订单状态匹配;
- 关联表是否完整;
- 唯一键和幂等键是否仍然有效;
- 恢复点之后不应出现的数据是否确实不存在。
“数据库能启动”和“关键订单状态正确”是两个不同的成功条件。
八、复制延迟和切换时的数据损失如何计算
1. 以时间计算 RPO
假设:
- 主库最后确认提交的测试事务时间为
10:00:00.800; - 备用库最后已经持久化或执行到的事务时间为
09:59:58.300; - 故障发生在
10:00:01.000。
若切换到备用库后只能确认到 09:59:58.300,则可观测数据缺口约为:
这只是时间近似。严格 RPO 应根据事务提交顺序、WAL/binlog 位点和恢复目标计算,而不是仅看 Seconds_Behind_Source 或某个监控采样值。
2. 以日志位点计算积压
PostgreSQL 可以用:
SELECT pg_wal_lsn_diff(
pg_current_wal_lsn(),
replay_lsn
)
FROM pg_stat_replication;
结果表示尚未被备用库重放的 WAL 字节数。它不能直接转换成秒数,除非结合一段时间内的 WAL 生成速率:
其中:
- :积压 WAL 字节数;
- :单位时间 WAL 生成字节数。
如果写入速率剧烈波动,这个估算会失真。
MySQL 使用 GTID 时,优先比较已执行事务集合;仅比较日志文件和位置时,必须确认日志没有轮转、过滤规则一致,并且位置语义对应同一 source。
3. 一个常见反例:备用库“延迟为零”仍可能丢数据
假设:
- 备用库在
10:00:00追平; 10:00:01到10:00:05没有复制流量;- 监控在
10:00:05看到延迟为零; - 主库在
10:00:06接收一个事务; 10:00:06.2主库断电;- 备用库尚未收到该事务。
监控中的“零延迟”只说明采样时没有可见积压,不能保证下一瞬间提交的事务已经复制。切换前需要冻结写入或使用明确的同步确认,而不是把历史监控值当作切换时刻的保证。
九、把故障注入、观测和恢复串成一个可重复流程
1. 演练前置检查
演练开始前:
- 确认数据已隔离或允许损失;
- 确认备份和恢复链路有最近成功记录;
- 确认有数据库超级用户或等价运维权限;
- 确认 SSH、带外管理和监控不依赖被注入的链路;
- 确认故障注入命令有明确的撤销命令;
- 确认停止条件,例如错误率、磁盘剩余空间和复制积压阈值;
- 确认谁有权中止演练;
- 记录所有时间点和命令输出。
2. 故障中的观测维度
数据库可观测性不能只看 CPU 和连接数,应同时观察:
连接
- 新连接成功率;
- 连接建立耗时;
- 连接池等待时间;
- 连接数是否达到上限;
- 失效连接是否被清理。
事务
- 提交、回滚和超时数量;
- 长事务;
- 活跃事务年龄;
- 客户端超时后数据库是否仍有提交;
- 重试是否带来重复业务操作。
锁和执行
wait_event或锁等待;- 慢查询;
- 执行计划是否因资源不足改变;
- 临时文件和排序是否放大磁盘压力。
复制
- 连接状态;
- 接收、持久化、重放位点;
- 延迟和积压;
- WAL、binlog、relay log 增长;
- 复制错误和重连次数。
SLO
- 成功率;
- p95、p99 延迟;
- 错误类型;
- 数据库不可写持续时间;
- 切换后恢复到正常流量的时间。
3. 恢复后的检查顺序
推荐顺序如下:
- 撤销故障注入;
- 确认操作系统资源恢复;
- 确认数据库进程和监听恢复;
- 确认复制线程或流复制恢复;
- 确认复制积压下降并最终追平;
- 确认路由只指向唯一主库;
- 清理或重建失效连接池连接;
- 执行探针事务;
- 执行数据库一致性检查;
- 验证备份、归档和监控重新正常;
- 持续观察一段时间,再宣布演练结束。
如果只恢复网络、不检查复制状态,可能留下“业务已恢复、备用库已永久落后”的隐性故障。
十、常见误解和失败表现
误解一:请求超时就代表事务失败
错误表现:
- 应用超时;
- 操作人员重试;
- 恢复后发现同一订单有两条支付或两次扣库存。
正确做法是使用幂等键查询数据库最终状态,并在事务设计中明确客户端无法确认提交结果时的处理方式。
误解二:主备切换只需要执行一个 promote 命令
提升数据库只是改变数据库角色的一步。没有旧主隔离、路由更新和连接池处理,仍可能出现:
- 双主;
- 部分请求访问旧主;
- 只读节点被误当成新主;
- 监控继续监控旧主;
- 新主没有备份和复制。
误解三:删除大文件就完成磁盘恢复
如果空间被已删除但仍打开的文件占用,删除目录项后空间不会立即归还;如果真正原因是复制槽、归档失败或 relay log,删除日志还可能破坏恢复链。应先定位增长源,再用数据库和运维工具按其生命周期清理。
误解四:Seconds_Behind_Source = 0 就没有复制风险
该值受 source 时间、复制线程状态和采样时机影响,不能独立证明数据已持久化、已执行或切换时不会丢失。
误解五:备份文件存在就代表可恢复
备份可能存在以下问题:
- 文件损坏;
- 缺少连续 WAL 或 binlog;
- 密钥不可用;
- 权限和版本不匹配;
- 恢复后缺少扩展、事件或权限;
- 数据能恢复但业务不变量已破坏。
恢复演练必须真正启动隔离实例,并执行数据和业务验证。
误解六:网络故障一定比进程崩溃安全
网络故障更容易产生“双方都活着但互相看不见”的状态。若故障转移系统没有仲裁和 fencing,网络分区比单节点宕机更容易导致脑裂。
十一、一次完整演练的最小算例
下面给出一个不绑定具体厂商编排器的流程。设定:
- PostgreSQL 一主一备;
- 业务表中使用
request_id唯一约束; - 目标 RPO 不超过 5 秒;
- 目标 RTO 不超过 60 秒;
- 演练只针对测试租户。
测试事务:
CREATE TABLE IF NOT EXISTS chaos_business_probe (
request_id text PRIMARY KEY,
tenant_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
BEGIN;
INSERT INTO chaos_business_probe(request_id, tenant_id, amount)
VALUES ('chaos-20250301-0001', 99, 10.00);
COMMIT;
执行步骤:
- 记录主库
pg_is_in_recovery()、复制状态、磁盘状态和当前时间; - 在客户端到数据库链路增加延迟;
- 提交上述事务,记录客户端耗时;
- 在主库到备用库链路制造短暂断网;
- 确认备用库停止接收或重放,并记录积压;
- 恢复网络;
- 等待备用库追平;
- 在旧主库仍可控时执行计划内切换,或先隔离旧主再执行故障切换;
- 通过新路由查询
request_id = 'chaos-20250301-0001'; - 在新主库写入另一个唯一探针;
- 确认旧主库不能看到或接受新的业务写入;
- 检查复制、备份、日志和应用错误率;
- 恢复旧主库为备用库,而不是直接把它重新当主库接入。
成功条件必须同时满足:
- 事务结果符合预期;
- 切换期间没有双主写入;
- 新主库对外可写;
- 旧主库对外不可写;
- 复制链路重新建立;
- 恢复时间不超过 60 秒;
- 若发生数据缺口,缺口不超过 5 秒;
- 应用重试没有制造重复业务结果;
- 恢复后的备份链路仍然可用。
十二、如何判断演练结果真实可信
演练结果应分为三类,而不是简单写“成功”或“失败”。
1. 数据库层成功,业务层失败
例如:
- PostgreSQL 成功提升;
- SQL 查询正常;
- 但应用连接池仍访问旧主;
- 订单服务持续报连接错误。
这说明数据库操作成功,但切换编排或路由失败。
2. 服务层成功,数据层失败
例如:
- 恢复实例成功启动;
- 表可以查询;
- 但恢复点缺少最后 30 秒订单;
- 或订单存在而支付流水缺失。
这说明 RTO 可能满足,但 RPO 或业务一致性不满足。
3. 故障已消除,系统仍未恢复
例如:
- 磁盘空间已释放;
- 但 PostgreSQL 复制槽导致 WAL 继续保留;
- MySQL replica 的 SQL 线程仍停止;
- 监控恢复为绿色,但备用库实际上没有追平。
这说明恢复验证不完整,故障后的状态机没有回到健康状态。
数据库故障演练的核心不是制造更大的破坏,而是验证系统能否在明确的事务边界、复制语义、资源约束和路由规则下,完成“发现故障—隔离风险—切换或恢复—验证数据—重建冗余”的闭环。延迟揭示等待和超时问题,断网揭示分区和脑裂问题,磁盘满揭示资源耗尽与日志保留问题,主库切换揭示角色和路由问题,而恢复验证最终回答的是:系统恢复后,数据是否仍然可信。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:数据契约与 Schema Registry:兼容模式、演进、验证和消费者治理
- 延伸:数据库备份与恢复:全量、增量、PITR、RPO/RTO 和恢复演练
- 延伸:数据库可观测性:连接、事务、锁、执行计划、复制和 SLO
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论