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,就看作有冲突。
解决流程是
顺序在前的得到commit
在后的在发起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节点可发生的变化
primary节点离开集群,主动或异常,一个新的primary节点会被选出来。
使用
group_replication_set_as_primary()
函数指定新的primary节点。在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,可以正常运行。
摸索出一个流程
从0开始
docker compose中只配个root密码和volumes。cnf可以为空。
直接启动
INSTALL PLUGIN group_replication SONAME 'group_replication.so'
安装mgr插件.cnf里填写所有参数。包括mgr参数。
重启容器
此时数据库已经安装了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切换主节点时,可能有积压的操作,这些操作仍需要处理。
新主节点可以
立即接收请求。不等待积压的操作。
可能造成读操作读到老数据。等处理完积压的操作,才开始接收请求。
数据没问题。但是有延时。
数据流控#
数据流控
对单主和多主都有效。这里只看单主。
通常mgr的读写分离会把读方到主,把写均匀分给从。
因为mgr的一致性,大体可认为主的写操作一定会在从上同时完成。
但是细节上如果不进行处理,还是可能出现读到老数据。见图18.3。
要解决的不一致问题基本就是,如果不处理一致性,客户端1在主节点写了数据,会立刻返回成功,而从节点可能因为各种原因要过一会才能同步到这个数据。
那么就有可能出现客户看到操作成功,但再次读数据时读到了老数据。这就出了大问题。
下面的consistency guarantee
的各种配置就是要按需要解决这个问题。
同步点#
基于事务的同步时间点,可以对组的consistency guarantee
进行配置。
方便起见,这一节把把同步点分为读操作时
和写操作时
。
如果在读时同步,用户session会等所有之前的更新事务搜完成,再执行自己的操作。
只有这个session受影响。如果在写时同步,写操作会等待所有从节点完成各自积压的写操作,再返回。
这两种都会保证不会出现上述的读到老数据。
各有优劣,得看情况使用。
想对读操作做负载均衡,同时不想限制读操作的节点。读操作远远多于写操作。
组中数据主要为只读,想要写操作即刻再所有节点生效,后续读不能读到老数据。
情况1/2用写时同步。
同1。但写远远多于读。
绝不允许读到老数据。
情况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操作等待所有积压的操作commit,才apply。
此操作在所有节点apply,才返回。
ro操作等待所有积压操作commit后,才执行。
结合了BEFORE
和AFTER
。等级最高。
这个参数可以设定在Global和Session。
所以可以产生各种组合,可以做的比较复杂。
如何选择#
想对读操作负载均衡,不想读到老数据,读远多于写。用
AFTER
。有大量的写,偶尔读一下,不想读到老数据。用
BEFORE
。。。。
有大量只读数据,不想在ro上有延迟,可以在rw上有延迟。用
AFTER
。全都要。用
BEFORE_AND_AFTER
。绝大多数操作不需要很强的一致性。某些重要操作必须要严格立即生效。
比如设置某个权限,或者充值等,要求一旦客户端返回成功,绝不允许读到老数据。
可以全局默认用EVENTUAL
,把这个操作的session设成AFTER
。
SET @@SESSION.group_replication_consistency='AFTER'
跟6场景相似,平时不需要很强的一致性。但某些少量的操作要求读最新数据。
可以全局默认用EVENTUAL
,把这个操作的session设成BEFORE
。
SET @@SESSION.group_replication_consistency='BEFORE'
所有rw操作一定是有序的。
consistency guarantee的影响#
另一个角度是consistency guarantee对各个成员有什么影响。
BEFORE
只影响本地,我自己等待然后执行就完事儿。不影响其他人。
AFTER
和BEFORE_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_hostgroups
的writer_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很长时间。