MySQL复制(二)半同步复制

node-current node-current

1. 半同步复制介绍

1.1 原理图

1.2 介绍

传统的异步复制

MySQL 缺省使用异步复制策略。简单说所谓异步复制,指的是主库写二进制日志、从库的 I/O 线程读主库的二进制日志写本地中继日志、从库的 SQL 线程重放中继日志,这三步操作都是异步进行的。如此选择的主要理由是出于性能考虑,与同步复制相比,异步复制显然更快,同时能承载更高的吞吐量。但异步复制的缺点同样明显,不能保证主从数据实时一致,也无法控制从库的延迟时间,因此它不适于要求主从数据实时同步的场景。例如,为了分解读写压力,同一程序写主库读从库,但要求读到的数据与读主库的相同,异步复制不满足这种强数据一致性需求。异步复制的另一个问题是可能会有数据丢失,例如主库宕机时,已经提交的事务可能还没有传到从库上,如果此时强行主从切换,可能导致新主库上的数据不完整。

半同步复制

于是在 MySQL 在 5.5 中就顺其自然地引入了半同步复制 (semi-sync,Semi synchronous Replication),可用作异步复制的替代方案。如上图所示,半同步复制模式下,在主库上提交一个事务/event,它会等待至少一个slave通知主库,slave 已经接收到传递过来的 events 并写入 relay log,才返回给回话层确认写入成功,或者直到传送日志发生超时,所以不会有 commit 丢失的情况。它具有以下特性:

  • 从库在连接主库时表明它是否支持半同步复制。
  • 如果在主库启用了半同步复制,并且至少有一个支持半同步复制的从库,则主库上执行事务提交的线程将等待,直到至少一个半同步从库确认已收到事务的所有事件(此时从库会向主库发送 ACK,Acknowledgement),或者直到发生超时。
  • 只有在将事件写入其中继日志并刷新到磁盘后,从库才会确认收到事务的事件,向主库发送 ACK。
  • 如果在没有任何从库确认事务的情况下发生超时,则主库将退化为异步复制。当至少有一个半同步从库赶上时,主库恢复半同步复制。
  • 必须在主库和从库都启用半同步复制,否则使用异步复制。

当主库阻塞 (等待来自从库确认) 时,它不会返回执行事务的会话。阻塞结束时,主库返回到会话,然后该会话可以继续执行其它语句。此时,事务已在主库提交,并且至少一个从库已确认其事件的接收。在继续之前,主库必须收到的确认从库的数量,可使用rpl_semi_sync_master_wait_for_slave_count系统变量进行配置,默认值为 1。不只是事务提交时,事务回滚时主库也会发生同样的阻塞。MySQL 同时支持多个数据库引擎,当一个事务中既包含事务表又包含非事务表时,回滚即使对事务表没有影响,二进制日志中也会记录非事务表的事件,因为对非事务表的修改无法回滚并且必须发送到从库。在没以START TRANSACTIONSET autocommit = 0开启事务时,每个语句都自动隐式提交。使用半同步复制时,主库上的这类语句就像显式事务提交一样。

为了加深对半同步复制中“半”的理解,简单将其与异步和全同步复制进行比较:

  • 异步复制:主库提交事务时,将事件写入它的二进制日志,而从库在准备就绪时请求它们。主库无需等待从库的ACK回复,直接提交事务并返回客户端。异步复制不确保所有事件都能到达从库,无法保证数据完整性。
  • 全同步复制:当主库提交事务时,所有从库也将在主库返回执行事务的会话之前提交事务。这样做的缺点是完成事务可能会有很大延迟。
  • 半同步复制 (semi-sync):介于异步和完全同步复制之间。主库仅等待至少一个从库接收并记录事件。它不会等待所有从库确认收到,并且从库只需要确认接收,而不是事件已在从库完全执行和提交。

与异步复制相比,半同步复制提供了改进的数据完整性,因为当提交成功返回时,已知数据至少存在于两个位置。但半同步复制确实会对性能产生一些影响,因为需要等待从库,提交速度会变慢,延迟至少是将提交发送到从库并等待从库确认收到的 TCP/IP 往返时间。这意味着半同步复制最好在低延时的网络中使用。

2. 性能提升

2.1 支持发送二进制日志事件和接收 ACK 的异步化

旧版本的半同步复制受限于Binlog Dump线程,原因是该线程承担了两份不同且又十分频繁的任务:

  • 传送二进制日志事件给从库 ;
  • 接收从库的 ACK 反馈信息。

这两个任务是串行的,Binlog Dump线程必须等待从库返回之后才会传送下一个事件。Binlog Dump 线程已然成为整个半同步复制性能的瓶颈。在高并发业务场景下,这样的机制会影响数据库整体的 TPS。单一Binlog Dump线程发送接收的工作流程如下图所示。

为了解决上述问题,在 5.7.4 版本的半同步复制框架中,独立出一个 Ack Receiver 线程 ,专门用于接收从库返回的 ACK 请求,这将之前 Binlog Dump 线程的发送和接收工作分为了两个线程来处理。这样主库上有两个线程独立工作,可以同时发送二进制日志事件到从库,和接收从库的 ACK 信息。因此半同步复制得到了极大的性能提升。Binlog Dump 线程与 Ack Receiver 线程工作流程如下图所示。

Ack Receiver线程在主库启用半同步复制时创建,并在主库禁用半同步复制时销毁。它是自动创建和销毁的,因此不受用户控制。它的状态信息可以在performance_schema中查询得到:

mysql> 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 线程有以下三个状态:

  • Waiting for semi-sync slave connection
  • Waiting for semi-sync ACK from slave
  • Reading semi-sync ACK from slave

在 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 句柄数理论上没有上限了,原因是它是基于链表来存储的。

2.2 控制主库接收确认反馈从库的数量

MySQL 5.7 新增了rpl_semi_sync_master_wait_for_slave_count系统变量,可以用来控制主库接收多少个从库写事务成功反馈,给高可用架构切换提供了灵活性。如下图所示,当该变量值为 2 时,主库需等待两个从库的 ACK。

使用这个功能,可以在不同机房部署主服务器和两个从服务器,并配置半同步复制以将事务复制到至少两个从库,以便在多个服务器一次性崩溃的情况下减少数据丢失的可能,从库越多,数据越安全。

2.3 二进制日志互斥锁改进

旧版本半同步复制在主库提交二进制日志的写会话和Binlog Dump线程读取二进制日志的操作都会对二进制日志添加binlog lock互斥锁,用于保护二进制日志的读写操作。使用此互斥锁,二进制日志读写操作是安全的,但会导致二进制日志文件的读写串行化。不仅Binlog Dump线程和用户会话不能同时读写二进制日志,就连多个Binlog Dump线程本身也无法同时读取。每当一个会话正在读取或写入二进制日志文件时,所有其它会话都必须等待。如此顺序读写可能是一个瓶颈,尤其是当读写操作很慢时。串行化读写二进制日志如下图所示。

MySQL 5.7.2 对binlog lock进行了以下两方面优化:

  • Binlog Dump线程中移除binlog lock
  • 加入了安全边际保证二进制日志的读安全。

二进制日志文件看起来像一个仅追加的日志文件,可以安全地读取没有锁定的二进制事件。因此从Binlog Dump线程中删除了binlog锁。不使用binlog锁,而是为活动 binlog 维护安全读取边界(最大位置)。Binlog Dump线程永远不会读取超过安全读取边界的位置。当边界到达边界时,它将等待边界更新。用户会话负责在追加了二进制事件后更新安全读取边界。改进后的二进制日志读写如下图所示。

从图中可知:

  • 读取二进制日志事件时,Binlog Dump线程不会相互阻塞。
  • 正在写二进制日志事件的用户会话不会阻止Binlog Dump线程。
  • 读取二进制日志事件的Binlog Dump不会阻塞用户会话。

因此,Binlog Dump线程和用户会话都可以获得更好的吞吐量,尤其是当有很多从库时,这种改进非常显着。

3. 数据一致性

3.1 源码剖析

以下源码版本均为官方 MySQL 5.7。MySQL semi-sync是以插件方式引入,在plugin/semisync目录下。这里以semi-sync主要的函数调用为入口,学习semi-sync源码。

plugin/semisync/semisync_master.cc
403 /*******************************************************************************
404  *
405  * <ReplSemiSyncMaster> class: the basic code layer for sync-replication master.
406  * <ReplSemiSyncSlave>  class: the basic code layer for sync-replication slave.
407  *
408  * The most important functions during semi-syn replication listed:
409  *
410  * Master:
          # 实际由Ack_receiver线程调用,处理semi-sync复制状态,获取备库最新binlog位点,唤醒对应线程
411  *  . reportReplyBinlog():  called by the binlog dump thread when it receives
412  *                          the slave's status information.
          # 根据semi-sync运行状态设置数据包头semi-sync标记
413  *  . updateSyncHeader():   based on transaction waiting information, decide
414  *                          whether to request the slave to reply.
          # 存储当前binlog 文件名和偏移量,更新当前最大的事务 binlog 位置
415  *  . writeTranxInBinlog(): called by the transaction thread when it finishes
416  *                          writing all transaction events in binlog.
          # 实现客户端同步等待逻辑
417  *  . commitTrx():          transaction thread wait for the slave reply.
418  *
419  * Slave:
          # 确认网络包头是否有semi-sync标记
420  *  . slaveReadSyncHeader(): read the semi-sync header from the master, get the
421  *                           sync status and get the payload for events.
          # 给Master发送ACK报文
422  *  . slaveReply():          reply to the master about the replication progress.
423  *
424  ******************************************************************************/
#Ack_receiver线程,不断遍历slave,通过select监听slave网络包,处理semi-sync复制状态,唤醒等待线程。
plugin/semisync/semisync_master_ack_receiver.cc Ack_receiver::run()
->plugin/semisync/semisync_master.cc ReplSemiSyncMaster::reportReplyPacket
  ->plugin/semisync/semisync_master.cc ReplSemiSyncMaster::reportReplyBinlog
 
#binlog Dump线程。如果slave是semi-slave,通过add_slave将slave添加到监听队列,在发送网络包时根据semi-sync运行状态设置包头的semi-sync标记。
sql/rpl_binlog_sender.cc Binlog_sender::run()
->sql/rpl_binlog_sender.cc Binlog_sender::send_binlog
  ->sql/rpl_binlog_sender.cc Binlog_sender::send_events
    ->sql/rpl_binlog_sender.cc Binlog_sender::before_send_hook
      ->plugin/semisync/semisync_master_plugin.cc repl_semi_before_send_event
        ->plugin/semisync/semisync_master.cc ReplSemiSyncMaster::updateSyncHeader
 
#事务提交阶段,在flush binlog后,存储当前binlog 文件名和偏移量,更新当前最大的事务 binlog 位置。
sql/binlog.cc MYSQL_BIN_LOG::ordered_commit
 ->plugin/semisync/semisync_master_plugin.cc repl_semi_report_binlog_update//after_flush
   ->plugin/semisync/semisync_master.cc repl_semisync.writeTranxInBinlog
 
#事务提交阶段,客户端等待处理逻辑,分为after_sync和after_commit两种情况
sql/binlog.cc MYSQL_BIN_LOG::ordered_commit
  ->sql/binlog.cc process_after_commit_stage_queue || call_after_sync_hook
    ->plugin/semisync/semisync_master_plugin.cc repl_semi_report_commit || repl_semi_report_binlog_sync
      ->plugin/semisync/semisync_master.cc ReplSemiSyncMaster::commitTrx
 
#Slave IO线程,读取数据后后检查包头是否有semi-sync标记。
sql/rpl_slave.cc handle_slave_io
  ->plugin/semisync/semisync_slave_plugin.cc repl_semi_slave_read_event
    ->plugin/semisync/semisync_slave.cc ReplSemiSyncSlave::slaveReadSyncHeader
 
#Slave IO线程,在queue event后,在需要回复Master ACK报文的时候,回复Master ACK报文。
sql/rpl_slave.cc handle_slave_io
  ->plugin/semisync/semisync_slave_plugin.cc repl_semi_slave_queue_event
    ->plugin/semisync/semisync_slave.cc ReplSemiSyncSlave::slaveReply

首先半同步方式,主库在等待备库ack时候,如果超时会退化为异步,这就可能导致数据丢失。在接下来分析中,先假设rpl_semi_sync_master_timeout足够大,不会退化为异步方式。这里通过三个参数rpl_semi_sync_master_wait_pointsync_binlogsync_relay_log的配置来对semi-sync做数据一致性的分析。

3.2 rpl_semi_sync_master_wait_point的配置

3.2.1 源码剖析

plugin/semisync/semisync_master_plugin.cc
 
68 int repl_semi_report_binlog_sync(Binlog_storage_param *param,
69                                  const char *log_file,
70                                  my_off_t log_pos)
71 {
72   if (rpl_semi_sync_master_wait_point == WAIT_AFTER_SYNC)
73     return repl_semisync.commitTrx(log_file, log_pos);
74   return 0;
75 }
 
97 int repl_semi_report_commit(Trans_param *param)
   ...
102   if (rpl_semi_sync_master_wait_point == WAIT_AFTER_COMMIT &&
106     return repl_semisync.commitTrx(binlog_name, param->log_pos);

3.2.2 配置为WAIT_AFTER_COMMIT

rpl_semi_sync_master_wait_pointWAIT_AFTER_COMMIT时,commitTrx的调用在engine层commit之后(在ordered_commit函数中process_after_commit_stage_queue调用),如上图所示。即在等待 Slave ACK 时候,虽然没有返回当前客户端,但事务已经提交,其他客户端会读取到已提交事务。如果 Slave 端还没有读到该事务的 events,同时主库发生了 crash,然后切换到备库。那么之前读到的事务就不见了,出现了幻读,如下图所示。

补充一点:

除了幻读,这种场景还有一个问题是,如果客户端会重试提交该事务到新的主上,当宕机的主库重新启动后,以从库的身份重新加入到该主从结构中。此时会发现,该事务在从库中被提交了两次,一次是之前作为主的时候,一次是被新主同步过来的,结果依然是主从数据不一致。

3.2.3 配置为WAIT_AFTER_SYNC

MySQL 针对上述问题,在 5.7.2 引入了Loss-less Semi-Synchronous,在调用binlog sync之后,engine 层 commit 之前等待 Slave ACK。这样只有在确认 Slave 收到事务 events 后,事务才会提交。在 commit 之前等待 Slave ACK,同时可以堆积事务,利于group commit,有利于提升性能。如下图所示:

其实上图流程中存在着会导致主备数据不一致,使主备同步失败的情形。见下面sync_binlog配置的分析。

3.3 sync_binlog的配置

3.3.1 源码剖析

sql/binlog.cc ordered_commit
		#当sync_period(sync_binlog)为1时,在sync之后update binlog end pos
9002   update_binlog_end_pos_after_sync= (get_sync_period() == 1);
       ...
9021     if (!update_binlog_end_pos_after_sync)
		#更新binlog end position,dump线程会发送更新后的events
9022       update_binlog_end_pos();
       ...
		#
9057     std::pair<bool, bool> result= sync_binlog_file(false);
       ...
9061   if (update_binlog_end_pos_after_sync)
9062   {
       ...
9068       update_binlog_end_pos(tmp_thd->get_trans_pos());
9069   }
 
sql/binlog.cc sync_binlog_file
8618 std::pair<bool, bool>
8619 MYSQL_BIN_LOG::sync_binlog_file(bool force)
8620 {
8621   bool synced= false;
8622   unsigned int sync_period= get_sync_period();//sync_binlog值
		#sync_period为0不做sync操作,其他值为达到sync调用次数后sync
8623   if (force || (sync_period && ++sync_counter >= sync_period))
8624   {

3.3.2 配置分析

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_SYNCWAIT_AFTER_COMMIT两图中Send Events的位置,也可能导致主备数据不一致,出现同步失败的情形。实际在rpl_semi_sync_master_wait_point分析的图中是sync binlog大于 1 的情况。根据上面源码,流程如下图所示。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。流程如下图所示。这时候,对于每一个事务都需要sync binlog,同时sync binlog和网络发送 events 会是一个串行的过程,性能下降明显。

3.4 sync_relay_log的配置

3.4.1 源码剖析

sql/rpl_slave.cc handle_slave_io
 
5764       if (queue_event(mi, event_buf, event_len))
           ...
5771       if (RUN_HOOK(binlog_relay_io, after_queue_event,
5772                    (thd, mi, event_buf, event_len, synced)))
 
after_queue_event
->plugin/semisync/semisync_slave_plugin.cc repl_semi_slave_queue_event
->plugin/semisync/semisync_slave.cc ReplSemiSyncSlave::slaveReply
 
queue_event
->sql/binlog.cc MYSQL_BIN_LOG::append_buffer(const char* buf, uint len, Master_info *mi)
->sql/binlog.cc after_append_to_relay_log(mi);
->sql/binlog.cc flush_and_sync(0)
->sql/binlog.cc sync_binlog_file(force)

3.4.2 配置分析

在 Slave 的 IO 线程中get_sync_period获得的是sync_relay_log的值,与sync_binlog对 sync 控制一样。当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 就会发现数据丢失。

我们可以发现当前原生的 MySQL 主从复制实现要同时满足数据一致性、高可用和高性能,依然是力有不逮。

4. 部署半同步复制

4.1 服务器环境部署

在异步复制的环境基础上继续配置,参考【MySQL复制环境搭建】【MySQL复制(一)异步复制之Position】部署一主一从环境。

主机名 IP Server ID OS MySQL version
Mysql57 192.168.2.57 57 Centos 7.8 Mysql 5.7.40
Mysql57-Slave 192.168.2.58 58 Centos 7.8 Mysql 5.7.40

4.2 启用半同步前置条件

半同步复制是使用插件实现的,因此必须将插件安装到 MySQL 服务器中才能使用它们。安装插件后,可以通过与之关联的系统变量来控制它。安装插件之前,这些系统变量是不可用的。要使用半同步复制,必须满足以下要求:

  • MySQL 5.5 及以上版本。
  • 安装插件需要 MySQL 服务器支持动态加载。
    • 要验证这一点,检查have_dynamic_loading系统变量的值是否为 YES。MySQL 5 和 8 缺省为 YES。
  • 已经启动了异步复制。
    • 主从同步要配置基于整个数据库的,不要配置基于某个库的同步,即同步时不要过滤库。
  • 半同步不支持多源复制(multi-source replication)。
#1、MySQL 5.5及以上版本
[root@MySQL57 ~]# mysql -V
mysql  Ver 14.14 Distrib 5.7.40, for Linux (x86_64) using  EditLine wrapper
#2、变量have_dynamic_loading为YES,MySQL 5和8缺省为YES
mysql> show variables like "have_dynamic_loading";
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| have_dynamic_loading | YES   |
+----------------------+-------+
1 row in set (0.06 sec)

4.3 安装半同步复制插件

安装设置半同步复制,需要REPLICATION_SLAVE_ADMIN或 SUPER 权限。MySQL 发行版包括主、从端的半同步复制插件文件semisync_master.sosemisync_slave.so,缺省位于MySQL安装目录下的lib/plugin目录下,本例中为/usr/local/mysql/lib/plugin。也可以通过设置plugin_dir系统变量的值指定插件目录位置。执行下面的 SQL 语句加载插件。

# 查看MYSQL服务器是否支持动态插件
mysql> SELECT @@have_dynamic_loading;
+------------------------+
| @@have_dynamic_loading |
+------------------------+
| YES                    |
+------------------------+
# 主
install plugin rpl_semi_sync_master soname 'semisync_master.so';
# 从
install plugin rpl_semi_sync_slave soname 'semisync_slave.so';
# 如果尝试安装插件导致Linux上出现类似于此处所示的错误,则必须安装libimf:
# 可以从https://dev.mysql.com/downloads/os-linux.html获取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)
# 检查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        |
+----------------------+---------------+
#从
mysql> select plugin_name, plugin_status from information_schema.plugins where plugin_name like '%semi%';
+---------------------+---------------+
| plugin_name         | plugin_status |
+---------------------+---------------+
| rpl_semi_sync_slave | ACTIVE        |
+---------------------+---------------+
# 使用 show variables
#主 
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)
# 或使用 show plugins;
# 主
mysql> show plugins;
....
| rpl_semi_sync_master       | ACTIVE   | REPLICATION        | semisync_master.so | GPL     |
....
# 从
mysql> show plugins;
....
| rpl_semi_sync_slave        | ACTIVE   | REPLICATION        | semisync_slave.so | GPL     |
....
45 rows in set (0.01 sec)

4.4 启用半同步复制

安装半同步复制插件后,默认情况下会禁用它。必须在主库和从库都启用插件才能启用半同步复制。如果仅启用一侧,则复制将是异步的。

4.4.1 启用半同步复制

#主
set global rpl_semi_sync_master_enabled = 1;
#从
set global rpl_semi_sync_slave_enabled = 1;

以上的启动方式是在命令行操作,也可写在配置文件中持久化。

# 主
[root@Master ~]# cat /etc/my.cnf
[mysqld]
plugin-load="rpl_semi_sync_master=semisync_master.so"
rpl_semi_sync_master_enabled=1
# 从
[root@Slave ~]# cat /etc/my.cnf
[mysqld]
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

4.4.2 重启从库上的I/O线程

#从
mysql> 
stop slave io_thread;
start slave io_thread;

如果没有重启,则默认还是异步复制。重启后,从库会在主库上注册为半同步复制的从库角色。

4.4.3 查看半同步是否在运行

-- 主
mysql> show global status like '%semi%';
+--------------------------------------------+-------+
| Variable_name                              | Value |
+--------------------------------------------+-------+
| Rpl_semi_sync_master_clients               | 1     |
| Rpl_semi_sync_master_net_avg_wait_time     | 0     |
| Rpl_semi_sync_master_net_wait_time         | 0     |
| Rpl_semi_sync_master_net_waits             | 0     |
| 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      | 0     |
| Rpl_semi_sync_master_tx_wait_time          | 0     |
| Rpl_semi_sync_master_tx_waits              | 0     |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0     |
| Rpl_semi_sync_master_wait_sessions         | 0     |
| Rpl_semi_sync_master_yes_tx                | 0     |
+--------------------------------------------+-------+
 
-- 从
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 半同步复制搭建完毕。

5. 半同步复制相关变量

5.1 系统变量

仅当使用 INSTALL PLUGIN 安装了相应的插件时,系统和状态变量才可用。

  • 主相关系统变量:
    • rpl_semi_sync_master_enabled:主库是否启用了半同步复制,默认为 OFF。
    • rpl_semi_sync_master_timeout:等待从库的 ACK 回复的超时时间(主库在超时并退化到异步复制之前等待来自从库确认提交的时间),默认为 1000 ( 10 秒 )。
    • rpl_semi_sync_master_trace_level:半同步复制时主库的调试级别。
    • rpl_semi_sync_master_wait_for_slave_count:主库在超时时间内需要收到多少个 ACK 回复才认为此次提交成功,否则就降级为异步复制。该变量在MySQL 5.7.3才提供,在此之前的版本都默认为收到 1 个 ACK 则确认成功,且不可更改。MySQL 5.7.3 之后该变量的默认值也是 1。
    • rpl_semi_sync_master_wait_no_slave
      • 默认值为 ON,当状态变量Rpl_semi_sync_master_clients中的值小于rpl_semi_sync_master_wait_for_slave_count时,Rpl_semi_sync_master_status依旧为 ON,只有当事务提交后等待rpl_semi_sync_master_timeout超时后,Rpl_semi_sync_master_status才会变为 OFF,即降级为异步复制;
      • 为 OFF 时,当状态变量Rpl_semi_sync_master_clients中的值小于rpl_semi_sync_master_wait_for_slave_count时,Rpl_semi_sync_master_status立即显示为 OFF,即立即降级为异步复制。
    • rpl_semi_sync_master_wait_point:控制主库上 commit、接收 ack、返回消息给客户端的时间点。值为AFTER_SYNCAFTER_COMMIT。该选项是 MySQL5.7.2 后引入的,默认值为AFTER_SYNC。此版本之前,等价于使用了AFTER_COMMIT模式。
  • 从相关系统变量:
    • rpl_semi_sync_slave_enabled:从库是否开启半同步复制,默认为 OFF。
    • rpl_semi_sync_slave_trace_level:从库的调试级别。

5.2 状态变量

半同步复制功能的插件公开了几个状态变量,可以检查这些变量以确定其操作状态。这些变量仅当安装了半同步复制插件以后才可用。

-- 主
mysql> show status like 'rpl_semi_sync%';
+--------------------------------------------+-------+
| Variable_name                              | Value |
+--------------------------------------------+-------+
| Rpl_semi_sync_master_clients               | 1     |
| Rpl_semi_sync_master_net_avg_wait_time     | 0     |
| Rpl_semi_sync_master_net_wait_time         | 0     |
| Rpl_semi_sync_master_net_waits             | 0     |
| 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      | 0     |
| Rpl_semi_sync_master_tx_wait_time          | 0     |
| Rpl_semi_sync_master_tx_waits              | 0     |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0     |
| Rpl_semi_sync_master_wait_sessions         | 0     |
| Rpl_semi_sync_master_yes_tx                | 0     |
+--------------------------------------------+-------+
14 rows in set (0.00 sec)
-- 从
mysql> show status like 'rpl_semi_sync%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Rpl_semi_sync_slave_status | ON    |
+----------------------------+-------+
1 row in set (0.01 sec)
  • 主库有 14 个半同步复制相关的状态变量:
    • Rpl_semi_sync_master_clients:当前连接了多少个半同步从库。
    • Rpl_semi_sync_master_net_avg_wait_time:主库等待从库回复的平均时间,以微秒为单位。此变量始终为 0,不推荐使用,并且将在以后的版本中删除。
    • Rpl_semi_sync_master_net_wait_time:主库等待从库回复的总时间,以微秒为单位。此变量始终为 0,不推荐使用,并且将在以后的版本中删除。
    • Rpl_semi_sync_master_net_waits:主库等待从库回复的总次数。
    • Rpl_semi_sync_master_no_times:主库关闭半同步复制的次数。
    • Rpl_semi_sync_master_no_tx:从库未成功确认的事务数。
    • Rpl_semi_sync_master_status:为 ON 时表示主库使用半同步复制,OFF 表示主库使用异步复制。
    • Rpl_semi_sync_master_timefunc_failures:调用 gettimeofday 等时间函数时主库失败的次数。
    • Rpl_semi_sync_master_tx_avg_wait_time:主库等待一个事务的平均时间,以微秒为单位。
    • Rpl_semi_sync_master_tx_wait_time:主等待事务的总时间,以微秒为单位。
    • Rpl_semi_sync_master_tx_waits:主库等待事务的总次数。
    • Rpl_semi_sync_master_wait_pos_backtraverse:主库等待事件的二进制坐标低于之前等待事件的总次数。当事务开始等待回复的顺序与其二进制日志事件的写入顺序不同时,就会发生这种情况。
    • Rpl_semi_sync_master_wait_sessions:当前等待从库回复的会话数。
    • Rpl_semi_sync_master_yes_tx:从库成功确认的事务数。
  • 从库
    • Rpl_semi_sync_slave_status:从库上只有这一个半同步复制相关的状态变量,为 ON 时表示从库使用半同步复制,OFF 表示从库使用异步复制。

6. 数据测试

6.1 正常提交事务

-- 主
mysql> drop database test;
mysql> create database test;
Query OK, 1 row affected (0.00 sec)
 
mysql> use test;
Database changed
mysql> create table 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               | 1     |
| Rpl_semi_sync_master_net_avg_wait_time     | 0     |
| Rpl_semi_sync_master_net_wait_time         | 0     |
| Rpl_semi_sync_master_net_waits             | 4     |
| 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      | 1300  |
| Rpl_semi_sync_master_tx_wait_time          | 5200  |
| Rpl_semi_sync_master_tx_waits              | 4     |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0     |
| Rpl_semi_sync_master_wait_sessions         | 0     |
| Rpl_semi_sync_master_yes_tx                | 4     |
+--------------------------------------------+-------+
14 rows in set (0.00 sec) 
-- 从
# 测试环境主库drop了之前的test,导致从库 Slave_SQL_Running: No
# 解决方法就是从库让指针往下移动
        #mysql> stop slave;
        #mysql> set GLOBAL SQL_SLAVE_SKIP_COUNTER=1;
        #mysql> start slave;
mysql> use test;
mysql> select * from test.t1;
+------+
| a    |
+------+
|    1 |
+------+
1 row in set (0.00 sec)

MySQL 5 和 8 缺省是每条语句自动提交的。主库等待 3 个事务的确认,分别对应create database、create table、insert语句。平均每个事务等待 1300 微妙,从库正常确认了 3 个事务。

6.2 回滚事务

-- 主
# 默认开启
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON   |
+---------------+-------+
1 row in set (0.01 sec)
 
mysql> set session autocommit=0;                  -- 关闭自动提交,开启事务
Query OK, 0 rows affected (0.00 sec)
 
mysql> insert into t1 values(2);                  -- 向事务表插入记录
Query OK, 1 row affected (0.00 sec)
 
mysql> create table t2 (a int) engine=myisam;     -- 执行一个DDL语句,创建非事务表t2
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 couldnt 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               | 1     |
| Rpl_semi_sync_master_net_avg_wait_time     | 0     |
| Rpl_semi_sync_master_net_wait_time         | 0     |
| Rpl_semi_sync_master_net_waits             | 7     |
| 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      | 1332  |
| Rpl_semi_sync_master_tx_wait_time          | 9326  |
| Rpl_semi_sync_master_tx_waits              | 7     |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0     |
| Rpl_semi_sync_master_wait_sessions         | 0     |
| Rpl_semi_sync_master_yes_tx                | 7     |
+--------------------------------------------+-------+
14 rows in set (0.00 sec)
mysql> 
-- 从
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)
mysql> select * from test.t1;
+------+
| a    |
+------+
|    1 |
|    2 |
+------+
2 rows in set (0.00 sec)
mysql> select * from test.t2;
+------+
| a    |
+------+
|    3 |
+------+
1 row in set (0.01 sec)
mysql>

从上面的测试可以得出以下结论:

  • set session autocommit=0语句不被复制。
  • DDL 语句会触发一个 commit,自动提交 DDL 语句本身及其前面未提交的事务。
  • 非事务表不能回滚。
  • Rpl_semi_sync_master_tx_waitsRpl_semi_sync_master_yes_tx都增加了 3,分别对应三个事务
    • insert into t1 values(2);
    • create table t2 (a int) engine=myisam;
    • insert into t2 values(3);

6.3 rpl_semi_sync_master_wait_no_slave 为 ON 时从库数小于 rpl_semi_sync_master_wait_for_slave_count

-- 关闭从库的复制
mysql> stop slave;
Query OK, 0 rows affected (0.01 sec)
-- 查看当前是否启用半同步复制
mysql> show status like 'rpl_semi_sync%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Rpl_semi_sync_slave_status | OFF   |
+----------------------------+-------+
1 row in set (0.00 sec)
mysql>
 
-- 主
-- 查看当前半同步从库数
mysql> show status like 'Rpl_semi_sync_master_clients';  
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| Rpl_semi_sync_master_clients | 0     |
+------------------------------+-------+
1 row in set (0.00 sec)
-- 查看当前是否启用半同步复制
mysql> show status like 'Rpl_semi_sync_master_status';   
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | ON    |
+-----------------------------+-------+
1 row in set (0.00 sec)
 
mysql> insert into t1 values(3);
Query OK, 1 row affected (0.00 sec)
 
mysql> commit;
Query OK, 0 rows affected (10.00 sec)
-- 等待了10秒
mysql> select * from t1;
+------+
| a    |
+------+
|    1 |
|    2 |
|    3 |
+------+
3 rows in set (0.00 sec)
mysql> show status like 'Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | OFF   |
+-----------------------------+-------+
1 row in set (0.01 sec)
mysql>

可以看到,主库提交后等待了 10 秒 (rpl_semi_sync_master_timeout的缺省值) 才完成,而且此时主库已经降级为异步复制。

-- 启动一个从库的复制
mysql> start slave;
Query OK, 0 rows affected (0.00 sec)
 
mysql> select * from test.t1;
+------+
| a    |
+------+
|    1 |
|    2 |
|    3 |
+------+
3 rows in set (0.00 sec)
 
mysql> show status like 'rpl_semi_sync%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Rpl_semi_sync_slave_status | ON    |
+----------------------------+-------+
1 row in set (0.01 sec)
mysql>
 
-- 主
mysql> show variables like 'rpl_semi_sync_master_wait_for_slave_count';
+-------------------------------------------+-------+
| Variable_name                             | Value |
+-------------------------------------------+-------+
| rpl_semi_sync_master_wait_for_slave_count | 1     |
+-------------------------------------------+-------+
1 row in set (0.00 sec)
 
mysql> show status like 'Rpl_semi_sync_master_clients';
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| Rpl_semi_sync_master_clients | 1     |
+------------------------------+-------+
1 row in set (0.00 sec)
 
mysql> show status like 'Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | ON    |
+-----------------------------+-------+
1 row in set (0.00 sec)
 
mysql> insert into t1 values(4);
Query OK, 1 row affected (0.00 sec)
 
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> 
 
-- 从
mysql> select * from test.t1;
+------+
| a    |
+------+
|    1 |
|    2 |
|    3 |
|    4 |
+------+
4 rows in set (0.00 sec)
mysql> 

Rpl_semi_sync_master_clients大于等于rpl_semi_sync_master_wait_for_slave_count时,主库立即恢复为半同步复制,并发送那些未被复制的binlog

6.4 rpl_semi_sync_master_wait_no_slave 为 OFF 时从库数小于 rpl_semi_sync_master_wait_for_slave_count

-- 主
-- 关闭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)
 
mysql> show variables like 'rpl_semi_sync_master_wait_no_slave';
+------------------------------------+-------+
| Variable_name                      | Value |
+------------------------------------+-------+
| rpl_semi_sync_master_wait_no_slave | OFF   |
+------------------------------------+-------+
1 row in set (0.00 sec)
 
mysql> 
-- 从
-- 关闭所有从库的复制
mysql> stop slave;
Query OK, 0 rows affected (0.01 sec)
mysql> 
 
-- 主
mysql> show status like 'Rpl_semi_sync_master_clients';  -- 查看当前半同步从库数
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| Rpl_semi_sync_master_clients | 0     |
+------------------------------+-------+
1 row in set (0.00 sec)
 
mysql> show status like 'Rpl_semi_sync_master_clients';
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| Rpl_semi_sync_master_clients | 0     |
+------------------------------+-------+
1 row in set (0.00 sec)
 
mysql> show status like 'Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | OFF   |
+-----------------------------+-------+
1 row in set (0.00 sec)
 
mysql> insert into t1 values(5);
Query OK, 1 row affected (0.00 sec)
 
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
 
mysql> select * from t1;
+------+
| a    |
+------+
|    1 |
|    2 |
|    3 |
|    4 |
|    5 |
+------+
5 rows in set (0.00 sec)
 
mysql> 

可以看到,当rpl_semi_sync_master_wait_no_slave设置为 OFF 时,一旦Rpl_semi_sync_master_clients小于rpl_semi_sync_master_wait_for_slave_countRpl_semi_sync_master_status立即变为 OFF,即立即降为异步复制。

扫码关注公众号,获取最新发布。

posted on 2023-11-13 14:54  一介IT  阅读(356)  评论(0)    收藏  举报

导航