深入浅出mysql_全网最全MySQL多线程复制原理,深入浅出的进军数据库开发

前言

MySQL通过Binlog进行主从复制,一直是用户爱恨交加的一个实现方式。所谓爱,在于它维护容易、分析简单且架构设计可以变化多端,这在使用MySQL的过程中,可以发挥DBA的想象来解决各种各样的问题,所以受到了业界朋友的青睐。说到恨,有一个问题很是令DBA头疼,即主从复制延迟的问题。一般在问题出现时,DBA只能看着,一脸茫然,无法下手,只能静静地等着它追上来(当然也有一些方法,可以适当地提升其速度,但一般都是补救,不能将速度一下子提升几倍之多),这时DBA可能就会对它“恨铁不成钢”了吧。

64347b5f1dcd46b344c37bda5b0a5fdd.png

行之有效的延迟优化方法

那么在延迟的时候,如何适当地提升速度呢?一般有如下这些方式。

  • 增大从库参数innodb_buffer_pool_size 的值,可以缓存更多数据,减少由于转换导致的IO压力。
  • 增大参数innodb_log_file_size_innodb_log_files_in_group 的值,减少BUFFER POOL的刷盘IO,提升写入性能。
  • 修改参数innodb_fush_method 为O_DIRECT,提升写入性能(在ssd下,或者磁盘IO能力强的时候推荐使用)。
  • 如果可以的话,把从库Binlog 关掉,或者关掉参数log_slave_updates。
  • 修改参数innodb_flush_log at_trx_commit 为0或2。
  • 如果Binlog没有关掉,修改sync_binlog 参数为0或一个很大的数,减少磁盘I0压力。
  • 如果binlog_format 为ROW模式,并且被修改表没有主键,则需要加上主键。
  • 如果binlog_format为ROW模式,则可以在从库中删掉一些不必要的索引(同步完毕之后再加上)。
  • 了解清楚写库上的操作内容,适当地在从库中预热一下数据,可以减少在复制时等待的时间。
  • 如果binlog format为STATEMENT模式,或者存在DDL复制,则可以将tmpdir参数改到内存中,比如/dev/shm。
  • 修改参数master_info_repository_relay_log_info_ repository 为TABLE,,少直接IO导致的磁盘压力。
  • 将从库的服务迁走,当然这是指简单的处理,比如使VIP飘到其他节点上等。
  • 升级硬件,这种方法比较暴力,但一般情况下不太实际。
  • 如果当前版本是MySQL 5.6版,并且实例中数据库比较多,写人比较均匀,则可以打开多线程复制。
  • 升级成MySQL 5.7版吧。

MySQL 5.7的多线程复制

首先,MySQL 5.7的并行复制基于一个前提,即所有已经处于prepare阶段的事务,都是可以并行提交的。这些当然也可以在从库中并行提交,因为处理这个阶段的事务,都是没有冲突的,该获取的资源都已经获取了。反过来说,如果有冲突,则后来的会等已经获取资源的事务完成之后才能继续,故而不会进入prepare阶段。这是一种新的并行复制思路,完全摆脱了原来一直致力于为了防止冲突而做的分发算法、等待策略等复杂而又效率低下的工作,有一种“山重水复疑无路,柳暗花明又一村”的感觉。

根据以上描述,这里的重点是如何来定义哪些事务是处于prepare阶段的,以及在生成的Binlog内容中该如何告诉SLAVE哪些事务是可以并行复制的。MySQL5.7 为了兼容5.6版的库级复制,增加了一个参数slave_parallel type, 用来与之前5.6版的库级复制区分。5.6版的库级复制参数值为DATABASE,而5.7版的并行复制参数值为LOGICAL_CLOCK。

首先来看一下5.7版中生成的Binlog内容,如下图所示。

图中所示的只是将GTID事件过滤了出来,其他的和以前版本是一样的。可以看出,GTID这个事件相比5.6版本,多了如下两个内容。

30cd2f0930fe9b01cd1437143495853f.png
  • last_committed。
  • sequence_number 。

如上图图所示,last. committed有三个值,分别是0、1. 4,这就表示当前Binlog包括三个组。也就是说,last_committed中的每个值对应于一个组的编号。last_committed为4 (1) 的有三个事务,这三个事务在5.7版本中,就被定义为可以并行复制(提交)的,而sequence_number是顺序增长的,每一个事务对应一个序列号(sequence_number)。

另外,还有一个细节可能不太容易被发现,其实每一个组的last_committed 值,都是上一个组中事务的sequence_number 最大值,也是本组中事务sequence_number 最小值减1。同时这两个值的有效作用域都在文件内,只要换一个文件(Aush binary logs),这两个值就都会从0开始计数。

那么此时,还有一个很重要的技术问题一MySQL是如何做到将这些事务分组的呢?要搞清楚这个问题,首先需要了解一下MySQL的提交方式——ordered_commit。

int MYSQL_ BIN_LOG: :ordered_ commit(THD *thd, bool all, bool skip_commit)

先看他的逻辑图,如下图所示

2589e1384810b7f4ef140480cb2f2266.png

从上图中可以看到,只要事务提交(调用ordered. commit),就都会先加入队列中。而提交有三个步骤,包括FLUSH. SYNC及COMMIT,相应地也有三个队列。首先要加入的是FLUSH队列,如果某个事务加入时,队列还是空的,则这个事务就担任队长,来代表其他事务执行提交操作。而在其他事务继续加入时,就会发现此时队列已经不为空了,那么这些事务就会等待队长帮它们完成提交操作。

在上图中,事务2~6都是这种坐享其成之辈,事务1就是队长了。不过这里需要注意一点,不是说队长会一直等待要提交的事务不停地加入,而是有一个时限,只有在这个时限之内成功加入到队列的,才能帮它提交。这个时限就是从队长加入开始,到它去处理队列的时间,这个时间实际上非常小,基本上就是程序从这行到那行的一个过程,也没有刻意去等待。只要队长将这个队列中的事务取出,其他事务就可以加入这个队列了。第一个加入的还是队长,但此时必须要等待。因为此时有事务正在做FLUSH,做完FLUSH之后,其他的队长才能带着队员做FLUSH。而在同-个时刻,只能有一个组在做FLUSH。这就是上图中所示的等待事务组2和等待事务组3,此时队长会按照顺序依次做FLUSH,做FLUSH的过程中,有一些很重要的事务需要去做,如下。

  • 要保证顺序必须是提交加入到队列的顺序。
  • 如果有新的事务提交,此时队列为空,则可以加入到FLUSH队列中。不过,因为此时FLUSH临界区正在被占用,所以新事务组必须要等待。
  • 给每一个事务分配sequence_ number, 如果是第一个事务,则将这个组的last_committed设置为sequence_number-1。
  • 将带着last_committed 与sequence_number的GTID事件FLUSH到Binlog文件中。
  • 将当前事务所产生的Binlog内容FLUSH到Binlog文件中。

这样,一个事务的FLUSH就完成了。接下来,依次做完组内所有事务的FLUSH,然后做SYNC。如果SYNC的临界区是空的,则直接做SYNC操作,而如果已经有事务组在做,则必须要等待。同样地,做完FLUSH之后,FLUSH 临界区会空闲出来,那么此时在等待这个临界区的组就可以做FLUSH操作了。总而言之,每一个步骤都会有事务组在做,就像一个流水线-一样。完成-件产品需要三个工序,每个工序都可以批量来做,那么每个工序车间都不会闲着,都一直重复着相同的事情,最终每个产品都是以完全相同的顺序完成。

到COMMIT这道工序时,实际做的是存储引擎提交,参数binlog. order_ commits会影响提交行为。如果设置为ON,那么此时提交就变为串行操作了,就以队列的顺序为提交顺序。而如果设置为OFF,提交就不会在这里进行,而会在每个事务(包括队长及队员)做finish_commit (FINISH)时各自做存储引擎的提交操作。组内每个事务做finish_commit是在队长完成COMMIT工序之后进行,到步骤DONE时,便会唤醒每一个等待提交完成的事务,告诉它可以继续了,那么每个事务就会去做finish_commit。 而后,自己再去做finish_commit。

这样,一个组的事务就都按部就班地提交完成了。现在也可以知道,与这个组中同时在做提交的,最多还有另外两个事务,一个是在做FLUSH, 一个是在做SYNC。

多线程复制分发原理

知道了order commit原理之后,现在应该可以很容易想到,在从库端是如何分发的。从库是以事务为单位做APPLY的,每一个 事务有一个GTID事件,从而都有一个last_ committed 及sequence_ number 值,分发原理如下。

从库SQL线程拿到一个新事务,取出last_committed及sequence_number 值。

判断当前last_committed 是不是大于当前已经执行的sequence_number 的最小值(low water mark,下面称lwm)。

如果大于,则说明上一个组的事务还没有完成。此时等待lwm变大,直到last_committed与lwm相等,才可以继续。

如果小于或等于,则说明当前事务与正在执行的组是同一个组,不需要等待。

SQL线程通过统计,找到一个空闲的worker线程,如果没有空闲的,则SQL线程转入等待状态,直到找到一个为止。

将当前事务打包,交给选定的worker,之后worker线程会去APPLY这个事务,此时的SQL线程就会处理下一个事务。

当然,上面的步骤是以事务为单位介绍的,其实实际处理中还是一个事件一个事件地分发。如果一个事务已经选定了worker,而新的event还在那个事务中,则直接交给那个worker处理即可。

从上面的分发原理来看,同时执行的都是具有相同last_comitted 值的事务,不同的只是后面的需要等前面做完了才能执行,这样的执行方式有点如下图所示。

cb017e704a4fa49f81ac1e3371cb16f1.png

可以看出,事务都是随机分配到了worker线程中,但是执行的话,必须是一行一行地执行。一行事务个数越多,并行度越高,也说明主库瞬时压力越大。

异常故障恢复

上面已经讲清楚了MySQL 5.7版中的并行复制原理。但是,并行总是存在一个不可避免的问题,那就是在从库并行执行的过程中,如果数据库或操作系统挂了,那么此时每个线程执行的点就都是不确定的。也就是说,顺序的Binlog在被分发出去之后,从最小位置到最大位置之间这块连续的内容之间是存在断点的。如此一来, 从库恢复之后,开始执行时就需要准确无误地还原哪些已经执行,哪些还没有执行,这是这节需要解决的问题。

下图所示的情况,就是一个正在执行并行执行的事务队列,也可以说这些事务的last_com-mitted都是相同的,并且从前到后。此外,sequence_number 是顺序增长的,也就是说图中所示的是以Binlog文件内容的顺序排列的。

2ae31d759a1b77bed80fdb3da2526bea.png

上图所表示的就是整个队列被分配之后,在某一时刻,队列被执行的状态,假设此时从库挂了,那么再次启动之后,如何继续执行,下面就这个问题做详细描述。

MySQL为了实现对执行状态的记录,做了很多工作,首先就是维护一个队列,这个队列叫GAQ (Group Assigned Queue)。SQL线程在分配某一个事务时,首先会将这个事务加入到这个队列(队列如上图所示),之后,系统会将当前事务分发到一个线程来执行。可以想到,在某一个时刻,任务队列GAQ及每个线程的执行队列如下图所示。

7fd1a87d9aafa6a183d66bab11cb4519.png

每一个事务,在分发之后,都会有一个编号。这个编号在某一段时间内,都是相对固定的,如下图所示,每一个事务都有一个编号,编号只要被分配了,就不会再变。在事务获取编号且被所属线程执行之后,它的位置信息会被FLUSH一次,这与MySQL5.5版本中的relay_info 是类似的,用来存储从库执行的位置。但现在已经变为多线程了,那么每个线程执行到什么位置,是需要记录下来的,此时就用另一个表mysql.slave_worker_ jinfo 来存储。

每个线程在执行完-一个事务之后,会把执行位置信息写入slave_worker_info 表中。但是,GAQ队列长度是有限的,不可能一-直增长 下去(否则Checkpoint_ seqno就会一直增长下去)。所以必须要在这个队列中,找到一个位置,这个位置是GAQ的起点,并且之前的Binlog都是已经执行过的,而这个过程,就被称为做检查点。那么,多线程复制就可以归结为:检查点在GAQ中周而复始地向前推进复制位置,每个线程在不断地APPLYBinlog,并且通过Checkpoint group_ bitmap 记录已经执行的事务与最新检查点的相对位置。

其实从这个意义上讲,这里的检查点和日志检查点是一样的,其共同特性如下。

  • 有限的空间范围,必须要清出一些空间来,继续后面的工作。
  • 在最新检查点之前的位置,所有的工作都已经落地。
  • 在最新检查点之后的位置,都具有不确定性。
  • 需要不断地做,来保证操作不断地推进。

每个线程在接收到新的事务时,可以不断地执行下去,但总有一个位置,它之前所有的Binlog都已经执行过了。从库需要记录下这个点,以便在复制中断后,再次开始复制时,可以有一个合适的起点位置,这个就需要检查点来保证。

检查点执行过程的大概步骤如下。

在GAQ队列中,从队尾开始扫描,如果是已经执行过的事务,则直接将其从队列中删掉。

一直扫描GAQ,直到找到一个事务的状态为没有处理过,则停止扫描。

上面扫描到的最后一个事务被确定为检查点的最新位置,并且被标记为lwm(lowwaternark), 前面在讲事务分发的时候,已经讲过这个概念了。

将当前lwm这个事务对应的位置(master. log pos及relay_ log_ pos) 设置为此次检查点对应的位置。

通知每个线程执行自己的检查点,也就是更新每个线程的Checkpoint. seqno值。关于这个步骤有这样- -些说明: 如果不更新,则这个值的参照对象为上-一个检查点,而如果更新了,则其参照对象为当前最新检查点。因为这个原因,所以在slave_ worker_ jinfo 表中,每一个Checkpoint _seqno 的值其实都是相对每个线程自己的最新检查点而言的,而不一定是全局的最新检查点。更新与不更新的决定权,在于这个线程有没有再去做事务的提交操作,如果提交就会更新这个值。

好了,这就是笔者介绍的MySQL多线程复习内容,喜欢的朋友们请多多点赞评论转发,关注小笔,你们的支持就是笔者最大的动力~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值