美国服务器 MySQL 跨洋复制延迟 300 秒:一份国内机房的 sysctl,把 150ms RTT 的 BDP 焊死在 42KB

一、现场

主库:美国洛杉矶裸金属,MySQL 8.0.34,2TB NVMe,跑核心订单库,平均 QPS 3000,日常 binlog 写入 1.5MB/s。

从库:新加坡,同版本同配置,纯只读 + 报表盘点。

链路:公网复制,mtr 常年 152~158ms RTT,0% 丢包,路径抖动小于 5ms。

现象很奇怪:Seconds_Behind_Master 平时老老实实贴着 0,只要主库那边跑一次归档批处理,就会一路飙到 240~300 秒,批处理结束后还得磨蹭十几分钟才回落到 0,然后下一个整点再来一遍。

这套从库跑在轻云互联的新加坡节点上,跟洛杉矶主库之间的公网 RTT 一天 24 小时画出来基本是一条直线。网络没问题,那就只能往自己身上找。

二、先切清楚是"拉"慢还是"放"慢

SHOW REPLICA STATUS\G

只看三个坐标,别的先不看:

  • Master_Log_File / Read_Master_Log_Pos → I/O 线程拉到哪了
  • Relay_Master_Log_File / Exec_Master_Log_Pos → SQL 线程回放到哪了
  • Relay_Log_Space → relay log 里积压了多少

抓延迟最高那一刻的快照,结论非常干脆:Exec_Master_Log_Pos 几乎贴着 Read_Master_Log_Pos,两者只差几百字节;而 Read_Master_Log_Pos 距离主库当前的 binlog 位点差了整整 70MB。

也就是说 SQL 线程根本没压力,I/O 线程拉不动。relay log 里根本没积压,是无米下锅。

顺手排掉报错重连:

SELECT SERVICE_STATE, LAST_ERROR_NUMBER, LAST_ERROR_MESSAGE,
       SOURCE_LOG_FILE, READ_SOURCE_LOG_POS, COUNT_TRANSACTIONS_RETRIES
FROM performance_schema.replication_connection_status\G

没错误、没重试、没重连。I/O 线程一直在 ON,但就是慢。

三、往 socket 里看

复制连接是长连接,问题大概率在 TCP 层。在从库上对着主库端口抓:

ss -tinm dst <master_ip>:3306
ESTAB 0 0 10.10.2.31:49216 203.0.113.7:3306
     skmem:(r40960,rb87380,t0,tb87040,f0,w0,o0,bl0,d0)
     cubic wscale:7,7 rto:301 rtt:152.74/0.86 ato:40 mss:1448 pmtu:1500
     rcvmss:1448 advmss:1448 cwnd:589 ssthresh:1023
     retrans:0/0 rcv_space:1456

两个数字立刻扎眼:rb87380,接收缓冲区上限 85KB;cwnd:589,拥塞窗口已经涨到 589×1448≈852KB。

cwnd 想给的带宽是 852KB/0.1527s ≈ 5.5MB/s,但接收缓冲区只有 85KB,而且 tcp_adv_win_scale 默认是 1,意味着内核只会把其中一半用作通告窗口:

有效接收窗口 = 87380 × (1 - 1/2^1) = 42690 B
理论吞吐上限 = 42690 / 0.1527 ≈ 279 KB/s

实际跑的吞吐量也确实是 260~280KB/s 这个量级,跟 cwnd 一点关系都没有了——cwnd 再大也被通告窗口锁死

查一下这个值是谁设的:

sysctl net.core.rmem_max net.ipv4.tcp_rmem net.ipv4.tcp_adv_win_scale net.ipv4.tcp_moderate_rcvbuf
net.core.rmem_max = 87380
net.ipv4.tcp_rmem = 4096	87380	87380
net.ipv4.tcp_adv_win_scale = 1
net.ipv4.tcp_moderate_rcvbuf = 1

破案了。net.core.rmem_max=87380 把自动调优的天花板直接焊死,tcp_rmem 第三位也是 87380。这套 sysctl 是从国内同城机房那套模板原封不动抄过来的——在那个 RTT 0.3ms 的环境里,85KB 窗口对应的吞吐是 280MB/s,绰绰有余;搬到了跨太平洋 152ms 的链路上,它直接把单连接吞吐压到了 279KB/s

顺带算一下 BDP 有多离谱

BDP = 152.7ms × 100Mbps ÷ 8 = 1.9 MB   (这才刚够 100Mbps)
BDP = 152.7ms × 1Gbps ÷ 8  = 19.1 MB   (想跑满 1Gbps 得 19MB 窗口)

我们给了 42KB。差了整整三个数量级。

过程中还顺手抓到一个次要问题

采样脚本跑着跑着发现,cwnd 在主库批量任务间歇期会莫名其妙掉回 10:

while true; do
  ss -tinm dst <master_ip>:3306 | grep -oP 'cwnd:\d+' | tail -1
  sleep 2
done

net.ipv4.tcp_slow_start_after_idle 默认是 1,空闲超过一个 RTO(这里约 300ms)就把 cwnd 重置回 10,突发流量得重新慢启动。

但我要说清楚:这不是本次的主因。因为接收窗口卡在 42KB,慢启动两个 RTT 就撞到天花板了,重启慢启动的代价微乎其微。它只是个排错过程中的噪声,别被带偏。

四、算一下账,验证因果

  • 批量归档在主库侧产生约 70MB 增量 binlog
  • 从库以 279KB/s 拉:70 × 1024 ÷ 279 ≈ 257 秒
  • 实测 SBM 峰值 248 秒,量级完全吻合

因果链闭合。这也解释了为什么"批处理结束十几分钟才恢复"——积压一直是靠网络慢慢啃掉的,跟 SQL 回放能力一点关系没有。

五、修复

1. 撕掉那份抄来的 sysctl

# /etc/sysctl.d/99-mysql-repl-net.conf

# 按 1Gbps × 152ms 的 BDP 留出 2 倍余量
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432

net.ipv4.tcp_rmem = 4096 1048576 33554432
net.ipv4.tcp_wmem = 4096 1048576 33554432

# 接收窗口不再打对折,rcvbuf 的 3/4 参与通告窗口
net.ipv4.tcp_adv_win_scale = 2

# 长肥管道上,空闲后别把 cwnd 拍回 10
net.ipv4.tcp_slow_start_after_idle = 0

net.ipv4.tcp_moderate_rcvbuf = 1
sysctl --system

改完必须把复制 I/O 线程重启一次,现有的 socket 不会自己吃到新参数:

STOP REPLICA IO_THREAD;
START REPLICA IO_THREAD;

2. 顺手用压缩削掉一半字节

主库 8.0.20 以上,直接给 binlog 上 zstd 压缩,跨洋链路上省下的字节是真金白银:

SET PERSIST binlog_transaction_compression = ON;
SET PERSIST binlog_transaction_compression_level_zstd = 3;

订单类文本日志压缩比普遍在 3~5 倍,等于把 70MB 的突发量直接砍到 20MB 上下。代价是主库多花一点 CPU,这笔账在跨洋场景下闭着眼都划算。

3. 复查

ss -tinm dst <master_ip>:3306
skmem:(r425984,rb6291456,t0,tb87040,...)
     cubic wscale:7,7 rto:301 rtt:152.71/0.42 mss:1448
     cwnd:2145 ssthresh:4096 retrans:0/0

rb 从 87380 涨到 6291456,接收窗口从 42KB 推到 4.7MB,理论吞吐上限干到 30MB/s 以上。同样的批量归档再跑一次,SBM 峰值 8 秒,5 秒内归零。

六、把这类坑做成监控

别等着下一次再翻车。复制 socket 的窗口和 cwnd 完全能采出来做曲线:

#!/bin/bash
# /usr/local/bin/repl_sock_sample.sh
MASTER_IP="$1"
TS=$(date +%s)
LINE=$(ss -tinm dst ${MASTER_IP}:3306 | tr -d '\n' | grep -oP 'rb\d+.*?cwnd:\d+')
echo "${TS} ${LINE}" >> /var/log/repl_sock.log

告警规则很简单:当 RTT > 80ms 且接收窗口计算出的吞吐上限低于主库当前 binlog 写入速率时,直接告警。这比盯着 Seconds_Behind_Master 事后救火强太多——延迟涨起来的时候,业务已经在读脏数据了。

七、最后说两句

国内机房那套 sysctl「一键优化」模板,在 RTT 0.3ms 的环境里跑十年都不会出事,因为那时候 85KB 窗口 ≈ 280MB/s,压根不是瓶颈。但美国服务器、新加坡节点这类跨洋部署,RTT 一旦上了 100ms,同一份配置就是一把钝刀子:它不报错、不丢包、不影响任何健康检查,只是把单连接吞吐悄悄削到 1/100。

跨洋机器上,任何从国内机房平移过来的内核参数,都值得按 BDP = RTT × 带宽 重新算一遍。这一步省不了。