使用容器实现 MariaDB 半同步复制

在博客文章使用容器实现 MariaDB 复制中,我们展示了如何使用 Docker 容器在 MariaDB 中正确复制数据。
我们使用了标准或异步或延迟复制。


这种复制类型的问题在于,如果主库发生故障,可能会导致数据丢失。在主库上提交的事务不会发送到副本,并且副本也不会提交更改。在这种情况下,从主库到副本的故障转移可能导致与主库相比缺失事务。


为了克服这类错误,有半同步复制,它自 MariaDB 10.4 版本起集成,还有完全同步复制,我们计划最终将其实现为MDEV-19140。另一种选择是使用 Galera
在完全同步复制中,所有副本都必须验证一个事务,然后主库才会返回执行该事务的会话;而在半同步复制中,至少一个副本需要确认事务事件已成功传输。与完全同步复制相比,这提供了更快的复制速度。
与异步复制相比,半同步复制提供了改进的数据完整性,因为当主库收到至少一个副本的确认并提交更改时,我们可以确信数据至少存在于 2 个地方。


在本博客中,我们首先探讨

  1. 如何通过示例演示设置半同步复制。

之后,对于更感兴趣的读者,我们将以图表的形式可视化以下内容

  1. 标准复制配置
  2. 标准复制事务示例
  3. 半同步复制配置
  4. 半同步复制事务示例

1. 使用容器的半同步演示示例

  • 在本示例中,我们将使用无状态应用程序,仅作为概念验证。
  • 在我们的mariadb-docker GitHub仓库中有一个Docker compose 文件
    • 在主库上启用半同步的关键是设置 --rpl_semi_sync_master_enabled,在副本上启用半同步是设置 --rpl_semi_sync_slave_enabled
  • 我们将使用 GTID(它们是自动启用的),并在下面的示例中展示它们如何变化。
    • 这样,复制将从最后复制到副本的 GTID 的位置开始(从系统变量 gtid_slave_pos 中查看)。
  • 本节包含以下内容
    1.1 创建和检查容器
    1.2 检查主库
    1.3 检查二进制日志
    1.4 检查副本
    1.5 开始复制

1.1 创建和检查容器

  • 启动容器
$ docker compose -f compose-replication-semisync.yml up
  • 检查容器状态(确保它们处于健康状态)
$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS                          PORTS      NAMES
1ddb1fa67fef   mariadb:lts    "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes (healthy)          3306/tcp   mariadb-replica-2
dca7a79526ca   mariadb:lts    "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes (healthy)          3306/tcp   mariadb-replica-1
addabd306bb4   mariadb:lts    "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes (healthy)          3306/tcp   mariadb-primary
  • 检查日志

1.2 检查主库

  • 使用 mariadb 客户端检查半同步是否启用
$ docker exec -it mariadb-primary mariadb -uroot -psecret -e "select @@rpl_semi_sync_master_enabled;"
+--------------------------------+
| @@rpl_semi_sync_master_enabled |
+--------------------------------+
|                              1 |
+--------------------------------+
  • 检查 rpl_semi_sync_master_timeout 变量(默认为 10 [s])。当这段时间过去,主库未从副本获得任何确认时,它会切换回异步复制。
$ docker exec -it mariadb-primary mariadb -uroot -psecret -e "select @@rpl_semi_sync_master_timeout;"
+--------------------------------+
| @@rpl_semi_sync_master_timeout |
+--------------------------------+
|                          10000 |
+--------------------------------+
  • 检查主库状态(同时检查二进制日志中的位置和二进制日志的名称)。
$ docker exec mariadb-primary mariadb -uroot -psecret -e "show master status\G;"
*************************** 1. row ***************************
            File: mariadb-bin.000002
        Position: 344
    Binlog_Do_DB: 
Binlog_Ignore_DB:
  • 检查用于复制的数据库
$ docker exec -it mariadb-primary mariadb -uroot -psecret -e "show databases like '%test%'";
+-------------------+
| Database (%test%) |
+-------------------+
| testdb            |
+-------------------+

1.3 检查二进制日志

  • 有两个二进制日志和一个索引文件
$ docker exec --workdir /var/lib/mysql mariadb-primary bash -c "ls maria*"
mariadb-bin.000001
mariadb-bin.000002
mariadb-bin.index
  • 通过 mariadb 客户端也可以看到同样的信息
$ docker exec mariadb-primary mariadb -uroot -psecret -e "show binary logs\G;"
*************************** 1. row ***************************
 Log_name: mariadb-bin.000001
File_size: 1166
*************************** 2. row ***************************
 Log_name: mariadb-bin.000002
File_size: 344

你可以使用 mariadb-binlog 客户端自由地检查二进制日志。

  • 全新启动示例(注释将在后面的示例中排除)
$ docker exec mariadb-primary mariadb-binlog /var/lib/mysql/mariadb-bin.000002
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!40019 SET @@session.max_delayed_threads=0*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#231123 12:21:47 server id 1  end_log_pos 256 CRC32 0x21866123 	Start: binlog v 4, server v 10.11.6-MariaDB-1:10.11.6+maria~ubu2204-log created 231123 12:21:47 at startup
# Warning: this binlog is either in use or was not closed properly.
ROLLBACK/*!*/;
BINLOG '
20NfZQ8BAAAA/AAAAAABAAABAAQAMTAuMTEuNi1NYXJpYURCLTE6MTAuMTEuNittYXJpYX51YnUy
MjA0LWxvZwAAAAAAAADbQ19lEzgNAAgAEgAEBAQEEgAA5AAEGggAAAAICAgCAAAACgoKAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAEEwQADQgICAoKCgEjYYYh
'/*!*/;
# at 256
#231123 12:21:47 server id 1  end_log_pos 299 CRC32 0xdd5f27d6 	Gtid list [0-1-5]
# at 299
#231123 12:21:47 server id 1  end_log_pos 344 CRC32 0xca6cbf75 	Binlog checkpoint mariadb-bin.000002
DELIMITER ;
# End of log file
ROLLBACK /* added by mysqlbinlog */;
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
  • 创建表和插入新值时,此文件中将有新的更改。

1.4 检查副本

  • 检查副本的半同步状态(我们只检查一个副本。对于第二个副本,应该也会发生同样的情况)
$ docker exec -it mariadb-replica-2 mariadb -uroot -psecret -e "select @@rpl_semi_sync_slave_enabled;"
+-------------------------------+
| @@rpl_semi_sync_slave_enabled |
+-------------------------------+
|                             1 |
+-------------------------------+
  • 检查复制的数据库
$ docker exec -it mariadb-replica-1 mariadb -uroot -psecret -e "show databases like '%test%'";
+-------------------+
| Database (%test%) |
+-------------------+
| testdb            |
+-------------------+

$ docker exec -it mariadb-replica-2 mariadb -uroot -psecret -e "show databases like '%test%'";
+-------------------+
| Database (%test%) |
+-------------------+
| testdb            |
+-------------------+

1.5 开始复制

  • 我们将执行 2 个事务。
  • 对于每个事务,我们将检查
    • 主库状态查询(见下文状态),
    • 使用 mariadb-binlog 客户端验证二进制日志(见下文二进制日志),
    • 在副本上验证的复制结果(见下文副本)。
  • 事务 1: create table t(t int)
    • 状态(我们获取到新的文件名和二进制日志中的当前位置)
MariaDB [testdb]> show master status;
+--------------------+----------+--------------+------------------+
| File               | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+--------------------+----------+--------------+------------------+
| mariadb-bin.000003 |      485 |              |                  |
+--------------------+----------+--------------+------------------+
1 row in set (0.000 sec)
  • 事务 1: create table t(t int)
    • 二进制日志
$ docker exec mariadb-primary mariadb-binlog /var/lib/mysql/mariadb-bin.000003
#231123 12:57:18 server id 1  end_log_pos 299 CRC32 0x0f7f30b3 	Gtid list [0-1-5]
# at 299
#231123 12:57:18 server id 1  end_log_pos 344 CRC32 0x263247e6 	Binlog checkpoint mariadb-bin.000003
# at 344
#231123 14:05:49 server id 1  end_log_pos 386 CRC32 0x9f9ba209 	GTID 0-1-6 ddl
...
create table t(t int)
...
  • 事务 1: create table t(t int)
    • 副本 – 我们看到表已被复制,并且 IO/SQL 线程正在运行(来自 show replica hosts 的结果)。
$ docker exec -it mariadb-replica-2 mariadb -uroot -psecret -e "use testdb; show tables; show create table t;"
+------------------+
| Tables_in_testdb |
+------------------+
| t                |
+------------------+
+-------+------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                     |
+-------+------------------------------------------------------------------------------------------------------------------+
| t     | CREATE TABLE `t` (
`t` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci |
+-------+------------------------------------------------------------------------------------------------------------------+
MariaDB [(none)]> show replica status \G
*************************** 1. row ***************************
            Slave_IO_State: Waiting for master to send event
            Master_Host: mariadb-primary
            Master_User: repl
            Master_Port: 3306
            Connect_Retry: 10
        Master_Log_File: mariadb-bin.000003
    Read_Master_Log_Pos: 485
            Relay_Log_File: mariadb-relay-bin.000002
            Relay_Log_Pos: 786
    Relay_Master_Log_File: mariadb-bin.000003
        Slave_IO_Running: Yes
        Slave_SQL_Running: Yes
    Replicate_Rewrite_DB: 
        Replicate_Do_DB: 
    Replicate_Ignore_DB: 
        Replicate_Do_Table: 
    Replicate_Ignore_Table: 
Replicate_Wild_Do_Table: 
Replicate_Wild_Ignore_Table: 
                Last_Errno: 0
                Last_Error: 
            Skip_Counter: 0
    Exec_Master_Log_Pos: 485
        Relay_Log_Space: 1097
        Until_Condition: None
            Until_Log_File: 
            Until_Log_Pos: 0
        Master_SSL_Allowed: No
        Master_SSL_CA_File: 
        Master_SSL_CA_Path: 
        Master_SSL_Cert: 
        Master_SSL_Cipher: 
            Master_SSL_Key: 
    Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
            Last_IO_Errno: 0
            Last_IO_Error: 
            Last_SQL_Errno: 0
            Last_SQL_Error: 
Replicate_Ignore_Server_Ids: 
        Master_Server_Id: 1
            Master_SSL_Crl: 
        Master_SSL_Crlpath: 
                Using_Gtid: Slave_Pos
            Gtid_IO_Pos: 0-1-6
Replicate_Do_Domain_Ids: 
Replicate_Ignore_Domain_Ids: 
            Parallel_Mode: optimistic
                SQL_Delay: 0
    SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
        Slave_DDL_Groups: 1
Slave_Non_Transactional_Groups: 0
Slave_Transactional_Groups: 0
1 row in set (0.000 sec)
  • 事务 2: insert into t values (1),(2),(314),(1618);
    • 状态
MariaDB [testdb]> show master status;
+--------------------+----------+--------------+------------------+
| File               | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+--------------------+----------+--------------+------------------+
| mariadb-bin.000003 |      668 |              |                  |
+--------------------+----------+--------------+------------------+
1 row in set (0.000 sec)
  • 事务 2: insert into t values (1),(2),(314),(1618);
    • 二进制日志(我们看到新事务的 GTID 数据流(
# at 485
#231123 14:46:19 server id 1  end_log_pos 527 CRC32 0x9ef73274 	GTID 0-1-7 trans
START TRANSACTION
..
insert into t values (1),(2),(314),(1618)
...
  • 事务 2: insert into t values (1),(2),(314),(1618);
    • 副本(检查复制的数据)
$ docker exec -it mariadb-replica-2 mariadb -uroot -psecret -e "select * from testdb.t"
+------+
| t    |
+------+
|    1 |
|    2 |
|  314 |
| 1618 |
+------+
# Replica status changes
MariaDB [(none)]> show replica status \G
                Slave_IO_State: Waiting for master to send event
               Master_Log_File: mariadb-bin.000003
           Read_Master_Log_Pos: 668
                Relay_Log_File: mariadb-relay-bin.000002
                 Relay_Log_Pos: 969
           Exec_Master_Log_Pos: 668
               Relay_Log_Space: 1280
                    Using_Gtid: Slave_Pos
                   Gtid_IO_Pos: 0-1-7
       Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates

2. 标准复制配置

  • 在这里,我们将可视化标准复制如何配置,并稍后与半同步配置进行比较。
  • 配置在上一篇博客中实现的标准复制非常直接

3. 标准复制事务

  • 在这里,我们将可视化标准复制事务的流程,并稍后与半同步事务进行比较。
  • 在线程级别(参见 复制线程),我们可以将活动事务的流程表达如下
  1. 客户端发起一个事务
  2. IO 线程通知 binlog-dump 线程它们已准备好接收事件
  3. Binlog-dump 线程转储事件
  4. IO 线程写入中继日志并更新 master.info 文件
  5. SQL 线程从中继日志读取并执行事件。
  • 这种复制类型是异步的,这意味着我们无法从副本获得关于事件已被副本成功接收的反馈,如图所示。

4. 半同步复制配置

在确认客户端请求之前,至少一个副本只需确认已接收到数据更改(IO thread),而不是副本实际上已应用了这些数据更改。要配置半同步复制,我们需要停止副本并在主库和副本上设置环境变量。

在主库上,设置 rpl_semi_sync_master_enabled ;在副本上,设置 rpl_semi_sync_slave_enabled。如果设置正确,则相应的服务器上的 rpl_semi_sync_master_status 和 rpl_semi_sync_slave_status 应为开启状态。

我们将仅展示与第 2 节相比更新的配置。

5.  半同步复制事务示例

半同步通过引入一个额外的主线程(称为 “ACK 接收线程”)来克服这个问题。只需要一个副本确认它已接收并记录了事件,如下图所示。

流程如下

  1. 客户端发起一个事务连接暂停直到收到 ACK [半同步]
  2. Binlog-dump 线程设置通知给 ACK 线程,以接收来自相关副本的 ACK [半同步]
  3. IO 线程通知 binlog-dump 线程它们已准备好接收事件
  4. Binlog-dump 线程转储事件
  5. IO 线程写入中继日志并更新 master.info 文件
  6. IO 线程通知 ACK 线程事件已接收。如果在配置的超时时间内没有 ACK,半同步将切换到异步 [半同步]
  7. ACK 线程解除客户端连接的暂停客户端已准备好接收新事务 [半同步]
  8. SQL 线程从中继日志读取并执行事件。

结论与进一步阅读

特别感谢 Brandon Nesterenko、Daniel Black 和 Ian Gilfillan 审阅此博客文章。要获取有关副本状态的更多信息,可以使用 show replica hosts (我们正在准备 MDEV-21322 ,以便为半同步状态提供更多详细信息)。

如果您在本博客、设计或不按预期工作的边缘情况中遇到任何问题,请告知我们。欢迎您在Zulip上讨论。一如既往,您可以在 MDEV 项目中使用我们的JIRA提交任何您遇到的 bug 或功能请求。