前年我们团队接手了一个电商系统的重构,当时主库已经跑到了 800 QPS,从库还是单台,一到晚高峰报表查询一开,主库 CPU 直接飙到 90%,接口响应从 200ms 掉到 1.5s。那时候我意识到,不把读写分离和主从复制做扎实,系统随时会崩。
我选择直接上 MySQL 8.4 LTS(2024 年 4 月 30 日发布),而不是 5.7 或 8.0 老版本。原因很直接:8.4 的 GTID 复制更稳定,而且 基于 WRITESET 的并行回放 在从库重放日志时效率提升非常明显,这对我们这种写多读多的场景至关重要。
早期我们用过基于 binlog file + position 的复制,主库宕机切换时,找新主库的同步起点非常痛苦,经常要人工算偏移量。GTID 的出现直接解决了这个问题:每一个事务都有一个全局唯一的事务 ID,从库知道哪些事务已经执行过,不需要再去数 binlog 位置。
在我们的订单库中,单表数据量已经突破 1200 万,每天新增 30 万订单。如果不用 GTID,一次主从切换后,光是核对数据一致性就能花掉我半个下午。
下面是我们生产环境主库的真实配置片段,我去掉了业务敏感参数,保留了复制相关的核心部分:
binlog_format = ROW 是我们踩过坑后坚持用的。之前有人为了省磁盘用 STATEMENT,结果一条 UPDATE ... LIMIT 在主从执行结果不一致,导致库存数据对不上。ROW 格式虽然日志大一点,但数据一致性是底线。
rpl_semi_sync_master_timeout = 3000 表示如果 3 秒内从库没确认收到日志,就自动降级为异步复制。我们当时在跨机房部署时,网络抖动频繁,这个值设成 1 秒会导致写入频繁超时,改成 3 秒后系统稳定性明显提升。
从库的 my.cnf 核心配置如下:
配置完成后,我在从库执行了以下操作:
这里我特意用了 SOURCE_AUTO_POSITION=1,这就是 GTID 的精髓。以前我们还要 MASTER_LOG_FILE 和 MASTER_LOG_POS,现在完全不需要。有一次我们主库机器故障,我把一台从库提升为新主库,其他从库重新指向新主库,只用了不到 2 分钟就完成了拓扑变更。
我习惯用这个命令检查复制是否真正健康:
重点看两个字段:
Replica_IO_Running: YesReplica_SQL_Running: YesRetrieved_Gtid_Set 和 Executed_Gtid_Set 是否在持续更新有一次 Replica_SQL_Running 显示 No,排查后发现是一个开发在从库上手动执行了 DDL,导致事务冲突。从那以后,我们在从库统一加了 read_only = ON,并且通过堡垒机控制权限,再也没出现过类似问题。
我们那个社区产品,日活稳定在 110 万左右,晚上 9 点到 11 点是高峰,主库写入 QPS 能到 1200,读取 QPS 超过 4000。单靠主从复制,如果不做读写分离,主库压力根本扛不住。
我调研过 MySQL Router,但最后选了 ProxySQL,原因很简单:它的 读写分离规则更灵活,支持基于 SQL 正则匹配、用户、schema 维度路由,而且有完整的监控和在线配置能力,不用重启就能生效。
早期我们尝试在应用代码里用 Spring 的 AbstractRoutingDataSource 手动切数据源,结果问题一堆:
ProxySQL 把这些逻辑下沉到数据库中间件层,应用层完全无感,这才是我想要的架构。
以下是我们生产环境 ProxySQL 的真实配置过程,我截取了关键步骤。
先配置后端 MySQL 服务器:
这里我用了两个 hostgroup:10 是写组,20 是读组。权重设置成 1000 和 500,是为了让主库在必要时也承担一部分读压力(后面规则会控制)。
接着配置读写分离规则:
第一条规则非常关键:SELECT ... FOR UPDATE 是显式加锁读,必须走主库,否则会出现库存超扣的问题。我们之前就因为漏掉这条规则,在抢购活动中出现了数据不一致。
我们用 sysbench 模拟了高峰期的混合读写场景,数据量 1000 万,并发 200 线程,持续 10 分钟。
改造前(直连主库):
改造后(ProxySQL + 一主两从):
延迟从 380ms 降到 120ms,这个变化用户体感非常明显。尤其是帖子列表页和评论加载,以前偶尔会转圈,后来基本秒开。
上线第一周,有用户反馈发完帖子后刷新列表看不到内容。我查了 ProxySQL 日志,发现 SELECT 被路由到了从库,而从库当时有 6 秒的延迟。
解决方案不是简单去掉读写分离,而是做了两件事:
slave_parallel_workers 从 4 调到 8,并开启 binlog_transaction_dependency_tracking = WRITESET调整后,从库延迟稳定在 0.5 秒以内,这类投诉再也没出现过。
去年双十一前,我们做过一次压测,模拟大促时的订单洪峰。结果在写入 3000 万测试数据后,从库突然出现了 12 秒的主从延迟,而且延迟还在持续增大。
当时我第一反应是网络问题,但 Bytes_sent 和 Bytes_received 都很正常。后来查 SHOW REPLICA STATUS,发现 Seconds_Behind_Master 一直在涨,而 Replica_SQL_Running 虽然显示 Yes,但应用日志的速度极慢。
我打开了主库的 binlog 分析:
发现有一个事务一次性更新了 40 万行数据,是一个定时任务在清理过期日志表。这个事务在主库执行用了 3 秒,但在从库,因为 单线程回放,硬生生跑了 15 秒。
这就是传统并行复制的痛点:基于 schema 的并行度不够,大事务直接卡住整个回放队列。
MySQL 8.4 的 WRITESET 并行复制 正是为了解决这个问题。它的原理是:只要两个事务修改的数据行没有重叠,就可以在从库并行回放,而不管它们是不是同一个 schema。
我做了如下调整:
这两个参数一开,从库重启复制后,效果立竿见影。同样那个 40 万行的大事务,从库回放时间从 15 秒降到了 4 秒,主从延迟迅速从 12 秒收敛到 0.8 秒。
传统并行复制依赖“逻辑时钟”,同一个 commit parent 的事务才能并行。而 WRITESET 会计算每个事务修改行的 hash 集合,只要 write set 不冲突,就可以并行。
在我们的订单系统里,不同用户的订单更新几乎不会冲突,WRITESET 让从库的并行度直接提升了 4 倍。
参数优化只是缓解,根本问题还是那个定时任务。我后来把“一次删 40 万行”改成了:
然后用外部脚本循环调用,每次删除 5000 行,每次事务控制在 0.2 秒以内。改造后,从库延迟再也没有超过 1 秒。
这次排查让我明白一件事:中间件和参数优化能救命,但架构和 SQL 设计才是根本。如果当初放任那个大事务上线,再强的 WRITESET 也扛不住持续的大批量写入。
去年我们给一个做跨境电商的团队做重构,他们的订单库跑在 MySQL 5.7 上,主从延迟经常飙到 10 秒以上,大促的时候客服后台查不到刚下的单,被投诉了好几次。当时我坐在他们会议室里,盯着监控大屏,就在想:到底是接着修修补补传统主从,还是直接上 MGR,或者干脆换 NewSQL?
先说传统主从复制。我手头现在维护着三个生产环境还在用这套架构,都是 MySQL 8.0.33。它的好处是简单,运维心智负担低。我们那个内容管理系统,读多写少,主库写 QPS 也就 800 左右,挂了三个从库做读写分离,跑了三年没出过大问题。但痛点也明显,就是那个主从延迟。有一次线上接口突然变慢,排查下来发现是一个 2GB 的大事务在导历史数据,从库的 SQL 线程是单线程回放(当时还没开并行),硬生生堵了 40 秒。后来我们升级到 MySQL 8.4 LTS(2024年4月刚发的长期支持版),开了基于 WRITESET 的并行复制,情况才好转,延迟稳定在 200ms 以内。
再聊 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 的底层原理不熟悉,出了点问题查起来费劲。
所以我的取舍逻辑是这样的:
rpl_semi_sync_master_enabled = 1,虽然性能比异步低 5% 左右,但数据安全多了。上个月我参加了一个技术闭门会,大家聊到 MySQL 在云原生时代的尴尬。现在的趋势很明显,Kubernetes 已经成了基础设施的标准,但 MySQL 这种有状态数据库在 K8s 里跑,状态管理一直是个头疼事。我们现在的做法是把 MySQL 8.4 跑在裸金属服务器上,虽然稳定,但扩容一次从库得手动操作,申请机器、装环境、配主从,没个半小时下不来。
我最近在测试的一个方案是基于 Operator 的云原生部署。比如用 Presslabs 的 MySQL Operator,它能把主从复制的维护逻辑代码化。我写了一个简单的 YAML 配置,想扩容一个从库,只需要改一个数字,Operator 就会自动拉起 Pod,配置好 GTID 复制,并加入负载均衡。这比我们以前写一堆 Ansible 脚本爽多了。
再聊聊 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 的版本更稳定了,再考虑大规模迁移。毕竟,线上数据库的稳定性,永远比技术的新潮更重要。
去年接了个电商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(组复制)虽然香,但在跨地域部署时,网络抖动能让你怀疑人生。传统主从虽然土,但胜在简单粗暴,好排查。
别光看文章里的命令敲得爽,一定要在本地虚拟机里把主库搞挂一次,试试手动切换主从。纸上谈兵终觉浅,真到了凌晨三点报警响,能救你的不是文档,是你手感。