告别理论:基于MySQL 8.4 LTS的GTID主从复制极速配置

前年我们团队接手了一个电商系统的重构,当时主库已经跑到了 800 QPS,从库还是单台,一到晚高峰报表查询一开,主库 CPU 直接飙到 90%,接口响应从 200ms 掉到 1.5s。那时候我意识到,不把读写分离和主从复制做扎实,系统随时会崩。

我选择直接上 MySQL 8.4 LTS(2024 年 4 月 30 日发布),而不是 5.7 或 8.0 老版本。原因很直接:8.4 的 GTID 复制更稳定,而且 基于 WRITESET 的并行回放 在从库重放日志时效率提升非常明显,这对我们这种写多读多的场景至关重要。

为什么一定要用 GTID

早期我们用过基于 binlog file + position 的复制,主库宕机切换时,找新主库的同步起点非常痛苦,经常要人工算偏移量。GTID 的出现直接解决了这个问题:每一个事务都有一个全局唯一的事务 ID,从库知道哪些事务已经执行过,不需要再去数 binlog 位置。

在我们的订单库中,单表数据量已经突破 1200 万,每天新增 30 万订单。如果不用 GTID,一次主从切换后,光是核对数据一致性就能花掉我半个下午。

主库配置(my.cnf)

下面是我们生产环境主库的真实配置片段,我去掉了业务敏感参数,保留了复制相关的核心部分:

[mysqld] server-id = 1 log-bin = mysql-bin binlog_format = ROW gtid_mode = ON enforce_gtid_consistency = ON binlog_row_image = FULL # 半同步复制支持 plugin_load_add = 'semisync_master.so' rpl_semi_sync_master_enabled = 1 rpl_semi_sync_master_timeout = 3000 # 8.4 增强的并行复制 slave_parallel_workers = 8 slave_parallel_type = LOGICAL_CLOCK slave_preserve_commit_order = ON

binlog_format = ROW 是我们踩过坑后坚持用的。之前有人为了省磁盘用 STATEMENT,结果一条 UPDATE ... LIMIT 在主从执行结果不一致,导致库存数据对不上。ROW 格式虽然日志大一点,但数据一致性是底线。

rpl_semi_sync_master_timeout = 3000 表示如果 3 秒内从库没确认收到日志,就自动降级为异步复制。我们当时在跨机房部署时,网络抖动频繁,这个值设成 1 秒会导致写入频繁超时,改成 3 秒后系统稳定性明显提升。

从库配置与复制启动

从库的 my.cnf 核心配置如下:

[mysqld] server-id = 2 relay-log = relay-bin gtid_mode = ON enforce_gtid_consistency = ON read_only = ON plugin_load_add = 'semisync_slave.so' rpl_semi_sync_slave_enabled = 1 slave_parallel_workers = 8 slave_parallel_type = LOGICAL_CLOCK

配置完成后,我在从库执行了以下操作:

CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.10.11', SOURCE_USER='repl', SOURCE_PASSWORD='Repl@2024', SOURCE_AUTO_POSITION=1; START REPLICA;

这里我特意用了 SOURCE_AUTO_POSITION=1,这就是 GTID 的精髓。以前我们还要 MASTER_LOG_FILEMASTER_LOG_POS,现在完全不需要。有一次我们主库机器故障,我把一台从库提升为新主库,其他从库重新指向新主库,只用了不到 2 分钟就完成了拓扑变更。

验证复制状态

我习惯用这个命令检查复制是否真正健康:

SHOW REPLICA STATUS\G

重点看两个字段:

有一次 Replica_SQL_Running 显示 No,排查后发现是一个开发在从库上手动执行了 DDL,导致事务冲突。从那以后,我们在从库统一加了 read_only = ON,并且通过堡垒机控制权限,再也没出现过类似问题。

某百万日活社区实战:ProxySQL 读写分离配置与压测对比

我们那个社区产品,日活稳定在 110 万左右,晚上 9 点到 11 点是高峰,主库写入 QPS 能到 1200,读取 QPS 超过 4000。单靠主从复制,如果不做读写分离,主库压力根本扛不住。

我调研过 MySQL Router,但最后选了 ProxySQL,原因很简单:它的 读写分离规则更灵活,支持基于 SQL 正则匹配、用户、schema 维度路由,而且有完整的监控和在线配置能力,不用重启就能生效。

为什么不用代码层做读写分离

早期我们尝试在应用代码里用 Spring 的 AbstractRoutingDataSource 手动切数据源,结果问题一堆:

ProxySQL 把这些逻辑下沉到数据库中间件层,应用层完全无感,这才是我想要的架构。

ProxySQL 核心配置实战

以下是我们生产环境 ProxySQL 的真实配置过程,我截取了关键步骤。

先配置后端 MySQL 服务器:

INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight, comment) VALUES (10, '192.168.10.11', 3306, 1000, 'master'), (20, '192.168.10.12', 3306, 500, 'slave1'), (20, '192.168.10.13', 3306, 500, 'slave2'); LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;

这里我用了两个 hostgroup:10 是写组,20 是读组。权重设置成 1000 和 500,是为了让主库在必要时也承担一部分读压力(后面规则会控制)。

接着配置读写分离规则:

INSERT INTO mysql_query_rules (rule_id, active, match_digest, destination_hostgroup, apply) VALUES (1, 1, '^SELECT.*FOR UPDATE$', 10, 1), (2, 1, '^SELECT', 20, 1), (3, 1, '^INSERT', 10, 1), (4, 1, '^UPDATE', 10, 1), (5, 1, '^DELETE', 10, 1); LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;

第一条规则非常关键:SELECT ... FOR UPDATE 是显式加锁读,必须走主库,否则会出现库存超扣的问题。我们之前就因为漏掉这条规则,在抢购活动中出现了数据不一致。

压测数据对比

我们用 sysbench 模拟了高峰期的混合读写场景,数据量 1000 万,并发 200 线程,持续 10 分钟。

改造前(直连主库):

改造后(ProxySQL + 一主两从):

延迟从 380ms 降到 120ms,这个变化用户体感非常明显。尤其是帖子列表页和评论加载,以前偶尔会转圈,后来基本秒开。

一个真实故障:从库延迟导致“发帖不可见”

上线第一周,有用户反馈发完帖子后刷新列表看不到内容。我查了 ProxySQL 日志,发现 SELECT 被路由到了从库,而从库当时有 6 秒的延迟。

解决方案不是简单去掉读写分离,而是做了两件事:

调整后,从库延迟稳定在 0.5 秒以内,这类投诉再也没出现过。

踩坑实录:大事务导致的严重主从延迟排查与WRITESET优化

去年双十一前,我们做过一次压测,模拟大促时的订单洪峰。结果在写入 3000 万测试数据后,从库突然出现了 12 秒的主从延迟,而且延迟还在持续增大。

当时我第一反应是网络问题,但 Bytes_sentBytes_received 都很正常。后来查 SHOW REPLICA STATUS,发现 Seconds_Behind_Master 一直在涨,而 Replica_SQL_Running 虽然显示 Yes,但应用日志的速度极慢。

问题定位过程

我打开了主库的 binlog 分析:

mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.000012 | grep -A 10 "GTID"

发现有一个事务一次性更新了 40 万行数据,是一个定时任务在清理过期日志表。这个事务在主库执行用了 3 秒,但在从库,因为 单线程回放,硬生生跑了 15 秒。

这就是传统并行复制的痛点:基于 schema 的并行度不够,大事务直接卡住整个回放队列。

WRITESET 并行复制的救场

MySQL 8.4 的 WRITESET 并行复制 正是为了解决这个问题。它的原理是:只要两个事务修改的数据行没有重叠,就可以在从库并行回放,而不管它们是不是同一个 schema。

我做了如下调整:

[mysqld] binlog_transaction_dependency_tracking = WRITESET transaction_write_set_extraction = XXHASH64

这两个参数一开,从库重启复制后,效果立竿见影。同样那个 40 万行的大事务,从库回放时间从 15 秒降到了 4 秒,主从延迟迅速从 12 秒收敛到 0.8 秒。

为什么 WRITESET 这么有效

传统并行复制依赖“逻辑时钟”,同一个 commit parent 的事务才能并行。而 WRITESET 会计算每个事务修改行的 hash 集合,只要 write set 不冲突,就可以并行。

在我们的订单系统里,不同用户的订单更新几乎不会冲突,WRITESET 让从库的并行度直接提升了 4 倍。

一个教训:大事务必须拆分

参数优化只是缓解,根本问题还是那个定时任务。我后来把“一次删 40 万行”改成了:

DELETE FROM sys_log WHERE create_time < DATE_SUB(NOW(), INTERVAL 30 DAY) LIMIT 5000;

然后用外部脚本循环调用,每次删除 5000 行,每次事务控制在 0.2 秒以内。改造后,从库延迟再也没有超过 1 秒。

这次排查让我明白一件事:中间件和参数优化能救命,但架构和 SQL 设计才是根本。如果当初放任那个大事务上线,再强的 WRITESET 也扛不住持续的大批量写入。

4. 技术选型博弈:MGR组复制 vs 传统主从复制 vs NewSQL的取舍

去年我们给一个做跨境电商的团队做重构,他们的订单库跑在 MySQL 5.7 上,主从延迟经常飙到 10 秒以上,大促的时候客服后台查不到刚下的单,被投诉了好几次。当时我坐在他们会议室里,盯着监控大屏,就在想:到底是接着修修补补传统主从,还是直接上 MGR,或者干脆换 NewSQL?

先说传统主从复制。我手头现在维护着三个生产环境还在用这套架构,都是 MySQL 8.0.33。它的好处是简单,运维心智负担低。我们那个内容管理系统,读多写少,主库写 QPS 也就 800 左右,挂了三个从库做读写分离,跑了三年没出过大问题。但痛点也明显,就是那个主从延迟。有一次线上接口突然变慢,排查下来发现是一个 2GB 的大事务在导历史数据,从库的 SQL 线程是单线程回放(当时还没开并行),硬生生堵了 40 秒。后来我们升级到 MySQL 8.4 LTS(2024年4月刚发的长期支持版),开了基于 WRITESET 的并行复制,情况才好转,延迟稳定在 200ms 以内。

-- 这是我们生产环境在 my.cnf 里的关键配置,针对 8.4 版本的并行复制优化 [mysqld] server-id=2 relay-log=relay-bin # 开启基于 WRITESET 的并行回放,这是 8.0 之后的核心优化点 slave_parallel_workers=8 slave_parallel_type=LOGICAL_CLOCK # 确保事务依赖跟踪,减少冲突检测开销 binlog_transaction_dependency_tracking=WRITESET transaction_write_set_extraction=XXHASH64

再聊 MGR(MySQL Group Replication)。今年初我们在一个金融级的支付流水系统试过,用的是单主模式。MGR 基于 Paxos 协议,数据一致性确实强,主库挂了能自动选主,RTO(恢复时间目标)能压到 10 秒以内。但代价是性能损耗。我们压测的时候发现,同等硬件下,MGR 的写 QPS 比传统主从掉了大概 15% 左右,因为节点间要同步通信。而且它对网络抖动非常敏感,有一次机房内网络抖动了 200ms,MGR 集群直接把主节点踢出去了,虽然自动切了,但业务还是抖了一下。所以我的判断是,如果你对数据一致性要求极高(比如金融交易),且网络环境稳定,MGR 是好选择;但如果你只是想解决读扩展问题,MGR 有点杀鸡用牛刀,还增加了运维复杂度。

最后说说 NewSQL,比如 TiDB。我们去年在一个日均订单量 500 万的新业务上做过 POC(概念验证)。TiDB 的弹性扩容确实爽,不用分库分表,直接加节点。但问题是,它的运维门槛高,而且对于强事务一致性的场景,延迟比 MySQL 高。我们当时跑了一个对比测试,同样是 100 并发下的转账事务,MySQL 8.4 主从平均耗时 12ms,TiDB 6.5 平均要 22ms。而且我们团队当时对 TiDB 的底层原理不熟悉,出了点问题查起来费劲。

所以我的取舍逻辑是这样的:

5. 面向未来:云原生架构下的弹性伸缩与 AI 自治运维展望

上个月我参加了一个技术闭门会,大家聊到 MySQL 在云原生时代的尴尬。现在的趋势很明显,Kubernetes 已经成了基础设施的标准,但 MySQL 这种有状态数据库在 K8s 里跑,状态管理一直是个头疼事。我们现在的做法是把 MySQL 8.4 跑在裸金属服务器上,虽然稳定,但扩容一次从库得手动操作,申请机器、装环境、配主从,没个半小时下不来。

我最近在测试的一个方案是基于 Operator 的云原生部署。比如用 Presslabs 的 MySQL Operator,它能把主从复制的维护逻辑代码化。我写了一个简单的 YAML 配置,想扩容一个从库,只需要改一个数字,Operator 就会自动拉起 Pod,配置好 GTID 复制,并加入负载均衡。这比我们以前写一堆 Ansible 脚本爽多了。

# 这是我测试环境用的 mysql-operator 配置片段,定义集群拓扑 apiVersion: mysql.presslabs.org/v1alpha1 kind: MysqlCluster metadata: name: order-db-cluster spec: replicas: 3 # 1主2从,改成4就是1主3从,自动扩容 mysqlVersion: "8.4.0" # 指定 2024 年发布的 LTS 版本 # 配置读写分离,通过 Service 自动暴露读写端点 mysqlConf: binlog_format: "ROW" gtid_mode: "ON" enforce_gtid_consistency: "ON" backup: schedule: "0 2 * * *" storage: s3: bucket: "my-db-backup"

再聊聊 AI 自治运维。现在的 DBA 大部分时间都在盯着慢查询和主从延迟。我预感未来这两块会被 AI 接管。比如主从延迟,以前我们得手动去调 slave_parallel_workers,或者去优化大事务。现在有一些实验性的工具,能根据历史负载自动预测延迟。我看过一个基于机器学习的模型,它分析我们过去三个月的 Binlog,发现每周五下午 3 点都会有一个定时任务导致延迟,于是它自动在那个时间段前把并行度调高,并在任务结束后恢复。这种自治能力在 MySQL 9.0 的路线图里也有提及,未来复制可能会支持向量数据的同步,这意味着 AI 模型训练用的向量数据也能像普通数据一样做读写分离了。

还有一个趋势是 Serverless 化。我们现在为了应对大促,平时得备着冗余的从库,平时闲着也是浪费。如果 MySQL 能像 Aurora Serverless 那样,根据 QPS 自动拉起或销毁读节点,成本能省不少。我算过一笔账,我们现在的从库平时 CPU 利用率不到 10%,如果按量付费且能自动缩容,一年能省下 20% 的数据库成本。

不过我也担心一个问题,就是技术栈越来越复杂。以前我们只要会装 MySQL、配主从就行了,现在还得懂 K8s、Operator、Prometheus 监控,甚至还得懂点 AI 算法。对于我们这种一线开发来说,精力是有限的。所以我的建议是,别盲目追新,先把 MySQL 8.4 的 GTID 和并行复制玩透,把监控告警做好,等云原生的生态再成熟一点,比如 Operator 的版本更稳定了,再考虑大规模迁移。毕竟,线上数据库的稳定性,永远比技术的新潮更重要。

站长实战手记

一次差点让我背P0故障的“主从延迟”

去年接了个电商SaaS项目,日订单量刚破10万,老板嫌主库压力大,让我把读写分离上了。我那时候年轻气盛,直接上了 MySQL 8.0 + GTID 复制 + ProxySQL,配置顺滑得不行,压测也过了。

结果上线第三天,运营反馈:有商家刚改了商品库存,刷新页面又变回去了。我一看监控,主从延迟居然飙到了 8秒。排查下来,罪魁祸首是一个定时任务在凌晨跑大批量更新,一个事务里塞了3万条UPDATE。从库回放慢得像蜗牛,因为8.0默认还是按事务提交顺序回放,单线程卡死。

我折腾了一宿,最后没敢直接改线上,而是先给那个定时任务拆成了每500条一提交,延迟瞬间掉到1秒内。后来才去研究 WRITESET 并行复制,把 binlog_transaction_dependency_tracking 改成 WRITESET,从库回放能力直接翻倍,才算彻底稳住。

我的真实取舍看法

* 别为了“高大上”而上读写分离。如果你的QPS还没到5000,或者业务主要是写多读少,强上ProxySQL只会多一个故障点。单库加个好点的SSD,比啥都强。

* GTID是好东西,但别迷信它。遇到那种需要跳过事务的极端情况,GTID的排错逻辑比传统日志名+位点要绕得多,新手容易懵。

* MGR(组复制)虽然香,但在跨地域部署时,网络抖动能让你怀疑人生。传统主从虽然土,但胜在简单粗暴,好排查。

给读者的真心话

别光看文章里的命令敲得爽,一定要在本地虚拟机里把主库搞挂一次,试试手动切换主从。纸上谈兵终觉浅,真到了凌晨三点报警响,能救你的不是文档,是你手感。