文章

MySQL8 搭建 innodb cluster 实验

innodb cluster 搭建

介绍

一个事务在 MGR 中的执行流程大致如下:

  1. 事务写 Binlog 之前会进入到 MGR 层;
  2. 事务消息通过 Paxos 广播到各个节点;
  3. 在各个节点上进行冲突检测(冲突检测详细过程会在稍后介绍);
  4. 认证通过后本地节点写 Binlog 完成提交;
  5. 其他节点写 Relay Log 后并完成回放。

环境

mysqlshell 和 mysqlrouter 和实例装在一台机器上,如果是三台服务器建集群需要配置每台服务器的 hosts。我的是一台 CentOS7 使用 mysql_multi 启动3个 mysql 实例

下载安装 mysql shell 和 mysql router

MySQL Router MySQL Shell MySQL 二进制包,现在最新二进制包是 8.0.33。 我的解压文件夹是 /opt/mysql8

配置 mysql_multi

#创建 mysql 用户
groupadd -g 30 mysql
useradd -u 30 -g mysql mysql
#创建相关目录
mkdir -p /data/mysql/{mysql_3306,mysql_3307,mysql_3308}
mkdir /data/mysql/mysql_3306/{data,log,tmp}
mkdir /data/mysql/mysql_3307/{data,log,tmp}
mkdir /data/mysql/mysql_3308/{data,log,tmp}
#修改权限
chown -R mysql:mysql /data/mysql/
chown -R mysql:mysql /opt/mysql8/
#将 mysql 下 bin 加入环境变量
echo 'export PATH=$PATH:/opt/mysql8/bin' >>  /etc/profile
source /etc/profile
#复制my.cnf文件到etc目录,配置参考下方

#逐一启动3个实例,每次启动修改 root 密码,首次启动生成的密码可在 error log 中查看
mysqld_multi start ...

#配置mysql_config_edit
mysql_config_editor set --user=root --password
#mysqld_multi 命令
#启动所有/单个实例
mysqld_multi start
mysqld_multi start 3306
#关闭实例
mysqld_multi stop
mysqld_multi stop 3306
#查看实例状态
mysqld_multi report
mysqld_multi report 3306

my.cnf配置参考

[client]
port=3306
socket=/tmp/mysql_3306.sock
[mysqld_multi]
mysqld = /opt/mysql8/bin/mysqld_safe
mysqladmin = /opt/mysql8/bin/mysqladmin
log = /data/mysql/mysqld_multi.log
[mysqld]
user=mysql
basedir = /opt/mysql8
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
[mysqld3306]
mysqld=mysqld
mysqladmin=mysqladmin
datadir=/data/mysql/mysql_3306/data
port=3306
server_id=3306
socket=/tmp/mysql_3306.sock
log-output=file
slow_query_log = 1
long_query_time = 1
slow_query_log_file = /data/mysql/mysql_3306/log/slow.log
log-error = /data/mysql/mysql_3306/log/error.log
log-bin = /data/mysql/mysql_3306/log/mysql3306_bin
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_checksum=NONE
log_slave_updates=ON
binlog_format=ROW
master_info_repository=TABLE
relay_log_info_repository=TABLE
# 组复制设置
# server必须为每个事务收集写集合,并使用XXHASH64哈希算法将其编码为散列
transaction_write_set_extraction=XXHASH64
binlog_transaction_dependency_tracking=WRITESET
slave_preserve_commit_order=ON
slave_parallel_type=LOGICAL_CLOCK
# 告知插件加入或创建组命名,UUID
loose-group_replication_group_name="d7fb5035-5912-4b8d-9160-a49b5bbcbba6"
# server启动时不自启组复制,为了避免每次启动自动引导具有相同名称的第二个组,所以设置为OFF。
loose-group_replication_start_on_boot=off
# 告诉插件使用IP地址,端口24901用于接收组中其他成员转入连接
loose-group_replication_local_address="172.16.22.163:24901"
# 启动组server,种子server,加入组应该连接这些的ip和端口;其他server要加入组得由组成员同意
loose-group_replication_group_seeds="172.16.22.163:24901,172.16.22.163:24902,172.16.22.163:24903"
loose-group_replication_bootstrap_group=off
# 使用MGR的单主模式
loose-group_replication_single_primary_mode = on
disabled_storage_engines = MyISAM,BLACKHOLE,FEDERATED,CSV,ARCHIVE
report_port=3306

[mysqld3307]
mysqld=mysqld
mysqladmin=mysqladmin
datadir=/data/mysql/mysql_3307/data
port=3307
server_id=3307
socket=/tmp/mysql_3307.sock
log-output=file
slow_query_log = 1
long_query_time = 1
slow_query_log_file = /data/mysql/mysql_3307/log/slow.log
log-error = /data/mysql/mysql_3307/log/error.log
log-bin = /data/mysql/mysql_3307/log/mysql3307_bin
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_checksum=NONE
log_slave_updates=ON
binlog_format=ROW
master_info_repository=TABLE
relay_log_info_repository=TABLE
# 组复制设置
# server必须为每个事务收集写集合,并使用XXHASH64哈希算法将其编码为散列
transaction_write_set_extraction=XXHASH64
binlog_transaction_dependency_tracking=WRITESET
slave_preserve_commit_order=ON
slave_parallel_type=LOGICAL_CLOCK
# 告知插件加入或创建组命名,UUID
loose-group_replication_group_name="d71acb2a-06d8-4edf-a08a-f37c262d616f"
# server启动时不自启组复制,为了避免每次启动自动引导具有相同名称的第二个组,所以设置为OFF。
loose-group_replication_start_on_boot=off
# 告诉插件使用IP地址,端口24901用于接收组中其他成员转入连接
loose-group_replication_local_address="172.16.22.163:24902"
# 启动组server,种子server,加入组应该连接这些的ip和端口;其他server要加入组得由组成员同意
loose-group_replication_group_seeds="172.16.22.163:24901,172.16.22.163:24902,172.16.22.163:24903"
loose-group_replication_bootstrap_group=off
# 使用MGR的单主模式
loose-group_replication_single_primary_mode = on
disabled_storage_engines = MyISAM,BLACKHOLE,FEDERATED,CSV,ARCHIVE
report_port=3307

[mysqld3308]
mysqld=mysqld
mysqladmin=mysqladmin
datadir=/data/mysql/mysql_3308/data
port=3308
server_id=3308
socket=/tmp/mysql_3308.sock
log-output=file
slow_query_log = 1
long_query_time = 1
slow_query_log_file = /data/mysql/mysql_3308/log/slow.log
log-error = /data/mysql/mysql_3308/log/error.log
log-bin = /data/mysql/mysql_3308/log/mysql3308_bin
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_checksum=NONE
log_slave_updates=ON
binlog_format=ROW
master_info_repository=TABLE
relay_log_info_repository=TABLE
# 组复制设置
# server必须为每个事务收集写集合,并使用XXHASH64哈希算法将其编码为散列
transaction_write_set_extraction=XXHASH64
# 启用事务依赖跟踪
binlog_transaction_dependency_tracking=WRITESET
# 保持从库提交顺序
slave_preserve_commit_order=ON
# 设置并行复制类型
slave_parallel_type=LOGICAL_CLOCK
# 告知插件加入或创建组命名,UUID
loose-group_replication_group_name="93755aff-b5e2-4d81-b9ac-aebde0503632"
# server启动时不自启组复制,为了避免每次启动自动引导具有相同名称的第二个组,所以设置为OFF。
loose-group_replication_start_on_boot=off
# 告诉插件使用IP地址,端口24901用于接收组中其他成员转入连接
loose-group_replication_local_address="172.16.22.163:24903"
# 启动组server,种子server,加入组应该连接这些的ip和端口;其他server要加入组得由组成员同意
loose-group_replication_group_seeds="172.16.22.163:24901,172.16.22.163:24902,172.16.22.163:24903"
# 禁止服务器启动时自动引导组
loose-group_replication_bootstrap_group=off
# 使用MGR的单主模式
loose-group_replication_single_primary_mode = on
disabled_storage_engines = MyISAM,BLACKHOLE,FEDERATED,CSV,ARCHIVE
# 指定报告的端口
report_port=3308

节点配置检查

#进入 mysqlshell 连接到3306
\connect mysqlx:root@172.16.22.163:33060
#检查配置
dba.checkInstanceConfiguration('root@172.16.22.163:3306')
#如果提示权限不够则根据提示添加权限
#有 error 就运行下面的命令修改配置
dba.configureInstance('root@172.16.22.163:3306')
#重启实例

初始化

#连接到 3306实例
\connect mysqlx://root@172.16.22.3:33060
#创建集群
var cluster = dba.createCluster('mycluster')
#添加副本实例到创建好的集群
var mycluster = dba.getCluster()
#添加节点,提示 group_name 不在组内,按提示选择 clone 即可
mycluster.addInstance('root@172.16.22.163:3307')
#添加后会重启 mysql 实例,如果实例没启动,手动启动然后执行
mycluster.rescan()
#查看集群状态
mycluster.status()

创建用户

#集群管理员用户
mycluster.setupAdminAccount('icadmin')
#router 用户
mycluster.setupRouterAccount('icrouter')

启动 mysqlrouter

MySQL Classic protocol:

  • Read/Write Connections: localhost:6446, /opt/myrouter/mysql.sock
  • Read/Only Connections: localhost:6447, /opt/myrouter/mysqlro.sock MySQL X protocol
  • Read/Write Connections: localhost:6448, /opt/myrouter/mysqlx.sock
  • Read/Only Connections: localhost:6449, /opt/myrouter/mysqlxro.sock
#配置
mysqlrouter --bootstrap root@localhost:3306 --directory=/opt/myrouter --conf-use-sockets --account icrouter --user=mysql --force
#启动
sh myrouter/start.sh
#停止
sh myrouter/stop.sh

维护命令

#获取帮助
dba.help()
#获取 cluster
var cluster=dba.getCluster()
#集群状态
cluster.status()
#查看集群结构(静态信息)
cluster.describe()
#更新集群元数据(重新扫描)
cluster.rescan()
#列出集群中实例的配置
cluster.options()
#更改集群全局配置
cluster.setOption(option, value)
#删除实例
cluster.removeInstance(instance)
#实例重新加入集群
cluster.rejoinInstance(instance)
#重启 
dba.rebootClusterFromCompleteOutage('myCluster')
#删除源数据 schema
dba.dropMetadataSchema()
#检查 cluster 里实例状态
cluster.checkInstanceState("root@localhost:3306")
#解散集群
cluster.dissolve({force:true})
#强制删除实例
cluster.removeInstance("root@localhost:3306",{force:true})
#停止集群,进入 sql 模式停止组复制
stop group_replication;
#切换到多主模式
cluster.switchToMultiPrimaryMode()
#切换到单主模式
cluster.switchToSinglePrimaryMode('node3:3306')

备注

选举算法

单主模式下,主出现故障,会考虑下面的因素选择新的主:

  1. 考虑的第一个因素是哪个成员运行的是最低的 MySQL 版本。如果所有成员都运行 MySQL 8.0.17 或更高版本,那么组成员将首先按照发布的补丁版本进行排序。如果任何成员运行 MySQL 8.0.16 或更低的版本,成员将首先按其发布的主要版本排序,并忽略补丁版本。低版本优先是考虑到高版本同步到低版本,高版本可能有一些新特性,无法在从库正常回放,导致同步出现问题。
  2. 如果有多个成员运行最低的 MySQL 版本,则要考虑的第二个因素是每个成员的权重,由参数 group_replication_member_weight 指定。如果 MySQL 的版本为 5.7,则该参数不可用,将不考虑这个因素。系统变量 group_replication_member_weight 指定一个范围为 0-100 的数字。值越大,权重越高。
  3. 如果前面两个因素都一样,则考虑的是,每个成员生成的服务器 UUID 的词法顺序,如果 server_uuid 系统变量都指定了,则选择 UUID 排序最靠前的成员作为主。

多主模式下,如果一个成员出现故障,连接到它的客户端可以重定向或故障转移到处于读写模式的任何其他成员。Group Replication 本身并不处理客户端故障转移,因此需要使用中间件框架,比如 MySQL Route。

故障转移

从节点提升为主前,要处理积压的事务,通常有两种选择:

  • 可靠性优先:如果有积压的事务,需要等积压的事务全被应用完,才能在新主上进行读写操作。
  • 可用性优先:不管是否有积压的事务,直接在新主上进行读写操作。

如果将 group_replication_consistency 设置为BEFORE_ON_PRIMARY_FAILOVER,则表示设置了可靠性优先。

流控

在多主模式中,速度较慢的成员还可能积累过多的事务以进行认证和应用,可能会导致冲突、认证失败或者读到过期数据等风险。为了解决这些问题,可以激活和调优 Group Replication 的流控制机制,以最小化快成员和慢成员之间的差异。 参数 group_replication_flow_control_mode 控制流控是否开启,如果为 QUOTA,表示开启,DISABLED 表示关闭。 两种情况会触发流控:

  • 证书队列中等待的事务数超过 group_replication_flow_control_certifier_threshold 配置的值时。
  • 应用程序队列中等待的事务数超过 group_replication_flow_control_applier_threshold 配置的值时。

冲突检测

在 MGR 中,为了防止多个节点同时更新了同一条记录,设置了冲突检测机制,具体步骤如下:

  • 首先计算出对 write set(write set 的组成是:索引名 + DB 名+ DB 名长度 + 表名 + 表名长度 + 构成索引唯一性的每个列的值 + 值长度) 做 murmur hash 算法的值,判断这个值在 certification_info 是否有相同的记录,有则表示冲突,事务回滚,没有则会把 write set 写入 certification_info ,并进行下一步。
  • 然后判断事务执行时执行节点的 gtid_executed 和 certification_info 里面对应的 gtid_set。
  • 如果 gtid_executed 是 gtid_set 的子集,说明该节点的事务执行时,其他节点已经对事务操作的数据进行了更改,则不能进行更新,事务回滚。
  • 如果 gtid_executed 不是 gtid_set 的子集,表示其他节点没有对事务操作的数据有修改操作,则事务可以正常提交。

事务一致性配置

在 MGR 中,可以通过配置 group_replication_consistency 变量来配置一致性级别,具体可配置的值及含义如下:

  • EVENTUAL

RO 和 RW 事务在执行之前都不会等待前面的事务被应用。在发生主故障转移的情况下,在前一个主事务全部应用之前,新的主事务可以接受新的 RO 和 RW 事务。RO 事务可能会导致过期的值,RW 事务可能会由于冲突导致回滚。

  • BEFORE_ON_PRIMARY_FAILOVER

新选出的主服务器需要应用完旧主服务器的事务,在应用任何待办事项之前,将不应用新的 RO 或 RW 事务。这确保了当主节点发生故障转移时,无论是否有意,客户机总是看到主节点上的最新值。这保证了一致性,但也意味着客户端必须能够在应用 backlog 时处理延迟。通常这种延迟应该是最小的,但它确实取决于积压的大小(可靠性优先)。

  • BEFORE

RW 等待所有前面的事务完成后才应用新的事务。RO 事务等待所有前一个事务完成后才执行。这确保该事务仅通过影响事务的延迟读取最新的值。通过确保只在 RO 事务上使用同步,这减少了每个 RW 事务上的同步开销。这个一致性级别还包括 BEFORE_ON_PRIMARY_FAILOVER 提供的一致性保证。

  • AFTER

RW 会等待它的更改被应用到所有其他成员。该值对 RO 事务没有影响。此模式确保在本地成员上提交事务时,任何后续事务都将读取已写入的值或任何组成员上最近的值。应用程序可以使用这一点来确保后续读取最新的数据,其中包括最新的写操作。这个一致性级别还包括 BEFORE_ON_PRIMARY_FAILOVER 提供的一致性保证。

  • BEFORE_AND_AFTER

RW,RO 事务都需要等待所有之前的事务完成后才被应用。这个一致性级别还包括BEFORE_ON_PRIMARY_FAILOVER提供的一致性保证。

事务一致性选择

MGR 设置 BEFORE_ON_PRIMARY_FAILOVER 的目的,也是为了方便我们根据不同的业务场景选择合适的一致性级别,下面就列出一些场景下的一致性级别选择:

  • 通常情况下,不建议设置为 AFTER 模式,一方面可能导致集群吞吐量下降,另一方面在节点故障切换时,等待时间相对其他模式会非常长。
  • 有大量写操作,偶尔读取数据,并且不必担心读取到过期数据,则可以选择 BEFORE。
  • 希望特定事务总是从组中读取最新的数据,这种情况,选择 BEFORE。
  • 有一个组,其中主要是只读数据,希望读写(RW)事务总是从组中读取最新的数据,并在提交后应用到所有地方, 这样,后续的读操作都是在最新的数据上完成的,包括你最新的写操作,你不需要在每个只读(RO)事务上花费同步成本,而只在 RW 事务上。在本例中,应该选择 BEFORE_AND_AFTER
  • 每天都有一条指令需要做一些分析处理,因此它总是需要读取最新的数据。要实现这一点,您只需要设置 SET @@SESSION.group_replication_consistency= 'BEFORE'。
License:  CC BY 4.0