Mysql Group Replication学习#

https://dev.mysql.com/doc/refman/8.1/en/group-replication.html


背景#

要做到可容灾,最常用的方法就是要做redundant(冗余)。
存多份数据,随时拿掉一部分,系统仍能正常运转。

本质上需要让多个副本达成某种一致状态。
mysql Group Replication实现了这种分布式状态机。所有节点都会保持状态一致。

在single-primary模式下,会自动选出primary节点,只有这个节点能执行更新。 在multi-primary模式下,所有节点都能执行更新操作。当然代价会更大。

内部会有一个视图,监视member情况。

要commit一个事务,必须经过大多数的同意。如果有意外导致无法达成一致,整个系统会停滞,直到问题解决。

背后的协议叫Group Communication System(GCS)。包含失败检测,member服务,消息的安全和顺序投递等功能。


Source to Replica Replication#

即传统的master-slave那一套。
slave并不断获取master的binlog。master的execute可以不管replica,直接自己commit。
也可以设置为在slave apply之后再commit。

Group Replication#

读写事务都要经过一致性处理,达成统一才会执行。
只读事务不需要。

发起一个读写事务时,发起节点会广播这个操作(更新哪些行)。这个广播也是原子的,要么所有节点都收到这个操作,要么所有都收不到。
如果收到了,所有操作的顺序一定是一致的。

有一种冲突情况。在一个certification过程中,如果两个不同的server发起的更新操作中,有相同的row,就看作有冲突。

解决流程是

  1. 顺序在前的得到commit

  2. 在后的在发起server上rollback,并且在其他server上丢弃。

事务的执行顺序在不同节点上可以不同,因为已经经过certification排除了冲突。
那么在确保安全的情况下可以把后面的操作先commit,提高了效率。

图18.3可以看到,execute后必须进行一致性验证。通过后再commit。

Group Replication用例#

一个重点,连到集群的客户端必须自己做好容错。
比如客户端程序初始连到server1。这时如果server1挂了,server2仍可用,那么客户端程序必须自己去连server2。
后面我们用Proxysql来处理。

一些用例

  • 弹性副本
    需要动态添加/删除副本。例如各种云服务。

  • 高可用Shards
    每个shard用Group Replication实现。

  • 替代传统的master-slave

  • 为了省事儿,内置了一致性,少操心。


Multi-Primary和Single-Primary模式#

group_replication_single_primary_mode参数,默认ON也就是single模式,OFF是multi模式。
所有节点的配置必须一样。

不允许在运行时直接改这个参数。
可以用group_replication_switch_to_multi_primary_mode()group_replication_switch_to_single_primary_mode()来切换。
它们会保证数据的安全。

Single-Primary模式#

Single-Primary模式中,只有一个节点是read-write模式,其他都是只读(super_read_only=ON)。
一般primary就是先跑起来的那个server,其他server加入进来,根据配置自动设置为只读。

只有这个primary节点可更新数据。跟multi-primary模式相比,一致性检查肯定会更轻松。
single模式中group_replication_enforce_update_everywhere_checks这个参数一定是OFF。

primary节点可发生的变化

  1. primary节点离开集群,主动或异常,一个新的primary节点会被选出来。

  2. 使用group_replication_set_as_primary()函数指定新的primary节点。

  3. 在multi模式下用group_replication_switch_to_single_primary_mode()转成single模式,可指定primary节点,也可让系统自己选出。

这些函数需要mysql版本高于8.0.13。

新的primary节点自动变为读写模式,其他变为只读。

当选出或指定新的primary节点时,可能老primary节点仍存在积压的操作。
在新primary节点追上老节点之前,可能会出现冲突,rollback,然后只读操作可能会读到老数据。
后面的流控章节会讲这个问题。

不同版本的对于选举的影响,有一系列流程。略过。

performance_schema.replication_group_members可查看member的role。


Multi-Primary模式#

所有节点都是read-write模式,没有特殊。

图18.5演示了一个节点fail的情况。


Group Replication服务#

Group成员#

一堆server形成一个组。组有名称,是个uuid。server可随时加入或离开。

加入时,会自动获取已有的数据,进行追赶。
离开时,比如进行维护,其他成员会得知,并进行自动的相关更新。

group membership service提供成员的各种信息。信息形成一个view

成员不仅要在事务上达成一致,也要在这个view上达成一致。

当一个成员主动离开时,它先发起一个dynamic group reconfiguration,对新的不包含此成员的view达成一致。
如果不是主要离开,失败检测机制会在一定时间后认定此成员离开,并发起dynamic group reconfiguration
主动离开时,如果这个reconfiguration不能达成一致,系统挂起。需要管理员介入。

在失败检测机制检测到失败之前,一个成员可以短暂下线,再重连上来。
此时该成员会忘记之前的状态。如果其他成员这时发了针对其老状态的信息,会导致数据不一致。
为解决这个问题,会检查该成员的新老连接数据,等待老连接的相关信息被删除,才会让新连接连上来。

失败检测#

失败检测机制会检测到某个成员无法和其他成员通信,这个结果达成一致后,会驱逐这个成员。

在replication group中,成员之间有两两的通信渠道。tcp/ip。XCom(Paxos变种)。
两条通道,一个收一个发。

如果一个成员a超过5秒没收到成员b的消息,a会认为b失联,把b标记为UNREACHABLE
group_replication_member_expel_timeout可设置该时长。
replication_group_members表可查这个信息。
经常会出现两个成员相互标记失联。
也可以标记自己失联。

如果超过10秒,a就要开始通知其他成员b失联。

有可能极端情况下所有成员都被标记失联。Group Communication System(GCS)协议会进行处理,保证至少有一个成员存活。

容灾#

允许出异常后,系统仍能正常运行的节点数量关系n = 2 x f + 1

n是节点总数。f是允许异常的节点数。

如果想要可容灾1个节点,那么总共需要3个节点。

如果总共有6个节点,可允许2个节点挂掉。

可视化#

各种状态可直接查表


实战single-primary(单主)模式#

先看一些官方给的必要参数。

# mgr只能用innodb。其他关掉。  
disabled_storage_engines="MyISAM,BLACKHOLE,FEDERATED,ARCHIVE,MEMORY"
server_id=1 # 每个节点必须不同
gtid_mode=ON # 必须打开
enforce_gtid_consistency=ON # 必须打开
plugin_load_add='group_replication.so' 
group_replication_group_name="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
group_replication_start_on_boot=off 
group_replication_local_address="s1:33061"
group_replication_group_seeds="s1:33061,s2:33061,s3:33061" 
group_replication_bootstrap_group=off

如果把这些参数放到docker-compose或.cnf配置中

第一次启动会报

[Server] Ignoring --plugin-load[_add] list as the server is running with --initialize(-insecure).
[Server] unknown variable 'group-replication-single-primary-mode=on'.

因为ignore了plugin,所以mgr参数无法识别。

再次启动

'mysqld: Table 'mysql.plugin' doesn't exist'
[Server] Could not open the mysql.plugin table. Please perform the MySQL upgrade procedure.

mysql.plugin不存在,需要upgrade。
再加一个upgrade=force的强制更新参数。可以启动,但怎么也连不上去。
太费劲,感觉是plugin_load_add相关的流程在docker里可能有什么说法。
折腾两天放弃。


如果去掉mgr相关配置,可以正常运行。
后续全部手动操作mgr,可以正常运行。

摸索出一个流程

  1. 从0开始

  2. docker compose中只配个root密码和volumes。cnf可以为空。

  3. 直接启动

  4. INSTALL PLUGIN group_replication SONAME 'group_replication.so'安装mgr插件

  5. .cnf里填写所有参数。包括mgr参数。

  6. 重启容器

此时数据库已经安装了mgr插件。.cnf也设置了默认参数。
可以认为是成功初始化了一个干净的数据库。
此后可以随意重启容器,.cnf会生效。


mysql_1/mysql_2/mysql_3 三个节点。

# 最终mysql_1.cnf的内容

server-id = 1
bind-address = 0.0.0.0
max_connections = 10000
enforce_gtid_consistency=on      # 必须打开才能使用gtid
gtid_mode=on                     # 使用gtid
binlog_format=ROW                # default
binlog-expire-logs-seconds=2592000 # binlog 默认保存30天

# 稳定以后所有节点可以设置为只读。
# 加入群组后,主节点会自动关闭只读。不会影响功能。
# 这样如果重启,可以防一手误改数据。
# 离开的节点如果自己改了数据,再想加入群组,就非常烦人。
# 因为这样包含了组里不存在的数据,得修。
# read-only=on
# super-read-only=on

disabled_storage_engines="MyISAM,BLACKHOLE,FEDERATED,ARCHIVE,MEMORY"

#plugin_load_add="group_replication.so"

# 下面的mgr参数第一次启动时不要打开
# 第一次启动后运行INSTALL PLUGIN group_replication SONAME 'group_replication.so'; 
# 再打开并重启容器。
group_replication_group_name='1ae7a52b-4609-11ee-a76e-0242ac120002' # 组名可以select uuid()生成
group_replication_start_on_boot=OFF
group_replication_bootstrap_group=OFF
group_replication_single_primary_mode=ON
group_replication_local_address=mysql_1:33061
group_replication_group_seeds='mysql_1:33061,mysql_2:33061,mysql_3:33061'
# docker compose

services:
    mysql_1:
        hostname: mysql_1

        image: mysql:8.1.0
        
        networks:
            - app_mysql

        ports:
            - "3311:3306"

        volumes:
            - "/xxx/xxx/test/mgr/mysql_1/conf:/etc/mysql/conf.d"
            - "/xxx/xxx/test/mgr/mysql_1/log:/var/log/mysql"
            - "/xxx/xxx/test/mgr/mysql_1/data:/var/lib/mysql"

        environment:
            MYSQL_ROOT_PASSWORD: 123456 # 数据初始化完成后即可从此删除。自己另外妥善保管。
            TZ: Asia/Shanghai

    mysql_2:
        ...

    mysql_3:
        ...

此时docker-compose up -d是可以一步起来的,有了3个干净的节点。
然后操作mgr。

操作节点mysql_1

# 创建User Credentials For Distributed Recovery
# 这个user是通用的
# 其他节点连上来也会同步过来这个user
# 如果不创建,其他节点无法同步数据。
CREATE USER rpl_user@'%' IDENTIFIED WITH mysql_native_password BY 'rpl_wtf';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';
GRANT CONNECTION_ADMIN ON *.* TO rpl_user@'%';
GRANT BACKUP_ADMIN ON *.* TO rpl_user@'%';
GRANT GROUP_REPLICATION_STREAM ON *.* TO rpl_user@'%';
FLUSH PRIVILEGES;

SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION USER='rpl_user', PASSWORD='rpl_wtf';
# START GROUP_REPLICATION后必须
SET GLOBAL group_replication_bootstrap_group=OFF;

成功后可看到mgr的相关信息。

select * from performance_schema.replication_group_members;

+---------------------------+--------------------------------------+-------------+-------------+--------------+-------------+----------------+----------------------------+
| CHANNEL_NAME              | MEMBER_ID                            | MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE | MEMBER_VERSION | MEMBER_COMMUNICATION_STACK |
+---------------------------+--------------------------------------+-------------+-------------+--------------+-------------+----------------+----------------------------+
| group_replication_applier | 9ab75936-468c-11ee-af7c-0242ac140002 | mysql_1     |        3306 | ONLINE       | PRIMARY     | 8.1.0          | XCom                       |
+---------------------------+--------------------------------------+-------------+-------------+--------------+-------------+----------------+----------------------------+

这时操作mysql_2

# 不用创建user。进入组后会从主同步过来。
# 不用设置group_replication_bootstrap_group
# SET GLOBAL group_replication_bootstrap_group=ON;
# 直接启动
START GROUP_REPLICATION USER='rpl_user', PASSWORD='rpl_wtf';
# START GROUP_REPLICATION后必须
# SET GLOBAL group_replication_bootstrap_group=OFF;

会失败,sudo docker-compose logs --tail=1000 mysql_2看log。
gtid不匹配。mysql_2中存在组中不存在的数据。
我理解是干净的数据库也会有一些初始操作,产生少量log,可以扔掉。

[ERROR] [MY-011526] [Repl] Plugin group_replication reported: 'This member has more executed transactions than those present in the group. Local transactions: c017e578-4706-11ee-b13b-0242ac150004:1-5 > Group 

transactions: 1ae7a52b-4609-11ee-a76e-0242ac120002:1, bfeb9cab-4706-11ee-b009-0242ac150003:1-12'                                                                                                                                                               

mgr-mysql_2-1  | 2023-08-30T07:35:14.693130Z 0 [ERROR] [MY-011522] [Repl] Plugin group_replication reported: 'The member contains transactions not present in the group. The member will now exit the group.'


# 在每个节点查看binlog信息对比。可看到确实不一样。  
SHOW BINLOG EVENTS;
show master status;
SELECT @@global.gtid_executed;

这时mysql_2进行reset master
清除binlog和gtid后再START GROUP_REPLICATION ...
成功后可看到

+---------------------------+--------------------------------------+-------------+-------------+--------------+-------------+----------------+----------------------------+
| CHANNEL_NAME              | MEMBER_ID                            | MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE | MEMBER_VERSION | MEMBER_COMMUNICATION_STACK |
+---------------------------+--------------------------------------+-------------+-------------+--------------+-------------+----------------+----------------------------+
| group_replication_applier | 013309d2-4610-11ee-ae4e-0242ac140004 | mysql_2     |        3306 | ONLINE       | SECONDARY   | 8.1.0          | XCom                       |
| group_replication_applier | 9ab75936-468c-11ee-af7c-0242ac140002 | mysql_1     |        3306 | ONLINE       | PRIMARY     | 8.1.0          | XCom                       |
+---------------------------+--------------------------------------+-------------+-------------+--------------+-------------+----------------+----------------------------+

此时mysql_2就成功加入了组。

如果mysql_1做了操作比如创建了数据库,这时mysql_2成功连上来后会去同步数据,达到一致。rpl_user这个账号也会同步过来。
如果mysql_1有大量数据,那么同步会慢,应该导出mysql_1的数据导入mysql_2。

和mysql_2一样操作mysql_3。
此时3个节点都起来了。


插入一些数据。可以看到只能在主上更新。对副更新会报错,因为副节点会自动开启super_read_only
主更新成功时,副的数据最终一定是一致的,这个是mgr最基本的性质。

# mysql一些有用的命令

# 打印plugin
show plugins;  

# 打印各种参数
show variables; 

# 显示mgr相关参数
select * from performance_schema.replication_group_members;  

# 打印binlog。可查各种操作历史。
SHOW BINLOG EVENTS;  

# 查看最新的binlog位置
show master status;

# 危险。删除所有binlog,删除gtid历史。
reset master;

# 危险。删除日志到某个点。可腾出磁盘空间。
PURGE BINARY LOGS TO xxx;

其他问题#

后续在多台机器起8.3.0版本时有多种问题叠加。

group_replication_local_address
各种报错。最后docker网络改为host,填服务器内网ip解决。

group_replication_group_seeds同上

创建用户时不要加WITH mysql_native_password(不十分确定)。
同时group_replication_recovery_get_public_key设置为on。

docker-compose中hostname设置为有效的域名。
从节点会直接连此hostname的3306来recover。
随意填的话连不上。

group_replication_ip_allowlist也填上
不确定是否必填

应该有其他解决方法,暂时先这样。

group_replication_member_weight可设置成为primary的优先级。


要求和限制#

  • 必须使用InnoDB

  • 必须有主键

  • 需要良好的网络条件

certification过程不会考虑表锁

一个组最多9个成员

  • 事务的尺寸
    之前看过有个5秒超时。如果事务太大导致数据包过大,有可能被认为出了问题。
    有一系列参数设置超时时间/事务大小限制/事务压缩等来缓解这个问题。


状态监控#

SELECT * FROM performance_schema.replication_group_members

SELECT * FROM performance_schema.replication_group_member_stats

SELECT * FROM performance_schema.replication_group_communication_information

SELECT * FROM performance_schema.replication_connection_status

SELECT * FROM performance_schema.replication_applier_status


操作#

操作在线的组#

  • 组中成员必须都正常在线,比如不处于某些恢复过程中。

  • 连上任意一个正常的节点都可操作。

  • 操作过程中不允许新进成员

  • 同一时刻只存在一个操作

  • mysql版本必须都在8.0.13以上

# 指定成员为新的主  
select group_replication_set_as_primary('c017e578-4706-11ee-b13b-0242ac150004')  

# 转为单主
SELECT group_replication_switch_to_single_primary_mode()

# 转为多主
SELECT group_replication_switch_to_single_primary_mode()

# Group Write Consensus
# 组可以并行执行多个一致性实例。
# 如果网络不好,可以调大这个最大值,同时容纳更多实例。
SELECT group_replication_get_write_concurrency();

# 设置Group Write Consensus
SELECT group_replication_set_write_concurrency(20);

# 通信协议版本
# 所有节点无脑最新把
SELECT group_replication_get_communication_protocol();


# 显示member_actions
SELECT * FROM performance_schema.replication_group_member_actions 

# 可对其进行设置。调整一些流程。
SELECT group_replication_enable_member_action(_name_, _event_)

重启组#

成员全stop后重启。各个成员可能有不同的进度,就像之前第一次重启时遇到的。

这时要以成员为主,其他节点去追它的数据。
这需要我们来操作。

# 查看节点的GTID_EXECUTED
SELECT @@GLOBAL.GTID_EXECUTED

# 查看节点的received_transaction_set
SELECT received_transaction_set FROM performance_schema.replication_connection_status WHERE channel_name="group_replication_applier";

需要找出gtid最大的节点。

https://dev.mysql.com/doc/refman/8.1/en/replication-gtids-functions.html
这里有各种gtid的操作。example17.3是找出最新节点的方法。

大致意思是GTID_EXECUTED存了节点已经commit的gtid。
received_transaction_set存的是将要commit的gtid。
所以要union起来看谁最大。

挺折腾的,没全懂。而且创建function也会涨gtid,造成混乱。
后续再研究

选好主以后

SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION USER='rpl_user', PASSWORD='rpl_wtf';
# START GROUP_REPLICATION后必须
SET GLOBAL group_replication_bootstrap_group=OFF;

Transaction Consistency Guarantees#

consistency guarantee与主failover#

单主模式下发生failover切换主节点时,可能有积压的操作,这些操作仍需要处理。
新主节点可以

  1. 立即接收请求。不等待积压的操作。
    可能造成读操作读到老数据。

  2. 等处理完积压的操作,才开始接收请求。
    数据没问题。但是有延时。

数据流控#

数据流控对单主和多主都有效。这里只看单主。
通常mgr的读写分离会把读方到主,把写均匀分给从。
因为mgr的一致性,大体可认为主的写操作一定会在从上同时完成。
但是细节上如果不进行处理,还是可能出现读到老数据。见图18.3

要解决的不一致问题基本就是,如果不处理一致性,客户端1在主节点写了数据,会立刻返回成功,而从节点可能因为各种原因要过一会才能同步到这个数据。
那么就有可能出现客户看到操作成功,但再次读数据时读到了老数据。这就出了大问题。
下面的consistency guarantee的各种配置就是要按需要解决这个问题。

同步点#

基于事务的同步时间点,可以对组的consistency guarantee进行配置。

方便起见,这一节把把同步点分为读操作时写操作时

  • 如果在读时同步,用户session会等所有之前的更新事务搜完成,再执行自己的操作。
    只有这个session受影响。

  • 如果在写时同步,写操作会等待所有从节点完成各自积压的写操作,再返回。

这两种都会保证不会出现上述的读到老数据。
各有优劣,得看情况使用。

  1. 想对读操作做负载均衡,同时不想限制读操作的节点。读操作远远多于写操作。

  2. 组中数据主要为只读,想要写操作即刻再所有节点生效,后续读不能读到老数据。

情况1/2用写时同步。

  1. 同1。但写远远多于读。

  2. 绝不允许读到老数据。

情况3/4用读时同步。

它这文档说得有点多余。上面已经说了两种都会保证不会读到老数据,它还在这说这个要求。

总之就是读时同步就是读时等待,写时同步就是写时等待,希望减少等待的时间。
那么如果场景中读多就用写时同步,反之就用读时同步


consistency guarantee的实际配置#

上一节是简化的概念,这一节看实际的配置。

group_replication_consistency参数配置事务的一致性。

  • EVENTUAL
    ro和rw事务的执行,不会等待之前的事务apply。
    ro可能读到老数据。rw可能因冲突而rollback。
    等于啥也不管

  • BEFORE_ON_PRIMARY_FAILOVER
    针对failover时的积压数据而言。
    ro和rw事务会等待新的primary节点完成所有积压的操作,再执行。
    这样会保证新的主的数据一定是最新,但可能有延迟。

  • BEFORE
    rw操作等待该节点的所有积压操作commit后再apply。
    ro操作等待该节点的所有积压操作commit后再执行。
    这个级别包含了BEFORE_ON_PRIMARY_FAILOVER
    可以认为是BEFORE_ON_PRIMARY_FAILOVER的一般化。在所有节点都能读到最新。

  • AFTER
    rw操作等待此次操作在所有节点上apply后再返回。
    ro操作没有限制。
    保证了当一个操作commit时,后续任何节点上的读一定会读到最新数据。
    简单说就是我发起更新,等所有节点都commit了,我才返回。更新很费劲。
    这个级别包含了BEFORE_ON_PRIMARY_FAILOVER

  • BEFORE_AND_AFTER
    rw操作等待

    1. 所有积压的操作commit,才apply。

    2. 此操作在所有节点apply,才返回。
      ro操作等待所有积压操作commit后,才执行。
      结合了BEFOREAFTER。等级最高。

这个参数可以设定在Global和Session。
所以可以产生各种组合,可以做的比较复杂。


如何选择#

  1. 想对读操作负载均衡,不想读到老数据,读远多于写。用AFTER

  2. 有大量的写,偶尔读一下,不想读到老数据。用BEFORE

  3. 。。。

  4. 有大量只读数据,不想在ro上有延迟,可以在rw上有延迟。用AFTER

  5. 全都要。用BEFORE_AND_AFTER

  6. 绝大多数操作不需要很强的一致性。某些重要操作必须要严格立即生效。
    比如设置某个权限,或者充值等,要求一旦客户端返回成功,绝不允许读到老数据。
    可以全局默认用EVENTUAL,把这个操作的session设成AFTER
    SET @@SESSION.group_replication_consistency='AFTER'

  7. 跟6场景相似,平时不需要很强的一致性。但某些少量的操作要求读最新数据。
    可以全局默认用EVENTUAL,把这个操作的session设成BEFORE
    SET @@SESSION.group_replication_consistency='BEFORE'

所有rw操作一定是有序的。


consistency guarantee的影响#

另一个角度是consistency guarantee对各个成员有什么影响。

BEFORE只影响本地,我自己等待然后执行就完事儿。不影响其他人。

AFTERBEFORE_AND_AFTER会导致别的节点的事务等待,
即使别的节点是EVENTUAL,也会强制等待包含after的事务完成。

BEFORE/AFTER/BEFORE_AND_AFTER只能用在online的节点。用在其他状态的会报错。

BEFORE_ON_PRIMARY_FAILOVER的failover期间也会允许一些特别的读操作,用于查状态,debug等。


分布式数据恢复#

当一个成员加入冲重新加入组,它必须追赶组的数据。

加入时会检查replication_connection_status
SELECT * FROM performance_schema.replication_connection_status
看有没有之前没apply的数据,先apply。

然后连上一个成员,实施状态转换。
获取所有在它离开期间的事务,提供这个数据的节点称为donor
然后进行apply。完成后数据就追上了组,开始正式参与进组。

系统默认会使用donor的binlog来追赶数据,通过group_replication_recovery通道。
在此期间新的事务会缓存起来,等追上以后再进行apply。


做实验#

副节点离开#

节点2离开组(STOP GROUP_REPLICATION)或者重启,然后等待一会。

  • 2不做任何操作直接重新加入组(START GROUP_REPLICATION),原则上是没问题的。

  • 2做了一些修改后再加入
    这时gtid会对不上,2有了新的操作,又要加入组,就得先同步到组的gtid。

    • 如果2的数据非常混乱,可以完全重开,等于2重做数据库再连上来,肯定没问题。

    • 如果执行了reset master,就是把gtid清了,那貌似也只能重做。
      重做的话如果数据量很大,dump出来导入。不要从0开始同步,太慢。

    • 否则就要手动修,把数据同步到gtid的分歧点。

    • 原则上如果要回来,就不应该做修改。

STOP GROUP_REPLICATION离开组后还是会保持只读状态。

如果重启数据库,cnf又没设置read_only,有机会误操作。
如果所有节点的cnf打开read_only是否可以基本杜绝?
进入组后如果成为主,会自动关掉read_only,不会影响正常使用。

总之原则上禁止在从节点上改任何数据


如果2上误写了数据,或者搞混乱了。可以暴力重做数据库。

# 主上dump所有
mysqldump -u root -p --all-databases --source-data --triggers --routines --events > dump_1.sql

# 2直接重做数据库。
# 清除初始的binlog和gtid。
reset master
mysql -u root -p < dump_1.sql

# 此时START GROUP_REPLICATION可以连上

数据多的话就慢。


还可以操作gtid,插入假的gtid。
对比group和节点2的gtid状态,SELECT @@global.gtid_executed;
可以发现节点2多了一份自己的gtid。
比如

1ae7a52b-4609-11ee-a76e-0242ac120002:1-5, bfeb9cab-4706-11ee-b009-0242ac150003:1-12, c017e578-4706-11ee-b13b-0242ac150004:1

最后的c017e578-4706-11ee-b13b-0242ac150004:1是节点2误操作生成的gtid。  
前边的是之前组里正常的gtid。  

这种情况下要绝对清楚节点2做了哪些操作,需要先把这些操作手动还原,确保数据完全无误,和刚离开组时完全一样。

然后查看gtid状态。
然后在主节点上手动插入假的gtid诸如c017e578-4706-11ee-b13b-0242ac150004:1-3
造成主节点执行过它的假象。从而让节点2能加入组。

SET @@SESSION.gtid_next='c017e578-4706-11ee-b13b-0242ac150004:1';

BEGIN; # 假操作
COMMIT;

# 如果存在多个,要一个个来。
SET @@SESSION.gtid_next='c017e578-4706-11ee-b13b-0242ac150004:2';

BEGIN; # 假操作
COMMIT;

# 最后恢复
SET @@SESSION.gtid_next='AUTOMATIC';

如果节点2有些数据没还原,可能会一直卡在recover状态。
比如节点2离开后误操作创建了表a,而此时组中也创建了表a。那么连上去后会同步组中的表a,但节点2中已经存在。就会不停报错。
所以一定要清楚数据的状态。


Proxysql#

proxysql配置文档https://proxysql.com/Documentation/configuring-proxySQL/

proxysql启动时先读.cnf。如果.cnf语法不对会报错。
如果磁盘上的proxysql数据库不存在(丢失或第一次启动),使用.cnf的配置,存到数据库和runtime。否则忽略.cnf

所以启动过后,proxysql数据库存在了,再改.cnf并重启是没用的。
可以load xxx from config从cnf到数据库,再load xxx to runtime到runtime。

只有runtime的配置是目前生效的。

save xxx to memory从runtime到memory。save xxx to disk从memory到数据库。

需要形成一个自己的改配置习惯,能追溯到老配置。

如果有的配置不方便用cnf?那么可以需要自己维护一套sql作为当前配置。

连默认6080端口的状态页面,用https。


proxysql配置#

# proxysql.cnf

datadir="/var/lib/proxysql"

admin_variables=
{
    admin_credentials="admin:admin;radmin:radmin" # proxysql账号密码
    mysql_ifaces="0.0.0.0:6032" # 连proxysql的ip端口
    refresh_interval=2000
    web_enabled=true # 启用状态页面
    web_port=6080
    stats_credentials="stats:admin2" # 状态页面账号密码
}

mysql_variables=
{
    threads=4
    max_connections=2048
    default_query_delay=0
    default_query_timeout=36000000
    have_compress=true
    poll_timeout=2000
    interfaces="0.0.0.0:6033" # 应用层的入口。跟直连mysql一样连上来。
    default_schema="information_schema"
    stacksize=1048576
    server_version="8.1.0" # 后端mysql版本
    connect_timeout_server=3000
    monitor_username="root" # 监控账号。需要在mysql组里创建好。
    monitor_password="123456"
    monitor_history=600000
    monitor_connect_interval=60000
    monitor_ping_interval=10000
    monitor_read_only_interval=1500
    monitor_read_only_timeout=500
    ping_interval_server_msec=120000
    ping_timeout_server=500
    commands_stats=true
    sessions_sort=true
    connect_retries_on_failure=10
}

mysql_users=
(
    {
        # 某项目的指定账号,即为应用层创建的账号。需要在mgr中创建。
        # 每次新建账号也都要在此添加。
        username = "test_user"
        password = "123456"
        default_hostgroup = 1
        max_connections=2000
        default_schema="test" # 默认数据库
        active = 1
    }
)

# mgr的配置
mysql_group_replication_hostgroups=
(
    {
        # 设置分组编号
        writer_hostgroup=1
        reader_hostgroup=2
        backup_writer_hostgroup=3
        offline_hostgroup=4
        active=1
        max_writers=1 # 单主。最多一个写。
        writer_is_also_reader=0
        max_transactions_behind=100
        comment="wtf"
    }
)

# 后端mysql
mysql_servers=
(
    {
        # 默认放到写组
        hostgroup_id=1
        hostname="mysql_1"
        port=3306
    },
    {
        hostgroup_id=1
        hostname="mysql_2"
        port=3306
    },
    {
        hostgroup_id=1
        hostname="mysql_3"
        port=3306
    }
)

# 最常见的读写分离。把读转到reader_hostgroup。
mysql_query_rules=
(
    {
        rule_id=1
        active=1
        match_pattern="^SELECT .* FOR UPDATE$"
        destination_hostgroup=1
        apply=1
    },
    {
        rule_id=2
        active=1
        match_pattern="^SELECT"
        destination_hostgroup=2
        apply=1
    }
)
# docker compose

services:

    proxysql:
        hostname: proxysql

        image: proxysql/proxysql:2.5.5

        networks:
            - app_mysql

        ports:
            - "6032:6032"
            - "6033:6033"
            - "6070:6070"
            - "6080:6080"

        volumes: 
            - "/xxx/xxx/test/proxysql/conf/proxysql.cnf:/etc/proxysql.cnf"
            - "/xxx/xxx/test/proxysql/data:/var/lib/proxysql"

proxysql操作#

# 进入proxysql容器
sudo docker-compose exec proxysql bash

# 进入proxysql数据库
# 以admin形式。连到6032
mysql -u admin -p -P 6032 -h 127.0.0.1 --prompt='ProxySQLAdmin>'

# 查看所有表。有个整体映像。
show schemas;
show tables;

# 查看一些常用配置
select * from global_variables;
select * from mysql_servers;
select * from mysql_users;
select * from mysql_query_rules;
select * from runtime_mysql_servers;

# proxysql查看组的基本设置
show create table mysql_group_replication_hostgroups\G;


# 如果修改了cnf,需要加载到数据库,再加载到runtime。
load xxx from config;
load xxx to runtime;


# 进入proxysql数据库
# 以client形式。连到6033
mysql -u admin -p -P 6033 -h 127.0.0.1 --prompt='ProxySQLClient>'

简单测试#

把主节点mysql_3关掉,再连上,模拟发生切换。

初始状态是这样

ProxySQLAdmin>select * from runtime_mysql_servers;
+--------------+----------+------+-----------+--------+
| hostgroup_id | hostname | port | gtid_port | status |
+--------------+----------+------+-----------+--------+
| 1            | mysql_3  | 3306 | 0         | ONLINE |
| 2            | mysql_1  | 3306 | 0         | ONLINE |
| 2            | mysql_2  | 3306 | 0         | ONLINE |
+--------------+----------+------+-----------+--------+

sudo docker-compose stop mysql_3  

进mysql的组里  
select * from performance_schema.replication_group_members;
可以看到mysql_1变成了主。此时  

ProxySQLAdmin>select * from runtime_mysql_servers;
+--------------+----------+------+-----------+--------------+
| hostgroup_id | hostname | port | gtid_port | status       |
+--------------+----------+------+-----------+--------------+
| 1            | mysql_1  | 3306 | 0         | ONLINE       |
| 2            | mysql_1  | 3306 | 0         | OFFLINE_HARD |
| 2            | mysql_2  | 3306 | 0         | ONLINE       |
| 4            | mysql_3  | 3306 | 0         | SHUNNED      |
+--------------+----------+------+-----------+--------------+

hostgroup_id发生了变化。mysql_1变成了写组。    

sudo docker-compose start mysql_3  
START GROUP_REPLICATION user='rpl_user', password='rpl_wtf';

可以看到3个都ONLINE了。

ProxySQLAdmin>select * from runtime_mysql_servers;
+--------------+----------+------+-----------+--------+
| hostgroup_id | hostname | port | gtid_port | status |
+--------------+----------+------+-----------+--------+
| 1            | mysql_1  | 3306 | 0         | ONLINE |
| 2            | mysql_2  | 3306 | 0         | ONLINE |
| 2            | mysql_3  | 3306 | 0         | ONLINE |
+--------------+----------+------+-----------+--------+

大计量测试#

写测试代码看表现。
起几个读循环,几个写循环,并发请求。整个每秒上千的读写。


一个从节点离开。看读是否正常。
偶尔可出现Lost connection to MySQL server during query
代码处理错误后再次请求会成功,因为转到了其他从节点。

再加入,看是否正常。
过了几秒后,请求再次开始分配到该节点。恢复到正常状态。


把主断开,看是否正常。
选举期间,可出现不同的错误
sqlalchemy.exc.OperationalError Error on observer while running replication hook 'before_commit'. The MySQL server is running with the --read-only option so it cannot execute this statement
需捕获错误。几秒后选举完成恢复正常。

再连上老主。十几秒后(时间稍长一点),老主变为从节点,开始接收请求。


只剩一个主,但配置为主上不可读。
会持续报错,不可用。
如果把mysql_group_replication_hostgroupswriter_is_also_reader打开。可恢复。这样配置可以在只有一个节点时还可用。

两个从节点重新加入,如果断开期间有大量写,会有recover过程。

能否配置成主平时不读,只剩它一个节点时可读?


只剩一个主时,还能正常读写,如果写了大量数据,这时加入一个从,从会recover。
期间完全不可用!
此时看proxysql的数据是不存在写组的。读组也是offline。
直到recover完成才自动恢复。

后续再查原理。按说应该能让它先recover且不影响主。

3个节点挂一个是高危,挂两个算是极度危险,可以算大型意外事故。

毕竟按一致性协议,原则上只允许挂一个节点。


关掉从1,剩一个主一个从2,写大量数据。
从重新加入,进行recover,此时关掉从2?
一样进入完全不可用状态。
直到recover完成才自动恢复。

如果把从2加入,立马恢复。


关掉从1,剩一个主一个从2,写大量数据。
从重新加入,进行recover,此时关掉主?
一样进入完全不可用状态。
从2会变成主,然后从1会recover。
直到recover完成才自动恢复。


可以看到数据没出现过错乱,这点很好。
3个节点只要不挂2个节点,基本能保持可用。
挂1个时,需要快速修复。
如果挂2个,可能出现长时间recover。


可以发明很多其他玩法。
比如给每个节点都加个副本,不参与一致性组,只复制数据,基本不影响3节点运行。
这样当一个节点出故障时,特别是没有及时发现,或者直接数据损坏时,可以快速补上一个健康的节点。
因为它是健康的,一直在持续复制,所以几乎任何情况下都可以快速顶上来,不需要recover很长时间。