MySQL在万兆链路“便秘”?大带宽服务器结果集吞吐的socket缓冲与协议调优
拿着10Gbps的大带宽,MySQL却连500Mbps都跑不满,远程拉一个1GB的结果集能耗时半分钟。这不是InnoDB慢,也不是SQL写得烂,而是从MySQL协议到Linux TCP缓冲区之间那一段“看不见的管道”没通。前阵子在一台轻云互联的10Gbps大带宽服务器上排查同样问题,顺手把整套调优参数固化到了部署脚本里,分享出来。
1. 先定位:MySQL为什么跑不满带宽?
别急着改参数,先确认瓶颈到底在哪一层。
# 查看网卡实时吞吐
sar -n DEV 1 5
# 观察mysqld进程CPU/内存
top -p $(pgrep -d, mysqld)
# 查看指向mysql 3306端口的TCP连接状态
ss -tni | grep -A1 3306
如果 sar 显示网络PPS不高、吞吐远低于带宽上限,且 mysqld CPU占用不到50%,那基本可以判定问题出在socket缓冲区或MySQL发送数据包的方式上。
重点看 ss -tni 中的 Send-Q 和 send 字段:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 490240 10.0.0.5:3306 10.0.0.9:52314
cubic wscale:7,7 rto:204 rtt:1.251/1.251 ato:40 mss:1448 rcv_rtt:1.251
... send 208K ...
其中的 send 208K 表示该TCP连接当前发送缓冲区上限只有约208KB。这条链路RTT约1.25ms,带宽10Gbps,理论BDP(带宽时延积)至少1.5MB。208K的发送窗口显然不够,吞吐被卡死在“网速×窗口/时延”这个公式里。
2. MySQL协议层的两个参数
MySQL客户端/服务器通信的每个packet,初始缓冲大小由 net_buffer_length 控制,默认只有16KB。当结果集/单行数据超过这个值,MySQL会不断扩展buffer并触发内存拷贝。
编辑 /etc/my.cnf:
[mysqld]
max_allowed_packet = 134217728 # 128MB,单行或单条SQL的最大包体
net_buffer_length = 1048576 # 1MB,减小内存扩展次数
[client]
max_allowed_packet = 134217728
net_buffer_length = 1048576
重启MySQL:
systemctl restart mysqld
# 验证
mysql -e "SHOW VARIABLES LIKE 'max_allowed_packet'; SHOW VARIABLES LIKE 'net_buffer_length';"
注意:net_buffer_length 最大只能设到1MB,不要写更大。这个值越大,每个线程初始占用内存越大,连接数高的时候反而可能拖垮内存。
3. TCP socket buffer:真正被忽略的“管径”
MySQL协议的buffer即使调好了,最终数据还是要从用户态写到内核的socket发送队列。如果socket buffer太小,TCP窗口扩大机制会被抑制,大带宽完全发挥不出来。
在MySQL服务器和客户端所在机器上,都添加以下系统参数:
cat >> /etc/sysctl.conf <<'EOF'
# TCP socket send/receive buffer 上限
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# TCP 自动调优范围:最小、默认、最大
net.ipv4.tcp_rmem = 4096 65536 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# 关闭不适用于高吞吐场景的窗口收缩
net.ipv4.tcp_adv_win_scale = 1
# 显式开启窗口缩放(2.6+内核默认已开,但确认下)
net.ipv4.tcp_window_scaling = 1
EOF
sysctl -p
这里最关键的是 tcp_rmem/tcp_wmem 的最大值设为16MB。BDP按照“带宽(bps)× RTT(秒)”估算,万兆/千兆+内网RTT 1ms也就1.25MB,16MB留出了足够的余量,也避免TCP拥塞窗口频繁受限。
改完后不必重启mysqld,新建立的TCP连接会自动使用新的默认缓冲区上限。已有的老连接需要断开重连才生效。
4. 客户端别傻等“全量结果集”
服务端调完,客户端如果用默认的 mysql_store_result(),会把整个结果集先缓存到客户端内存,边接收边等待,反而拖慢网络流水。对于大结果集,必须改用流式读取。
命令行场景
mysql -h 192.168.1.10 --quick --max_allowed_packet=134217728 \
-e "SELECT * FROM big_metadata" > /dev/null
--quick 会让mysql客户端逐行获取结果,而不是一次性拉到内存,配合前面的socket buffer调优,能让发送动作持续处于“满载”状态。
应用侧场景
- PHP mysqli:使用
MYSQLI_USE_RESULT代替MYSQLI_STORE_RESULT。 - Java JDBC:为Statement设置
stmt.setFetchSize(Integer.MIN_VALUE),触发流式读取。 - Python PyMySQL:使用
pymysql.cursors.SSCursor(服务端游标)。
5. 压测对比:从400Mbps到9.2Gbps
在轻云互联的10Gbps带宽服务器上,用一张3000万行的元数据表做实际拉取验证。表里有一个255字节的varchar字段,结果集总量约7.5GB。
# 调整前(未改socket buffer,仅改max_allowed_packet)
time mysql --quick -h 10.0.0.5 -e "SELECT id, pad FROM t_meta" > /dev/null
# 输出:real 2m15.7s (约550Mbps)
# 调整后(socket buffer + net_buffer_length + 流式)
time mysql --quick -h 10.0.0.5 -e "SELECT id, pad FROM t_meta" > /dev/null
# 输出:real 0m7.9s (约9.2Gbps)
同时用 sar -n DEV 1 观察,网卡TX吞吐稳定在9Gbps以上,ss -tni 中的 send 也变为4M左右,说明TCP窗口彻底打开了。
小结
大带宽服务器上跑MySQL,如果只是拼命调InnoDB缓冲区,却放着socket buffer和协议包buffer不管,跟“八车道高速路口设了个单车道收费站”没区别。这组参数不限于物理机,云服务器同样适用。
另外,选大带宽服务器时别只盯着带宽数字,像轻云互联这样把网卡队列、内网MTU和基础网络参数都预先优好的机房,能帮你少踩很多“带宽充足但跑不满”的暗坑。