InnoDB两种持久化策略
InnoDB内存部分包括缓冲池(buffer pool) 和日志缓冲(log buffer),两者刷盘方式不同,前者走direct_io模式(直接绕过Page Cache来访问磁盘),后者走Page Cache模式(IO操作需要委托操作系统来完成)。
是否使用Page Cache的区别是什么?
OS的Page Cache对读写做了不少优化,包括按顺序预读取(按页读取)、在成簇磁盘块(n次方个扇区)上执行IO、允许访问同一文件的多个进程共享高速缓存的缓冲区等,但数据必须在用户进程与内核互相拷贝。
direct_io的优点是减少操作系统缓冲区和用户地址空间的拷贝次数,降低了CPU和内存带宽的开销。而InnoDB本身也已处理好buffer pool与磁盘数据的对应关系,所以可以舍去Page Cache。
先刷buffer pool还是先刷log buffer?
先写日志,再写磁盘(WAL机制,Write-Ahead Logging),即redolog和binlog等日志数据刷盘到log文件完成后,才会将脏页从buffer pool刷盘到表文件。
为什么运用WAL机制?因为顺序写磁盘的性能堪比写内存,所以写日志会比数据刷盘的性能高很多,只要保证日志写入成功,再通过代码保证日志和需刷盘数据的一致性,就能在保证数据不丢失的情况下大大提高性能。
顺序写运用很广泛,比如kafka追加写实现了事务消息,即提交或回滚事务时,会追加写入一条控制类型的消息来标识是commit或rollback。
buffer pool持久化过程
buffer pool刷盘时机主要有以下四种:
- MySQL正常关闭之前,会把所有的脏页刷盘;
- Master Thread会以每秒或者每10秒一次的频率定期将适量的脏页刷盘。上文讲到buffer pool通过变种lru算法区分冷热数据,故后台线程会优先刷冷数据,因为热数据在短时间可能被多次修改,如果优先刷盘热数据页,这个页很快又会被修改,又需要再刷盘,不如等它变成冷数据再刷盘。
- lru空闲列表不足、log buffer或磁盘空间不足时,page cleaner线程会异步将脏页刷盘。
- buffer pool空间不足时,用户线程从磁盘读取某个页要链入lru list,lru list会释放尾部的一个页。假设这个释放的页是一个脏页,那么用户线程就不得不亲自把这个脏页刷盘,这样就会降低响应用户请求的速度。之所以需要后台线程定时刷盘脏页就是为了尽可能避免发生这种主动刷盘的情况。
InnoDB还引入了double write buffer物理存储空间,来处理buffer pool刷盘时的异常情况。buffer pool的脏页要刷盘时,数据页的空间为16KB,OS文件系统的页空间一般为4KB,磁盘的扇区每片一般为512B,最终都会一片片的刷扇区。计算机硬件和操作系统,在极端情况下(比如断电)往往并不能保证这一操作的原子性,如果16KB的数据在写入4KB时发生了系统断电/os crash,只有一部分写是成功的,这种情况写就是partial page write。
mysql在恢复的过程中是检查页的checksum(页的校验和,见上文),发生partial page write问题时, Page已经损坏,找不到该页的checksum,就无法通过redolog恢复。
因此根据上述问题,InnoDB将buffer pool中的脏页刷盘时,会先通过memcpy函数将Page刷到double write buffer,再将数据拷贝到数据文件对应的位置。
double write buffer是物理磁盘上共享表空间中连续的128个页(每页16KB,大小共2MB, 每次写入1MB)。
- 如果写double write buffer失败,那么这些数据不会刷盘,InnoDB会载入磁盘原始数据和redo日志比较,并重新刷到double write buffer,然后再刷盘。
- 如果写double write buffer成功,但是刷盘失败(partial page write问题),那么InnoDB就不会通过事务日志来恢复了,而是直接用double write buffer中的数据刷盘。
redolog持久化过程
redolog包括两部分:一是内存中的日志缓冲(log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redolog file,以ib_logfile0、ib_logfile1…命名),该部分日志是持久的。
redolog可以通过参数InnoDB_log_files_in_group配置成多个文件(最大100),另外一个参数InnoDB_log_file_size表示每个文件的大小,因此总的redolog大小为InnoDB_log_files_in_group * InnoDB_log_file_size。
上文讲到内存中log buffer是由多个redolog block组成的(日志块头占12B、日志块尾占4B),那么redolog file也是如此,每个redolog file的前4个block用于表示文件头,存储了一些管理信息,往后则存储log buffer中的block镜像。文件头主要存储了标记redolog file开始的LSN值(Log Sequence Number的简称)、标记redolog已刷盘的全局变量flushed_to_disk_lsn值、标记脏页已刷盘的全局变量checkpoint_lsn等。
- LSN:LSN记录了已经写入的redolog的日志量,是一个全局变量,初始值为8704。每次写入一个mtr时,LSN就会累加上mtr所占的空间字节数和相应的block头尾空间字节数。比如mtr_1产生的redolog为200B,那么LSN就变成了8704+12+200=8916,之后mtr_2又产生了1000B的redolog,那么LSN就变成了8916+296+4+512+12+208=9948。
- flushed_to_disk_lsn:系统第一次启动时,flushed_to_disk_lsn值和初始的LSN值是相同的,都是8704。随着系统的运行,redolog被不断写入log buffer,但是并不会立即刷盘,LSN的值就和flushed_to_disk_lsn的值拉开了差距,如果两者的值相同时,说明log buffer中的所有redolog都已经刷盘了。
- checkpoint_lsn:checkpoint_lsn的初始值也是8704,当flush链表中的脏页按顺序被刷盘时,mtr生成的对应redolog就可以被覆盖了,所以我们可以进行一个增加checkpoint_lsn的操作,我们把这个过程称之为做一次checkpoint。脏页是与redolog有关联的,记录了redolog的LSN信息,通过脏页可以找到对应的redolog,通过redolog也可以恢复对应的脏页。
下图展示了一组4个文件的redolog日志,checkpoint_lsn之前的空间表示可以进行写的文件。
我们再看下log buffer刷盘的具体过程:
- 客户端向数据库发送写命令。
- 数据库收到写命令。
- 数据库通过系统调用将数据写入内核缓冲区(Page Cache)。
- 操作系统将缓冲区数据传输至磁盘控制器,暂存在磁盘缓冲区。
- 磁盘控制器将数据精准的写入物理磁盘。
如果数据库停机,那么第三步之后操作系统可以保证数据写入磁盘;如果是操作系统停机,此时磁盘也无法正常工作,那就必须完成这五步才能保证数据落盘。
如上所述,在将写操作写入redolog的过程中也不是直接就进行磁盘IO来完成的,而是分为三个步骤:
- 写入log buffer中,这部分是属于MySQL的内存中,是全局公用的。
- 在事务编写完成后,就可以执行write操作,写到文件系统的Page Cache中。
- 执行fsync(持久化)操作,将Page Cache中的数据正式写入磁盘上的redolog文件中,也就是图中的hard disk。
InnoDB_flush_log_at_trx_commit参数控制了log buffer的刷盘时机(值可为0、1、2,默认1):
- 设置为0:每隔1秒从log buffer写入Page cache,并马上刷盘,mysql服务故障或者主机宕机则丢失1秒(由log buffer的innodb_flush_log_at_timeout参数控制)数据。
- 设置为1:事务提交时,立刻从log buffer写入Page cache, 并马上刷盘,mysql服务故障或者主机宕机不会丢失数据,但会频繁发生磁盘IO。
- 设置为2:事务提交时,立刻从log buffer写入Page cache,每隔1秒刷盘,mysql服务故障不会丢失数据,因为数据已经进入操作系统缓存,与mysql进程无关了,主机宕机则丢失1秒数据。
除此之外,当log buffer空间不足、做checkpoint、Mysql正常关闭、binlog切换等情况也会触发redolog刷盘。刷盘操作是异步IO,由专门的线程完成这件事,不会阻塞用户请求的处理。redolog如果没有及时刷盘或者只刷盘一部分,是会导致事务丢失的。
undolog持久化过程
InnoDB的undolog严格的讲不是Log,而是数据,因此他的管理和落盘都跟数据一样:
- undolog的磁盘结构并不是顺序的,而是像数据一样按Page管理。
- undolog写入时,也像数据一样产生对应的redolog。
- undolog的Page也像数据一样缓存在Buffer Pool中,跟数据Page一起做lru换入换出,以及刷脏。undo page的刷脏也像数据一样要等到对应的redolog落盘之后。
之所以这样实现,首要的原因是undolog需要承担MVCC对历史版本的管理作用,设计目标是高事务并发,方便的管理和维护,因此当做数据更合适。
binlog持久化过程
binlog也有独立的刷盘策略,通过sync_binlog参数控制(值分别为0、1、N,默认为1):
- 设置为0 :每次提交事务都只将binlog cache进行write,不fsync。
- 设置为1 :每次提交事务都会将binlog cache进行write,并执行fsync。
- 设置为N :表示每次提交事务都会将binlog cache进行write,但累积N个事务后才fsync。
由于binlog是属于MySQL Server层面的日志,只需追加写入即可。