和业界几乎所有的数据库一样,openGauss通过将事务对于数据库的修改写入可以永久(长时间)保存的存储介质中,来保证事务的持久性。这个过程被称为事务的持久化过程。持久化过程是保证事务持久性所必不可少的环节,其效率对于数据库整体性能影响很大,常常成为数据库的性能瓶颈所在。
最常用的存储介质是磁盘。对于磁盘来说,其每次读写操作都有一个“启动”代价,因此在单位时间内(每秒内),一个磁盘可以进行的读写操作次数(Input/Output Operations Per Second,简称IOPS)是有上限的。HDD磁盘的IOPS一般在1000次/秒以下,SSD磁盘的IOPS可以达到10000次/秒左右。另外一方面,如果多个磁盘读写请求的数据在磁盘上是相邻的,那么可以被合并为一次读写操作,这导致磁盘顺序读写的性能通常要远优于随机读写。
一般来说,尤其是在OLTP场景下,用户对于数据库数据的修改是比较分散随机的。如果在持久化过程中,直接将这些分散的数据写入磁盘,那么这个随机写入的性能是比较差的。因此,数据库通常都采用预写日志(Write Ahead Log,简称WAL)来避免持久化过程中的随机IO,如图4(a)所示。所谓预写日志,是指在事务提交的时候,先将事务对于数据库的修改写入一个顺序追加的WAL日志文件中。由于WAL日志的写操作是顺序IO,因此其可以达到一个比较高的性能。另一方面,对于真正修改的物理数据文件,再等待合适的时机写入磁盘,以尽可能合并该数据文件上的IO操作。
在一个事务完成日志的下盘操作(即写入磁盘)以后,该事务就可以完成提交动作。如果在此之后数据库发生宕机,那么数据库会首先从已经写入磁盘的WAL文件中恢复出该事务对于数据库的修改操作,从而保证事务一旦提交即具备持久性的特点。
下面结合图4(b)中的例子,简单说明数据库故障恢复的原理。假设一个事务需要在表A(对应数据文件A)和表B(对应数据文件B)中各插入一行新的记录,在数据库内部,其执行的顺序如下:
(1)记录修改数据文件A的日志,
(2)记录修改数据文件B的日志,
(3)在数据文件A中写入新的记录,
(4)在数据文件B中写入新的记录。
在上述过程中,如果在第(4)步执行时数据库发生宕机,那么该事务对于数据文件B的修改可能全部或部分丢失。当数据库再次启动以后,在其能够接受新的业务之前,需要将这些可能丢失的修改从日志中找回来(该操作被称为日志回放操作)。
在日志回放过程中,数据库会根据日志记录的先后顺序,依次读取每个日志的内容,然后判断该日志记录的事务对数据库数据文件的修改是否和当前相关数据文件的内容一致。如果一致,说明上次数据库停机之前修改已经写入数据文件中,该日志修改无需回放;如果不一致,说明上次数据库停机之前修改还未写入数据文件中,上次数据库停机可能是异常宕机导致,该日志对应的事务操作需要重新在相关数据文件中再次执行,才能保证恢复成功。
对于本例,在数据库恢复过程中,首先读取到在数据文件A中插入记录的日志,将数据文件A读取上来之后,发现数据文件A中已经包含该记录,因此该日志无需回放;然后读取到在数据文件B中插入记录的日志,将数据文件B读取上来之后,发现数据文件B中未包含新插入的记录,因此需要将日志中的记录再次写入到数据文件B中,从而完成恢复。最终,该事务对于数据库所有的修改都得以恢复出来,事务的持久性得到了保证。
(a) WAL日志和数据页面的关系示意图
(b)WAL日志和故障恢复示意图
图4 WAL日志和事务持久性示意图
WAL(Write-Ahead Logging)日志记录了数据库的物理变更(如数据页修改)和逻辑操作(如事务提交),是主备同步的基础。主备之间通过WAL 日志的传输和重放实现数据一致性:
WAL Sender
进程发送给备库。WAL Receiver
接收日志,并应用(重放)到本地数据文件,保持与主库一致。关键点:WAL 日志包含了事务的最终结果(如提交 / 回滚),但不直接同步事务的中间状态(如未提交的事务)。
在 OpenGauss 的主备架构中,Undo 日志本身不直接同步到备库,但与 Undo 相关的关键信息会通过 WAL 日志间接同步:
WAL 日志记录了事务对 Undo 日志的修改操作(如创建、更新 Undo 记录),但不包含 Undo 日志的完整内容。例如:
备库在重放 WAL 日志时,会本地生成自己的 Undo 日志,用于支持 MVCC 和事务回滚。这些 Undo 日志基于主库 WAL 中的操作重建,而非直接复制主库的 Undo 日志。
虽然备库上存在 Undo 日志,但无法直接通过 Undo 日志判断主库事务的实时执行情况:
备库的 Undo 日志是重放 WAL 后的结果,仅反映备库自身的事务状态。例如:
Undo 日志主要记录数据变更前的状态,而非事务的执行流程。例如:
备库通常作为只读节点(支持读操作),其 Undo 日志仅用于支持查询的多版本读,而非处理写事务。因此,备库只需根据 WAL 重放结果本地维护 Undo 日志,无需与主库的 Undo 日志完全一致,备库的 Undo 日志状态与主库的写事务状态无直接关联。
用户可以通过参数设置当前事务的同步方式。
该参数属于USERSET类型参数,可被任何用户在任何时刻设置。
通常情况下,一个事务产生的日志的同步顺序如下:
参数取值范围:枚举类型
状态类型 | 是否同步到备库? | 同步方式 | 一致性保障 |
---|---|---|---|
WAL 日志 | ✅ 必须同步 | 实时传输与重放 | 数据变更的强一致性 |
事务状态 | ✅ 最终状态(提交 / 回滚) | 通过 WAL 日志中的提交记录(CLOG)同步 | 最终一致性 |
锁状态 | ❌ 不同步 | 备库不维护锁表,提升为主库时重建 | 切换后重新初始化 |
快照状态 | ❌ 不同步 | 查询时本地生成快照 | 查询时刻的一致性 |
本文作者:司小远
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!