1、主从复制理论

1.1 主从复制概念

MySQL主从复制是指由多个MySQL实例组成一个MySQL集群,集群中有一个MySQL实例是主库,其他MySQL实例是从库。主库负责写入,从库负责读取,结合KeepAlived高可用架构可以实现MySQL主节点宕机后自动从从节点中选出新的主节点。主节点会异步地将数据同步给从节点,保持数据的一致性。
主从复制主要有2个优点:

  • 解决了MySQL单点故障引起的数据库服务不可用的问题,结合高可用模块可以实现自动选主;
  • 提高了数据库服务的性能,读写分离,使每一个数据库实例分得的请求数不会那么多,当数据库集群TPS很高(2000以上)时不会宕机。

    1.2 bin log

    1.2.1 bin log简介

    bin log用于记录护具看执行的写操作,不包含查询操作,在写事务提交后以二进制的格式保存在磁盘中。bin log由MySQL的Server层进行记录,后端可以对接任意的存储引擎。binlog是通过追加的方式进行写入的,可以通过max_binlog_size参数设置每个binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。

    1.2.2 bin log使用场景

    在实际应用中,binlog的主要使用场景有两个,分别是主从复制和数据恢复。

  • 主从复制:在Master端开启binlog,然后将binlog发送到各个Slave端,Slave端重放binlog从而达到主从数据一致;

  • 数据恢复:通过使用mysqlbinlog工具来恢复数据。

    1.2.3 binlog刷盘时机

    对于InnoDB存储引擎而言,只有在事务提交时才会记录bin log,此时记录还在内存中,那么bin log是什么时候刷到磁盘中的呢?mysql通过sync_binlog参数控制bin log的刷盘时机,取值范围是0-N:

  • 0:不去强制要求,由系统自行判断何时写入磁盘;

  • 1:每次commit的时候都要将binlog写入磁盘;
  • N:每N个事务,才会将binlog写入磁盘。

从上面可以看出,sync_binlog最安全的是设置是1,这也是MySQL 5.7.7之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。

1.2.4 bin log日志格式

binlog日志有三种格式,分别为STATMENT、ROW和MIXED。
(1)STATMENT
基于SQL语句的复制(statement-based replication, SBR),每一条会修改数据的sql语句会记录到binlog中。

  • 优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO, 从而提高了性能;
  • 缺点:在某些情况下会导致主从数据不一致,比如执行sysdate()、slepp()等。

(2)ROW
基于行的复制(row-based replication, RBR),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了。

  • 优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题;
  • 缺点:会产生大量的日志,尤其是alter table的时候会让日志暴涨

(3)MIXED
基于STATMENT和ROW两种模式的混合复制(mixed-based replication, MBR),一般的复制使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog。

1.3 MySQL主从复制原理

Mysql的主从复制中主要有三个线程:Master有一个线程:binlog dump thread,Slave有两个线程:I/O thread 和SQL thread。
具体步骤如下:

  1. 当Master节点进行insert、update、delete操作时,会按顺序写入到binlog中;
  2. Salve从库连接master主库,Master有多少个Salve节点,Master节点就会创建多少个binlog dump线程;
  3. 当Master节点的binlog发生变化时,binlog dump 线程会通知所有的Salve节点,并将相应的binlog内容推送给Salve节点;
  4. Salve节点的I/O线程接收到 binlog 内容后,将内容写入到本地的 relay-log(中继日志);
  5. Salve节点的SQL线程读取I/O线程写入的relay-log,并且根据 relay-log 的内容对从数据库做对应的回放操作。

如下图所示:
主从复制 - 图1
这里有一个非常重要的一点,就是从库同步主库数据的过程(Salve节点的SQL线程)是串行化的,也就是说主库上并行的操作,在从库上会串行执行。由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。这就会造成微小时间段内的主从库的数据不一致问题。

1.4 MySQL主从复制方式

(1)异步复制
MySQL主从复制的默认方式就是异步复制,所谓异步复制是指MySQL会在后台另起线程去完整数据同步的工作,具体的步骤可以参考1.3。需要注意的是:对于支持事务的存储引擎来说,异步复制,是事务提交、将更改的操作写入到Master的bin log后,才向客户端返回结果。
(2)半同步复制
异步复制的机制会导致这样一个问题:当主库提交一个事务,将修改写入主库的bin log日志,之后向客户端返回,而此时从库尚未收到主库推送的这条事务对应的bin log日志,如果此时主库宕机造成主库上事务对应的bin log日志丢失,会导致从库无法同步该事务对应的bin log,会造成主从数据不一致。
为了解决这个问题,MySQL引入了半同步复制。半同步复制是指:主库在提交事务后,提交事务的线程会一直等待,直到至少有一个半同步的从库确认已接收到bin log;从库收到主库推送来的bin log后,会将其写入到从库的relay log并刷新到磁盘,之后才会向主库返回ack确认;主库收到从库返回的ack确认后,才会向客户端返回响应结果。
需要注意的是:

  • 主库和从库都需要启用半同步复制模式才会进行半同步复制功能,否则主库会还原为默认额异步复制;
  • 当主库等待从库返回ack的过程中超时时,也会还原为默认的异步复制,当至少有一个从库的数据赶上主库时,主库才会恢复到半同步复制模式。

(3)并行复制
并行复制又叫多线程复制。MySQL5.5版本之前不支持并行复制;5.6版本的并行复制仅是不同数据库(schema)之间是并行复制,同一个数据库中数据的复制还是串行复制;5.7版本的并行复制是基事务提交组的复制,做到了同一个数据库中的数据也可以并行复制。

  • 5.6版本:并行只是基于schema的,也就是基于库的。当有多个库时多个库可以并行进行复制,而库与库之间互不干扰。其核心思想是:不同schema下的表并发提交时的数据不会相互影响,即slave节点可以对relay log中不同的schema各分配一个类似SQL功能的线程,来重放relay log中主库已经提交的事务,保持数据与主库一致。
  • 5.7版本:5.7版本是基于组提交的并行复制,即同一个schema下的数据也可以并行复制,相当于把并行复制的粒度变细了一个维度。其核心思想:组与组之间提交的事务都是可以并行回放(配合binary log group commit),slave机器的relay log中 last_committed相同的事务(sequence_num不同)可以并发执行。

    2、基于docker-compose的MySQL主从复制集群

    网上很多教程是去mysql容器内部通过手动执行命令的方式指定主从关系的,这种情况生产环境里肯定不允许。我司的MySQL集群时基于K8S的,K8S提供高可用性。本节的目的不是模拟一个真实的生产环境里MySQL主从集群的搭建,而是一个自己Demo的环境,目的是感受一下MySQL的主从复制模式和读写分离,为后面学习Sharding-JDBC的分库分表做铺垫。
    下面我就基于docker-compose搭建一个1主2从的mysql主从复制集群。注意我这里并没有实现高可用,即主节点宕机后会自动从从节点选主,并自动完成主从切换,实现这个效果还要引入KeepAlived高可用进程才可以。

    2.1 搭建步骤

    (1)编写Master中相关用户的脚本
    该脚本的内容主要有3个:

  • 定义主从复制用的账号密码;

  • 定义主节点连接客户端时的用户和密码;
  • 给主从复制的账号赋予两个权限:
    • REPLICATION SLAVE:从节点副本权限;
    • REPLICATION CLIENT:副本客户端权限。

create_sync_user.sh脚本内容:

  1. #!/bin/bash
  2. # 定义用于同步的用户名
  3. MASTER_SYNC_USER=${MASTER_SYNC_USER:-sync_admin}
  4. # 定义用于同步的用户密码
  5. MASTER_SYNC_PASSWORD=${MASTER_SYNC_PASSWORD:-123456}
  6. # 定义用于登录mysql的用户名
  7. ADMIN_USER=${ADMIN_USER:-root}
  8. # 定义用于登录mysql的用户密码
  9. ADMIN_PASSWORD=${ADMIN_PASSWORD:-123456}
  10. # 定义运行登录的host地址
  11. ALLOW_HOST=${ALLOW_HOST:-%}
  12. # 定义创建账号的sql语句
  13. CREATE_USER_SQL="CREATE USER '$MASTER_SYNC_USER'@'$ALLOW_HOST' IDENTIFIED BY '$MASTER_SYNC_PASSWORD';"
  14. # 定义赋予同步账号权限的sql,这里设置两个权限,REPLICATION SLAVE,属于从节点副本的权限,REPLICATION CLIENT是副本客户端的权限,可以执行show master status语句
  15. GRANT_PRIVILEGES_SQL="GRANT REPLICATION SLAVE,REPLICATION CLIENT ON *.* TO '$MASTER_SYNC_USER'@'$ALLOW_HOST';"
  16. # 定义刷新权限的sql
  17. FLUSH_PRIVILEGES_SQL="FLUSH PRIVILEGES;"
  18. # 执行sql
  19. mysql -u"$ADMIN_USER" -p"$ADMIN_PASSWORD" -e "$CREATE_USER_SQL $GRANT_PRIVILEGES_SQL $FLUSH_PRIVILEGES_SQL"

注意:

  • 上述脚本中,通过环境变量${}的方式给shell脚本中的变量赋值,环境变量是在docker-compose.yml里每一个service的配置中通过environment设置的。

(2)编写slave相关脚本
该脚本主要做以下3件事:

  • 定义从节点连接主节点的账号和密码;
  • 连接主节点,解析出logfile和pos参数;
  • 根据上面解析出的logfile和pos参数,与主节点建立主从关系,并开启主从复制。

slave.sh脚本内容如下:

  1. # 定义连接master进行同步的账号
  2. SLAVE_SYNC_USER="${SLAVE_SYNC_USER:-sync_admin}"
  3. # 定义连接master进行同步的账号密码
  4. SLAVE_SYNC_PASSWORD="${SLAVE_SYNC_PASSWORD:-123456}"
  5. # 定义slave数据库账号
  6. ADMIN_USER="${ADMIN_USER:-root}"
  7. # 定义slave数据库密码
  8. ADMIN_PASSWORD="${ADMIN_PASSWORD:-123456}"
  9. # 定义连接master数据库host地址
  10. MASTER_HOST="${MASTER_HOST:-%}"
  11. # 等待10s,保证master数据库启动成功,不然会连接失败
  12. sleep 10
  13. # 连接master数据库,查询二进制数据,并解析出logfile和pos,这里同步用户要开启 REPLICATION CLIENT权限,才能使用SHOW MASTER STATUS;
  14. RESULT=`mysql -u"$SLAVE_SYNC_USER" -h$MASTER_HOST -p"$SLAVE_SYNC_PASSWORD" -e "SHOW MASTER STATUS;" | grep -v grep |tail -n +2| awk '{print $1,$2}'`
  15. # 解析出logfile
  16. LOG_FILE_NAME=`echo $RESULT | grep -v grep | awk '{print $1}'`
  17. # 解析出pos
  18. LOG_FILE_POS=`echo $RESULT | grep -v grep | awk '{print $2}'`
  19. # 设置连接master的同步相关信息
  20. SYNC_SQL="change master to master_host='$MASTER_HOST',master_user='$SLAVE_SYNC_USER',master_password='$SLAVE_SYNC_PASSWORD',master_log_file='$LOG_FILE_NAME',master_log_pos=$LOG_FILE_POS;"
  21. # 开启同步
  22. START_SYNC_SQL="start slave;"
  23. # 查看同步状态
  24. STATUS_SQL="show slave status\G;"
  25. mysql -u"$ADMIN_USER" -p"$ADMIN_PASSWORD" -e "$SYNC_SQL $START_SYNC_SQL $STATUS_SQL"

说明:

  • 从节点需要在主节点成功启动之后,再进行主从关系绑定并开启同步,因此脚本中会sleep个10s;
  • 设置主从关系并启动主从复制是开启mysql客户端后最核心的两条命令,即:
    • change master to master_host…
    • start slave。

(3)编写docker-compose.yml
docker-compose.yml内容如下:

  1. version: '3'
  2. services:
  3. mysql-master:
  4. image: mysql:8.0.13
  5. container_name: mysql-master
  6. ports:
  7. - 3306:3306
  8. environment:
  9. MYSQL_ROOT_PASSWORD: "123456"
  10. # 设置脚本中定义的用于同步的账号
  11. MASTER_SYNC_USER: "sync_admin"
  12. # 设置脚本中定义的用于同步的密码
  13. MASTER_SYNC_PASSWORD: "123456"
  14. # 当前容器用于拥有创建账号功能的数据库账号
  15. ADMIN_USER: "root"
  16. ADMIN_PASSWORD: "123456"
  17. # 允许同步账号的host地址
  18. ALLOW_HOST: "10.10.%.%"
  19. # 解决时区问题
  20. TZ: "Asia/Shanghai"
  21. networks:
  22. mysql:
  23. ipv4_address: "10.10.10.10"
  24. volumes:
  25. # 挂载master节点MySQL第一次启动要执行的脚本
  26. - ./init/master:/docker-entrypoint-initdb.d
  27. command:
  28. # 每个MySQL实例都对应一个server-id,在启动MySQL实例时指定
  29. - "--server-id=1"
  30. - "--character-set-server=utf8mb4"
  31. - "--collation-server=utf8mb4_unicode_ci"
  32. - "--log-bin=mysql-bin"
  33. - "--sync_binlog=1"
  34. mysql-slave1:
  35. image: mysql:8.0.13
  36. container_name: mysql-slave1
  37. ports:
  38. - 3307:3306
  39. environment:
  40. MYSQL_ROOT_PASSWORD: "123456"
  41. # 用于同步的账号,由master创建
  42. SLAVE_SYNC_USER: "sync_admin"
  43. SLAVE_SYNC_PASSWORD: "123456"
  44. ADMIN_USER: "root"
  45. ADMIN_PASSWORD: "123456"
  46. # master地址,开启主从同步需要连接master
  47. MASTER_HOST: "10.10.10.10"
  48. # 设置时区
  49. TZ: "Asia/Shanghai"
  50. networks:
  51. - mysql
  52. volumes:
  53. # 挂载slave节点第一次启动MySQL时要执行的脚本
  54. - ./init/slave:/docker-entrypoint-initdb.d
  55. command:
  56. - "--server-id=2"
  57. - "--character-set-server=utf8mb4"
  58. - "--collation-server=utf8mb4_unicode_ci"
  59. mysql-slave2:
  60. image: mysql:8.0.13
  61. container_name: mysql-slave2
  62. ports:
  63. - 3308:3306
  64. environment:
  65. MYSQL_ROOT_PASSWORD: "123456"
  66. SLAVE_SYNC_USER: "sync_admin"
  67. SLAVE_SYNC_PASSWORD: "123456"
  68. ADMIN_USER: "root"
  69. ADMIN_PASSWORD: "123456"
  70. MASTER_HOST: "10.10.10.10"
  71. TZ: "Asia/Shanghai"
  72. networks:
  73. - mysql
  74. volumes:
  75. - ./init/slave:/docker-entrypoint-initdb.d
  76. command:
  77. # 这里需要修改server-id,保证每个mysql容器的server-id都不一样
  78. - "--server-id=3"
  79. - "--character-set-server=utf8mb4"
  80. - "--collation-server=utf8mb4_unicode_ci"
  81. networks:
  82. mysql:
  83. driver: bridge
  84. ipam:
  85. driver: default
  86. config:
  87. - subnet: "10.10.0.0/16"

说明:

  • 在docker-compose.yml文件中创建网络mysql,使主节点和从节点在一个网络;
  • docker-compose.yml里是通过脚本的方式,也是就是(1)、(2)步骤创建的脚本实现主从的,docker容器里面的mysql服务端,是通过entrypoint.sh来启动mysql的,如果mysql服务端是第一次启动,在初始化完成之后,会去加载/docker-entrypoint-initdb.d这个目录下面的脚本执行,因此我们将(1)、(2)步骤主节点的create_sync_user.sh脚本和从节点的slave.sh脚本对应的宿主机的目录与mysql容器里的docker-entrypoint-initdb.d目录做卷挂载,确保mysql容器第一次启动时执行我们(1)、(2)步骤中的脚本实现主从;
  • 主节点从节点的容器端口都是3306,映射在宿主机上的端口,主节点是3306,从节点时3307和3308。

(4)目录结构
上面的脚本和yml文件的目录情况如下,我是在本机的/mysql目录下执行的tree命令,mysql目录是我在根目录下建立的:
image.png
(5)执行docker-compose.yml
在docker-compose.yml文件所在的目录下执行如下命令:

  1. mysql]# docker-compose up -d

结果如下:
image.png
出现上面的情况仅代表编写的docker-compose.yml文件没有问题,我们的mysql主从复制集群是否搭建成功还要进行2.2中的试验确认。

2.2 试验结果

(1)日志查询
主节点关键日志:

  1. # 查看mysql主节点和从节点对应的容器
  2. docker ps -a
  3. # 查看主节点的容器的日志
  4. docker logs -f 6c9a7a05a2e9

image.png
任意一个从节点的关键日志:

  1. # 查看mysql主节点和从节点对应的容器
  2. docker ps -a
  3. # 查看从节点的容器的日志
  4. docker logs -f 8b992f760d57

image.png
(2)测试数据同步
可以使用数据库连接图形化工具,比如Navicat,在主节点建立一个数据库和一张表,对表进行增删改,看从节点是否同步,这一步需要注意在宿主机上的防火墙将主从节点对应的3306、3307和3308端口放通,如果使用的阿里云ECS服务器还需要在安全组上放通端口,可以参考我这篇文章:https://www.yuque.com/docs/share/9214ef22-4760-405c-a7bc-cc84ea3ea872?# 《阿里云ECS实例打开端口》。
具体测试步骤我就不在这里测试了。

参考

【152期】面试官:你能说出MySQL主从复制的几种复制方式吗?
小白都能懂的Mysql主从复制原理(原理+实操)
必须了解的MySQL三大日志:binlog、redo log和undo log
Mysql复制方式(半同步复制,并行复制,多源复制)
docker篇-(docker-compose搭建mysql高可用主从集群)
mysql启动报InnoDB: mmap(137363456 bytes) failed; errno 12这类的错误