主从复制原理
我们先来看下主从复制的原理:
当Master有数据改动之后,会将数据写入自己的bin log文件,Slave上会有一个I/O thread 线程,拉取Master的bin log文件,写入自己的Relay log(中继日志),再由Slave上的SQL thread来读取解析加载Relay log中的数据
延迟原因
我们按照上面原理图来分析下主从复制同步延迟耗时的原因:
- 从Master写入bin log 耗时
- Slave的I/O thread拉取Master bin log的网络耗时
- I/O thread写入Relay log的磁盘等待耗时
- SQL thread读取解析处理Relay log的耗时
具体到实际应用场景,还有哪些可能会造成同步延时呢:
- Slave的机器性能比Master的性能差很多,导致数据的产生和数据的消费速度相差很大,机器性能不足会影响Slave的同步效率
- Salve充当读库,一般情况下主要写的压力在主库,Salve会分摊一部分读的压力,但是如果Slave查询的压力过大,会消耗大量Salve的系统资源(cpu,磁盘I/O等),会影响数据同步
- 大事务执行,如果Master一个事务执行了5分钟,那bin log必须等事务执行完成之后,才会传入Slave,此时已经延迟了5分钟。
- Master是顺序写入bin log,Slave单线程去Master顺序读取bin log,Slave读取到bin log之后再本地执行。MySql的主从复制都是单线程操作,由于Master是顺序写,所以效率会很高。Slave也是顺序读取bin log,此时效率也会很高,但是数据拉取之后执行就变成了随机操作,此时的成本会很高。
对应于上面的步骤1,2,3,这些都是顺序写和顺序读,一般耗时会很低,执行的效率很高。但是对于步骤4,由于是随机操作,效率会很低。
解释一下为什么步骤4是随机操作:比如现在对orderid=1和orderid=100的记录进行更新。Master写入bin log的顺序是orderid=1,orderid=100,直接向后append就可以了,Salve读取也是这个顺序,但是在实际执行更新数据时,可能就需要先去磁盘A扇区去更新orderid=1,然后再去磁盘B扇区更新orderid=100,完全是随机操作,效率会很低。
- Slave同步的同时,可能跟其他查询线程发送锁竞争,也会发生锁等待而耗时
- 当Master的QPS并发非常高的时候,产生的DDL数量超过一个线程所能承受的范围时,也可能会带来延迟
- 在bin log日志传输时,如果网络宽带不是很好,由于网络延迟也可能造成数据同步延迟
数据一致性
我们先抛开其他外部因素,单从同步上来讲,最直接影响同步效率的便是第4步Slave解析执行sql的步骤,如果是单线程进行执行,由于是随机操作,效率得不到保障。如果换成多线程来执行会是什么情况呢?
这里面就会涉及到数据一致性问题了。
假设Master的SQL执行顺序是A->B ,先将一个数据列改成的10,如何再改成5。
假设现在Salve采用多线程方式进行解析执行,一个线程执行SQL A,一个线程执行SQL B。可能的结果是B先执行完,再执行A,最终的结果是先将数据改成5,再修改为10,这样同步完成后,Slave的数据与Master数据时不一致的。
所以,同步时,Master如何执行,Slave也必须如何执行,必须有顺序保障。
组提交
综上,为了解决同步延迟问题,主要就是解决第4步随机问题。单线程执行必然是不合适的,多线程情况下需要解决数据一致性问题。所以在执行同步时,Slave执行的至少是以一个事务的维度进行操作,通过事务的方式,保证执行顺序和结果,与Master保持一致。当有多个事务一同提交、执行,这便就是组提交了。
将事务按组进行提交,此时就可以并发执行同步了,但是需要主要,一个组的事务不能相互干扰,需要是相互独立。
查看同步延迟
使用“show slave status”查看具体参数,有几个参数比较重要:
master_log_file:slave中的IO线程正读取的Master服务器二进制日志文件名称
read_master_log_pos:在当前Master服务器二进制日志中,slave中IO线程已经读取的位置
relay_log_file:sql线程当前正在读取和执行的中继文件的名称
relay_log_pos:在当前的中继日志中,sql线程已经读取和执行的位置
relay_master_log_file:由sql线程执行的包换多数近期事件的Master主服务器二进制日志文件名称
slave_io_runnig:IO线程是否被启动且成功连接到Master
slave_sql_running:sql线程是否被启动
seconds_behind_master:slave的sql线程和slave的IO线程之间的时间差距,以秒记
必须slave_io_runnig和slave_sql_running都是yes的时候,才能开始进行主从同步。seconds_behind_master这个参数就表示当前slave和master同步延迟了多长时间,那么这个时间是怎么计算出来的呢?
我们来看下几个关键的时间点:
- Master执行完事务,写入binlog,此时这个时刻为T1
- 将binlog传给slave,slave接收完这个binlog的时刻记为T2
- slave执行事务完毕,这个时刻记为T3
所谓的主从同步延迟,就是同一个事务,在slave完成时间和master执行完的时间差值,也就是T3 - T1。seconds_behind_master在计算时也是按照这个方式,每个事务的binlog中都有一个时间字段,用于记录Master写入时间,slave取当前正在执行的事务时间,计算她和当前系统时间的差值,从而计算出seconds_behind_master。
解决延迟问题
- 采用分库架构,让不同业务分散到不同的数据库服务器上,减轻单台机器的压力
- 升级硬件,主频更高的cpu,更快的ssd等,提升服务器的性能,使用专线进行网络通信等
- 修改配置:
我们先理清楚binlog写入磁盘的流程:
每一个事务从开始执行到最后的提交,在内存中都会有自己的binlog cache,但是所有事务共同持有一份binlog ,事务commit完成之后,将各自的binlog数据append进binlog file,最后再由操作系统写入磁盘
所以MySql的刷盘流程,是先将每个事务的binlog 写入os buffer,也就是将日志写入文件系统的page cache,此时数据并没有持久化到磁盘,纯内存操作,速度非常快。
然后是fsync操作,从os buffer将数据持久化到磁盘。一般情况下,我们可以认为只有fsync操作才会占用磁盘的IOPS。
所以,如果我们一次积攒足够多的数据,然后统一提交一次fsync,相较于一有数据就执行fsync,效率会高出很多。
图中的write和fsync的写入时机,是由参数sync_binlog来控制的:
- 当sync_binlog=0,表示每次提交事务都只write,不fsync,等os buffer满了之后再自动进行fsync
- 当sync_binlog=1,表示每次事务提交都执行fsync
- 当sync_binlog=n,表示每次事务提交都write,但是在积累n个事务之后才fsnyc
在公司的一般实际应用中,这个参数会设置为1,这样能够保证数据的安全性。但是如果出现主从复制延迟问题,需要考虑将此值设置为n(需要按照实际业务量做配置),非常不建议设置为0,因为设置为0时,没有办法控制什么时候fsnyc到磁盘,在极端情况下,可能会造成很严重的数据丢失,比如积攒了1w条事务commit,此时os buffer还没有满,突然断电了,这些数据还没来得及fsnyc进磁盘,数据就丢失了,无法同步。设置为n时也会有数据丢失问题,但是相对来讲会可控一些。
如果slave机器后面没有再接slave机器,可以考虑禁用slave上的binlog。当slave在做数据同步的时候,slave上的binlog也会进行记录,这样也是会消耗IO资源的。
设置innodb_flush_log_at_trx_commit属性
在前面的章节中,我们有讲过,在innodb存储引擎中,redo log 和binlog共同组成了两阶段提交,是MySql支持事务的基础。innodb_flush_log_at_trx_commit这个属性用来表示每一次的事务提交是否需要将redo log写入磁盘,一共有三个值:
0:存储在当前线程缓存中(log buffer),每一秒由 InnoDB 的主线程执行一次刷盘
1:每一次事务提交都刷盘
2:每次事务提交都写到os buffer,每一秒执行一次刷盘
如果为了保证数据安全性,一般情况下建议数据库配置双1,即rodo log和binlog 都每一次提交都写入磁盘,数据安全性最高,但是由于每一次都会fsync刷盘,占用磁盘的IOPS,可能会影响到同步的效率,导致延迟变高。
对数据同步要求较高时,binlog配置为n,redo log配置为2(此时极端情况下会有1秒的数据丢失),这样可以在延迟和数据安全性上找到较好的平衡点。
- 不使用主从复制
并不是所有场景都适合使用主从复制,一般情况下,是读的场景要远多于写的场景,同时对读的数据时效性要求没那么高。如果实际业务中,当Master更新一笔数据之后,要立即从slave中读出,并且要求数据是最新更新后的,这种场景不适合主从复制,应该强制读取Master,完全避免同步延时是不可能的。 - 并行复制(MTS)
MySql 5.6版本开始引入了MTS,也就是并行复制。原来是单线程sql thread执行中继日志的数据同步,现在是由coordinator(协调器)负责读取日志信息以及事务分发,真正的日志执行由worker线程执行,多个线程并行。
“set global slave_parallel_workers=n” ,可以用设置有多少个worker并发执行,一般n可以设置为cpu的核心数。
加入我们设置为4,“show processlist”查看当前的运行列表 :
我们可以看到当前已经有了4个worker在等待coordinator分配任务。
现在问题来了,这4个worker如何保证同步完的slave和Master的数据一致性呢?
假设现在有2个事务,事务A将数列更新为10,事务B将数据列更新为5。Master执行顺序是A->B,同步时 coordinator将事务A,B分别分派给两个worker,worker1执行A,worker2执行B,由于是并发执行,worker2先完成,worker1后完成,那最终同步结果是先将数据列更新为5,再更新为10。此时问题来了,Master的结果是5,slave同步后结果是10,数据产生不一致。
同步时,我们可以按照库的维度同步,也可以按照表的维护同步,也可以按照行的维度同步,coordinator是怎么分配的呢?默认是一个worker处理一个库,可以通过设置属性的方式调整worker的工作粒度,“set global slave_parallel_type=‘logical_check’ ”,设置为按照行维护同步。
由上面的例子,我们可以得出coordinator在分配任务时,必须遵循下面两个原则:
- 更新同一行数据的不同事物,必须由一个worker执行
- 一个事务里面的所有动作,都必须由一个worker执行,不可拆开来由多个worker分别执行
MySql 5.7的MTS基于组提交实现
我们来看下事务的提交过程,1.redo log prepare阶段,将数据写入内存,2.将binlog写入内存 ,3.redo log prepare阶段,将内存中的数据刷盘,4.binlog进行刷盘,5.redo log commit阶段,更新事务的状态。
在这些事务的执行过程中,1,3,5 ; 2,4每个执行都是时间差,每个时间差之间,可以有n多个事务同时执行。
当Master同时有大量事务并发执行时,我们可以按照事务的执行阶段,对他们进行分组。
事务执行时,可以分为prepare阶段和commit阶段,将事务分组之后,每个组的redo log和bin log在prepare阶段和commit阶段,都是彼此独立的,此时可以按照组的维护分发给多个worker,实现在slave上的并发执行。具体如何实现分组,后续章节介绍。