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-Qsend 字段:

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和基础网络参数都预先优好的机房,能帮你少踩很多“带宽充足但跑不满”的暗坑。