MySql8主从复制介绍
xc20200316
本文介绍mysql主从复制的基本流程。
和两种方法:
– binlog
+ log位置
– GTID
我们用docker的方式运行mysql。清爽简洁。升级等操作也方便。
获取镜像
docker镜像信息
https://hub.docker.com/_/mysql/
docker配置文档
https://dev.mysql.com/doc/mysql-installation-excerpt/8.0/en/docker-mysql-getting-started.html
拉取最新8.0.19镜像。
sudo docker pull mysql:8.0.19
主从复制方法1:binlog
+ log位置
docker-compose的配置
mysql_8:
image: mysql:8.0.19
...
volumes: # 数据目录映射到我们指定的位置
- "/airm2m_data/airm2m/database/mysql_8/data:/var/lib/mysql"
environment: # 环境变量 默认root密码
MYSQL_ROOT_PASSWORD: xxxxxxx
command: # 主从复制会用到server-id,所有节点id必须全局唯一。
--server-id=1
主从复制需要开启binlog。mysql8默认开启,和数据存在同个目录。不用管了。
docker-compose up -d mysql_8
启动后可看到data目录下生成了一批文件
进入mysql后
show variables;
可以查看各种系统变量和配置
到此master就起来了。再用同样方式启动slave。注意server-id要不同。
给master创建一个slave账号,授予REPLICATION SLAVE权限。
进入slave,指定master的ip和端口。
CHANGE MASTER TO
MASTER_HOST='xxxxxxx',
MASTER_PORT=3307
启动复制,用刚才创建的slave账号。
START SLAVE USER='xxx' PASSWORD='xxx'
这时在master上创建数据就会同步到slave了。
在slave上可以查看slave同步的重要数据
show slave status
binlog+pos的原理简单来说就是master产生binlog,slave复制master的binlog到自己的relay log,slave再重放relay log,插入实际数据。
slave会记录最后读取的数据的log文件和位置(master_log_file和read_master_log_pos),保证不会重复同步数据。
出错情景
情景1:stop slave容器再起,或服务器重启。
slave启动后,默认的master_user会变成test,造成连不上master。先stop slave再用用户名密码start slave即可。
master_user估计哪里可以永久设定。先不细究。
情景2:rm slave容器再起。
删除容器再创建,启动slave时会报错
START SLAVE USER='xxx' PASSWORD='xxx'
Error Code: 1872. Slave failed to initialize relay log info structure from the repository
解法:
在slave上
show slave status
记下master_log_file和read_master_log_pos
reset slave; // 清空了master_log_file。read_master_log_pos设为4
CHANGE MASTER TO
MASTER_HOST='xxx',
MASTER_PORT=3307,
master_log_file='binlog.000005', // 填记下的master_log_file和read_master_log_pos
master_log_pos=11957
START SLAVE USER='xxx' PASSWORD='xxx' // 启动。完成。
如果read_master_log_pos与rm之前的值不一样,会造成数据不一致。
(填小,会缺数据。而且无论怎么折腾,重设参数,都回复不过来了。目前只好转到情况4,完全重做slave)
情景3:master容器stop再起。rm master容器再起。
不用处理。slave断开后一分钟左右会自动重连。
情景4:slave数据已错乱,无法同步。完全重做slave。需清空slave,dump出master数据,导入slave,再开始同步。
参考:
https://blog.51cto.com/liaoxz/2141797
https://tecadmin.net/reset-re-sync-mysql-master-slave-replication/
目前看到的最好的办法:
用mysqldump,加–master-data=1参数
可不停机dump出现有数据,并附带master_log_file和master_log_pos信息。
总而言之就是dump出一份通用的数据以建立一个新的slave
老版本客户端连8.0会报错,因为密码策略更新了。先升级客户端。(网上解法都是修改服务器默认密码策略,很难受。)
https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/
ubuntu14.04会出错,16.04可以。
先sudo apt-cache search mysql-client。尝试安装的话,都是5的版本。
wget https://dev.mysql.com/get/mysql-apt-config_0.8.15-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.15-1_all.deb
server选8.0
sudo apt-get update
sudo apt-cache search mysql-client
sudo apt install mysql-client //看到显示是的8.0.
装好后看mysql –version。变成8了
dump出master的数据
mysqldump -uroot -pxxxxx --host xxx --port xxx --single-transaction --master-data --databases test > master_test.sql
打开test.sql
看到CHANGE MASTER TO MASTER_LOG_FILE='binlog.000008', MASTER_LOG_POS=2841;
这里就包含了master的信息
stop slave
reset slave
把slave的数据库都删掉
进入slave。
mysql -uroot -pxxxx --host 127.0.0.1 --port xxxx
执行dump出的sql。
source master_all.sql
START SLAVE USER='xxx' PASSWORD='xxx'
到此数据已经同步
主从复制方法2:GTID
https://dev.mysql.com/doc/refman/8.0/en/replication-gtids.html
GTID的格式
GTID = source_id:transaction_id
例如3E11FA47-71CA-11E1-9E33-C80AA9429562:23
所谓GTID(global transaction identifier)就是在一个集群里,数据库每个事务(不同的节点都会区分)都有一个全局的id。
可以理解为集群里所有操作都有一个唯一的id。那么主从复制中的差异对比和防重复就相对容易了,
不再需要指定MASTER_LOG_FILE和MASTER_LOG_POS这些参数。-
可以很方便地描述GTID集合,例如
d683ed20-5f54-11ea-b5cd-0242ac120002:1-4
,表示transaction_id从1到4的集合。
有了集合就可以用很少的空间表示大量连续的GTID了。
各系统变量和内部表的说明:
https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html
mysql.gtid_executed
表
GTID会存在mysql内部mysql.gtid_executed
表中。
binlog
开启gtid-mode后。GTID也会存在于binlog里。
gtid_next
系统变量
控制如何得到下一个GTID,默认值AUTOMATIC
,事务执行后自动生成。
还可以手动设为某个特定的GTID,这是非常规操作,不做深究。
gtid_executed
系统变量(注意和mysql.gtid_executed
表并不同)
包含已执行的gtid
mysql启动时会进行初始化。初始化的依据:现有的binlog
和mysql.gtid_executed
表
gtid_purged
系统变量
包含已执行但不存在于任何binlog的gtid,是gtid_executed
的子集。
为什么不存在于任何binlog?因为binlog通常会被删除(如purge binary logs
删除binlog或mysql自动清理binlog(expire_logs_days参数,默认为0不自动删除))
GTID的存在有这么个公式:gtid_executed
= binlog
+ gtid_purged
mysql启动时会进行初始化。初始化的依据:现有的binlog
和gtid_executed
系统变量
存在于gtid_purged
中的三种GTID:
1. 已执行的GTID,但slave的binlog没打开。
2. 写入binlog后,binlog被删除。
3. 手动设置。SET @@GLOBAL.gtid_purged=’xxxxxxxxxxxxx
思考:
这样看来GTID的实际记录组成就是mysql.gtid_executed
表加现有的binlog。其他几个变量都是会开机重新计算并实时变化,系统同步时会频繁用到,也可用于一些手动操作和状态查询。
GTID的生命周期
- 一个事务在master上被执行。那么给这个事物分配一个GTID(master的id+transaction_id)并存入binlog。GTID一定是连续的(某些特殊情况出现缺口时,会自动进行填充),并存入binlog的。如果某条没存binlog,那么就不会分配给它GTID。
- GTID在分配给一个事务后,在commit时作为一个
Gtid_log_event
存到binlog。在binlog进行滚动或mysql关闭等情况下会最终写到mysql.gtid_executed
。 - GTID在分配给一个事务后,会存入
@@GLOBAL.gtid_executed
系统变量里。这个变量是一个GTID集合,用来表示server当前的一个状态。gtid_executed
系统变量包含所有已执行的GTID,而mysql.gtid_executed
表并不一定包含全部,因为如第2点所说会先存入binlog,在后续的某个时机才会存入mysql.gtid_executed
表。
(我的理解,简单来说就是同时存入@@GLOBAL.gtid_executed
系统变量和binlog(可看作是实时的,总是最新的),后续某个时机(binlog滚动等)再存入mysql.gtid_executed
表(可看作是非实时的,非最新的)。) - binlog数据传到slave并存入slave的relay log后。slave读出GTID并存入系统变量
gtid_next
,告诉系统待同步的事务。 - slave拿到这个GTID,验证是否自己独占(slave可多线程进行复制),验证这个GTID是否已经用过等等。
- 各种验证都没问题,slave执行这个事务。
- 如果slave的binlog是打开的,为一个
Gtid_log_event
存到binlog。在binlog进行滚动或mysql关闭等情况下会最终写到mysql.gtid_executed
- 如果slave的binlog是关闭的,直接存入slave的
mysql.gtid_executed
表。 - slave执行这个GTID之后,会存到
@@GLOBAL.gtid_executed
系统变量。跟master一样,@@GLOBAL.gtid_executed
系统变量也包含所有已执行的GTID。
大致就是这几个表和系统变量标识了GTID的各种状态和阶段,根据binlog的开关和滚动,以及其他设置,最终让所有GTID得到比较完美的同步。
看着挺复杂的,文档内容也比较多,需要仔细看文档甚至源码去学习了。
非事务性的操作也可能分配GTID。比如磁盘故障,造成binlog出现一个缺口,那么这个缺口也会分配一个GTID。
一条语句也能差分成多个事务,从而分配多个GTID。
GTID自动寻址
slave发送已同步过的GTID集合给master。master把slave没有的数据发回去。某些拓扑结构会造成slave收到重复的数据,但slave自己也会判断重复并跳过的。
- 出错情景1
如果master将要发送的事务在@@GLOBAL.gtid_purged
中,说明master已经执行,但对应的binlog没了。这个情况已经没法正常往slave同步了。master会报错,并给出ER_FOUND_MISSING_GTIDS
(缺失的GTID),slave要么手动补上这些数据(去master或者别的slave上找),要么就直接重做数据了。思考:同步是要依赖master的binlog的。删除最近的binlog是危险的。一般情况保留越久越好。
- 出错情景2
交换数据时master发现slave的某个GTID并不存在于自己的数据中。master会报错ER_SLAVE_HAS_MORE_GTIDS_THAN_MASTER
。出现这个问题的一个可能的原因是mysql的sync_binlog参数没有设为1,master的binlog的写入有滞后,造成GTID对应的数据还没写入binlog。
GTID配置方法
打开gtid_mode
和enforce-gtid-consistency
# docker-compose 配置
mysql_8:
image: mysql:8.0.19
command:
--server-id=1
--gtid_mode=on
--enforce-gtid-consistency=on
--skip-slave-start=off # for slave 不在启动时自动同步,而是手动启动同步。
启动后show variables,可看到都为ON。
1. 完全重做数据库。
起一个干净的mysql作为master。创建slave账号。
起一个干净的mysql作为slave。slave要加个skip-slave-start=off,不在启动时自动同步,而是手动启动同步。
slave启动后开始同步
change master to
master_host='xxxxx',
master_user='xxx',
MASTER_AUTO_POSITION = 1;
2.出错场景
如果数据不一致了,有几种解决方法。
这里介绍个最简单的,把数据库整个导出。
比如现有好几个数据库,在slave上把其中一个的几个表或一批数据误删了。这时reset slave再重启也无效。
那么:
1.slave停止复制
stop slave
2.从master上dump出不一致的数据库
mysqldump -uroot -pxxxx --host xxxx --port xxxx --single-transaction --master-data=1 --set-gtid-purged=on --databases test > master_test.sql
3.导入slave。因为都是带GTID的,所以不会重复导入。导入后可以看到数据全了。
mysql -uroot -pxxxxx --host 127.0.0.1 --port xxxx
source master_all.sql
4.slave启动复制。回到正常状态。
START SLAVE USER='xxx' PASSWORD='xxx'