checkpoint产生的背景
如果执行一条DML语句,如update或者delete改变了页中的记录,那么此时页是脏的,即缓冲池中的页的版本要比磁盘的新,数据库需要将新的版本的页从缓冲池刷新到磁盘
倘若每次一个页的变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的,若热点数据集中在某几个页中,那么数据库的性能就会变得非常差。同时,如果在从缓冲池将页的的新版本刷新到磁盘时发生了宕机,那么数据就不能恢复了,为了避免这种情况,当前事务数据库系统普遍都采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页,当由于发生宕机而导致数据丢失时,可以通过重做日志来完成数据的恢复。这也是事务ACID中D(Durability持久性)的要求。
redo log 物理格式的日志,记录的是物理数据页的修改的信息,redo log是顺序写入redo log file的物理文件中去的。
- 物理层面看,这些日志都指明了对哪个表空间的哪个页进行了修改。
- 逻辑层面看,在系统奔溃重启时,并不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统奔溃前的样子。
如果做了一条插入操作。只是把在本页面中插入一条记录所有必备的要素记了下来,之后系统奔溃重启时,服务器会调用相关向某个页面插入一条记录的那个函数,而redo日志中的那些数据就可以被当成是调用这个函数所需的参数,在调用完该函数后,页面中的PAGE_N_DIR_SLOTS、PAGE_HEAP_TOP、PAGE_N_HEAP等等的值也就都被恢复到系统奔溃前的样子了。这就是所谓的逻辑日志的意思。
但是不停的执行增删改,MySQL会不停的产生大量的redo log写入日志文件,那么日志文件就用一个写入全部的redo log?对磁盘占用空间越来越大怎么办?
别担心,这些问题都可以解决,实际上默认情况下,redo log都会写入一个目录中的文件里,这个目录可以通过show variables like 'datadir'来查看,可以通过innodb_log_group_home_dir参数来设置这个目录的。
checkpoint的过程
然后redo log是有多个的,写满了一个就会写下一个redo log,而且可以限制redo log文件的数量,通过innodb_log_file_size可以指定每个redo log文件的大小,默认是48MB,通过innodb_log_files_in_group可以指定日志文件的数量,默认就2个。
所以默认情况下,目录里就两个日志文件,分别为ib_logfile0和ib_logfile1,每个48MB,最多就这2个日志文件,就是先写第一个,写满了写第二个。那么如果第二个也写满了呢?别担心,继续写第一个,覆盖第一个日志文件里原来的redo log就可以了。
所以最多这个redo log,mysql就给你保留了最近的96MB的redo log而已,不过这其实已经很多了,毕竟redo log真的很小,一条通常就几个字节到几十个字节不等,96MB足够你存储上百万条redo log了!
如果你还想保留更多的redo log,其实调节上述两个参数就可以了,比如每个redo log文件是96MB,最多保留100个redo log文件。下面图里,给大家展示出来了多个redo log文件循环写入的示意。
redo日志文件组容量是有限的,我们不得不选择循环使用redo日志文件组中的文件,但是这会造成最后写的redo日志与最开始写的redo日志追尾。
redo日志只是为了系统奔溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,也就是说即使现在系统奔溃,那么在重启后也用不着使用redo日志恢复该页面了,所以该redo日志也就没有存在的必要了,那么它占用的磁盘空间就可以被后续的redo日志所重用。
也就是说:判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。
孔乙己案例
不知道你还记不记得《孔乙己》这篇文章,酒店掌柜有一个粉板,专门用来记录客人的赊账记录。如果赊账的人不多,那么他可以把顾客名和账目写在板上。但如果赊账的人多了,粉板(redolog)总会有记不下的时候,这个时候掌柜一定还有一个专门记录赊账的账本(磁盘)。
如果有人要赊账或者还账的话,掌柜一般有两种做法:
一种做法是直接把账本翻出来,把这次赊的账加上去或者扣除掉; 另一种做法是先在粉板上记下这次的账,等打烊以后再把账本翻出来核算。 在生意红火柜台很忙时,掌柜一定会选择后者,因为前者操作实在是太麻烦了。首先,你得找到这个人的赊账总额那条记录。你想想,密密麻麻几十页,掌柜要找到那个名字,可能还得带上老花镜慢慢找,找到之后再拿出算盘计算,最后再将结果写回到账本上,相当于随机读写。
这整个过程想想都麻烦。相比之下,还是先在粉板上记一下方便,相当于顺序写。你想想,如果掌柜没有粉板的帮助,每次记账都得翻账本(查找旧记录),效率是不是低得让人难以忍受? 同样,在MySQL里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程IO成本、查找成本都很高。为了解决这个问题,MySQL的设计者就用了类似酒店掌柜粉板的思路来提升更新效率。
而粉板和账本配合的整个过程,其实就是MySQL里经常说到的WAL技术,WAL的全称是Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。
具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。
如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,粉板写满了,又怎么办呢?
这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。与此类似,InnoDB的redo日志文件是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么这块“粉板”总共就可以记录4GB的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的。
write pos和checkpoint都是从0号文件开始记录,write pos写完0号文件和1号文件,checkpoint在后面慢慢擦除, 擦除完0号文件开始擦除1号文件,此时粉板空着的部分是2 3 0号文件。
如果write pos写完2 3 0号文件,在1号文件追上checkpoint,代表此时粉板写满。
擦除记录前要把记录更新到数据文件即刷脏页,把脏页中的数据刷到对应的数据页中。
write pos和checkpoint之间的是粉板上还空着的部分(write pos 到3号文件末尾,再加上0号文件开头到checkpoint 的部分),可以用来记录新的操作。如果write pos追上checkpoint,表示粉板满了,追上的意思是write pos写完了4个文件并且到达了目前checkpoint所在的位置,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。要理解crash-safe这个概念,可以想想我们前面赊账记录的例子。
只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。
redolog日志文件
上面已经提到redo log 的空间是有限的,例如我们定义每个文件大小为1G,文件数量为4个,那么redo log 最多能存储4G 大小的内容。所以会存在一种情况:修改内容较多,且事务持续时间很长(所谓的大事务),就会导致redo log 写满而无法写入。
在一般情况下,redo log 又是怎么工作的呢?
redo log 是一个环,通过擦除没用的信息达到空间的重复利用。 我们知道redo log 是为了防止Buffer Pool中的脏页丢失而设计的,那么如果随着系统运行,将redologBuffer 中的脏页同步到redolog然后刷新到了磁盘中,那么redo log文件中 对应的记录也就没用了。 一次checkpoint 的过程就是脏页刷新到磁盘中变成干净页,将修改的记录保存在文件系统。然后标记redo log 哪些记录可以被覆盖的过程。
redo log设置多大
redo log太小的话,会导致很快就被写满,然后不得不强行刷redo log,这样WAL机制的能力就发挥不出来了。所以,如果是现在常见的几个TB的磁盘的话,就不要太小气了,直接将redo log设置为4个文件、每个文件1GB吧。
redo log产生时机
事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。
redo log释放时机
当事务(Transaction)需要修改某条记录(row)时,InnoDB需要将该数据所在的page从disk读到buffer pool中,事务提交后,InnoDB修改page中的记录(row)。这时buffer pool中的page就已经和disk中的不一样了,我们称buffer pool中的page为dirty page。
当对应事务的脏页写入到磁盘之后,redo log的使命也就完成了,重做日志占用的空间就可以重用即被覆盖
redo log是固定大小的,所以只能循环写,从头开始写,写到末尾就又回到开头,相当于一个环形。当日志写满了,就需要对旧的记录进行擦除,但在擦除之前,需要确保这些要被擦除记录对应在内存中的数据页都已经刷到磁盘中了。在redo log满了到擦除旧记录腾出新空间这段期间,是不能再接收新的更新请求,所以有可能会导致MySQL卡顿。(所以针对并发量大的系统,适当设置redo log的文件大小非常重要!!!)
checkpoint时机
checkpoint都是将buffer pool中的脏页刷新到磁盘,但是在不同的情况下,checkpoint会被以不同的方式触发,同时写入到磁盘的脏页的数量也不同。
Master Thread checkpoint
在Master Thread中,会以每秒或者每10秒一次的频率,将部分脏页从内存中刷新到磁盘,这个过程是异步的。正常的用户线程对数据的操作不会被阻塞。
FLUSH_LRU_LIST checkpoint
FLUSH_LRU_LIST checkpoint是在单独的page cleaner线程中执行的。MySQL对缓存的管理是通过buffer pool中的LRU列表实现的,LRU 空闲列表中要保留一定数量的空闲页面,来保证buffer pool中有足够的空闲页面来相应外界对数据库的请求。当这个空间页面数量不足的时候,发生FLUSH_LRU_LIST checkpoint。 空闲页的数量由innodb_lru_scan_depth参数表来控制的,因此在空闲列表页面数量少于配置的值的时候,会发生checkpoint,剔除部分LRU列表尾端的页面。
Async/Sync Flush checkpoint
Async/Sync Flush checkpoint是在单独的page cleaner线程中执行的。 Async/Sync Flush checkpoint 发生在重做日志不可用的时候,将buffer pool中的一部分脏页刷新到磁盘中,在脏页写入磁盘之后,事物对应的重做日志也就可以释放了。
关于redo_log文件的的大小,可以通过innodb_log_file_size来配置。
对于是执行Async Flush checkpoint还是Sync Flush checkpoint,由checkpoint_age以及async_water_mark和sync_water_mark来决定。 定义:checkpoint_age = redo_lsn-checkpoint_lsn,也即checkpoint_age等于最新的lsn减去已经刷新到磁盘的lsn的值 async_water_mark = 75% * innodb_log_file_size sync_water_mark = 90% * innodb_log_file_size
1)当checkpoint_age<sync_water_mark的时候,无需执行Flush checkpoint。也就说,redo log剩余空间超过25%的时候,无需执行Async/Sync Flush checkpoint。
2)当async_water_mark<checkpoint_age<sync_water_mark的时候,执行Async Flush checkpoint,也就说,redo log剩余空间不足25%,但是大于10%的时候,执行Async Flush checkpoint,刷新到满足条件1
3)当checkpoint_age>sync_water_mark的时候,执行sync Flush checkpoint。也就说,redo log剩余空间不足10%的时候,执行Sync Flush checkpoint,刷新到满足条件1。
在mysql 5.6之后,不管是Async Flush checkpoint还是Sync Flush checkpoint,都不会阻塞用户的查询进程。
个人认为: 由于磁盘是一种相对较慢的存储设备,内存与磁盘的交互是一个相对较慢的过程 由于innodb_log_file_size定义的是一个相对较大的值,正常情况下,由前面两种checkpoint刷新脏页到磁盘,在前面两种checkpoint刷新脏页到磁盘之后,脏页对应的redo log空间随即释放,一般不会发生Async/Sync Flush checkpoint。同时也要意识到,为了避免频繁低发生Async/Sync Flush checkpoint,也应该将innodb_log_file_size配置的相对较大一些。
Dirty Page too much Checkpoint
Dirty Page too much Checkpoint是在Master Thread 线程中每秒一次的频率实现的。 Dirty Page too much 意味着buffer pool中的脏页过多,执行checkpoint脏页刷入磁盘,保证buffer pool中有足够的可用页面。 Dirty Page 由innodb_max_dirty_pages_pct配置,innodb_max_dirty_pages_pct的默认值在innodb 1.0之前是90%,之后是75%。
总结
MySQL数据库(当然其他关系数据也有类似的机制),为了提高事物操作的效率,在事物提交之后并不会立即将修改后的数据写入磁盘,而是通过日志先行(write log ahead)的方式保证事物的持久性。 对于将事物修改的数据页面,也即脏页,通过异步的方式刷新到磁盘中,checkpoint正是实现这种异步刷新脏页到磁盘的实施者。 不同的情况下,会发生不同的checkpoint,将不同数量的脏页刷新到磁盘,从而到达管理内存(第1,2,4种checkpoint)和redo log可用空间(第3种checkpoint)的目的。
checkpoint的作用
缩短数据库的恢复时间
当数据库发生宕机时,数据库不需要重做所有的日志,因为checkpoint之前的页都已经刷新回磁盘。故数据库只需要对Checkpoint后的重做日志进行恢复。这样就大大缩短了恢复时间
缓冲池不够用时,刷脏页
此外,当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么就需要强制执行checkpiont,将脏页也就是页的新版本刷回磁盘
重做日志不可用,刷脏页
重做日志出现不可用的情况是因为当前事务数据库系统对重做日志的设计都是循坏使用的,并不是让其无限增大的,这从成本及管理上都是比较困难的,重做日志可以被重用的部分是只这些重做日志已经不再需要,即当数据库发生宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖使用。如此时重做日志还需要使用,那么必须强制产生checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。如下图
checkpoint机制
从官方提供的说明中checkpoint分为两个 :
Fuzzy checkpoint:进行部分脏页的刷新,有效循环利用Redo日志。
Sharp checkpoint:发生在关闭数据库时,将所有脏页刷回磁盘。
通过以上两个方式,在不同的情况下触发checkpoint。
对于MySQL的checkpoint机制来说,是对IO和内存做了平衡操作。
通过调节参数,对于不同的应用系统,都是提升性能的一种方式,普遍情况下采取默认方式。
另一个思路:重做日志可以无限增大,磁盘足够大,同时缓冲池足够大,能够缓存所有数据,那么就不需要将缓冲池中的脏页频繁刷新
参考:baijiahao.baidu.com/s?id=171020…