香港CN2服务器跑MySQL:InnoDB参数一个没动,只改TCP initcwnd和认证插件,跨境点查P99从212ms压到68ms
先说结论,省得你往下翻
我们有一批业务部署在香港,应用侧在深圳、上海、北京三地。MySQL 8.0.34 单机跑在香港 CN2 机器上,8C16G + NVMe。有段时间北京那边的接口超时告警天天响,我一开始也怀疑是 InnoDB 的问题——buffer pool 不够、redo 太小、刷盘策略不对,这些都是刻板印象。
后来 sysbench 一压就发现方向全错了:这台机器的 InnoDB 一点毛病没有,同机房客户端打上来 oltp_point_select 能跑 4.2 万 QPS,P99 0.4ms。所有损耗全部发生在数据包落地之前。跨境的 RTT 税,才是香港 CN2 服务器跑 MySQL 时真正的大头。
三个数字先摆出来,北京客户端(实测 RTT 48ms)跑同一套 oltp_point_select:
- 改之前:QPS 12000,P99 212ms,连接数峰值 1800
- 只做连接池 + 内核参数 + 认证插件调整(InnoDB 一个参数没碰):QPS 39000,P99 68ms,连接数稳定在 120
- 再把 initcwnd 从 10 拉到 44:点查 P99 不变,但间隔型报表查询从 1.9s 掉到 0.7s
下面全是实测过程,能直接抄。
一、测试床
机器是轻云互联的香港 CN2 GIA 机型,8C16G,NVMe 系统盘,回程走 59.43 段。选它就是因为线路稳定、丢包低,做 RTT 相关的对照实验时变量少——如果线路本身在抖,测出来的数字全是噪声。
# 三地客户端 ping 实测(各 200 次取 P50)
深圳 10.9.xx.xx rtt min/avg/max/mdev = 11.8/12.3/19.4/1.2 ms
上海 10.9.xx.xx rtt min/avg/max/mdev = 31.6/32.4/45.1/2.8 ms
北京 10.9.xx.xx rtt min/avg/max/mdev = 47.2/48.6/71.3/4.5 ms
MySQL 侧硬件和配置(改之前):
[mysqld]
innodb_buffer_pool_size = 10G
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
max_connections = 2000
thread_cache_size = 9 # 默认值,没人改过
back_log = 151 # 默认值
wait_timeout = 28800 # 默认值
net_write_timeout = 60
tls_version = TLSv1.2,TLSv1.3
require_secure_transport = ON
注意最后一行,require_secure_transport = ON。绝大多数合规要求下的跨境链路都会开 TLS,这个开关直接决定了后面要算几个 RTT。
二、第一刀:抓包看一次冷连接到底要几个 RTT
不看不知道。客户端侧执行:
tcpdump -i any -s 0 -w /tmp/conn.pcap 'tcp port 3306 and host 203.0.113.10'
然后用 tshark 按帧间隔打出来,只看新建连接的时序:
tshark -r /tmp/conn.pcap -Y 'tcp.flags.syn==1 || mysql || tls.handshake' \
-T fields -e frame.number -e frame.time_relative -e _ws.col.Info | head -30
一次完整冷连接的时间线摊开是这样:
- RTT 1:SYN → SYN/ACK(TCP 三次握手,服务端 greeting 包搭在 SYN/ACK 后面立刻发出,不额外耗 RTT)
- RTT 2:TLS 1.3 完整握手(如果服务端没开 session cache,每次都是完整握手)
- RTT 3:MySQL HandshakeResponse,客户端带用户名 + auth data
- RTT 4~5:
caching_sha2_password走 full authentication——服务端回AuthMoreData(0x04),客户端要额外请求 RSA 公钥、再发加密密码,两次往返
合计 4~5 个 RTT。北京 48ms × 4.4 ≈ 211ms,和实测 P99 212ms 对上了。
而 MySQL 侧的开销呢?SHOW STATUS LIKE 'Connections' 对一下 QPS,mysqld 每秒 accept 上千次,但 CPU 占用不到 15%。也就是说这 212ms 里有 190ms 是纯粹在等光信号来回跑。
三、把 4~5 个 RTT 压回去的三件事
3.1 连接池保活,把建连次数干到接近零
这是收益最大的一条,也是最不需要讲道理的。HikariCP 两个参数必须显式配,别用默认:
# 应用侧
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=20
spring.datasource.hikari.keepalive-time=60000 # 关键,默认 0 即关闭
spring.datasource.hikari.max-lifetime=600000
spring.datasource.hikari.connection-timeout=3000
spring.datasource.hikari.idle-timeout=0 # 不要让它回收
# JDBC URL 加 keepAlive
jdbc:mysql://hk-mysql:3306/biz?useSSL=true&tcpKeepAlive=true&socketTimeout=30000&connectTimeout=3000
keepalive-time 默认是 0(关闭),很多人不知道。不配这个,连接在池里躺 5 分钟就会被中间链路的 NAT 会话表清掉,然后业务侧突然一片 Communications link failure。后面第五节专门讲这个。
3.2 TLS session 复用,省掉 1 个 RTT
MySQL 8.0 的 tls_session_cache_size 默认值一直是 0(等于不复用),这是个隐藏很深的坑。开上:
[mysqld]
tls_session_cache_size = 32M
tls_session_cache_timeout = 3600
改完 systemctl restart mysqld,抓包确认第二次连接走的是 Client Hello + ChangeCipherSpec 的 PSK 恢复路径,TLS 握手从 1 RTT 掉到 0 RTT。北京侧冷连接直接省 48ms。
验证方式:
SHOW STATUS LIKE 'Ssl_session_cache%';
# Ssl_session_cache_hits 要能看到增长,一直是 0 说明没生效
3.3 caching_sha2_password:那个 2 RTT 的坑
MySQL 8.0 默认认证插件是 caching_sha2_password。它有一个服务端缓存,命中缓存时走 fast auth(1 RTT),未命中时走 full auth——客户端必须先请求 RSA 公钥,再用它加密密码发过去,白白多两个 RTT。
问题在于这个缓存的失效时机很刁钻:FLUSH PRIVILEGES、ALTER USER、mysqld 重启,都会清空。跨境链路上一次 full auth 就是 96ms(北京),如果你刚好在做账号轮换,那一波请求全部吃这个延迟。
三个应对层次,按推荐度排:
- 首选:连接池预热。应用启动时把
minimum-idle条连接全部建出来,让 full auth 只发生在启动阶段。 - 次选:客户端侧配置
serverPublicKey(JDBC 用allowPublicKeyRetrieval=true是下策,有中间人风险),或者把公钥文件放到客户端本地用--server-public-key-path指定,省掉请求公钥那 1 个 RTT。 - 无奈之举:改回
mysql_native_password。注意 8.0.34 起这个插件已经被标记为 deprecated,从 8.4 开始默认禁用,只能在 8.0 上做过渡。
ALTER USER 'biz'@'%' IDENTIFIED WITH mysql_native_password BY '***';
-- 只对确认无法改连接池的老系统做,改完记得在 8.4 升级前迁走
四、initcwnd:单流吞吐的真正天花板
这一节跟点查 P99 没关系,但跟"报表类查询、大结果集拉取、mysqldump 备份"关系巨大。
Linux 默认 initcwnd = 10,配合默认的 tcp_slow_start_after_idle = 1,意味着任何空闲超过 RTO 的 TCP 连接,拥塞窗口会被重置回 10 个 MSS,下次传输重新慢启动。
这个场景在跨境 MySQL 上太常见了:定时任务每 30 秒拉一次数据、报表页面用户手动刷新、监控轮询。每次都是"连接是热的,但窗口是冷的"。
实测:北京客户端,每次拉 2MB 结果集,两次查询间隔 10 秒。
# 默认参数
time mysql -h hk-mysql -e "SELECT * FROM t_report WHERE dt='2026-10-01'" > /dev/null
real 0m1.912s
# 调完
time mysql -h hk-mysql -e "SELECT * FROM t_report WHERE dt='2026-10-01'" > /dev/null
real 0m0.708s
改的就是这两行(服务端和客户端两侧都要改,因为数据是双向的):
# 1. 关掉空闲后重置窗口
sysctl -w net.ipv4.tcp_slow_start_after_idle=0
echo 'net.ipv4.tcp_slow_start_after_idle = 0' >> /etc/sysctl.d/99-mysql-net.conf
# 2. 提高初始窗口到 44 个 MSS(大约对应 64KB)
ip route change default via 172.17.0.1 dev eth0 initcwnd 44 initrwnd 44
ip route show default | grep initcwnd # 必须能看到 initcwnd 44 才算生效
踩过的坑:ip route change 是运行时生效的,机器重启或者 NetworkManager 重连网卡就丢了。要么写 NetworkManager dispatcher 脚本,要么放 /etc/networkd-dispatcher/routable.d/,别指望 sysctl.conf 能管这个——它管不了路由属性。
验证窗口的姿势,别看 netstat,看 ss:
ss -tin dst 203.0.113.10:3306
# 关注这几个字段:
# cwnd:10 ← 传给外面的是 10,说明 initcwnd 没生效或者被重置了
# cwnd:44
# ssthresh:
# rtt:48.2/2.1 ← 后面的 mdev 抖动值,跨境大于 10 就要考虑线路了
顺带算一下 BDP,别调了个寂寞。北京 RTT 48ms,如果你的目标是单流跑 200Mbps:
BDP = 200,000,000 bit/s × 0.048s / 8 = 1.2 MB
# 检查接收窗口上限够不够
sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem
# 默认 4096 131072 6291456,6MB 上限对 1.2MB 的 BDP 是够的
# 但要跑 1Gbps 单流:BDP = 6MB,刚好卡在默认上限,必须调大
五、NAT 老化:wait_timeout 28800 秒是个假象
这一条是我认为跨境 MySQL 最容易翻车的地方,而且报错信息完全不指向根因。
现象:凌晨低峰期或者业务空闲时段,应用日志里零星出现 The last packet successfully received from the server was 300,012 milliseconds ago。重启应用就好了,第二天又来。
根因:MySQL 的 wait_timeout 默认 28800 秒(8 小时),连接池认为连接是活的。但中间任何一跳的设备——云厂商的 NAT 网关、防火墙、负载均衡——它的会话表老化时间是分钟级的。NAT 把会话表项删了,但 MySQL 和客户端都不知道,因为 TCP 连接本身没有 FIN/RST 交互。下一次发包过去,NAT 直接丢或者回 RST。
所以真正要调的不是 wait_timeout,是 TCP keepalive 探测:
# 全局缩短 keepalive 探测,跨境内网 60 秒一次足够
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 6
# 让新连接带上 SO_KEEPALIVE,不用应用自己设
# MySQL 客户端侧在 JDBC URL 里加 tcpKeepAlive=true 即可
同时把 MySQL 侧的 wait_timeout 往下调到比 NAT 老化时间短,让服务端主动清:
[mysqld]
wait_timeout = 600
interactive_timeout = 600
net_write_timeout = 120 # 跨境大结果集传输,60 秒容易不够
net_read_timeout = 120
配完用这个命令在服务端持续观察,看 Aborted_clients 是不是还在涨:
mysqladmin ext -i5 -c20 | grep -E 'Aborted_clients|Threads_connected'
如果 Aborted_clients 还在涨,说明有客户端在连接被服务端 kill 时没有正常关闭(比如进程被 kill -9),这个是指标,不是 bug。
六、服务端还得补两刀
thread_cache_size = 9 这个默认值在跨境短连接场景下是灾难。mysql 每处理一个连接要 pthread_create,跨境链路下建连慢,线程堆积更快。
[mysqld]
thread_cache_size = 64 # 至少等于并发峰值的 1/8
back_log = 512 # 默认 151,跨境 SYN 到 accept 的延迟会撑爆它
max_connections = 2000
table_open_cache = 4000
innodb_buffer_pool_size = 10G # 这个反而是唯一没动过的
怎么定 thread_cache_size?看这两个指标的差值,差值持续大于 0 就说明缓存不够:
SHOW GLOBAL STATUS LIKE 'Threads_created';
SHOW GLOBAL STATUS LIKE 'Connections';
-- Threads_created / Connections 的比值要压到 0.01 以下
七、完整对照表
三地客户端,sysbench 1.0.20 oltp_point_select,64 表 × 1000 万行,并发 64,跑 10 分钟:
- 深圳 12ms / 基线:QPS 31000,P99 68ms,峰值连接 1800
- 深圳 12ms / 优化后:QPS 41000,P99 20ms,连接 120
- 上海 32ms / 基线:QPS 19500,P99 148ms,峰值连接 1800
- 上海 32ms / 优化后:QPS 35000,P99 41ms,连接 120
- 北京 48ms / 基线:QPS 12000,P99 212ms,峰值连接 1800
- 北京 48ms / 优化后:QPS 39000,P99 68ms,连接 120
- 同机房 0.1ms / 参照:QPS 42000,P99 0.4ms
北京从 12000 到 39000,翻了 3.25 倍,而同机房上限只有 42000——也就是说优化之后,跨境的损耗只剩 7%,剩下 93% 的性能你本来就买到了。
注意深圳优化后 QPS 41000 反而略低于北京 39000……不,是高于。但深圳 P99 20ms 是因为 RTT 只有 12ms,一次往返就是 12ms,加上应用侧处理。这个数字基本贴着物理下限,再优化也压不下去了,除非把 DB 搬到本地。
八、什么时候该上 ProxySQL
上面的方案有个前提:应用能改连接池配置。如果是老旧系统、或者一堆小服务各自连 MySQL,改不过来,那就在大陆侧放一层 ProxySQL。
我们实测过一个 2C2G 的轻量机型部署 ProxySQL,放在深圳,后面维持 100 条到香港的长连接,前面接 3000 条来自各地应用的短连接。效果等价于"全局连接池",建连成本被这层吃掉了。
关键配置:
# proxysql.cnf
mysql_servers =
(
{ address="203.0.113.10", port=3306, hostgroup=1, max_connections=100 }
)
mysql_users =
(
{ username="biz", password="***", default_hostgroup=1 }
)
mysql_variables =
{
ping_interval_server_msec = 30000
ping_timeout_server = 500
connect_timeout_server = 3000
free_connections_pct = 20
}
ping_interval_server_msec 设 30 秒是有讲究的——它就是拿来对付第五节那个 NAT 老化问题的,ProxySQL 会定期对后端连接发 ping,保持 NAT 会话表项不过期。
但 ProxySQL 不是万能的:它只能省掉建连成本,省不掉每一次查询本身的往返。如果你的业务是大量单条点查串行执行,那 48ms 就是 48ms,ProxySQL 也救不了——这种场景唯一的解法是批量化(把 10 条查询合并成 1 次 UNION ALL)或者业务侧加缓存。
九、写在最后
这篇从头到尾没动 innodb_buffer_pool_size、没动 innodb_io_capacity、没碰 doublewrite、没调 redo log 大小。不是因为它们不重要,而是因为在香港 CN2 这个场景下,你把 InnoDB 调到花,也只能优化那个已经在 0.4ms 内完成的环节。
真正卡住你的是光在光纤里跑的那 48 毫秒——每一次建连、每一次认证、每一次窗口重置,都是在给这个物理常量交税。想清楚哪些税能避、哪些避不掉,比抄一百份 my.cnf 模板有用得多。
最后补一句实操建议:选香港 CN2 机器做 MySQL 主库的时候,先别急着上 sysbench 压 QPS,先 ping 一遍三地、抓包数一遍冷连接的 RTT 数、看一下 tcp_slow_start_after_idle 的值。这三件事花不了二十分钟,但能让你少走我走过的两个月弯路。