尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

数据库高可用与容灾实战(3):半同步复制与无损切换:RPO=0 能不能做到

数据库高可用与容灾实战(3):半同步复制与无损切换:RPO=0 能不能做到 本篇问题上一篇用 GTID 把切换的账算清楚了账能对上最多是知道丢了什么要根本不丢就得改变复制的确认语义——让事务在对客户端宣告提交之前先确保二进制日志在远端活了下来。这就是半同步复制的立身之本也是RPO0这个承诺在 MySQL 体系里的唯一正统路径。但承诺有保质期ACK 等不到会降级、ack 点选错会幽灵提交、本机落盘参数不牢则一切归零。本篇把这三个字面拆开半同步的机制与时延代价、降级窗口的真实大小、以及无损切换还需要哪些配套件。机制把一次提交变成长途旅行半同步semisynchronous replication在主库提交路径上插了一个等待点事务的 binlog 写入并按sync_binlog落盘后主库不立刻做引擎层提交而是等至少rpl_semi_sync_master_wait_for_slave_count个副本的 ACK——副本 IO 线程把事件写进 relay log 并落盘沿链路把收到了回传给主库的 semisync 等待器。等待有超时rpl_semi_sync_master_timeout毫秒超时不是失败而是放行并整体退回异步直到有 ACK 再次到达才恢复半同步。8.0 时代 AFTER_SYNC 是唯一模式AFTER_COMMIT 被废弃正是因为 AFTER_COMMIT 的 ack 点在引擎提交之后主库回滚时副本已收到会产生客户端看到回滚、从库却存在的幽灵事务——这个坑在后面的持久性矩阵里还会再见。代价是三重的。其一提交路径多出至少一个主库落盘 网络往返 从库落盘 网络返程同机房约 1~4 毫秒跨城按 RTT 成倍放大组提交会把同一批事务的等待合并摊薄但摊不掉。其二等待占住连接与会话ACK 抖动直接表现为业务提交毛刺。其三保护强度取决于谁在 ACK只要 ACK 来源失联整条链就退回异步而这恰恰是最需要保护的时刻。实验一降级是无声的丢的是降级之后的全部下面模拟 1000 txn/s 的稳定提交流前半段半同步健康10 秒时唯一 ACK 副本失联13 秒时主库整机报废。看三个阶段——正常期的提交时延、超时降级瞬间、报废时刻的未保护集合。RATE1000.0# 业务提交速率 txn/sRTT0.0015# 机房内单程网络时延(秒)RELAY_FSYNC0.0004# 从库 IO 线程 relay log 落盘BINLOG_FSYNC0.0002# 主库 binlog 落盘(sync_binlog1)TIMEOUT0.1# rpl_semi_sync_master_timeout 秒T_DIE10.0# 从库此刻失联T_CRASH13.0# 主库此刻整机报废ACK_ROUNDBINLOG_FSYNCRTTRELAY_FSYNCRTT# AFTER_SYNC 一轮确认rows[]semi_onTrueforiinrange(int(RATE*T_CRASH)):arrivei/RATEifsemi_onandarriveACK_ROUNDT_DIE:# 正常半同步: 提交前拿到 ackdone,ackedarriveACK_ROUND,Trueelifsemi_onandarriveT_DIETIMEOUT:# 失联瞬间在途的事务: 一起卡到超时done,ackedT_DIETIMEOUTBINLOG_FSYNC,Falsesemi_onFalse# 超时触发, 半同步整体关闭else:# 降级为异步done,ackedarriveBINLOG_FSYNC,Falserows.append((arrive,done,acked))print(异步提交往返 %.4fs; 半同步 AFTER_SYNC 提交往返 %.4fs (放大 %.0f 倍)%(BINLOG_FSYNC,ACK_ROUND,ACK_ROUND/BINLOG_FSYNC))acked[rforrinrowsifr[2]]blocked[rforrinrowsifnotr[2]andr[0]T_DIETIMEOUT]asyncd[rforrinrowsifnotr[2]andr[0]T_DIETIMEOUT]print(%.1fs 从库失联: %.3fs 起第一批卡满 %.1fs 超时, 半同步 - OFF%(T_DIE,blocked[0][1],TIMEOUT))print(此后 %d 笔以异步延迟 %.4fs 提交 —— 业务无感, 但已不受保护%(len(asyncd),BINLOG_FSYNC))lostlen(blocked)len(asyncd)print(%.1fs 主库报废: 已 ack %d 笔安全; 未受保护 %d 笔 - 实际 RPO %.2f 秒写入量%(T_CRASH,len(acked),lost,lost/RATE))BS_ROUNDBINLOG_FSYNC2*0.0008# binlog server 独立落盘回 ackbs_ok[rforrinrowsifr[0]BS_ROUNDT_CRASH]print(\n加一台 binlog server(第三接收方, 确认往返 %.4fs):%BS_ROUND)print( 主库报废瞬间, %d/%d 笔已双写持久化, 在途仅 %d 笔%(len(bs_ok),len(rows),len(rows)-len(bs_ok)))print( 切换流程: 先等 binlog server 收口 - 把新主缺的 GTID 段补放 - 集群 RPO 回到 0)print( 注意: AFTER_SYNC 的 ack 点是 relay log 落盘, 从库自己的 InnoDB 还没跑完, 读旧快照需另行处理)运行输出异步提交往返 0.0002s; 半同步 AFTER_SYNC 提交往返 0.0036s (放大 18 倍) 10.0s 从库失联: 10.100s 起第一批卡满 0.1s 超时, 半同步 - OFF 此后 2899 笔以异步延迟 0.0002s 提交 —— 业务无感, 但已不受保护 13.0s 主库报废: 已 ack 9997 笔安全; 未受保护 3003 笔 - 实际 RPO 3.00 秒写入量 加一台 binlog server(第三接收方, 确认往返 0.0018s): 主库报废瞬间, 12999/13000 笔已双写持久化, 在途仅 1 笔 切换流程: 先等 binlog server 收口 - 把新主缺的 GTID 段补放 - 集群 RPO 回到 0 注意: AFTER_SYNC 的 ack 点是 relay log 落盘, 从库自己的 InnoDB 还没跑完, 读旧快照需另行处理三个结论。第一半同步不是免费的同机房一次确认往返 3.6 毫秒是异步路径的 18 倍跨城部署时这个数字要按 RTT 重新算一遍账。第二降级是无声的超时后半同步状态变 OFF提交延迟立刻回到 0.2 毫秒业务监控看不出任何异常但从此每一笔提交都在裸奔——本例 3 秒的裸奔窗口就是 3 秒的 RPO。所以Rpl_semi_sync_master_status8.0.24 起为Rpl_semi_sync_source_status变 OFF 必须按 P1 级告警处理而不是仪表盘上的一个绿灯装饰。第三把 ACK 来源从某台从库换成专职 binlog server / 第二机房接收方后同一灾难场景的在途未确认事务只剩 1 笔RPO 的大小不取决于你开了半同步而取决于 ACK 背后有几个独立的持久化落点。实验二先保证自己家不着火——本地持久性矩阵半同步只解决binlog 送到别处这一半另一半是主库自己宣告提交的事务重启后必须还在。事务活着同时依赖两份日志InnoDB redo 与 binlog两把fsync旋钮innodb_flush_log_at_trx_commit与sync_binlog组成九宫格。下面把断电与进程崩溃两种灾难分别扫一遍。RATE1000.0# 业务提交速率 txn/sOS_BINLOG_LOSS30.0# sync_binlog0 时 binlog 留在 OS 缓存的保守损失窗口(秒)defredo_win(mode,crash):ifmode1:return0.0# 每次提交 fsync redoifmode2:return1.0ifcrash断电else0.0# 提交时写入 OS 文件不 fsync: 只惧断电return1.0# mode 0: redo 每秒刷盘, 进程/断电都丢约 1sdefbinlog_win(n):return0.0ifn1else(OS_BINLOG_LOSSifn0elsen/RATE)print(一笔事务要算已提交, binlog 与 redo 必须同时活过灾难: 损失窗口max(两者))forcrashin(断电,mysqld 进程崩溃):print(\n[%s]%crash)forsbin(1,100,0):fortrxin(1,2,0):r,bredo_win(trx,crash),binlog_win(sb)lostmax(r,b)divergerb# binlog 比 redo 活得久: 副本可能已有、主库将回滚tag -- 本地 RPO0 的唯一组合iflost0andnotdivergeelseprint( sync_binlog%-3d redo_per_commit%d - 丢至多 %4.1fs 流量; 主从分叉风险: %s%s%(sb,trx,lost,有(binlog 领先 redo)ifdivergeelse无,tag))print(\n推论:)print( 1) 1/1 只是本地 RPO0的入场券, 机房整体报废仍要靠复制把 binlog 送到别处)print( 2) sync_binlog1 redo2 是隐蔽炸弹: 半同步 AFTER_SYNC 在 binlog 落盘后即 ack,)print( 断电后主库回滚这些事务, 副本却已收到 - 比丢数据更糟的幽灵提交)print( 3) 从库侧同理: relay log / binlog 与 InnoDB 的落盘策略决定ack 了是否真的在)运行输出一笔事务要算已提交, binlog 与 redo 必须同时活过灾难: 损失窗口max(两者) [断电] sync_binlog1 redo_per_commit1 - 丢至多 0.0s 流量; 主从分叉风险: 无 -- 本地 RPO0 的唯一组合 sync_binlog1 redo_per_commit2 - 丢至多 1.0s 流量; 主从分叉风险: 有(binlog 领先 redo) sync_binlog1 redo_per_commit0 - 丢至多 1.0s 流量; 主从分叉风险: 有(binlog 领先 redo) sync_binlog100 redo_per_commit1 - 丢至多 0.1s 流量; 主从分叉风险: 无 sync_binlog100 redo_per_commit2 - 丢至多 1.0s 流量; 主从分叉风险: 有(binlog 领先 redo) sync_binlog100 redo_per_commit0 - 丢至多 1.0s 流量; 主从分叉风险: 有(binlog 领先 redo) sync_binlog0 redo_per_commit1 - 丢至多 30.0s 流量; 主从分叉风险: 无 sync_binlog0 redo_per_commit2 - 丢至多 30.0s 流量; 主从分叉风险: 无 sync_binlog0 redo_per_commit0 - 丢至多 30.0s 流量; 主从分叉风险: 无 [mysqld 进程崩溃] sync_binlog1 redo_per_commit1 - 丢至多 0.0s 流量; 主从分叉风险: 无 -- 本地 RPO0 的唯一组合 sync_binlog1 redo_per_commit2 - 丢至多 0.0s 流量; 主从分叉风险: 无 -- 本地 RPO0 的唯一组合 sync_binlog1 redo_per_commit0 - 丢至多 1.0s 流量; 主从分叉风险: 有(binlog 领先 redo) sync_binlog100 redo_per_commit1 - 丢至多 0.1s 流量; 主从分叉风险: 无 sync_binlog100 redo_per_commit2 - 丢至多 0.1s 流量; 主从分叉风险: 无 sync_binlog100 redo_per_commit0 - 丢至多 1.0s 流量; 主从分叉风险: 有(binlog 领先 redo) sync_binlog0 redo_per_commit1 - 丢至多 30.0s 流量; 主从分叉风险: 无 sync_binlog0 redo_per_commit2 - 丢至多 30.0s 流量; 主从分叉风险: 无 sync_binlog0 redo_per_commit0 - 丢至多 30.0s 流量; 主从分叉风险: 无 推论: 1) 1/1 只是本地 RPO0的入场券, 机房整体报废仍要靠复制把 binlog 送到别处 2) sync_binlog1 redo2 是隐蔽炸弹: 半同步 AFTER_SYNC 在 binlog 落盘后即 ack, 断电后主库回滚这些事务, 副本却已收到 - 比丢数据更糟的幽灵提交 3) 从库侧同理: relay log / binlog 与 InnoDB 的落盘策略决定ack 了是否真的在矩阵里最扎眼的是第二行sync_binlog1配redo2在断电下既丢 1 秒又制造分叉——binlog 活得比 redo 久副本或 binlog server拿着主库准备回滚的事务当宝贝。工程上还要补一句从库被提升的那一刻它的gtid_executed只能代表relay log 收到了SQL 线程还没回放的部分要靠新主自己追若从库也配了log_slave_updates sync_binlog1这些事务才算真正双落盘。无损切换从来不是单个参数的胜利而是主库、链路、从库三段持久性的短板效应。那到底能不能承诺 RPO0把上面的实验拼成一句话在至少两个独立持久化落点 提交路径等待多数确认 任一落点故障可从另一落点继续同时成立时RPO0 才成立。半同步 binlog server 是工程上的近似解把确认语义升级为多数派Paxos/Raft 类共识如 Group Replication/XPaxos 或外部共识存储是更严格的解代价是提交时延对最慢多数成员敏感、且需要仲裁数3/5 节点、跨 AZ 部署。用单台异步从库配秒级切换来宣称 RPO0 的是把平均值当成了上界。常见陷阱与落地清单只看Rpl_semi_sync_master_status不看Rpl_semi_sync_master_no_tx / yes_tx分不清一直健康与频繁降级又恢复。rpl_semi_sync_master_timeout设成默认 10 秒降级窗口白送 10 秒数据风险宁可设小如 1 秒并配告警也别让它默默吞掉承诺。从库read_only没配super_read_onlyDBA 手工写进从库的一笔既成游离事务又破坏 ack 语义上一篇的账本问题复发。rpl_semi_sync_master_wait_for_slave_count大于存活副本数永远等不到 ACK全量超时降级等于花钱买了个异步。把半同步当备份ACK 保证的是对方收到了不是历史可以回放误操作照样瞬间复制到所有节点备份体系见第 6、7 篇。跨城链路上开半同步不加压测RTT 20ms 时提交时延放大到 2 万微秒级先测业务 P99 再决定wait_count和机房布局。半同步把丢数据的窗口压缩到降级发生的那一瞬间但要让这个瞬间尽可能晚、且在发生时被自动处置就得有一套会探测、会选主、会执行切换的自动化系统。下一篇《数据库高可用与容灾实战4MHA 与 Orchestrator故障探测、选主与自动切换》讲这套自动驾驶怎么装、又怎么防止它发疯。参考来源MySQL 8.0 Reference ManualSemisynchronous Replicationhttps://dev.mysql.com/doc/refman/8.0/en/semisynchronous-replication.htmlMySQL 8.0 Reference ManualBinary Log Options and Variableshttps://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.htmlMySQL 8.0 Reference ManualInnoDB Startup Options and System Variableshttps://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.htmlMySQL 8.0 Reference ManualMySQL Group Replicationhttps://dev.mysql.com/doc/refman/8.0/en/group-replication.htmlWikipediaQuorum (distributed computing)https://en.wikipedia.org/wiki/Quorum_(distributed_computing)
返回列表