记录备忘
参考博文:https://blog.csdn.net/wzy0623/article/details/90267132
目录
一、概述
二、原理
三、演进
3.1 线程的改进
3.1.1 旧版本
3.1.2 改进
3.2 二进制日志互斥锁改进
3.2.1 旧版本
3.2.2 改进
四、一致性分析
4.1 rpl_semi_sync_master_wait_point的配置
4.1.1 配置为WAIT_AFTER_COMMIT
4.1.2 配置为WAIT_AFTER_SYNC
4.2 sync_binlog的配置
4.3 sync_relay_log的配置
五、半同步相关配置
5.1 控制类参数
5.2 状态类参数
六、半同步实验
6.1 环境规划
6.2 配置异步复制
6.3 配置半同步复制
6.3.1 安装插件
6.3.2 启用半同步复制
七、测试
7.1 正常提交事务
7.2 回滚事务
7.3 主库等待超时
7.3.1 rpl_semi_sync_master_wait_no_slave为ON
7.3.2 rpl_semi_sync_master_wait_no_slave为OFF
主从复制主要分三种类型:异步、半同步、同步。
与异步复制相比,半同步复制提供了改进的数据完整性,因为当提交成功返回时,已知数据至少存在于两个位置。但半同步复制确实会对性能产生一些影响,因为需要等待从库,提交速度会变慢,延迟至少是将提交发送到从库并等待从库确认收到的TCP/IP往返时间。这意味着半同步复制最好在低延时的网络中使用。
半同步复制时,为了保证主库上的每一个Binlog事务都能够被可靠的复制到从库上,主库在每次事务成功提交时,并不及时反馈给前端应用用户,而是等待其中的一个从库也接收到Binlog事务并成功写入中继日志后,主库才返回commit操作成功给客户端。半同步复制保证了事务成功提交后,至少有两份日志记录,一份在主库的Binlog日志上,另一份在至少一个从库的中继日志Relay log上,从而更近一步保证了数据的完整性。
旧版本的半同步复制受限于Binlog Dump线程,原因是该线程承担了两份不同且又十分频繁的任务:
这两个任务是串行的,Binlog Dump线程必须等待从库返回之后才会传送下一个事件。Binlog Dump线程已然成为整个半同步复制性能的瓶颈。在高并发业务场景下,这样的机制会影响数据库整体的TPS。单一Binlog Dump线程发送接收的工作流程如图1所示。
在5.7.4版本的半同步复制框架中,独立出一个Ack Receiver线程 ,专门用于接收从库返回的ACK请求,这将之前Binlog Dump线程的发送和接收工作分为了两个线程来处理。这样主库上有两个线程独立工作,可以同时发送二进制日志事件到从库,和接收从库的ACK信息。因此半同步复制得到了极大的性能提升。Binlog Dump线程与Ack Receiver线程工作流程如图2所示。
Ack Receiver线程在主库启用半同步复制时创建,并在主库禁用半同步复制时销毁。它是自动创建和销毁的,因此不受用户控制。它的状态信息可以在performance_schema中查询得到:
select name, type, processlist_state from performance_schema.threads where name like '%ack_receiver%';
+------------------------------+------------+--------------------------------------+
| name | type | processlist_state |
+------------------------------+------------+--------------------------------------+
| thread/semisync/Ack_receiver | BACKGROUND | Waiting for semi-sync ACK from slave |
+------------------------------+------------+--------------------------------------+
Ack receiver线程有以下三个状态:
在MySQL 5.7.17之前,这个Ack Receiver线程采用了select机制来监听从库返回的结果,然而select机制监控的文件句柄只能是0-1024,当超过1024时,用户在MySQL的错误日志中或许会收到类似如下的报错,更有甚者会导致MySQL发生宕机。
semi-sync master failed on net_flush() before waiting for slave reply.
MySQL 5.7.17版本开始,官方修复了这个bug,开始使用poll机制来替换原来的select机制,从而可以避免上面的问题。其实poll调用本质上和select没有区别,只是在I/O句柄数理论上没有上限了,原因是它是基于链表来存储的。
旧版本半同步复制在主库提交二进制日志的写会话和Binlog Dump线程读取二进制日志的操作都会对二进制日志添加binlog lock互斥锁,用于保护二进制日志的读写操作。使用此互斥锁,二进制日志读写操作是安全的,但会导致二进制日志文件的读写串行化。不仅Binlog Dump线程和用户会话不能同时读写二进制日志,就连多个Binlog Dump线程本身也无法同时读取。每当一个会话正在读取或写入二进制日志文件时,所有其它会话都必须等待。如此顺序读写可能是一个瓶颈,尤其是当读写操作很慢时。串行化读写二进制日志如图3所示。
MySQL 5.7.2对binlog lock进行了以下两方面优化:
二进制日志文件看起来像一个仅追加的日志文件,可以安全地读取没有锁定的二进制事件。因此从Binlog Dump线程中删除了binlog锁。不使用binlog锁,而是为活动binlog维护安全读取边界(最大位置)。Binlog Dump线程永远不会读取超过安全读取边界的位置。当边界到达边界时,它将等待边界更新。用户会话负责在追加了二进制事件后更新安全读取边界。改进后的二进制日志读写如图4所示。
从图中可知:
因此,Binlog Dump线程和用户会话都可以获得更好的吞吐量,尤其是当有很多从库时,这种改进非常显着。
主库在等待备库ack时候,如果超时会退化为异步,这就可能导致数据丢失。在接下来分析中,先假设rpl_semi_sync_master_timeout足够大,不会退化为异步方式。
这里通过以下三个参数分析:
rpl_semi_sync_master_wait_point为WAIT_AFTER_COMMIT时,commitTrx的调用在engine层commit之后,如图5所示。
主库在等待Slave ACK时候,虽然没有返回当前客户端,但事务已经提交,其他客户端会读取到已提交事务。如果Slave端还没有读到该事务的events,同时主库发生了crash,然后切换到备库。那么之前读到的事务就不见了,出现了幻读。另外,如果客户端会重试提交该事务到新的主上,当宕机的主库重新启动后,以从库的身份重新加入到该主从结构中。此时会发现,该事务在从库中被提交了两次,一次是之前作为主的时候,一次是被新主同步过来的,结果依然是主从数据不一致。
MySQL针对上述问题,在5.7.2中改为调用binlog sync之后,engine层commit之前等待Slave ACK。这样只有在确认Slave收到事务events后,事务才会提交。在commit之前等待Slave ACK,同时可以堆积事务,利于group commit,有利于提升性能。如图6所示。
当sync_binlog为0的时候,binlog sync磁盘由操作系统负责。当不为0的时候,其数值为定期sync磁盘的binlog commit group数。当sync_binlog值大于1的时候,sync binlog操作可能并没有使binlog落盘。如果没有落盘,事务在提交前,Master掉电,然后恢复,那么这个时候该事务被回滚。但是Slave上可能已经收到了该事务的events并且执行,这个时候就会出现Slave事务比Master多的情况,主备同步会失败。所以如果要保持主备一致,需要设置sync_binlog为1。
WAIT_AFTER_SYNC和WAIT_AFTER_COMMIT两图中Send Events的位置,也可能导致主备数据不一致,出现同步失败的情形。实际在rpl_semi_sync_master_wait_point分析的图中是sync binlog大于1的情况。根据上面源码,流程如图7所示。Master依次执行flush binlog,update binlog position,sync binlog。如果Master在update binlog position后,sync binlog前掉电,Master再次启动后原事务就会被回滚。但可能出现Slave获取到Events,这也会导致Slave数据比Master多,主备同步失败。
由于上面的原因,sync_binlog设置为1的时候,MySQL会update binlog end pos after sync。流程如图8所示。这时候,对于每一个事务都需要sync binlog,同时sync binlog和网络发送events会是一个串行的过程,性能下降明显。
当sync_relay_log不是1的时候,semisync返回给Master的position可能没有sync到磁盘。在gtid_mode下,在保证前面两个配置正确的情况下,sync_relay_log不是1的时候,仅发生Master或Slave的一次Crash并不会发生数据丢失或者主备同步失败情况。如果发生Slave没有sync relay log,Master端事务提交,客户端观察到事务提交,然后Slave端Crash。这样Slave端就会丢失掉已经回复Master ACK的事务events。
但当Slave再次启动,如果没有来得及从Master端同步丢失的事务Events,Master就Crash。这个时候,用户访问Slave就会发现数据丢失。
半同步配置是通过插件方式完成的。
仅当使用INSTALL PLUGIN安装了相应的插件时,系统和状态变量才可用。
IP | 角色 |
---|---|
192.168.56.11 | 主 |
192.168.56.12 | 从 |
192.168.56.13 | 从 |
软件名称 | 软件版本 |
---|---|
OS | CentOS Linux release 7.3.1611 (Core) |
MySQL | mysql-8.0.21 |
路径 | 用途 |
---|---|
/home/mysql/mysql8 | MySQL主目录 |
/home/mysql/mysql8/data | MySQL数据目录 |
/home/mysql/mysql8/logs/binlog | MySQL二进制日志目录 |
/home/mysql/mysql8/logs/relaylog | MySQL中继日志目录 |
见 “MySQL复制(一)—— 异步复制” 的配置过程
半同步复制是使用插件实现的,因此必须将插件安装到MySQL服务器中才能使用它们。安装插件后,可以通过与之关联的系统变量来控制它。安装插件之前,这些系统变量是不可用的。要使用半同步复制,必须满足以下要求:
MySQL发行版包括主、从端的半同步复制插件文件semisync_master.so和semisync_slave.so,缺省位于MySQL安装目录下的lib/plugin目录下,本例中为/usr/local/mysql/lib/plugin。也可以通过设置plugin_dir系统变量的值指定插件目录位置。执行下面的SQL语句加载插件。
-- 在主库
install plugin rpl_semi_sync_master soname 'semisync_master.so';
-- 在每个从库
install plugin rpl_semi_sync_slave soname 'semisync_slave.so';
建议在主从库都安装以上插件。这样便于主从切换
如果尝试安装插件导致Linux上出现类似于此处所示的错误,则必须安装libimf:
mysql> install plugin rpl_semi_sync_master soname 'semisync_master.so';
ERROR 1126 (HY000): Can't open shared library
'/usr/local/mysql/lib/plugin/semisync_master.so'
(errno: 22 libimf.so: cannot open shared object file:No such file or directory)
可以从https://dev.mysql.com/downloads/os-linux.html获取libimf。
检查INFORMATION_SCHEMA.PLUGINS表或使用SHOW PLUGINS语句验证插件安装,例如:
mysql> select plugin_name, plugin_status from information_schema.plugins where plugin_name like '%semi%';
+----------------------+---------------+
| plugin_name | plugin_status |
+----------------------+---------------+
| rpl_semi_sync_master | ACTIVE |
+----------------------+---------------+
1 row in set (0.00 sec)
查看一些初始化变量
-- 主库
mysql> show variables like '%semi%';
+-------------------------------------------+------------+
| Variable_name | Value |
+-------------------------------------------+------------+
| rpl_semi_sync_master_enabled | OFF |
| rpl_semi_sync_master_timeout | 10000 |
| rpl_semi_sync_master_trace_level | 32 |
| rpl_semi_sync_master_wait_for_slave_count | 1 |
| rpl_semi_sync_master_wait_no_slave | ON |
| rpl_semi_sync_master_wait_point | AFTER_SYNC |
+-------------------------------------------+------------+
6 rows in set (0.00 sec)
--从库
mysql> show variables like '%semi%';
+---------------------------------+-------+
| Variable_name | Value |
+---------------------------------+-------+
| rpl_semi_sync_slave_enabled | OFF |
| rpl_semi_sync_slave_trace_level | 32 |
+---------------------------------+-------+
2 rows in set (0.00 sec)
安装半同步复制插件后,默认情况下会禁用它。必须在主库和从库都启用插件才能启用半同步复制。如果仅启用一侧,则复制将是异步的
-- 主库
set global rpl_semi_sync_master_enabled = 1;
-- 从库
set global rpl_semi_sync_slave_enabled = 1;
以上的启动方式是在命令行操作,也可写在配置文件中。
# 主
plugin-load="rpl_semi_sync_master=semisync_master.so"
rpl_semi_sync_master_enabled=1
# 从
plugin-load="rpl_semi_sync_slave=semisync_slave.so"
rpl_semi_sync_slave_enabled=1
在有的高可用架构下,master和slave需同时启动,以便在切换后能继续使用半同步复制。
plugin-load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
rpl-semi-sync-master-enabled = 1
rpl-semi-sync-slave-enabled = 1
重启从库上的I/O线程,重启后,从库会在主库上注册为半同步复制的从库角色。
-- 从库执行
mysql> stop slave io_thread;
mysql> start slave io_thread;
查看半同步是否在运行
-- 主库
mysql> show status like 'Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | ON |
+-----------------------------+-------+
1 row in set (0.01 sec)
-- 从库
mysql> show status like 'Rpl_semi_sync_slave_status';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Rpl_semi_sync_slave_status | ON |
+----------------------------+-------+
1 row in set (0.00 sec)
这两个变量常用来监控主从是否运行在半同步复制模式下。至此,MySQL半同步复制搭建完毕。
-- 主
mysql> create database db_test;
Query OK, 1 row affected (0.00 sec)
mysql> use db_test;
Database changed
mysql> create table db_test.t1 (a int) engine=innodb;
Query OK, 0 rows affected (0.02 sec)
mysql> insert into t1 values(1);
Query OK, 1 row affected (0.02 sec)
mysql> show status like 'rpl_semi_sync%';
+--------------------------------------------+-------+
| Variable_name | Value |
+--------------------------------------------+-------+
| Rpl_semi_sync_master_clients | 2 |
| Rpl_semi_sync_master_net_avg_wait_time | 0 |
| Rpl_semi_sync_master_net_wait_time | 0 |
| Rpl_semi_sync_master_net_waits | 6 |
| Rpl_semi_sync_master_no_times | 0 |
| Rpl_semi_sync_master_no_tx | 0 |
| Rpl_semi_sync_master_status | ON |
| Rpl_semi_sync_master_timefunc_failures | 0 |
| Rpl_semi_sync_master_tx_avg_wait_time | 758 |
| Rpl_semi_sync_master_tx_wait_time | 2274 |
| Rpl_semi_sync_master_tx_waits | 3 |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0 |
| Rpl_semi_sync_master_wait_sessions | 0 |
| Rpl_semi_sync_master_yes_tx | 3 |
+--------------------------------------------+-------+
14 rows in set (0.00 sec)
mysql>
-- 从
mysql> select * from test.t1;
+------+
| a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
MySQL 8缺省是每条语句自动提交的。主库等待3个事务的确认,分别对应create database、create table、insert语句。平均每个事务等待758微妙,从库正常确认了3个事务。
-- 开启事物
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
-- 向事务表插入记录
mysql> insert into t1 values(2);
Query OK, 1 row affected (0.00 sec)
-- 执行一个DDL语句,创建非事务表t2
mysql> create table t2 (a int) engine=myisam;
Query OK, 0 rows affected (0.01 sec)
-- 向事务表插入记录
mysql> insert into t1 values(3);
Query OK, 1 row affected (0.00 sec)
-- 向非事务表插入记录
mysql> insert into t2 values(3);
Query OK, 1 row affected (0.01 sec)
-- 回滚事务
mysql> rollback;
Query OK, 0 rows affected, 1 warning (0.00 sec)
-- 查看警告
mysql> show warnings;
+---------+------+---------------------------------------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------------------------------------+
| Warning | 1196 | Some non-transactional changed tables couldn't be rolled back |
+---------+------+---------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> select * from t1;
+------+
| a |
+------+
| 1 |
| 2 |
+------+
2 rows in set (0.00 sec)
mysql> select * from t2;
+------+
| a |
+------+
| 3 |
+------+
1 row in set (0.00 sec)
mysql> show status like 'rpl_semi_sync%';
+--------------------------------------------+-------+
| Variable_name | Value |
+--------------------------------------------+-------+
| Rpl_semi_sync_master_clients | 2 |
| Rpl_semi_sync_master_net_avg_wait_time | 0 |
| Rpl_semi_sync_master_net_wait_time | 0 |
| Rpl_semi_sync_master_net_waits | 12 |
| Rpl_semi_sync_master_no_times | 0 |
| Rpl_semi_sync_master_no_tx | 0 |
| Rpl_semi_sync_master_status | ON |
| Rpl_semi_sync_master_timefunc_failures | 0 |
| Rpl_semi_sync_master_tx_avg_wait_time | 691 |
| Rpl_semi_sync_master_tx_wait_time | 4149 |
| Rpl_semi_sync_master_tx_waits | 6 |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0 |
| Rpl_semi_sync_master_wait_sessions | 0 |
| Rpl_semi_sync_master_yes_tx | 6 |
+--------------------------------------------+-------+
14 rows in set (0.00 sec)
从上面的测试可以得出以下结论:
从库数小于rpl_semi_sync_master_wait_for_slave_count值(收到多少个从库的回复认为事务完成,主库返回客户端commit,默认值1),需要等待rpl_semi_sync_master_timeout的时间后才会退化为异步。
-- 2从库都关闭IO线程
mysql> stop slave io_thread;
Query OK, 0 rows affected (0.01 sec)
-- 主库插入事物数据
mysql> insert into t1 values (4);
Query OK, 1 row affected (10.01 sec) -- 等待从库10秒
-- 主库转为异步
mysql> show status like '%Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | OFF |
+-----------------------------+-------+
从库数小于rpl_semi_sync_master_wait_for_slave_count值(收到多少个从库的回复认为事务完成,主库返回客户端commit,默认值1),无需等待rpl_semi_sync_master_timeout,直接退化为异步。
-- 主库关闭rpl_semi_sync_master_wait_no_slave
mysql> set global rpl_semi_sync_master_wait_no_slave=off;
Query OK, 0 rows affected (0.00 sec)
-- 2从库都关闭IO线程
mysql> stop slave io_thread;
Query OK, 0 rows affected (0.01 sec)
-- 主库插入事物数据
mysql> insert into t1 values (5);
Query OK, 1 row affected (0.01 sec) -- 无等待,立刻提交
-- 主库转为异步
mysql> show status like '%Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | OFF |
+-----------------------------+-------+
全文完。