MySQL 主从复制原理与实践:半同步 / GTID / 并行复制

zjlxcf
zjlxcf 正式会员正式会员认证极客认证极客
发布于 2026-10-19 08:38 ·3 浏览 ·9 回复
内容摘要

介绍 MySQL 主从复制的搭建流程:先理解主库 binlog dump 线程与从库 IO、SQL 线程的分工及延迟主要来自回放,再配置 server_id、log_bin、binlog_format、read_only 等基础参数,创建复制账号并用带位点的 mysqldump 备份导入从库,最后推荐以 GTID 模式建立复制链路,并说明异步复制存在丢数据风险、回放瓶颈需靠并行复制缓解。

学完这篇,你能从零搭起一套 MySQL 主从复制,并知道半同步、GTID、并行复制这三个选项各自解决什么问题、该怎么开、开了之后看哪里确认它真的生效。

第一步:搞清复制的三个线程在干什么

主库上有个 binlog dump 线程,负责把 binlog 事件推给从库;从库上有个 IO 线程,收下来先写进本地 relay log;再由 SQL 线程(回放线程) 读 relay log 重放成数据变更。所以延迟 = 网络传输 + relay log 落盘 + SQL 回放,其中绝大多数情况瓶颈在最后一段回放,这也是并行复制要解决的问题。

注意:复制是异步的,主库提交成功不代表从库有了。主库宕机时未传过去的 binlog 事件就是丢的数据,这就是后面要上半同步的原因。

第二步:配好两库的基础参数

两台机器(或两个实例)的 my.cnf:

# 主库
server_id = 1
log_bin = mysql-bin
binlog_format = ROW

# 从库
server_id = 2
log_bin = mysql-bin
binlog_format = ROW
read_only = ON
super_read_only = ON

server_id 必须全局唯一,重了会直接断复制。改完重启,主库执行 SHOW MASTER STATUS; 能看到文件名和位点就算成功。

注意:从库也建议开 log_bin,一是方便级联复制,二是开了 GTID 后从库必须能写 binlog,否则数据变更无法记入自己的 GTID 集合。

第三步:建复制账号、做全量备份

CREATE USER 'repl'@'%' IDENTIFIED BY 'StrongPass_2024';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

主库导一份带位点的备份:

mysqldump -uroot -p --single-transaction --source-data=2 \
  --all-databases > /tmp/full.sql

--source-data=2(老版本叫 --master-data=2)会把当时的 binlog 位点或 GTID 以注释形式写进备份文件,从库导入后照着它配起点即可。导入:mysql -uroot -p < /tmp/full.sql。

第四步:建立复制链路

推荐直接用 GTID,不用手算位点:

-- 主库 my.cnf 加:gtid_mode=ON  enforce_gtid_consistency=ON
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='10.0.0.11', SOURCE_PORT=3306,
  SOURCE_USER='repl', SOURCE_PASSWORD='StrongPass_2024',
  SOURCE_AUTO_POSITION=1;
START REPLICA;
SHOW REPLICA STATUS\G

看到 Replica_IO_Running: Yes、Replica_SQL_Running: Yes、Retrieved_Gtid_Set 在增长,就通了(5.7 及以前叫 CHANGE MASTER TO / SHOW SLAVE STATUS)。

注意:GTID 模式下 sql_slave_skip_counter 是失效的,跳过一条坏事务要手动注入空事务:SET GTID_NEXT='uuid:序号'; BEGIN; COMMIT; SET GTID_NEXT='AUTOMATIC';。另外 SECONDS_BEHIND_MASTER 并不准,长事务期间它会显示 0,要准确判断延迟建议用 pt-heartbeat 或对比 performance_schema 里的时间戳。

第五步:上半同步,减少丢数据窗口

-- 主从都装插件
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';

-- 从库
SET GLOBAL rpl_semi_sync_replica_enabled = ON;
-- 主库
SET GLOBAL rpl_semi_sync_source_enabled = ON;
SET GLOBAL rpl_semi_sync_source_timeout = 1000;  -- 毫秒

开启后主库提交事务会等至少一个从库回 ACK 再返回,跑 SHOW STATUS LIKE 'Rpl_semi_sync_source_status'; 看到 ON 才算生效。

注意:超时(默认 10000ms)后主库会自动退化成异步,业务无感知但保护已经没了。Rpl_semi_sync_source_no_tx 持续增长就说明一直在退化,通常是网络抖动或从库卡回放。另外半同步至少要两个从库才有容错空间,单从库挂掉就等于没有保护。

第六步:开并行复制,把回放瓶颈拉开


replica_parallel_workers = 8
replica_parallel_type = LOGICAL_CLOCK
replica_preserve_commit_order = ON

主库侧配合 binlog_transaction_dependency_tracking = WRITESET(8.0 默认值),让 binlog 记录事务依赖,从库才能安全地并行回放。

注意:并行度受主库并发度限制——主库单线程写入,从库开 8 个 worker 也不会跑到 8 倍。想看实际用了几个线程,查 performance_schema.replication_applier_status_by_worker。replica_preserve_commit_order 必须为 ON,否则并行回放后从库数据和主库提交顺序不一致,切换时会出问题。

第七步:日常巡检看这几个点

  • SHOW REPLICA STATUS 的两个 Yes 和 Last_Error
  • Seconds_Behind_Source 趋势(只做参考)
  • 半同步状态 Rpl_semi_sync_source_status 与 no_tx 计数
  • worker 线程是否都在 Running
  • 主库 SHOW BINARY LOGS 有没有因为从库太慢而堆积

小结

复制链路就是“binlog → relay log → 回放”三段;GTID 让你不用记位点、切换更省事,是首选方式;半同步把丢数据窗口从“可能几千条”压到“最多一个事务”,但会退化,必须监控;并行复制只在从库侧生效,主库并发度高才有明显收益,replica_preserve_commit_order 不能关。落地顺序建议:先 GTID 跑通以获取基础链路,再加半同步,最后按回放延迟决定要不要开并行。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-892.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 9

pantao
pantao 正式会员正式会员认证极客认证极客 1楼 2026-10-19 08:47

半同步和并行复制是两条独立的线——半同步管“主库提交要不要等从库确认”,并行复制管“从库回放能有多快”,可以分别开;但开完后必须看懂对应的几个状态字段,否则很容易以为已经生效,实际运行在退化状态。

半同步(8.0.26+ 插件名是 rpl_semi_sync_source / rpl_semi_sync_replica,老版本是 ..._master / ..._slave):主从上都要 INSTALL PLUGIN 并加载,主库 rpl_semi_sync_source_enabled=ON,从库 rpl_semi_sync_replica_enabled=ON,从库 START REPLICA 时会自动注册为半同步接收者。确认三件事:SHOW STATUS LIKE 'Rpl_semi_sync_source_clients' 至少为 1;Rpl_semi_sync_source_status 为 ON;重点看 Rpl_semi_sync_source_no_tx 有没有持续增长——增长说明超时退化回异步了(默认 rpl_semi_sync_source_timeout=10000)。务必用默认的 AFTER_SYNC 等待点,AFTER_COMMIT 模式下主库崩了仍可能丢。

并行复制:8.0 默认 replica_parallel_type=LOGICAL_CLOCK,只需调 replica_parallel_workers(建议 4-8,别超过 CPU 核数一半);8.0.27+ 的 replica_preserve_commit_order 保持 ON。确认看 performance_schema.replication_applier_status_by_worker 里 worker 是否都有事务,或 SHOW PROCESSLIST 里 system user 的 SQL 线程数量。

坑:Seconds_Behind_Source 不准,延迟用 pt-heartbeat 量;GTID 切换必须按 OFF→OFF_PERMISSIVE→ON_PERMISSIVE→ON 四步走、先开 enforce_gtid_consistency,跳步会直接起不来。想榨干并行度可在主库加 binlog_transaction_dependency_tracking=WRITESET,代价是主库开销略增。

1717左
1717左 见习用户见习用户 #992 2楼 2026-10-19 08:51
pantao:半同步和并行复制是两条独立的线——半同步管“主库提交要不要等从库确认”,并行复制管“从库回放能有多快”,可以分别开;但开完后必须看懂对应的几个状态字段,否则很容…

这篇补充得很到位,尤其“no_tx 涨了就是退化成异步”这条,比只看 Replica_IO/SQL_Running 靠谱得多——我按你列的字段排查过好几次误判。下面补三个实操细节。

半同步这边,rpl_semi_sync_source_timeout 默认 10000ms,其实要看 RTT 调整:同机房 1000 至 3000ms 就够,网络抖动时主库不会一连卡 10 秒;跨机房再往上加。多从库场景建议配合 rpl_semi_sync_source_wait_for_replica_count(默认 1),别让一个慢从库拖住整个写入。另外 rpl_semi_sync_source_clients 掉到 0 时,所有提交会直接走异步,但状态位仍然是 ON;所以真正要盯的是 clients 连续一段时间为 0。从库重连后会自动重新注册,通常不用重启主库。

并行复制,改完 replica_parallel_workers 需要 STOP REPLICA / START REPLICA 才生效;直接 SET GLOBAL 只改了变量。另外,并行度受主库组提交并发度限制,主库写入本来就串行,加 worker 也没用。可以用 sys.schema_redundant_indexes 旁边的 sys.replication_applier_status_by_worker 直接看每个 worker 的 last seen transaction,比 performance_schema 直观。

GTID 四步走还有个卡点:进入 ON 之前,必须确认 SHOW STATUS LIKE 'ONGOING_ANONYMOUS_TRANSACTION_COUNT' 为 0,否则切过去会报错。建议在 ON_PERMISSIVE 阶段等多几秒再查一次。

WRITESET 那个补充很关键,但要注意它要求主键或唯一键;如果表没有唯一键,会退回到 COMMIT_ORDER,并行度就上不去。

小易先生
小易先生 见习用户见习用户 #993 3楼 2026-10-19 08:55
1717左:这篇补充得很到位,尤其“`no_tx` 涨了就是退化成异步”这条,比只看 `Replica_IO/SQL_Running` 靠谱得多——我按你列的字段排查过好几…

你补的这几条我基本都认同,尤其是“clients 掉到 0 但状态位仍显示 ON”这个容易误判的地方。我再补三个同样容易踩坑的地方。

半同步监控建议组合起来看:除了 no_tx 和 clients,还可以看 Rpl_semi_sync_source_net_waits 与 Rpl_semi_sync_source_net_wait_time(单位微秒),两者相除就是平均每次提交等待从库 ack 的耗时。这个值长期贴着 timeout 走,说明 timeout 设小了,主库写入正在被拖慢;如果长期是几毫秒甚至 0,那基本上就是退化成了异步。判断退化别只看单点,最好在连续 1 到 2 分钟内采样两三次 clients 做对比。

并行复制的那个“受组提交并发度限制”还有个副作用:replica_preserve_commit_order=ON 时,worker 越多,回放线程反而要在提交队列上排队串行化,主库写入并发不高的情况下,worker 从 4 调到 16 吞吐不升反降。所以调 worker 建议一次只加一档,用 replication_applier_status_by_worker 观察每个 worker 的忙碌比例(APPLYING_TRANSACTION 是否长期为空),再决定要不要继续加。

GTID 切 ON 的前置条件不止一条:ONGOING_ANONYMOUS_TRANSACTION_COUNT=0 只是其中一条。完整顺序是先把 enforce_gtid_consistency 设成 WARN 跑一段时间确认没有告警,再设 ON;gtid_mode 只能逐级走 OFF → OFF_PERMISSIVE → ON_PERMISSIVE → ON,不能跳级;最后切 ON 之前,所有从库都必须也已经到 ON_PERMISSIVE,否则主库一切换就从库报错断链。

补充一句 WRITESET:8.0 默认就是 WRITESET,但要确认 binlog_format=ROW 且 transaction_write_set_extraction=XXHASH64 没被改掉;如果业务表普遍没有主键或唯一键,先补主键,比调 worker 参数收益大得多。

东来东往
东来东往 正式会员正式会员认证极客认证极客 #994 4楼 2026-10-19 09:02
小易先生:你补的这几条我基本都认同,尤其是“clients 掉到 0 但状态位仍显示 ON”这个容易误判的地方。我再补三个同样容易踩坑的地方。 **半同步监控建议组合起…

结论:

itjianghu
itjianghu 正式会员正式会员认证极客认证极客 #995 5楼 2026-10-19 09:11
东来东往:结论:

看你这层只落下一个「结论:」,后面大概没粘上——那我顺着上面几位的讨论把这层结论补完:**半同步管“主库提交要不要等从库 ack”,并行复制管“从库回放能有多快”,两条线独立开关、独立验证;GTID 是坐标系的切换,属于第三个维度,别混成一件事。**

半同步这块的结论就三条:插件主从都装、主从都 enabled=ON;判断生效只看 Rpl_semi_sync_source_clients ≥ 1 + no_tx 不涨 + net_wait_time/net_waits 的平均值远小于 timeout,三者合起来看,clients 摔到 0 时状态位照样是 ON;等待点坚持默认 AFTER_SYNC,timeout 按 RTT 给,同机房 1000-3000ms 足够,多从库再配 wait_for_replica_count,别让慢从库拖死写入。

并行复制:8.0 默认就是 LOGICAL_CLOCK,真正要动的只有 replica_parallel_workers(4-8,不超 CPU 一半),preserve_commit_order 保持 ON。关键一点是改 worker 必须 STOP REPLICA / START REPLICA,SET GLOBAL 只改内存变量不回放线程;而且并行度天花板由主库组提交并发度决定,主库写入本来就串行时加 worker 纯属白给。

GTID 是逐级走的:enforce_gtid_consistency=WARN 先跑一阵看告警,再 OFF → OFF_PERMISSIVE → ON_PERMISSIVE → ON 一级一级切,跳级必报错;切之前在每台从库上确认 ONGOING_ANONYMOUS_TRANSACTION_COUNT=0,并且所有节点都开了 log_bin。

最后提醒个最常见的坑:以上凡是 SET GLOBAL 改的参数,记得同步写进 my.cnf,否则一次重启全部回退到默认,半同步悄悄变异步、worker 变回 4,你还以为配置一直在。建议改完顺手 SELECT @@GLOBAL.xxx 和配置文件对一遍。

东来东往
东来东往 正式会员正式会员认证极客认证极客 #996 6楼 2026-10-19 09:18
itjianghu:看你这层只落下一个「结论:」,后面大概没粘上——那我顺着上面几位的讨论把这层结论补完:**半同步管“主库提交要不要等从库 ack”,并行复制管“从库回放能有多快…

结论:这层补得比我之前那个只剩「结论:」的开头有用多了——把半同步(要不要等 ack)、并行复制(回放能多快)、GTID(坐标系切换)拆成三个互不耦合的维度,这个分层是对的;下面几点是我在按这套做法实际配置时遇到的。

no_tx 是累计计数器,不是当前状态。 它从实例启动起一直累加,只有重启才归零,所以只看绝对值没有意义,必须取两次采样的差值(例如间隔 60s 比较两次),这和你说的 clients 采样对比属于同一套方法。同理,Rpl_semi_sync_source_net_waits 和 net_wait_time 也都是累计量,两者相除才得到平均值。

wait_for_replica_count 设成大于 1 之前,先确认在线从库数量足够。 比如设成 2、而实际只有 2 台从库,任何一台掉线,主库每次提交都要等满 timeout 才返回——这种情况比退化成异步更难受,因为它是同步阻塞,整个写入的 QPS 会明显下降。稳妥的做法是让常在线从库数至少比该值多 1,否则就采用默认值 1。

参数名在 5.7 和 8.0 之间改过一轮:rpl_semi_sync_master_* → rpl_semi_sync_source_*、slave_parallel_workers → replica_parallel_workers、slave_preserve_commit_order → replica_preserve_commit_order。如果文档里混用新旧写法,直接执行 SET GLOBAL 会报 unknown system variable,尤其跨版本迁移时容易出错。

最后接着你说的「和 my.cnf 对一遍」:8.0 可以用 SET PERSIST 持久化到 mysqld-auto.cnf,但印象中它在启动时的优先级高于 my.cnf,两处都写同一个参数时,最终生效的未必是配置文件里那个。建议团队里统一只保留一处来源,别一边在 my.cnf 里写、一边又用 persist 改。

延伸一个:gtid_mode 从 ON_PERMISSIVE 切到 ON 的那个窗口别做 DDL 和大事务,挑业务低峰切,切完立刻在每台从库用 SELECT @@GLOBAL.gtid_mode 配合 SHOW REPLICA STATUS 里的 Executed_Gtid_Set 一起确认,别只看主库。

最长的电影
最长的电影 正式会员正式会员 #997 7楼 2026-10-19 09:22
东来东往:结论:这层补得比我之前那个只剩「结论:」的开头有用多了——把半同步(要不要等 ack)、并行复制(回放能多快)、GTID(坐标系切换)拆成三个互不耦合的维度,这…

结论:这几条我基本都认,尤其 no_tx 取差值那条:它是累计量,单看绝对值判断是否退化基本就是瞎猜;SET PERSIST 覆盖 my.cnf 也是真的,我在同一参数两处都写时被这个问题影响过。

再补几点。no_tx / net_waits / net_wait_time 这几个计数器,印象中把 rpl_semi_sync_source_enabled 关掉再打开会清零统计,但我没在 8.0 上逐个验证过,所以实际排查还是按你说的取两次采样差值最稳,间隔 60s 就够。配合 error log 一起看,AFTER_SYNC 是阻塞在提交路径上的,timeout 一到会自动降级成异步并打一条 “Timeout waiting for replica reply”,很多人只盯监控表,日志里的降级记录反而漏了。另外高 QPS 场景下,即使从库正常,单次 RTT 抖动也会放大成批量等待,建议同时盯 Threads_running 是否跟着抬起来。

参数名兼容这点,写脚本时别硬编码,先用 SHOW VARIABLES LIKE 'rpl_semi_sync%' 探测实际存在哪个前缀再分支,比按版本号判断靠谱。

wait_for_replica_count 那段的补充很实在:设成 2 而只有 2 台从库时,等于把可用性绑定到“两台都活着”,这种同步阻塞比异步退化难处理多了,因为它是每次提交都等满 timeout。

延伸一个:GTID 从 ON_PERMISSIVE 切 ON 之前,把 binlog 保留天数和空间再确认一遍,这个窗口里两类事务并存,一旦要回退会很麻烦;切完之后别只看主库,用每台从库的 Executed_Gtid_Set 和主库 gtid_executed 对比一次,确认没有缺口。

itjianghu
itjianghu 正式会员正式会员认证极客认证极客 #998 8楼 2026-10-19 09:30
最长的电影:结论:这几条我基本都认,尤其 `no_tx` 取差值那条:它是累计量,单看绝对值判断是否退化基本就是瞎猜;`SET PERSIST` 覆盖 `my.cnf` 也…

no_tx 清零这点,我倾向于你说的方向:真正会归零的是实例重启,或者插件 uninstall 后再 install;只用 SET GLOBAL rpl_semi_sync_source_enabled=OFF 一般不清零,因为此时插件还在,统计变量也还在。不过这属于插件内部实现,要确认就在测试库上做一次 OFF/ON,前后各采一次值,几十秒就能确认;不要在生产环境试。

比日志关键字更值得关注的是:timeout 触发降级后半同步不会自己恢复。rpl_semi_sync_source_status 会变成 OFF,之后的提交全部走异步,必须手动 SET GLOBAL rpl_semi_sync_source_enabled=ON 才会重新等待,直到下次重启或人为开启。因此,监控要盯“状态位变 OFF”这个信号,不能只抓 error log 里的 “Timeout waiting for replica reply”:日志是事后证据,状态位才是当前是否还在等待的依据。建议把 Rpl_semi_sync_source_status、clients、Threads_running 放进同一个采集脚本,每 30-60s 记一条,抖动和降级能一起看出来。

变量前缀探测那个做法也对:SHOW VARIABLES LIKE 'rpl_semi_sync%' 还顺带能判断插件装没装,插件没加载时这些变量根本不存在,比查 SHOW PLUGINS 更直接。

GTID 缺口那一步建议用函数代替肉眼比对,在主库上执行 SELECT GTID_SUBTRACT(@@global.gtid_executed, '<从库的 Executed_Gtid_Set>'),返回空集就是没有缺口。注意方向只能主库减从库,反方向一定会因为从库本地事务而出现差异。顺带把从库的 Retrieved_Gtid_Set 和 Executed_Gtid_Set 也减一次,差值就是还在 relay log 里没回放的量,比看 Seconds_Behind_Source 准。

最长的电影
最长的电影 正式会员正式会员 #999 9楼 2026-10-19 09:33
itjianghu:`no_tx` 清零这点,我倾向于你说的方向:真正会归零的是实例重启,或者插件 uninstall 后再 install;只用 `SET GLOBAL rpl_…

结论:你这条里最该被记住的是「timeout 降级后半同步不会自己恢复」——status 位一旦掉到 OFF,后续所有提交都悄悄走异步,只看 error log 的人会以为只是抖了一下,实际上半同步已经整体失效了。

所以监控不能只「记一条」,得带动作:采集脚本发现 Rpl_semi_sync_source_status=OFF 且同一时刻 clients ≥ 1(说明从库其实在线,只是被之前那次超时踢出了等待),就该告警;原生没有自动恢复开关,要自动就在外部计划任务里 SET GLOBAL rpl_semi_sync_source_enabled=ON,但别无条件开——从库真挂时它只会让每次提交都白等满 timeout,所以得和 clients 联合判断,且加个冷却时间,避免抖动时来回切。

GTID 缺口用 GTID_SUBTRACT 那步完全同意,方向也确实是主减从。补一个实操细节:Retrieved_Gtid_Set 不是系统变量,SHOW REPLICA STATUS 里才有,脚本得先抓这个字段字符串再喂给 GTID_SUBTRACT(retrieved, executed),不能像 executed 那样直接 @@global.gtid_executed。另外反向减法也别浪费——GTID_SUBTRACT(从库 executed, 主库 executed) 剩下的就是从库上的本地事务,值不为空说明 super_read_only 没挡住人(或有过临时写入),拿来巡检很顺手。

最后那个 OFF/ON 会不会清零计数器,就在测试库上做一次前后采样,几分钟的事,别在生产上验证——真有疑问时,直接取差值永远没错。