数据库可靠性工程与生产排障
数据库故障往往不是单一的“数据库问题”。连接池耗尽可能来自应用重试,复制延迟可能来自大事务,磁盘写满可能由日志保留失控,慢查询也可能是统计信息、锁等待或数据分布变化。可靠性工程的目标,是建立从用户症状到连接、事务、锁、执行计划、存储和复制的证据链,并确保恢复操作不会扩大数据风险。
本文以 MySQL 与 PostgreSQL 的通用方法为主。具体命令、视图和参数应以实际版本官方文档为准,生产变更前必须备份并在测试环境验证。
一、数据库可靠性的四个目标
- 正确性:提交的数据不丢失、不重复、不产生不可解释的状态。
- 可用性:在目标时间内接受请求,并对故障进行切换或降级。
- 性能:在设计负载和数据规模下满足延迟与吞吐目标。
- 可恢复性:能够从误操作、损坏和灾难中恢复到明确时间点。
高可用不能替代备份:主库误删除会复制到从库,存储损坏也可能同步传播。备份同样不能替代高可用:恢复备份需要时间,无法承担秒级切换目标。
二、建立最小观测面
至少收集以下信息:
- 连接总数、活跃连接、等待连接和连接创建速率;
- QPS/TPS、事务提交和回滚数量;
- P95、P99 查询延迟及慢查询样本;
- 锁等待、死锁、长事务和空闲事务;
- 缓存命中、临时文件、排序和全表扫描;
- 磁盘使用、IOPS、吞吐、延迟和队列深度;
- WAL/binlog 增长速度、保留量和复制延迟;
- 备份结果、恢复演练结果和最近可恢复时间点。
监控标签应包含集群、实例、角色、数据库和版本,但不要把完整 SQL、用户输入或高基数 ID 直接作为指标标签。
三、生产故障的第一分钟
先控制风险,不要立即重启数据库:
- 确认用户症状、开始时间和影响范围;
- 判断是连接失败、查询变慢、写入失败还是数据错误;
- 暂停高风险发布、批处理和结构变更;
- 采集当前连接、锁、事务、复制和系统资源快照;
- 检查磁盘是否接近写满、文件系统是否只读;
- 建立事故时间线并指定操作人;
- 任何终止会话、切主或重启操作都记录理由和验证结果。
重启可能暂时清空连接和锁,但也会丢失现场证据、延长恢复、触发大规模重连,甚至使未完成恢复的节点反复启动。
四、连接耗尽怎么排查
现象
- 应用出现 connection timeout 或 too many connections;
- 数据库 CPU 不一定很高;
- 活跃连接少,但空闲连接很多;
- 连接创建速率突然增加。
排查顺序
- 对比连接上限、当前连接和真正执行中的连接;
- 按应用、用户、来源地址和状态分组;
- 检查连接池是否泄漏、最大池是否按实例数叠加过大;
- 检查长事务、idle in transaction 和网络中断;
- 检查应用是否在超时后无退避重试;
- 确认代理、负载均衡和数据库连接生命周期是否一致。
不要把 max_connections 无限调高。每个连接占用内存和后台结构,更多连接会放大上下文切换与锁竞争。优先控制应用连接池总量,并为管理、监控和故障处置保留连接配额。
五、慢查询不等于缺少索引
慢查询需要分层判断:
- 是否等待锁,而不是执行慢;
- 执行计划是否变化;
- 估算行数与实际行数是否偏差巨大;
- 是否读取了不必要的列和行;
- 排序、聚合是否写入临时磁盘;
- 缓存是否冷却,I/O 延迟是否异常;
- 参数值或数据倾斜是否导致计划不适用;
- 数据量增长后原有索引是否失去选择性。
使用 EXPLAIN 或 EXPLAIN ANALYZE 时要理解风险:后者会实际执行语句,针对写语句或高成本查询必须先在安全环境评估。不要为了一个查询建立大量重叠索引,索引会增加写放大、占用空间并延长维护。
六、锁等待与死锁
锁等待表示事务正在等待资源;死锁表示形成循环等待,数据库通常会中止其中一个事务。处理步骤:
- 找出阻塞会话、被阻塞会话和等待时长;
- 获取事务开始时间、SQL、应用来源和锁对象;
- 判断阻塞者是否仍在工作,还是遗留空闲事务;
- 优先联系业务确认,再决定取消语句或终止会话;
- 保留死锁图或数据库日志,修复访问顺序和事务边界。
常见根因包括事务过长、批量更新过大、不同代码路径以不同顺序更新资源、缺少索引导致锁定范围扩大,以及用户交互期间保持事务不提交。
七、长事务的隐性风险
长事务除了持有锁,还可能:
- 阻碍垃圾回收或旧版本清理;
- 导致 undo/WAL/binlog 大幅增长;
- 延长复制追赶和恢复时间;
- 让备份和 DDL 行为变得复杂;
- 在失败回滚时占用大量时间和 I/O。
应用应设置合理的语句和事务超时,大批量操作分批提交,并保证重试幂等。不能只在数据库侧强制终止而不修复应用逻辑。
八、复制延迟排查
先确认延迟定义:时间延迟、日志位置差、待应用字节数或回放时间可能含义不同。排查维度包括:
- 主库是否产生大事务或突发写入;
- 网络吞吐、丢包和跨区域延迟;
- 从库 I/O、CPU 和锁竞争;
- 单线程或并行回放能力;
- DDL、长查询或备份是否阻塞回放;
- 日志保留是否足以支撑从库追赶;
- 从库读取流量是否挤占恢复资源。
如果从库已经落后,不应盲目切主。先确认它的数据时间点、可接受的数据损失和客户端写入路径。
九、磁盘接近写满
数据库磁盘满属于高风险事故。首先确认增长来源:数据文件、事务日志、归档日志、临时文件、审计日志、慢日志、备份或快照。
安全处置原则:
- 不直接删除正在使用的数据或日志文件;
- 不在未确认恢复链的情况下删除 WAL/binlog;
- 优先停止异常增长来源和非必要写入;
- 扩容或迁移前确认文件系统和存储支持方式;
- 清理备份要遵守保留策略,并确认有其他有效副本;
- 恢复空间后检查实例是否只读、复制是否中断、归档是否恢复。
建议为数据、日志、临时文件和备份设置独立阈值,按增长速率预测耗尽时间,而不是等到固定的 90% 告警。
十、高可用切换前的决策
切主至少要回答:
- 原主库是否仍可能接受写入,如何避免双主;
- 候选节点的数据是否足够新;
- 允许的数据损失是多少;
- 应用、代理、DNS 和连接池如何切换;
- 原主恢复后如何隔离和重新加入;
- 切换后如何验证写入、读取、任务和复制;
- 谁批准可能的数据损失。
自动故障转移也要有仲裁和防脑裂设计。网络分区时“两个节点都认为对方故障”是比单节点停机更危险的情况。
十一、备份与恢复验证
备份成功日志不能证明可恢复。至少进行:
- 文件完整性和校验检查;
- 在隔离环境恢复全量备份;
- 应用增量日志恢复到指定时间点;
- 验证表数量、关键记录、约束和应用查询;
- 记录实际 RTO、可恢复时间点和操作步骤;
- 验证密钥、证书、账号和外部依赖是否齐全。
恢复演练应覆盖误删、实例损坏和区域不可用等不同场景。演练数据必须脱敏并限制访问。
十二、结构变更的生产保护
- 评估锁级别、扫描量、额外空间和日志量;
- 对大表使用分阶段、可暂停的方法;
- 避开备份、批处理和高峰窗口;
- 设置锁等待和语句超时,避免长时间阻塞;
- 先扩展结构,待应用兼容后再收缩旧结构;
- 观察复制延迟和从库查询;
- 明确停止条件,不能以“已经执行一半”为理由继续。
十三、故障证据模板
用户症状:订单查询超时,写入正常
开始时间:10:21
数据库角色:主库写入,从库读取
连接:总连接 92%,活跃 18%,等待连接增长
锁:无大规模锁等待
慢查询:某报表查询 P99 从 2s 升到 38s
系统资源:CPU 45%,磁盘读延迟从 3ms 升到 70ms
根因:报表全表扫描与备份读取重叠
止血:停止报表任务,限制备份读取速率
修复:增加合适索引并错开任务窗口
验证:P99 恢复、连接池恢复、复制延迟正常
十四、常见错误
- 看到慢就加索引,不检查锁和执行计划;
- 看到连接满就提高连接上限;
- 故障时先重启,丢失现场证据;
- 从库延迟时直接切主;
- 手工删除数据库日志释放磁盘;
- 只备份不恢复,密钥和配置也没有纳入;
- 监控只有实例存活,没有业务查询与复制状态;
- 数据库变更与应用全量发布同时执行。
十五、把排障结果转化为长期改进
事故恢复后应把临时命令和判断过程整理为可复用资产:
- 将有效查询写入只读诊断脚本,并标明适用版本和权限;
- 把“连接耗尽、复制延迟、磁盘增长”等症状映射到 Dashboard;
- 为危险命令增加确认、目标检查和影响说明;
- 将根因对应到代码、索引、参数、容量或流程整改;
- 为同类问题补充合成查询和提前量告警;
- 用恢复演练验证修复,而不是只关闭工单;
- 记录哪些证据当时缺失,并补齐日志或审计字段。
数据库 Runbook 应区分“只读取证”“可逆止血”和“可能改变数据”三类操作。终止会话、切主、跳过复制事件和修改系统表等操作不能与普通查询混在一起。对可能造成数据损失的步骤,应写明审批人、备份点、停止条件和业务验证。
十六、生产检查清单
- [ ] 连接、事务、锁、慢查询、磁盘和复制均有监控;
- [ ] 管理与故障处置保留独立连接能力;
- [ ] 长事务、空闲事务和语句超时有治理规则;
- [ ] 数据库变更具有兼容窗口和停止条件;
- [ ] 高可用方案有防脑裂和数据损失决策流程;
- [ ] 备份经过隔离恢复与时间点恢复验证;
- [ ] 容量预测包含数据、日志、临时空间和复制保留;
- [ ] Runbook 记录采证、止血、恢复和业务验证步骤。