1.3 MySQL笔记-日志
一条update sql执行(两阶段提交)
一.概念
-
WAL技术:WAL的全称是Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,是关系型数据库中用于实现事务性和持久性的一系列技术。简单来说就是,做一个操作之前先讲这件事情记录下来。
-
redo log:当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
-
binlog:MySQL整体来看,其实就有两块:一块是Server层,它主要做的是MySQL功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的粉板redo log是InnoDB引擎特有的日志,而Server层也有自己的日志,称为binlog(归档日志)。
因为最开始MySQL里并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统——也就是redo log来实现crash-safe能力。
redo log与binlog的差别:
-
redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
-
redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。
-
redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
二.数据库崩溃恢复
2.1知识储备
-
binlog采用“追加写”的形式,如果你的DBA承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
-
两阶段提交:如上图所示,update语句,先查询这一行;然后更新;然后写redo log 处于prepare阶段;然后写binlog日志;然后commit;
2.2崩溃如何恢复
-
首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
-
然后,从备份的时间点开始,将备份的binlog依次取出来,重放到中午误删表之前的那个时刻。
2.3crash-safe
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe
如果完整,提交事务;否则回滚
如果在图中时刻A的地方,也就是写入redo log 处于prepare阶段之后、写binlog之前,发生了崩溃(crash),由于此时binlog还没写,redo log也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog还没写,所以也不会传到备库。
在时刻B,也就是binlog写完,redo log还没commit前发生crash,那崩溃恢复的时候MySQL会怎么处理?
我们先来看一下崩溃恢复时的判断规则。
-
如果redo log里面的事务是完整的,也就是已经有了commit标识,则直接提交;
-
如果redo log里面的事务只有完整的prepare,则判断对应的事务binlog是否存在并完整:
a. 如果是,则提交事务;
b. 否则,回滚事务。
这里,时刻B发生crash对应的就是2(a)的情况,崩溃恢复过程中事务会被提交。
三.日志写入机制
|
0 |
1 |
2 |
备注 | |
|---|---|---|---|---|
|
redo log innodb_flush_log_at_trx_commit |
每次事务提交时都只是把redo log留在redo log buffer中; |
每次事务提交时都将redo log直接持久化到磁盘; |
每次事务提交时都只是把redo log写到page cache。 |
通常我们说MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog。 |
|
binlog sync_binlog |
每次提交事务都只write,不fsync; |
每次提交事务都会执行fsync; |
n 表示每次提交事务都write,但累积N个事务后才fsync。 |
3.1redo log写入机制
1.存在redo log buffer中,物理上是在MySQL进程内存中,就是图中的红色部分;
2.写到磁盘(write),但是没有持久化(fsync),物理上是在文件系统的page cache里面,也就是图中的黄色部分;
3.持久化到磁盘,对应的是hard disk,也就是图中的绿色部分。
日志写到redo log buffer是很快的,wirte到page cache也差不多,但是持久化到磁盘的速度就慢多了。
为了控制redo log的写入策略,InnoDB提供了innodb_flush_log_at_trx_commit参数,它有三种可能取值:
-
设置为0的时候,表示每次事务提交时都只是把redo log留在redo log buffer中;
-
设置为1的时候,表示每次事务提交时都将redo log直接持久化到磁盘;
-
设置为2的时候,表示每次事务提交时都只是把redo log写到page cache。
InnoDB有一个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘。
注意,事务执行中间过程的redo log也是直接写在redo log buffer中的,这些redo log也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的redo log,也是可能已经持久化到磁盘的。
实际上,除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的redo log写入到磁盘中。
-
一种是,redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是write,而没有调用fsync,也就是只留在了文件系统的page cache。
-
另一种是,并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘。假设一个事务A执行到一半,已经写了一些redo log到buffer中,这时候有另外一个线程的事务B提交,如果innodb_flush_log_at_trx_commit设置的是1,那么按照这个参数的逻辑,事务B要把redo log buffer里的日志全部持久化到磁盘。这时候,就会带上事务A在redo log buffer里的日志一起持久化到磁盘。
WAL技术,WAL的全称是Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,是关系型数据库中用于实现事务性和持久性的一系列技术。简单来说就是,做一个操作之前先讲这件事情记录下来。
具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos和checkpoint之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果write pos追上checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
3.2 binlog写入机制
binlog的写入逻辑比较简单:事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。
一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了binlog cache的保存问题。
系统给binlog cache分配了一片内存,每个线程一个,参数 binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
事务提交的时候,执行器把binlog cache里的完整事务写入到binlog中,并清空binlog cache。状态如图1所示。
图1 binlog写盘状态
可以看到,每个线程有自己binlog cache,但是共用同一份binlog文件。
-
图中的write,指的就是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快。
-
图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS。
write 和fsync的时机,是由参数sync_binlog控制的:
-
sync_binlog=0的时候,表示每次提交事务都只write,不fsync;
-
sync_binlog=1的时候,表示每次提交事务都会执行fsync;
-
sync_binlog=N(N>1)的时候,表示每次提交事务都write,但累积N个事务后才fsync。
因此,在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成0,比较常见的是将其设置为100~1000中的某个数值。
但是,将sync_binlog设置为N,对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog日志。
四.日志详解
这里需要说明的是,我们介绍两阶段提交的时候说过,时序上redo log先prepare, 再写binlog,最后再把redo log commit。
如果把innodb_flush_log_at_trx_commit设置成1,那么redo log在prepare阶段就要持久化一次,因为有一个崩溃恢复逻辑是要依赖于prepare 的redo log,再加上binlog来恢复的。(如果你印象有点儿模糊了,可以再回顾下第15篇文章中的相关内容)。
每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB就认为redo log在commit的时候就不需要fsync了,只会write到文件系统的page cache中就够了。
通常我们说MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog。
这时候,你可能有一个疑问,这意味着我从MySQL看到的TPS是每秒两万的话,每秒就会写四万次磁盘。但是,我用工具测试出来,磁盘能力也就两万左右,怎么能实现两万的TPS?
解释这个问题,就要用到组提交(group commit)机制了。
这里,我需要先和你介绍日志逻辑序列号(log sequence number,LSN)的概念。LSN是单调递增的,用来对应redo log的一个个写入点。每次写入长度为length的redo log, LSN的值就会加上length。
LSN也会写到InnoDB的数据页中,来确保数据页不会被多次执行重复的redo log。关于LSN和redo log、checkpoint的关系,我会在后面的文章中详细展开。
如图3所示,是三个并发事务(trx1, trx2, trx3)在prepare 阶段,都写完redo log buffer,持久化到磁盘的过程,对应的LSN分别是50、120 和160。
图3 redo log 组提交
从图中可以看到,
-
trx1是第一个到达的,会被选为这组的 leader;
-
等trx1要开始写盘的时候,这个组里面已经有了三个事务,这时候LSN也变成了160;
-
trx1去写盘的时候,带的就是LSN=160,因此等trx1返回时,所有LSN小于等于160的redo log,都已经被持久化到磁盘;
-
这时候trx2和trx3就可以直接返回了。
所以,一次组提交里面,组员越多,节约磁盘IOPS的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。
在并发更新场景下,第一个事务写完redo log buffer以后,接下来这个fsync越晚调用,组员可能越多,节约IOPS的效果就越好。
为了让一次fsync带的组员更多,MySQL有一个很有趣的优化:拖时间。在介绍两阶段提交的时候,我曾经给你画了一个图,现在我把它截过来。
图4 两阶段提交
图中,我把“写binlog”当成一个动作。但实际上,写binlog是分成两步的:
-
先把binlog从binlog cache中写到磁盘上的binlog文件;
-
调用fsync持久化。
MySQL为了让组提交的效果更好,把redo log做fsync的时间拖到了步骤1之后。也就是说,上面的图变成了这样:
图5 两阶段提交细化
这么一来,binlog也可以组提交了。在执行图5中第4步把binlog fsync到磁盘时,如果有多个事务的binlog已经写完了,也是一起持久化的,这样也可以减少IOPS的消耗。
不过通常情况下第3步执行得会很快,所以binlog的write和fsync间的间隔时间短,导致能集合到一起持久化的binlog比较少,因此binlog的组提交的效果通常不如redo log的效果那么好。
如果你想提升binlog组提交的效果,可以通过设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count来实现。
-
binlog_group_commit_sync_delay参数,表示延迟多少微秒后才调用fsync;
-
binlog_group_commit_sync_no_delay_count参数,表示累积多少次以后才调用fsync。
这两个条件是或的关系,也就是说只要有一个满足条件就会调用fsync。
所以,当binlog_group_commit_sync_delay设置为0的时候,binlog_group_commit_sync_no_delay_count也无效了。
之前有同学在评论区问到,WAL机制是减少磁盘写,可是每次提交事务都要写redo log和binlog,这磁盘读写次数也没变少呀?
现在你就能理解了,WAL机制主要得益于两个方面:
-
redo log 和 binlog都是顺序写,磁盘的顺序写比随机写速度要快;
-
组提交机制,可以大幅度降低磁盘的IOPS消耗。
分析到这里,我们再来回答这个问题:如果你的MySQL现在出现了性能瓶颈,而且瓶颈在IO上,可以通过哪些方法来提升性能呢?
针对这个问题,可以考虑以下三种方法:
-
设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
-
将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。
-
将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。
我不建议你把innodb_flush_log_at_trx_commit 设置成0。因为把这个参数设置成0,表示redo log只保存在内存中,这样的话MySQL本身异常重启也会丢数据,风险太大。而redo log写到文件系统的page cache的速度也是很快的,所以将这个参数设置成2跟设置成0其实性能差不多,但这样做MySQL异常重启时就不会丢数据了,相比之下风险会更小。
五.具体案例
在两阶段提交的不同瞬间,MySQL如果发生异常重启,是怎么保证数据完整性的?
现在,我们就从这个问题开始吧。
如果在图中时刻A的地方,也就是写入redo log 处于prepare阶段之后、写binlog之前,发生了崩溃(crash),由于此时binlog还没写,redo log也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog还没写,所以也不会传到备库。到这里,大家都可以理解。
大家出现问题的地方,主要集中在时刻B,也就是binlog写完,redo log还没commit前发生crash,那崩溃恢复的时候MySQL会怎么处理?
我们先来看一下崩溃恢复时的判断规则。
-
如果redo log里面的事务是完整的,也就是已经有了commit标识,则直接提交;
-
如果redo log里面的事务只有完整的prepare,则判断对应的事务binlog是否存在并完整:
a. 如果是,则提交事务;
b. 否则,回滚事务。
这里,时刻B发生crash对应的就是2(a)的情况,崩溃恢复过程中事务会被提交。
现在,我们继续延展一下这个问题。
追问1:MySQL怎么知道binlog是完整的?
回答:一个事务的binlog是有完整格式的:
-
statement格式的binlog,最后会有COMMIT;
-
row格式的binlog,最后会有一个XID event。
另外,在MySQL 5.6.2版本以后,还引入了binlog-checksum参数,用来验证binlog内容的正确性。对于binlog日志由于磁盘原因,可能会在日志中间出错的情况,MySQL可以通过校验checksum的结果来发现。所以,MySQL还是有办法验证事务binlog的完整性的。
追问2:redo log 和 binlog是怎么关联起来的?
回答:它们有一个共同的数据字段,叫XID。崩溃恢复的时候,会按顺序扫描redo log:
-
如果碰到既有prepare、又有commit的redo log,就直接提交;
-
如果碰到只有parepare、而没有commit的redo log,就拿着XID去binlog找对应的事务。
追问3:处于prepare阶段的redo log加上完整binlog,重启就能恢复,MySQL为什么要这么设计?
回答:其实,这个问题还是跟我们在反证法中说到的数据与备份的一致性有关。在时刻B,也就是binlog写完以后MySQL发生崩溃,这时候binlog已经写入了,之后就会被从库(或者用这个binlog恢复出来的库)使用。
所以,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。
追问4:如果这样的话,为什么还要两阶段提交呢?干脆先redo log写完,再写binlog。崩溃恢复的时候,必须得两个日志都完整才可以。是不是一样的逻辑?
回答:其实,两阶段提交是经典的分布式系统问题,并不是MySQL独有的。
如果必须要举一个场景,来说明这么做的必要性的话,那就是事务的持久性问题。
对于InnoDB引擎来说,如果redo log提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果redo log直接提交,然后binlog写入的时候失败,InnoDB又回滚不了,数据和binlog日志又不一致了。
两阶段提交就是为了给所有人一个机会,当每个人都说“我ok”的时候,再一起提交。
追问5:不引入两个日志,也就没有两阶段提交的必要了。只用binlog来支持崩溃恢复,又能支持归档,不就可以了?
回答:这位同学的意思是,只保留binlog,然后可以把提交流程改成这样:… -> “数据更新到内存” -> “写 binlog” -> “提交事务”,是不是也可以提供崩溃恢复的能力?
答案是不可以。
如果说历史原因的话,那就是InnoDB并不是MySQL的原生存储引擎。MySQL的原生引擎是MyISAM,设计之初就有没有支持崩溃恢复。
InnoDB在作为MySQL的插件加入MySQL引擎家族之前,就已经是一个提供了崩溃恢复和事务支持的引擎了。
InnoDB接入了MySQL后,发现既然binlog没有崩溃恢复的能力,那就用InnoDB原有的redo log好了。
而如果说实现上的原因的话,就有很多了。就按照问题中说的,只用binlog来实现崩溃恢复的流程,我画了一张示意图,这里就没有redo log了。
图2 只用binlog支持崩溃恢复
这样的流程下,binlog还是不能支持崩溃恢复的。我说一个不支持的点吧:binlog没有能力恢复“数据页”。
如果在图中标的位置,也就是binlog2写完了,但是整个事务还没有commit的时候,MySQL发生了crash。
重启后,引擎内部事务2会回滚,然后应用binlog2可以补回来;但是对于事务1来说,系统已经认为提交完成了,不会再应用一次binlog1。
但是,InnoDB引擎使用的是WAL技术,执行事务的时候,写完内存和日志,事务就算完成了。如果之后崩溃,要依赖于日志来恢复数据页。
也就是说在图中这个位置发生崩溃的话,事务1也是可能丢失了的,而且是数据页级的丢失。此时,binlog里面并没有记录数据页的更新细节,是补不回来的。
你如果要说,那我优化一下binlog的内容,让它来记录数据页的更改可以吗?但,这其实就是又做了一个redo log出来。
所以,至少现在的binlog能力,还不能支持崩溃恢复。
追问6:那能不能反过来,只用redo log,不要binlog?
回答:如果只从崩溃恢复的角度来讲是可以的。你可以把binlog关掉,这样就没有两阶段提交了,但系统依然是crash-safe的。
但是,如果你了解一下业界各个公司的使用场景的话,就会发现在正式的生产库上,binlog都是开着的。因为binlog有着redo log无法替代的功能。
一个是归档。redo log是循环写,写到末尾是要回到开头继续写的。这样历史日志没法保留,redo log也就起不到归档的作用。
一个就是MySQL系统依赖于binlog。binlog作为MySQL一开始就有的功能,被用在了很多地方。其中,MySQL系统高可用的基础,就是binlog复制。
还有很多公司有异构系统(比如一些数据分析系统),这些系统就靠消费MySQL的binlog来更新自己的数据。关掉binlog的话,这些下游系统就没法输入了。
总之,由于现在包括MySQL高可用在内的很多系统机制都依赖于binlog,所以“鸠占鹊巢”redo log还做不到。你看,发展生态是多么重要。
追问7:redo log一般设置多大?
回答:redo log太小的话,会导致很快就被写满,然后不得不强行刷redo log,这样WAL机制的能力就发挥不出来了。
所以,如果是现在常见的几个TB的磁盘的话,就不要太小气了,直接将redo log设置为4个文件、每个文件1GB吧。
追问8:正常运行中的实例,数据写入后的最终落盘,是从redo log更新过来的还是从buffer pool更新过来的呢?
回答:这个问题其实问得非常好。这里涉及到了,“redo log里面到底是什么”的问题。
实际上,redo log并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页,也就不存在“数据最终落盘,是由redo log更新过去”的情况。
-
如果是正常运行的实例的话,数据页被修改以后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的数据页写盘。这个过程,甚至与redo log毫无关系。
-
在崩溃恢复场景中,InnoDB如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让redo log更新内存内容。更新完成后,内存页变成脏页,就回到了第一种情况的状态。
追问9:redo log buffer是什么?是先修改内存,还是先写redo log文件?
回答:这两个问题可以一起回答。
在一个事务的更新过程中,日志是要写多次的。比如下面这个事务:
begin; insert into t1 ... insert into t2 ... commit;
这个事务要往两个表中插入记录,插入数据的过程中,生成的日志都得先保存起来,但又不能在还没commit的时候就直接写到redo log文件里。
所以,redo log buffer就是一块内存,用来先存redo日志的。也就是说,在执行第一个insert的时候,数据的内存被修改了,redo log buffer也写入了日志。
但是,真正把日志写到redo log文件(文件名是 ib_logfile+数字),是在执行commit语句的时候做的。
(这里说的是事务执行过程中不会“主动去刷盘”,以减少不必要的IO消耗。但是可能会出现“被动写入磁盘”,比如内存不够、其他事务提交等情况。这个问题我们会在后面第22篇文章《MySQL有哪些“饮鸩止渴”的提高性能的方法?》中再详细展开)。
单独执行一个更新语句的时候,InnoDB会自己启动一个事务,在语句执行完成的时候提交。过程跟上面是一样的,只不过是“压缩”到了一个语句里面完成。
以上这些问题,就是把大家提过的关于redo log和binlog的问题串起来,做的一次集中回答。如果你还有问题,可以在评论区继续留言补充。
浙公网安备 33010602011771号