ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

数据库故障恢复核心机制:从WAL、检查点到崩溃恢复

数据库故障恢复核心机制:从WAL、检查点到崩溃恢复 1. 先从地图说起用户看到的数据库和DBMS眼中的数据库很多人学了几年数据库写SQL写得很溜但一被问到数据库系统到底是什么反而会愣住。原因很简单日常开发只接触数据库的某个表面切片而数据库系统本身是一个分层非常明显的体系结构。搞清楚这个体系是理解后面所有机制包括故障恢复的地基。先站在用户视角看数据库。用户无论是程序员还是终端用户看到的是一张张表、一行行数据、一条条查询语句。你执行SELECT * FROM orders WHERE user_id 123数据库返回结果集仅此而已。但从用户的感知来说这背后至少包含三层结构外层视图层用户实际看到和操作的表结构、视图、权限控制。不同用户看到的是同一个数据库的不同投影比如财务部门看到工资表的特定列HR看到员工表的特定列。概念层逻辑层整个数据库的完整逻辑结构描述所有实体、属性、关系。它是全局的和具体用户无关。内层物理层数据在磁盘上到底怎么存、用什么索引结构、按什么顺序排列、文件怎么组织。这三层之间的转换就是两级映像外模式/概念模式映像负责把用户视图映射到全局逻辑结构概念模式/内模式映像负责把逻辑结构映射到物理存储结构。这两级映像的价值在于隔离变化——物理存储换了比如从行存储改成列存储、换一块磁盘文件布局用户的SQL感知不到逻辑结构调整了比如拆表、合并表用户视图也可以不变。提示理解两级映像最好的方式是想图书馆——读者看到的图书目录外模式不变但后端的书库可以在不同楼层之间搬运调整内模式甚至可以换一整套书架系统。读者完全不感知。DBMS内部视角则完全是另一幅图景。一个数据库管理系统在内部至少要拆出这些核心模块查询处理器负责解析SQL、生成执行计划、优化执行计划。你写的SQL是先被解析成语法树再经过逻辑优化、物理优化最终变成一堆操作符扫描、连接、聚合组成的执行计划。存储引擎负责数据在磁盘上的物理组织与存取。它管理数据文件、索引文件、日志文件实现记录的插入、删除、修改、查找以及缓冲池Buffer Pool的页缓存管理。事务管理器负责事务的开始、提交、中止协调并发控制。它和日志管理器配合实现事务的全部ACID属性。日志管理器负责记录Redo日志和Undo日志维护WALWrite-Ahead Logging预写日志规则。这是故障恢复机制的核心支撑。并发控制模块实现锁管理、隔离级别控制、死锁检测。MVCC多版本并发控制在这个层面实现。这两个视角合在一起你才能明白为什么数据库能同时做到用户用着简单和底层这么复杂还不出错——因为分层架构把复杂度包裹在内部对外只暴露一个简洁的SQL接口。2. 为什么分RDBS、OODBS、ORDBS数据模型的演进逻辑DBMS分类的根子在数据模型——也就是用什么方式组织和表达数据。数据库发展这几十年其实就是在回答一个问题真实世界里的对象和关系怎么用计算机结构来描述2.1 关系型数据库RDBS用二维表解决大部分业务问题关系模型Relational Model由E.F. Codd在1970年提出核心思想是把数据组织成关系表每个关系由行元组和列属性组成表与表之间通过键主键、外键建立关联。我们用SQL操作这些关系。关系模型赢在三个地方数学基础扎实以集合论和谓词逻辑为根基查询结果天然是集合操作结果可预测。物理独立性高逻辑层面用表描述数据物理层怎么存储对用户完全透明这是前面说的二级映像的直接受益者。标准化成熟SQL语言一统天下各种数据库产品之间有很强的互通性。所以你看到的企业级系统ERP、CRM、金融系统、电商平台绝大部分核心数据都跑在关系型数据库上。MySQL、PostgreSQL、Oracle、SQL Server都是RDBS。关系模型的短板也很明显表达能力有限。一个对象带复杂嵌套结构比如一个订单包含多个商品每个商品又有一堆属性在关系模型里就要拆成订单表、订单明细表、商品表、商品属性表查询时多做几次JOIN。更麻烦的是面向对象编程语言里的对象模型和关系模型的阻抗失配Impedance Mismatch问题一直存在——程序里一个对象到了数据库要被拆成多行多表程序员不得不写ORM框架来做转换。2.2 面向对象数据库OODBS让对象持久化回到本真面向对象数据库的出发点很直接既然程序设计都用对象思维为什么数据库不能直接存对象OODBS把对象作为基本存储单位支持类、继承、多态、引用、集合等概念。一个对象可以直接整体存入数据库取出时也完整还原不需要拆表不需要ORM。它特别适合那些数据天然具有复杂层次结构的场景CAD/CAM图纸数据、地理信息系统GIS的复杂图形对象、多媒体数据库视频、图片的属性集、生物信息学中的复杂序列数据等。但OODBS没有成为主流原因也很现实。关系模型经过几十年发展工具链极其成熟BI报表工具、数据仓库方案、各种驱动和中间件都是围绕SQL生态构建的。OODBS没有统一的查询语言标准各家方言林立用户迁移、开发门槛、运维经验都成了大问题。这是典型的最好的技术未必能赢生态最成熟的才能赢。2.3 对象关系数据库ORDBS站在巨人肩膀上缝缝补补对象关系数据库试图兼顾两者保留SQL和关系表的优势同时扩展对象能力。它是在关系模型基础上添加用户自定义类型UDT、继承、引用、嵌套表、自定义函数等面向对象特性。PostgreSQL就是这个路线的典型代表——它本质上是关系型的底子但支持CREATE TYPE创建复合类型支持表继承支持数组、JSON等复杂类型支持自定义操作符和函数。ORDBS的思路最务实不推翻关系模型而是把对象能力融入关系模型。它对开发者的实际价值体现在两个方向复杂数据类型原生支持比如直接用JSON/JSONB类型存半结构化数据不必拆表。用户自定义扩展能力定义自己的数据类型、索引方法、聚合函数数据库从通用工具变成领域专用平台。很多人没意识到现代关系数据库基本都成了事实上的ORDBS——MySQL 8.0的JSON类型、Oracle的对象类型、SQL Server的CLR集成全都在沿着这个方向走。类型存储单位核心优势核心劣势代表产品RDBS二维表行与列数学基础扎实、SQL标准、生态成熟复杂对象表达弱、阻抗失配MySQL, PostgreSQL, OracleOODBS对象含继承/引用表达能力强、开发模型统一、无需ORM缺乏统一标准、生态碎片化ObjectStore, db4oORDBS表对象扩展兼顾关系能力和对象能力实现复杂度高、概念学习成本PostgreSQL典型选型上的实际建议95%的常规业务系统直接上关系型如果你在PostgreSQL里已经开始大量使用JSON/复合类型/自定义函数来承接复杂业务那实际上你已经用到了ORDBS的能力只有当你处理的领域对象极其复杂、且查询模式以导航式访问为主时才值得考虑纯粹的OODBS方案。3. 故障恢复不是备份先搞清楚系统在防什么故障恢复在数据库体系里是那种平时没人注意、一出事就是大事的模块。我见过太多开发团队把数据安全等同于做备份这是很危险的认知。备份解决的是数据丢了能找回来的问题故障恢复解决的是系统出错后能回到一致状态的问题——两者目标交集大但机制完全不同。3.1 数据库故障的三种类型按故障发生的范围和对系统的影响程度数据库故障可以分三类事务故障单个事务在执行过程中出错比如程序抛出异常、违反约束、死锁被回滚。这类故障影响范围最小只需要撤销Undo该事务已做的修改让数据库回到该事务开始前的状态。系统故障整个数据库系统崩溃比如操作系统死机、数据库进程被杀、突然断电。这时内存中所有未写盘的数据全部丢失部分事务可能已经提交但数据页还没落盘违反持久性另一部分事务可能还没提交但修改了缓冲池中的页面需要撤销。系统故障恢复的核心目标是让所有已提交事务的修改持久化让所有未提交事务的修改彻底消失。介质故障磁盘损坏、存储阵列故障、文件被误删。这种故障意味着物理数据丢失内存中再完好也没用。唯一的恢复手段是从备份恢复再加上日志重放。介质故障的恢复时间最长对业务影响最大。很多工程团队在讨论容灾时混淆了这几种故障等级。日常进程崩溃属于系统故障几秒钟到几分钟就能恢复磁盘整个坏掉属于介质故障恢复时间以小时甚至天计。高可用架构设计和故障恢复预案如果按这个分类去设计才不会出现以为有备灾结果只是个进程守护的错配。3.2 为什么需要日志Redo和Undo的底层逻辑数据库为什么不能只把数据写进磁盘而必须额外维护一套日志直接回答磁盘的随机IO太慢但数据库又要保证事务持久性和原子性只能靠顺序写日志来换取性能和安全。具体来说缓冲池Buffer Pool会把磁盘数据页缓存到内存中修改先发生在内存页中。如果每次修改都立即同步到磁盘每次事务提交都要做一次随机写盘性能会差到无法接受。所以数据库采用延迟写盘策略事务提交时只强制把日志写盘顺序写快数据页可以留在缓冲池里等日后条件合适再刷盘。这就引出了WALWrite-Ahead Logging规则任何数据页的修改必须先写出对应的日志记录然后才允许修改数据页。日志里包含了Redo信息记录修改后的新值用于重做和Undo信息记录修改前的旧值用于撤销。拿一个转账事务举例账户A转100元给账户B。事务开始生成日志记录(LSN101, XID50, 修改A账户: 旧值1000, 新值900)。修改缓冲池中A的数据页。继续执行生成日志记录(LSN102, XID50, 修改B账户: 旧值500, 新值600)。修改缓冲池中B的数据页。事务提交强制将LSN102的日志刷入磁盘日志文件。此时数据页还留在内存中事务已经完成提交对外可见。如果步骤6之后、数据页刷盘之前系统崩溃了内存中的数据页丢失。但重启后数据库读取日志发现事务50已提交但数据页未落盘于是**重放Redo**LSN101和LSN102两条日志记录把A改成900、B改成600事务的持久性得到保证。反过来如果一个事务还没提交系统就崩溃了比如事务50只是修改了A账户但B账户还没改且未提交。重放日志时发现日志记录属于未提交事务怎么办这时需要撤销Undo根据Undo信息把A改回旧值1000。数据库不会只撤销部分操作而是把该事务的全部修改都回滚到一个中间一致状态。注意Redo和Undo的方向不可逆。Redo是向前重放要求日志里有修改后的完整新值Undo是向后回退要求日志里有修改前的完整旧值。所以同一条日志记录里往往同时包含旧值和新值这不算空间浪费是恢复路径上的双向保障。3.3 检查点Checkpoint机制为什么日志不能无限膨胀前面说的恢复过程隐含一个问题日志会无限增长。如果每次崩溃都要从最早的日志开始重放恢复时间会随着系统运行时间无限拉长这不可接受。检查点Checkpoint就是用来解决这个问题的。检查点做的事情是选定一个时刻把缓冲池中所有脏页被修改过但还没写盘的数据页刷入磁盘同时记录一个检查点日志(CKPT, 已刷盘的脏页列表, 所有活动事务列表)。有了检查点之后崩溃恢复时只需从最近一次检查点之后的日志开始处理检查点之前的数据已经落盘不用再管。所以恢复起点不是最早日志而是最后一个检查点。MySQL的InnoDB引擎中由后台线程周期性地刷新脏页、推进LSNLog Sequence Number日志序列号水位这就是检查点机制的实现。PostgreSQL的checkpoint_timeout参数同理。检查点有一个经典权衡频率越高崩溃后需要重放的日志越少恢复越快但检查点本身带来的刷盘和IO开销越大影响正常业务。生产环境里一般把检查点间隔和IO能力挂钩而不是无脑调小。比如SSD盘的随机写能力强检查点可以频繁一些机械盘则要考虑刷盘带来的IO负载。4. 崩溃恢复到底怎么执行三类故障的恢复流程详解理论讲清楚了现在看落地过程。这部分我建议你对着日志结构来读理解会顺很多。4.1 事务故障恢复最简单但最容易忽视的方向事务故障的恢复核心动作只有一个词Undo。当数据库检测到一个事务出错比如用户主动ROLLBACK或系统判断死锁选择了牺牲者恢复过程如下找到该事务的Undo日志记录从头开始遍历事务产生的日志。对每条Undo日志反向执行把修改前的旧值写回数据页。如果涉及索引同步撤销索引项的变动。写入一条事务回滚完成的日志记录并在事务表中标记该事务已中止。这个过程平时由事务管理器直接触发不需要等系统崩溃也不需要重新启动数据库。事务故障恢复的原则是部分修改有可见性泄漏之前就要全部撤回——因为并发事务可能读到这个未提交事务写了一半的数据会导致脏读级的逻辑错误。4.2 系统故障恢复Redo然后Undo顺序不能反系统崩溃后数据库重启恢复模块进入**恢复Recovery**流程。经典做法是三步第一步分析阶段Analysis。从最后一个检查点开始扫描日志确定三点信息哪些事务在崩溃时处于已提交但未完全落盘状态——这些事务需要Redo。哪些事务在崩溃时处于未提交/正在执行状态——这些事务需要Undo。所有数据页的初始状态和已刷盘情况确定哪些页可能不一致。第二步Redo阶段向前重做。从检查点后的第一条日志开始逐条重放所有已提交事务的修改把新值重新写入数据页。这个阶段要保证幂等性如果某页在崩溃前已经刷盘了一部分修改重放时要能跳过已生效的修改所以日志记录里要有LSN对比——数据页上记录着最后更新的LSN只有当日志LSN大于页LSN时才执行重放。第三步Undo阶段向后回滚。处理所有未提交事务的日志把旧值写回数据页同时写入补偿日志记录CLR, Compensation Log Record确保回滚过程本身也是可恢复的——如果回滚过程中又发生崩溃重启后可以继续回滚而不是把已回滚的事务重新Redo。顺序为什么必须是Redo在前、Undo在后如果先Undo后Redo系统崩溃的重放可能把未提交事务修改的值再次恢复出来破坏原子性。先Redo可以保证所有已提交修改的持久性再统一回滚未提交修改整个数据库的状态才是一致性持久性兼备的。4.3 介质故障恢复备份日志的接力赛介质故障是唯一必须依赖备份的故障类型因为物理文件可能整个没了。恢复流程大致分四步还原备份从最近一次全量备份或全量增量备份链还原数据库文件。这一步决定了恢复的上限时间是备份点而不是崩溃点。配置日志起点把归档日志Archive Log和在线日志回放起点定位到备份的时间点。重放日志从备份点开始顺序应用归档日志和在线日志的Redo信息把数据库推进到崩溃前的状态。回滚未提交事务依据日志执行Undo清理未完成事务。这里有个工程要点备份的时间点越新日志重放越少恢复越快。所以生产系统普遍使用周期性的全量备份高频的增量日志后备如每15分钟一次这样才能把RPO恢复点目标即最多丢多少数据控制在分钟级。另外我要专门提醒数据库日志的保存策略。介质故障的恢复完全依赖日志日志如果丢了备份还原出来的也是旧数据。所以日志本身也要做冗余——归档日志传异地、传对象存储和数据库数据文件分开存放。很多事故的真相是数据盘坏了日志和备份在同一块阵列上一起没了。4.4 一个贯穿始终的机制ARIES恢复协议前面讲的恢复逻辑在工业界有一个标准化的实现框架叫ARIESAlgorithm for Recovery and Isolation Exploiting Semantics。它几乎是所有现代数据库包括PostgreSQL、MySQL InnoDB、Oracle恢复模块的共同理论基础。ARIES有三个关键设计WAL强制先写日志所有数据修改之前先落日志。Redo时重放历史不管事务状态——已提交和未提交事务的修改都先重放未提交的在后面的Undo阶段处理这样可以避免对某页是否需要恢复做复杂判断。Undo时写补偿日志保证系统在回滚过程中再次崩溃也能安全恢复。ARIES的意义不仅在于理论严谨更在于它把恢复从碰运气变成可证明正确。你能看到的所有数据库参数比如innodb_flush_log_at_trx_commit、fsync策略、组提交Group Commit都是在ARIES这套协议框架下做的性能优化而不是对它的偏离。5. 把理论拉回工程这些概念在现代数据库里怎么体现如果你觉得前面内容偏教材下面这些是实际开发中能对应上的点。5.1 InnoDB的redo log和undo log肉眼可见的恢复组件MySQL的InnoDB存储引擎是ARIES协议最直观的落地案例。Redo Log物理日志记录页级别的物理修改操作比如把page 10的offset 100处写入值X。以顺序写方式写入ib_logfile文件默认两个文件循环使用。它保障的是持久性事务提交时innodb_flush_log_at_trx_commit1会强制把该事务的redo log刷入磁盘。Undo Log逻辑日志记录行记录修改前的镜像存放在系统表空间中。它服务的场景更多事务回滚时需要它来做反向操作MVCC的快照读也要通过它来构建历史版本。也就是说Undo Log不只是故障恢复用还是并发控制的一部分。实操层面DBA和开发需要关心两个参数innodb_flush_log_at_trx_commit取值1表示每次提交都刷日志最安全但最慢取值0表示每秒才刷性能最好但可能丢最后一秒内已提交事务的日志取值2表示每次提交写操作系统缓存但每秒刷盘可能丢操作系统崩溃或断电时的数据。生产主库我用1分析类从库可以用0或2。innodb_log_file_size日志文件大小越大承载检查点之间的写入量越大崩溃恢复时重放日志越多、恢复时间越长但系统运行时刷盘压力更小。我之前遇到过一个从库因为日志文件太小导致频繁发生checkpoint too old之类的性能抖动调大之后稳定很多。5.2 PostgreSQL的WAL、检查点与流复制PostgreSQL的WAL机制和InnoDB在思想上完全同源但实现细节有一些工程差异。PostgreSQL把WAL段文件按16MB切分通过archive_modeon开启归档再配合连续归档和流复制实现PITR时间点恢复和备库。一个很典型的排查场景pg_ctl promote提升备库后发现数据丢失或重复——这通常是因为备库在恢复过程中对WAL的处理和对未提交事务的回滚时机不对。理解了本文的恢复三步流程分析、Redo、Undo你就能明白备库提升的本质备库也是一套恢复系统它在持续应用WAL的同时要标记哪些事务是未提交的提升时把未提交事务回滚干净。5.3 分布式数据库对故障恢复的进一步扩展如果是分布式数据库比如TiDB、OceanBase故障恢复的思路会进一步扩展除了单机崩溃恢复还要处理节点不可用、网络分区、副本之间的日志同步。但底层的WAL检查点Redo/Undo框架依然适用只是范围从单机扩展到多副本。分布式场景下Raft或Paxos协议负责维护日志在多节点间的一致性分片内数据写入先走共识协议达成日志一致再做状态机的应用。理解单机WAL为什么先写日志再改数据就能理解分布式共识为什么先达成日志一致再提交状态变更——本质都是通过日志的持久化和一致性来保证状态的可恢复性。5.4 把恢复能力作为系统设计的一等公民在我的实际经验里数据库恢复不是一个出事才想的功能而是要在系统设计阶段就定好的工程约束。有几个原则我一直沿用第一能承受多少数据丢失决定了日志刷盘策略。如果业务绝对不允许丢已提交数据那就必须flush_log_at_trx_commit1不要为了性能妥协。如果允许秒级丢失比如日志流水型业务可以用0或2换来吞吐提升。第二备份必须可恢复验证否则等于没备份。我见过太多次备份文件在但恢复流程从来没演练过的翻车现场。应该定期做恢复演练尤其是灾难恢复DR演练从备份集重新搭建环境重放日志到目标时间点确认数据一致。恢复流程不演练真出事那天你一定是在现场边看文档边手忙脚乱。第三日志的冗余度要高于数据。数据可以从日志重建日志丢失则只能从旧备份开始追。异地多活系统里日志同步是比数据同步更优先的一环。
返回列表