文章

MySQL主从复制,以及数据一致性

MySQL主从复制,以及数据一致性

序言

如果没有主从复制,会面临什么样的问题?

比如ESRocketMQ的高可用,实现高可用的策略,就是主从复制。MySQL也要做高可用,既然做高可用,主从复制的环节就避免不了。主从复制的环境,就是主从集群的搭建。

Q:在MySQL主从复制的过程中,主从节点复制的是什么内容?

复制的不是具体数据,而是操作日志。

下面按照三大日志的脉络,简略看下Master节点上面事务提交的流程,以及Slave节点上面binlog同步的过程。

Desktop View Master节点上面事务提交的流程,Slave节点上面binlog同步的过程

  • 先看看事务从开始到提交过程中,三大日志的流程。数据从磁盘读入内存,先在内存中做数据更新,然后记录两大日志,一个是undolog,一个是redolog,等到提交事务时,做了两件事,一是记录binlog,一个是提交redolog
  • 在事务提交阶段,会写入binlog,如果从节点和主节点建立了复制关系,Master服务端会启动dump线程,Slave客户端会启动IO线程,两边建立连接,进行数据同步。
  • 从节点接收到binlog后,在从节点叫中继日志,SQL线程会对中继日志进行回放,形成sql再执行,生成实际上的数据。

一键搭建MySQL主从集群

docker

准备docker-compose.ymlMaster主配置文件my.cnfSlave从配置文件my.cnf

Desktop View docker-compose yaml

Desktop View Master主配置文件my.cnf

Desktop View Slave从配置文件my.cnf

创建对应的目录结构,为目录赋予权限后,直接启动MySQL服务主从集群。

1
2
3
4
5
6
7
8
9
$ cd /home/docker-compose/
$ chmod 777 -R /home/docker-compose/mysqlmasterslave
$ chmod 644 /home/docker-compose/mysqlmasterslave/master/my.cnf
$ chmod 644 /home/docker-compose/mysqlmasterslave/slave/my.cnf
$ chown -R mysql:mysql /home/docker-compose/mysqlmasterslave/slave/log
$ chown -R mysql:mysql /home/docker-compose/mysqlmasterslave/master/log
$ cd mysqlmasterslave
$ docker-compose --compatibility up -d
$ docker-compose  logs -f

查看启动的容器,如下,

Desktop View 运行起来的容器列表

从上面的yaml里,获取服务创建的网络名称,查询服务ip地址,如下,

Desktop View 服务网络拓扑

通过docker编排的方式,一键搭建MySQL高可用主从集群。如果不用docker编排的方式,就得一行行地手动敲shell命令来搭建,工作量非常大。当然对DBA另当别论,他们有很多工具可以方便的维护高可用集群,如果开发人员来弄这种环境,还是用docker编排比较方便,需要考虑的配置和因素也没有这么多,更重要地是为掌握主从复制、高可用这些基础原理做准备。

虽然服务起来,但是如果要做到主从复制的效果,还得对主从集群的MasterSlave进行配置。

配置Master

1
2
3
$ docker exec -it mysql-master bash
$ mysql -uroot -p123456
$ show variables like '%server_id%';

进入MySQL主服务,查看server_id是否生效,

Desktop View server id

1
$ show master status;

Master信息FilePosition, 从节点服务上要用,

Desktop View master status

root开远程权限,

1
2
3
$ GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456';  
$ grant replication slave,replication client on *.* to 'slave'@'%' identified by "123456";
$ flush privileges;

在主节点上,对从节点复制创建一个用户slave,配置上对应密码,这个用户账号有复制的权限,专门用来做主从同步的,

1
2
$ grant replication slave,replication client on *.* to 'slave'@'%' identified by "123456";
$ flush privileges;

Desktop View grant slave

配置Slave

1
2
3
$ docker exec -it mysql-slave bash
$ mysql -uroot -p123456
$ show variables like '%server_id%';

进入MySQL从服务,查看从节点server_id是否生效,

Desktop View slave server id

建立主从关系,从节点上通过在主节点建立的用户账号,连上去,开启主从同步机制(前面在主节点上,查看binlog的文件名和偏移量),

1
$ change master to master_host='mysql-master',master_user='slave',master_password='123456',master_port=3306,master_log_file='bin-log.000003',master_log_pos=1346,master_connect_retry=30;

连接主MySQL参数说明,

  • master_host:主节点的ip(或主机名称)
  • master_port:Master的端口号,指的是容器的端口号
  • master_user:用于数据同步的用户
  • master_password:用于同步的用户的密码
  • master_log_file:指定 Slave 从哪个日志文件开始复制数据,即上文中提到的 File 字段的值
  • master_log_pos:从哪个 Position 开始读,即上文中提到的 Position 字段的值
  • master_connect_retry:如果连接失败,重试的时间间隔,单位是秒,默认是60秒

在从库上,启动slave线程,

1
start slave;

Desktop View slave start

看下从节点状态,

1
$ show slave status \G;

Desktop View slave run ok

如果不配置change master to,就查不到slave status,执行start slave也会报错。

Desktop View start slave error

如果没执行start slave,查看从节点的状态,

Desktop View slave not running

Slave_IO_Running: No,说明从节点负责接收binlog的线程没有在运行,已经停下来了,主从关系现在有问题,检查下主节点的配置对不对。

如果主节点没正常授权slave,执行start slave也会报错error connecting to master 'slave@mysql-master:3306' - retry-time: 30  retries: 4

Desktop View start slave error

可以看到,主节点还没授权。

授权成功后,需要停止slave,并重置下slave

1
2
stop slave;
reset slave;

如果授权之后,没有重置,报错如下,

Desktop View start slave error

重置完之后,再看下从节点状态,可以看到接收binlog的线程起来了。

准备数据源

在主库创建表,确保从库也有以上两个表。这时,主库创建的表,会自动复制到从库。

只在主库执行创建,会看到从库也会同步生成。

Desktop View 主库建表

Desktop View 从库同步

把脉三大日志

下面看下MySQL三大日志,

  • binlog,归档日志,或者二进制日志
  • redolog,重做日志
  • undolog,回滚日志

binlog原理、使用场景、持久化策略

前面说过,在主从复制的过程中,主从节点复制的不是具体数据,而是操作日志。之前在配置主从节点时,在配置里也打开了log-bin选项,这样就可以把binlog持久化。

从节点开始同步时,即执行start slave,主从节点间就开始了日志的传输。从节点通过IO线程拿到数据,在从节点本地,就不叫binlog,而叫relay-log(中继日志),拿到中继日志后,通过SQL线程解析,重新执行这个SQL生成数据。

binlog的作用

  • 用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。
  • 用于基于时间点的数据还原,主要是用于增量数据还原。

binlog的使用场景

  • 平时会做数据备份,数据备份一般是周期性的备份,常用命令是mysqldump。假设第二天中午机器故障,MySQL崩溃,之前的备份数据还是昨天晚上,昨天晚上到今天中午没有来得及备份,但数据还是要恢复,恢复数据时分为两部分,一部分是把昨天晚上全量的数据导入进去,再把增量的数据倒进来。
  • 全量是每次周期性压缩成压缩包,需要时全量导入即可。
  • 增量的部分数据,就是在binlog里边,把binlog文件导出一个SQL,再把这个SQL导入到库里边。

binlog保存的内容,跟格式有关系,binlog有三种格式,分别是STATEMENTROWMIXED

  • STATEMENT模式:binlog里面记录的就是SQL语句的原文。优点是并不需要记录每一行的数据变化,减少了binlog日志量,节约IO,提高性能。缺点是在某些情况下会导致master-slave中的数据不一致,因为并没有记录修改之前的状态。
  • ROW模式:不记录每条SQL语句的上下文信息,仅需记录哪条数据被修改了,修改成什么样了,解决了STATEMENT模式下出现master-slave中的数据不一致。缺点是会产生大量的日志,尤其是alter table的时候会让日志暴涨,因为它会影响到所有的行。比如SQL里边只改了一个字段,在该模式下会把有变化的和没变化的一并记录了下来,即整个行都记录下来了,修改之前和修改后的都记录下来了,一行一行来,假如是一条update语句,命中改了3行就会记录3binlog
  • MIXED模式:折中方案,以上两种模式的混合使用,一般的复制使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlogMySQL会根据执行的SQL语句选择日志保存方式。

binlog持久化策略,可以用参数sync_binlog来配置,

  • 在进行事务的过程中,首先会把binlog写入到binlog cache中(因为写入到cache中会比较快,一个事务通常会有多个操作,避免每个操作都直接写磁盘导致性能降低),事务最终提交的时候再把binlog写入到磁盘中。当然事务在最终commit的时候,binlog是否马上写入到磁盘中是由参数sync_binlog(同步刷盘) 配置来决定的。
  • sync_binlog=0(不同步刷盘) 的时候(由后台线程异步刷盘),表示每次提交事务binlog不会马上写入到磁盘,而是先写到page cache(文件系统缓存),相对于磁盘写入来说,写page cache要快得多,但在异步刷盘的模式下,数据是不可靠的,在MySQL崩溃的时候会有丢失日志的风险。
  • sync_binlog=1(同步刷盘) 的时候,表示每次提交事务都会执行 fsync 写入到磁盘(binlog文件,文件目录在配置文件里配置)。
  • sync_binlog的值大于1(积累了n个事务才刷盘)的时候,表示每次提交事务都先写到page cache,只有等到积累了n个事务之后才fsync写入到磁盘,同样在此设置下MySQL崩溃的时候会有丢失n个事务日志的风险。

很显然三种模式下,sync_binlog=1是强一致的选择,选择0或者n的情况下,在极端情况下就会有丢失日志的风险,具体选择什么模式还是得看系统对于一致性的要求。

  • 对数据一致性要求很高,一点都不能丢失,就用同步刷盘;
  • 对数据可靠性要求不高,就用异步刷盘。

redolog原理、使用场景、持久化策略

redologundolog是和事务有关的,根本目的是保证事务的原子性和持久性,隔离性是通过锁来保障的。

  • redolog,就是在崩溃后,可以对提交的事务进行重做。
  • undolog,是在崩溃后,对回滚的事务做撤销。
  • binlog,不管有没有事务,MySQL都是有的。

MySQL中,事务和引擎有关,MySQL整体来看就有两块。一块是Server层,主要做的是MySQL功能层面的事情,比如binlogServer层自己的日志。还有一块是引擎层,负责存储相关的具体事宜,比如redologInnoDB引擎特有的日志。

MyISAMMySQL的默认数据库引擎(5.5版之前),由早期的ISAM所改良。虽然性能极佳,但却有一个缺点,不支持事务处理(transaction)。就是说MyISAM没有redologundolog。但不管是InnoDB,还是MyISAM,都有binlog

redolog能保证对于已经COMMIT的事务产生的数据变更,即使是系统宕机崩溃,也可以通过它来进行数据重做,达到数据的一致性,这也就是事务持久性的特征,一旦事务成功提交后,只要修改的数据都会进行持久化,不会因为异常、宕机而造成数据错误或丢失,所以解决异常、宕机而可能造成数据错误或丢失是redo log的核心职责。

redolog是怎样的保障机制?

WALWrite Ahead Log),预写日志(写前日志),当事务提交时,先写redolog,再修改页。

这和ES写日志完全相反,ES是反过来的,先写数据,后写日志。

保障数据能够有效的恢复,还有一个策略,就是两阶段提交。

通过预写和两阶段提交,来保障MySQL数据高可靠(事务的原子性和持久性)。

预写

看下流程,在事务里边修改数据,先把数据读到内存,先写redolog,然后再写数据(更新数据),写入日志和数据还不算,最后会提交日志(日志的写入分两阶段,准备阶段和提交阶段)。

先看写入的次序,数据和日志,谁先写?

先写日志,再写数据,就是所谓的预写日志。

为什么要做预写日志?

假设一个程序在执行某些操作的过程中,机器掉电了,在重新启动时,若是使用了WAL,程序就能够检查log文件,对忽然掉电时计划执行的操作内容,跟实际上执行的操作内容进行比较(看哪些数据丢失了,再补回来)。在这个比较的基础上,程序就能够决定,是撤销已做的操作,还是继续完成已做的操作,或者是保持原样。

具体到MySQL里边,当有一条记录需要更新的时候,InnoDB会先把记录写到redolog里面,并更新Buffer Poolpage(文件系统缓存里边的pageMySQL也有自己的page页,比如文件系统缓存里边的4K16K),这个时候更新操作就算完成了。

页面数据什么时候刷盘?这些页面会放在一个池子里边(Buffer Pool), Buffer Pool是物理页的缓存,对InnoDB的任何修改操作都会首先在Buffer Poolpage上进行,然后这样的页将被标记为脏页并被放到专门的Flush List上,后续将由专门的刷脏线程阶段性的将这些页面写入磁盘。

redolog的存储方式

不是滚动增长的方式,和RocketMQ的日志文件或者binlog不同,InnoDBredolog是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,循环使用,从头开始写,写到末尾就又回到开头循环写(顺序写,节省了随机写磁盘的IO消耗)。

里边有write_poscheck_point,简单理解下,

Desktop View redolog存储方式

check_point理解为尾巴,write_pos理解为头部,最开始时,这俩相等,每次增加,write_pos就往前推进,黄色部分是有redolog日志记录的。

redolog日志记录和数据之间,有什么关系呢?

这个图里只是日志,另外一部分是相关的数据,如果把数据更新到文件里边去之后,check_point就往前推动,也就是说黄色部分记录的是物理数据没有刷盘,如果物理数据刷盘了,标志位check_point会往后移动。这个环代表4G的日志,是在4个文件里边。

如果头部追上了尾巴,说明有很多MySQL的数据没有刷盘,全都在Buffer Pool(物理内存)里边,

redolog肯定不会每次都写文件,还有对应的缓存,写redolog时,不是立马就写到文件里边去了,InnoDB首先将redolog放入到redo log buffer,然后按一定频率将其刷新到redo log file。下列三种情况下会将redo log buffer刷新到redo log file

  • Master Thread每一秒将redo log buffer刷新到redo log file
  • 每个事务提交时会将redo log buffer刷新到redo log file
  • redolog缓冲池剩余空间小于1/2时,会将redo log buffer刷新到redo log file

两阶段提交

准备阶段、提交阶段

假设事务是执行一个更新语句,流程是怎样的?

举个例子,当我们执行update t_flow set c=c+1 where id=1;的时候大致流程如下,

  1. 从磁盘读取到id=1的记录,放到内存;
  2. 修改内存中的记录;
  3. 记录undo log日志;
  4. 记录redo logprepare预提交状态);
  5. 记录binlog
  6. 提交事务,写入redo logcommit状态)。

redolog,记录的是修改之后的值,用来重做,崩溃重启之后的重做。 undolog,记录的是修改之前的值,用来回滚,这里指的崩溃重启之后的回滚,不是每次回滚都会用它, 这俩次序不太分先后,三四步都是在内存里边进行的,这时做完预提交,还没有生效。 什么时候生效?要做一个提交的操作,提交事务时,先生成binlog,通过主从复制机制,还可以复制到从节点。binlog先写入内存,什么时候刷盘,由刷盘机制来确定。 写完binlogredolog进入第二阶段(提交阶段),使redolog生效,提交事务的工作就做完了。

两阶段提交的原因

两阶段提交,是为了binlogredolog两份日志之间的逻辑一致。 redologbinlog都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。 如果不用两阶段提交,那么有两种可能,要么就是先写完redolog,再写binlog,或者采用反过来的顺序。

为什么用两个阶段,而不是一个阶段就搞定? 因为有两个日志(binlogredolog),如果不用两阶段提交,就会有数据不一致。

update语句来做例子, 假设当前id=2的行,字段c的值是0,再假设执行update语句过程中,在写完第一个日志后,第二个日志还没有写完期间发生了crash崩溃,会出现什么情况呢?

  1. 先写redolog后写binlog 假设在redolog写完,binlog还没有写完的时候,MySQL进程异常重启。 由于redolog写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行c的值是1。 但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句。 因此,之后备份日志的时候,存起来的binlog里面就没有这条语句。 然后你会发现,如果需要用这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这一次更新,恢复出来的这一行c的值就是0,与原库的值不同。

  2. 先写binlog后写redolog 如果在binlog写完之后crash,由于redolog还没写,崩溃恢复以后这个事务无效,所以这一行c的值是0。 但是binlog里面已经记录了把c0改成1这个日志。 所以,在之后用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行c的值就是1,与原库的值不同。

如果不使用两阶段提交,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

使用两阶段提交,先写入redolog,再写binlog,写完binlog后,再标识redologcommit阶段(标识有效),这时binlog得到了一个保障,redolog也得到了一个保障,保证数据的一致性。

Desktop View 两阶段提交

redolog刷盘(写入)策略

也有一个参数来配置

redolog占用的空间是一定的,并不会无限增大(可以通过参数设置),写入的时候是顺序写的,所以写入的性能比较高。 当redolog空间满了之后,又会从头开始以循环的方式进行覆盖式的写入。 在写入redolog的时候也有一个redo log buffer,日志什么时候会刷到磁盘是通过innodb_flush_log_at_trx_commit参数决定。

  • innodb_flush_log_at_trx_commit=0,表示每次事务提交时,不刷盘,都只是把redolog留在redo log buffer中;
  • innodb_flush_log_at_trx_commit=1,表示每次事务提交时都将redolog直接持久化到磁盘;
  • innodb_flush_log_at_trx_commit=2,表示每次事务提交时都只是把redolog写到page cache

除了上面几种机制外,还有其它两种情况会把redo log buffer中的日志刷到磁盘,

  1. 定时处理:有线程会定时(每隔1秒)把redo log buffer中的数据刷盘。
  2. 根据空间处理:redo log buffer占用到了一定程度(innodb_log_buffer_size设置的值一半),这个时候也会把redo log buffer中的数据刷盘。

redolog & Write-Ahead的本质

Write-ahead,不是针对内存来说的,而是针对磁盘来说的。 不是说在内存里先写日志,再写数据。 在磁盘的角度,日志先落盘,数据再落盘。事务里边修改的实际业务数据的落盘。

这就回到了IO的本质,随机写,还是顺序写。 现在有两类数据,一个是日志数据,一个是业务数据, 业务数据在磁盘上是分散的,而日志数据写入是顺序的,随机写和顺序写在性能上能相差几百倍。

Write-ahead的本质,一个是顺序写提高性能,另一个是系统宕机,只要日志在,数据就可以恢复。为了提升性能,数据就干脆晚一点写,不是立即写入磁盘,而是由后台线程异步刷入磁盘。这就回到了三高里面的高性能架构部分。

提交事务时,把日志追加到redolog的尾部,所有的日志文件都是以追加的形式写入的。

一个事务要修改多张表的多条记录,多条记录分布在不同的Page里面,对应到磁盘的不同位置。如果每个事务都直接写磁盘,一次事务提交就要多次磁盘的随机IO,性能达不到要求。怎么办?

不写磁盘,在内存中进行事务提交。然后再通过后台线程,异步地把内存中的数据写入到磁盘中。 但有个问题,机器宕机,内存中的数据还没来得及刷盘,数据就丢失了。 为此,就有了Write-ahead Log的思路。

先在内存中提交事务,然后写日志(所谓的Write-ahead Log),然后后台任务把内存中的数据异步刷到磁盘。 是顺序地在尾部Append,从而也就避免了一个事务发生多次磁盘随机IO的问题。 明明是先在内存中提交事务,后写的日志,为什么叫作Write-Ahead呢? 这里的Ahead,其实是指相对于真正的数据刷到磁盘,因为是先写的日志,后把内存数据刷到磁盘,所以叫Write-Ahead Log

redolog和binlog日志的不同

redolog是物理记录,binlog是逻辑记录

binlog是逻辑记录,格式为row模式。比如update t_user set age =18 where name ='shouyuan',如果这条语句修改了三条记录的话,那么binlog记录就是,

1
2
3
UPDATE `db`.`t_user` WHERE @1=5 @2='shouyuan' @3=91 @4='1543571201' SET  @1=5 @2='shouyuan' @3=18 @4='1543571201'
UPDATE `db`.`t_user` WHERE @1=6 @2='shouyuan' @3=91 @4='1543571201' SET  @1=5 @2='shouyuan' @3=18 @4='1543571201'
UPDATE `db`.`t_user` WHERE @1=7 @2='shouyuan' @3=91 @4='1543571201' SET  @1=5 @2='shouyuan' @3=18 @4='1543571201'

redolog是物理记录,上面的修改语句,在redolog里面记录的可能就是下面的形式,

1
2
3
把表空间10、页号5、偏移量为10处的值更新为18。
把表空间11、页号1、偏移量为2处的值更新为18。
把表空间12、页号2、偏移量为9处的值更新为18。

redologInnoDB引擎特有的;binlogMySQLServer层实现的,所有引擎都可以使用;
redolog是物理日志,记录的是在磁盘上某个位置某个数据上做了什么修改;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如给id=2这一行的c字段加1
redolog是循环写的,空间固定会用完;binlog是可以追加写入的,binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

undolog原理、使用场景

回滚日志

  • 作用:保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读;
  • 内容:逻辑格式的日志,根据每行记录进行记录。

在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redolog的。

undologredolog的不同

  • undolog用于记录事务开始前的状态,用于事务失败时的回滚操作;
  • redolog记录事务执行后的状态,用来恢复未写入data file的已成功事务更新的数据。

例如,某一事务的事务序号为T1,其对数据c进行修改,假设c的原值是0,修改后的值为1,那么undolog<T1, c, 0>redolog<T1, c, 1>

redologundolog的核心是,为了保证InnoDB事务机制中的持久性和原子性,事务提交成功由redolog保证数据持久性,而事务可以进行回滚,来保证事务操作的原子性,即原子性是通过undolog来保证的。

要对事务数据回滚到历史的数据状态,所以我们也能猜到undolog是保存的是数据的历史版本,通过历史版本让数据在任何时候都可以回滚到某一个事务开始之前的状态。 undolog除了进行事务回滚的日志外还有一个作用,就是为数据库提供MVCC多版本数据读的功能。

binlog可以配置文件在哪,redolog可以配置一组是几个文件,undolog物理文件在哪?

  • MySQL5.6之前,文件目录不用配置,就是位于数据文件目录中,undo表空间位于共享表空间的回滚段中,共享表空间的默认的名称是ibdata,位于数据文件目录中。
  • MySQL5.6之后,undo表空间可以配置成独立的文件,但是提前需要在配置文件中配置,完成数据库初始化后生效且不可改变undolog文件的个数。

MySQL crash崩溃恢复

前面简单串一下三大日志的总体生成流程,

执行update t_flow set c=c+1 where id=1;的时候大致流程如下,

  1. 从磁盘读取到id=1的记录,放到内存;
  2. 修改内存中的记录;
  3. 记录undo log日志;
  4. 记录redo logprepare预提交状态);
  5. 记录binlog
  6. 提交事务,写入redo logcommit状态)。

我们根据上面的流程来看,如果在上面的某一个阶段MySQL数据库崩溃,如何恢复数据(保障数据的一致性)?

  1. 在第一步、第二步、第三步,执行时,数据库崩溃,因为这个时候数据还没有发生任何变化,所以没有任何影响,不需要做任何操作。
  2. 在第四步,修改内存中的记录时,数据库崩溃,因为此时事务没有commit,所以这里要进行数据回滚,所以这里会通过undolog进行数据回滚。
  3. 第五步,写入binlog时,数据库崩溃,这里和第四步一样的逻辑,此时事务没有commit,所以这里要进行数据回滚,会通过undolog进行数据回滚。 binlog不存在事务记录,那么这种情况事务还未提交成功,redolog也没有commit标记,所以会对数据进行回滚。
  4. 执行第六步,事务提交时,数据库崩溃,如果数据库在这个阶段崩溃,那其实事务还是没有提交成功,但是这里并不能像之前一样对数据进行回滚,因为在提交事务前,binlog可能成功写入磁盘了,所以这里要根据两种情况来做决定。
    • 一种情况,binlog不存在事务记录,主从也是一致的,那么这种情况事务还未提交成功,redolog就算刷盘了也没有commit标记,所以会对数据进行回滚。
    • 一种情况, 是binlog存在数据记录(已经刷盘了),而binlog写入后,那么依赖于binlog的其它扩展业务(比如从库已经同步了日志进行数据的变更)数据就已经产生了,如果这里进行数据回滚,那么势必就会造成主从数据的不一致,此时不能回滚(虽然事务在数据层还没提交,但在业务侧肯定做提交的动作了,不然不会走到这一步),怎么办呢?
    • 因为提交崩溃了,redolog不会有commit标志,但是这时也可以根据redolog来重做,我们之前说在第六阶段,写入redolog,实际上并不完全是这样, 在高可靠的场景下,如果把innodb_flush_log_at_trx_commit设置成1,那么redologprepare阶段就要持久化一次,崩溃恢复逻辑是要依赖于prepareredolog,再加上binlog来恢复的。结合binlog的状态,进行redo。如果binlog存在事务记录,那么就认为事务已经提交了,这里可以根据binlog对数据进行重做。其实这个阶段发生崩溃了,最终的事务是没提交成功的,这里应该对数据进行回滚。

如何解决主从服务之间的延时较大的问题

从库B和主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务从库B的这个长连接。一个事务binlog日志同步的完整过程如下,

  • 在从库B上通过change master命令,设置主库AIP、端口、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量;
  • 在从库B上执行start slave命令,这时从库会启动两个线程,就是图中的IO线程和SQL线程。其中IO线程负责与主库建立连接;
  • 主库A校验完用户名、密码后,开始按照从库B传过来的位置,从本地读取binlog,发给B
  • 从库B拿到binlog后,写到本地文件,称为中继日志relay log
  • SQL线程读取中继日志relay log,解析出日志里的命令,并执行。

由于多线程复制方案的引入,SQL线程演化成了多个线程。主从复制不是完全实时地进行同步,而是异步实时。

这中间存在主从服务之间的执行延时,如果主服务器的压力很大,则可能导致主从服务器延时较大。

首先能想到的是,从从节点上做优化,SQL回放线程是单线程的,为了加速回放,可以设置参数并行回放。

1
2
slave_parallel_type='LOGICAL_CLOCK'
slave_parallel_workers=8

从主节点来说,要减小主节点的压力,利用分库的方式,分成多个主节点,这样瓶颈压力就小些。

实现读写分离

Desktop View ShardingJDBC实现读写分离

Desktop View 写主节点

Desktop View 读从节点

从程序的角度,怎么规避主从复制延迟问题?

刚插入一条数据,然后马上就要去读取,这个时候有可能会读取不到。归根到底是因为主节点写入完之后数据是要复制给从节点的,读不到的原因是复制的时间比较长,也就是说数据还没复制到从节点,你就已经去从节点读取了,肯定读不到。 MySQL 5.7的主从复制是多线程了,意味着速度会变快,但是不一定能保证百分百马上读取到,这个问题我们可以有两种方式解决,

  • 业务层面妥协,是否操作完之后马上要进行读取;
  • 对于操作完马上要读出来的,且业务上不能妥协的,我们可以对于这类的读取直接走主库,当然ShardingJDBC也是考虑到这个问题的存在,所以给我们提供了一个功能,可以让用户在使用的时候。指定要不要走主库进行读取。

在读取前使用下面的方式进行设置就可以了。

Desktop View 读主节点

Canal实现数据一致性

什么是canal?可以简单理解为一个假的(伪装的)MySQL Slave

也是使用dump协议同步binlog数据,最大的区别在于,没有回放线程,或者说回放线程不一样,不是生成数据,而是进行转发。可以通过Socket转发出去,也可以发送RocketMQ,支持多种方式的转发。

比如,把canal.serverMode选项修改为rocketMQ类型,

Desktop View 修改canal.serverMode

canal [kə'næl],译意为水道/管道/沟渠。主要用途是基于MySQL数据库增量日志解析,提供增量数据订阅和消费。参见阿里云DTSData Transfer Service)的开源版本

canal的工作原理

  • canal模拟MySQL Slave的交互协议,伪装自己为MySQL Slave,向MySQL Master发送dump请求;
  • MySQL Master收到dump请求,开始推送binlogSlave(也就是canal);
  • canal解析binlog对象(原始为byte流);
  • canal将解析后的对象,根据业务场景,分发到比如MySQLRocketMQ或者ES中。

canal使用场景 在很多业务情况下,我们都会在系统中加入redis缓存做查询优化(比如三级缓存的数据一致性,删除或更新缓存),使用ES做全文检索,HBase/MongoDB做海量存储(分库分表后,需要做关联查询,或者跨库查询等复杂检索,引入索引和存储分离)。

如果数据库数据发生更新,这时候就需要在业务代码中写一段同步更新redisESHBase的代码。这种数据同步的代码跟业务代码糅合在一起会不太优雅,能不能把这些数据同步的代码抽出来形成一个独立的模块呢,答案是可以的,而且架构上也非常漂亮。

Desktop View canal架构

canal可以作为MySQL binlog增量订阅消费组件 + MQ消息队列,将增量数据更新到

  • redis高速缓存
  • ES做全文检索
  • HBase/MongoDB做海量存储。

图中的redis缓存操作服务、ES索引操作服务、HBase海量存储操作服务,都扮演了binlog适配器adapter的角色。

一般地,redis缓存和MySQL数据一致性解决方案

  • 延时双删策略
  • 异步更新缓存(基于订阅binlog的延迟更新机制)
  • …(更多的,比如金融业务需要强一致性做补偿的方案,以后再起一个议题单独聊聊)

基于canal的数据一致性搭建环境

Mastercanal创建专用账号,并且授权。登录MySQL输入以下命令,canal的原理是模拟自己为MySQL Slave,所以这里一定需要做为MySQL Slave的相关权限。

1
2
3
4
5
$ docker exec -it mysql-master bash
$ mysql -uroot -p123456
$ CREATE USER canal IDENTIFIED BY 'canal';  
$ grant select, replication slave,replication client on *.* to 'canal'@'%' identified by "canal";
$ flush privileges;

创建canal数据库表,

Desktop View canal数据库表

配置docker-compose编排文件,

Desktop View canal docker compose

canal-admin可以理解为配置中心,在没有admin之前,canal的配置文件都是通过文件的方式配置的,有了canal-admin,就可以在admin读取配置canal信息,角色有点像nacos

canal-admin连的数据库是mysql-master,用的dbcanal_manager,用的网络是搭建MySQL主从集群的时候创建的子网络mysql-canal-network,相对于canal编排文件来说,这个网络是一个外部网络,canal容器和MySQL容器处于一个子网内部,相互之间访问可以不用ip,可以直接用主机名,

实际canal服务canal-server,要连到配置中心,canal-admin:8089

上述准备工作做好之后,启动canal服务,

1
2
3
4
5
$ cd /home/docker-compose/
$ chmod 777 -R canal
$ cd canal
$ docker-compose --compatibility up -d
$ docker-compose logs -f

访问canal admin,并且配置实例/Instance。如果使用的默认的配置信息,用户名入admin,密码输入123456即可访问首页。

Desktop View canal首页

可以看到,canal-server已经注册进来了,现在要把它的日志发送到RocketMQ,修改serverMode

Desktop View canal配置serverMode

配置生产者groupnameserver

Desktop View canal配置生产者group、nameserver

新建实例配置,配置好之后启动。首先要从master接收binlog,需要配置下master,以及mq topic

Desktop View canal配置实例

启动后,查看日志,

Desktop View canal启动日志

master查看从节点,也可以验证Canal成功启动,

Desktop View canal启动成功

启动后打开RocketMQ,在MySQL侧改动数据,可以看到消息,就调通了。

Desktop View RocketMQ首页

Desktop View RocketMQ查询消息

Desktop View RocketMQ消息明细

Canal高可用集群架构

使用Cannel,为了保证系统达到49、甚至59的高可用性,Canal服务不能是单节点的,一定是高可用集群的形式存在。 为什么呢? 如果Canal保存数据不成功,就会导致数据库跟缓存或异构存储(比如ES、或者redis)数据不一致。 Canal单节点用于学习、用于测试是没问题的;但是Canal单节点用于生产,会严重影响系统健壮性,稳定性,所以得把Canal部署成高可用集群。

Canal部署成高可用集群的架构如下,

Desktop View canal集群架构

CanalHA(双机集群)分为两部分,Canal serverCanal client分别有对应的HA实现。

Canal server

为了减少对MySQL dump的请求,不同server上的实例(instance)要求同一时间只能有一个处于running,其他的处于standby状态。 或者说,由于instanceCanal server负责执行,所以同一个集群里边的Canal server,同一时间只能有一个处于running,其他的处于standby状态。

Canal client

为了保证有序性,一份实例(instance)同一时间只能由一个canal client进行get/ack/rollback等远程操作,否则客户端接收无法保证有序。

Zookeeper负责协调 整个HA机制的控制,主要是依赖了zookeeper的几个特性,watcherEPHEMERAL节点(和session生命周期绑定), 同一个集群里边的Canal server,需要去创建和监听属于Server的唯一的znode节点,成功则running,失败则standby; 同一个集群里边的Canal client, 需要去创建和监听属于Client的唯一的znode节点,成功则running,失败则standbystandby的空闲角色,一直监听唯一的znode节点过期状态,随时准备去争抢转正机会。

Canal高可用Server的协作流程

  1. canal server要启动某个canal instance时, 都先向zookeeper进行一次尝试启动判断(实现:创建EPHEMERAL节点,谁创建成功就允许谁启动)。
  2. 创建zookeeper节点成功后,对应的canal server就启动对应的canal instance,没有创建成功的canal instance就会处于standby状态。
  3. 一旦zookeeper发现canal server A创建的节点消失后,立即通知其他的canal server再次进行步骤1的操作,重新选出一个canal server启动instance
  4. canal client每次进行connect时,会首先向zookeeper询问当前是谁启动了canal instance,然后和其建立链接,一旦链接不可用,会重新尝试connect

注:canal client的方式和canal server方式类似,也是利用zookeeper的抢占EPHEMERAL节点的方式进行控制。

Canal的核心角色

再理解一下Canal的三大核心角色。

角色1:canal server

可以简单地把Canal理解为一个用来同步增量数据的一个工具。我们看一张官网提供的示意图,

Desktop View canal flow

Canal的工作原理,就是把自己伪装成MySQL Slave,模拟MySQL Slave的交互协议向MySQL Master发送dump协议,MySQL Master收到Canal发送过来的dump请求,开始推送binlogCanal,然后Canal解析binlog,再发送到存储目的地,比如MySQLRocketMQKafkaES等。 因为在TCP模式下,一个instance只能有一个Canal client订阅,即使同时有多个Canal client订阅相同的instance,也只会有一个Canal client成功获取binlog,所以Canal server写死clientId = 1001

  1. 也正是因为一个instance只有一个Canal client,所以Canal serverbinlog位点信息维护在了instance级别,即conf/content/meta.dat文件中
  2. TCP模式下,如果Canal client想重新获取以前的binlog,只能通过修改Canal serverinitial position配置,并重启服务来达到目的
  3. TCP模式下,Canal server主要提供了两个功能
    • 维护MySQL binlog position信息,目的是作为dump的请求参数,这也是canal server唯一保存的数据
    • 对客户端提供接口以查询binlog

canal.serverMode 的服务模式有:tcpkafkarocketMQrabbitMQ,可以把选项修改为rocketMQ类型,如下,

Desktop View canal配置serverMode

这时候,就是Canal server把收到的binlog,按照instance的过滤要求完成处理后,写入到rocketMQ

canal server负责canal instance的启动。canal server启动过程中的关键信息如下,

  1. 确定binlog first position(通过以上三步,就可以确定Canal server启动之后binlog初始位点)
    • 先从conf/content/meta.dat文件中查找last position, 也就是最后一次成功dump binlog的位点
    • 如果不存在last position, 则从conf/content/instance.properties配置文件中查找initial position, 这是我们人为配置的初始化位点
    • 如果不存在initial position,则执行show master status命令获取mysql binlog lastest position
  2. first position赋值给last position保存在内存中
  3. schema缓存到conf/content/h2.mv.db文件中

角色2:canal client

canal.serverMode的服务模式有tcpkafkarocketMQrabbitMQ。默认情况下,是tcp,就是开启一个Netty服务,发送binlogClient

canal client需要自己开发TCP客户端,可以参考官方的canal client实现。

  1. canal clientjava demo可以去官方GitHub上找一下,记得将destination等配置信息改正确。请参考alibaba canal wiki
  2. canal client connect
  3. canal client describe
    • 在收到客户端订阅请求之后,logs/content/content.log文件会打印出相关日志
    • conf/content/meta.dat文件记录了客户端的订阅信息,包括clientIddestinationfilter
  4. canal client getWithoutAck
    • canal server在收到canal client查询请求之后,以内存中的last position作为参数向MySQL server发送dump请求
    • 如果存在比last position更新的binlogcanal server会收到MySQL server的返回数据,然后将其转换为Message数据结构返回给canal client
  5. canal client ack
    • canal client收到canal server的数据之后,可以发送ack确认last position的同步位置。
    • canal server在收到canal client确认请求之后,更新内存中的last position,并同步保存到conf/content/meta.dat文件中,在logs/content/meta.log文件中打印日志

角色3:canal instance

canal server仅仅是保姆角色,真正完成解析binlog日志、binlog日志过滤、binlog日志转储、位点元数据管理等核心功能,是由canal instance角色完成。

Canal Instance的架构图如下图所示,

Desktop View canal instance架构图

Canal中数据的同步是由Canal Instance组件负责,一个Canal Server实例中可以创建多个Canal Instance实例。每一个Canal Instance可以看成是对应一个MySQL实例,即案例中需要同步两个数据库实例,故最终需要创建两个Canal Instance

其实也不难理解,因为MySQLbinlog就是以实例为维度进行存储的。

Canal Instance包含了4个核心组件:EventParseEventSinkEventStoreCanaMetaManager,在这里主要是阐明其作用,以便更好的指导实践。

  1. EventParse组件
    • 负责解析binlog日志,其职责就是根据binlog的存储格式将有效数据提取出来,这个不难理解,我们也可以通过该模块,进一步了解一下binglog的存储格式。
  2. EventSink组件
    • 在一个数据库实例上通常会创建多个Schema,但通常并不是所有的Schema都需要被同步,如果直接将EventParse解析出来的数据全部传入EventStore组件,将对EventStore带来不必要的性能消耗;
    • 另外本例中使用了分库分表,需要将多个库的数据同步到单一源,可能需要涉及到合并、归并等策略。
    • 以上等等等需求就是EventSink需要解决的问题域。
  3. EventStore组件
    • 用来存储经canal转换的数据,被Canal Client进行消费的数据,目前Canal只提供了基于内存的存储实现。
  4. CanalMetaManager组件 元数据存储管理器。 在Canal中,最基本的元数据至少应该包含EventParse组件解析的位点与消费端的消费位点。 Canal Server重启后,要能从上一次未同步位置开始同步,否则会丢失数据。

角色4:canal cluster集群

多个canal server,可以在创建的时候,归属到一个集群cluster下边。 一个集群cluster下边,同时只有一个cannel server running,其他的standby,实现高可用。

角色5:canal admin

  1. 通过图形化界面管理配置参数。
  2. 动态启停ServerInstance
  3. 查看日志信息

附录

一键搭建Canal集群

Zookeeper + MySQL + Canal + RocketMQ配置,感兴趣可以自己玩一下,这里暂且不作介绍,省略一万字。

应用:基于canal + RocketMQ实现微服务应用的数据一致性

实现微服务(Go或者Java)应用代码,实现监听RocketMQbinlog消息,实现刷RedisESNginx字典等目的。

本文由作者按照 CC BY 4.0 进行授权

© ManShouyuan. 保留部分权利。

本站总访问量 本站访客数人次

🚩🚩🚩🚩🚩🚩