马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
数据库主机网络连接异常排查实战:从 SYN 丢弃到内核队列调优
一、具体的问题
某天上午十点,业务方连续报障:应用日志里 Connection timed out 频发,连接池里堆积了上百个等建连的线程。第一反应往往是去看数据库负载——结果一查,MySQL 的 CPU、IO、Threads_running 全部平稳,show processlist 里干干净净。更蹊跷的是:报错集中在十点整到十点零三分这三分多钟,之后自愈,且报错的是新建连接,已建立的存量连接查询一切正常。
"新建连接超时、存量连接正常",这个特征组合基本可以把嫌疑从数据库负载上移开,指向主机的内核网络栈:建连的第一步(TCP 三次握手)卡住了,而握手之后的数据交互根本还没开始。如果这时候你选择重启数据库、加大连接数、重试——大概率是无效的,因为问题压根不在那一层。本文用一次真实的排查,把这类问题的定位路径和内核调优一次讲透。
二、核心原理
理解这类问题,只需要记住三次握手中的两个队列:
- 半连接队列(SYN 队列):服务端收到 SYN、回复 SYN-ACK 后,连接进入半连接队列等待第三次 ACK。由 net.ipv4.tcp_max_syn_backlog 控制。
- 全连接队列(accept 队列):完成握手、等待应用调用 accept() 取走的连接。大小取两个值的较小者:应用传入的 listen backlog 与内核的 net.core.somaxconn。
关键结论:队列满了之后,内核的默认行为是丢弃——新到的 SYN 或最后那个 ACK 被静默丢掉,客户端只能靠超时或重传慢慢熬,表现就是"连接超时"。队列为什么会满?最常见的原因不是流量太大,而是应用 accept 得太慢:数据库进程被某个大事务/导出任务占住了线程、或者突发连接风暴在几百毫秒内打进来。也就是说,网络参数调优是扩容队列的兜底手段,根因常常在应用侧的 accept 能力。
另外三个高频元凶:
- TIME_WAIT 洪水:主动关闭方进入 TIME_WAIT 等待 2MSL(默认 60 秒),本身是正常状态。真正的问题是短连接高频建连把本地临时端口耗尽(ip_local_port_range 默认 32768~60999 只有约 2.8 万个),新建连接直接失败。
- conntrack 表满:开了 iptables/conntrack 的机器,nf_conntrack_max 被打满后新连接被静默丢弃,dmesg 里能看到 nf_conntrack: table full, dropping packet。
- Nagle 与延迟 ACK:小包高频写场景,发送端攒包 + 接收端延迟确认叠加,能造出 40~200ms 的诡异延迟,表现为"查询偶尔慢",而不是"连不上"。
三、实例参考(动手步骤)
步骤 0:确认现状基线
- # 事件时间点的连接概览
- ss -s
- # 监听套接字:Recv-Q 是当前 accept 队列长度,Send-Q 是队列上限
- ss -lnt | grep -E '3306|1521|5432'
- # 健康标准:Recv-Q 长期远小于 Send-Q;若 Recv-Q 反复顶到 Send-Q,就是 accept 队列满的实锤
复制代码
步骤 1:查内核丢包计数,锁定队列溢出
- # 半连接队列溢出 / accept 队列溢出的累计计数(重点看是否在增长)
- netstat -s | grep -iE 'listen|SYN'
- # TCP request sockets discarded <-- SYN 被丢(半连接溢出)
- # TCP listen queue overflows <-- accept 队列溢出
- # TCP SYN cookies sent <-- 已触发 syncookie 保护
- # 间隔 10 秒采两次,确认是"正在发生"而不是历史累计
- netstat -s | grep -iE 'overflow|discarded'; sleep 10; netstat -s | grep -iE 'overflow|discarded'
复制代码
如果 overflow 计数在增长,同时 ss -lnt 里 Recv-Q 顶满,结论就坐实了:握手完成了但应用没来得及 accept。
步骤 2:抓包验证握手在哪一步断的
在客户端(应用主机)抓包,比在服务端更能反映真实体验:
- # 客户端抓 3306 端口的握手过程
- tcpdump -i any -nn 'host <db_ip> and tcp port 3306' -c 50
- # 典型故障形态:客户端反复发 [SYN],服务端毫无响应(半连接被丢)
- # 或:三次握手成功后应用层无响应(accept 慢)
复制代码
再看重传统计佐证网络质量:
- netstat -s | grep -i retrans
- # TCPTimeouts / TCPSynRetransmits 持续增长,配合丢包计数,就是链路质量问题
复制代码
步骤 3:检查 conntrack 与临时端口两个静默杀手
- # conntrack:当前条目 vs 上限
- cat /proc/sys/net/netfilter/nf_conntrack_count
- cat /proc/sys/net/netfilter/nf_conntrack_max
- dmesg | grep -i 'table full' # 出现过即中过招
- # 本地临时端口:短连接客户端重点看
- cat /proc/sys/net/ipv4/ip_local_port_range
- # 默认 32768 61000,约 2.8 万个端口;TIME_WAIT 60 秒内短连接超过这个量级就会耗尽
复制代码
步骤 4:内核与应用双侧调优(可照做)
写 /etc/sysctl.d/99-db-net.conf 并生效:
- cat >> /etc/sysctl.d/99-db-net.conf <<'EOF'
- # accept 队列上限:数据库主机建议拉到 4096 以上
- net.core.somaxconn = 4096
- # 半连接队列上限
- net.ipv4.tcp_max_syn_backlog = 4096
- # 诊断期临时打开:队列满时直接回 RST,让客户端立刻报错而不是傻等(定位完关掉)
- # net.ipv4.tcp_abort_on_overflow = 1
- # 客户端临时端口范围放宽
- net.ipv4.ip_local_port_range = 10240 65535
- # 仅对"主动连出去"的方向安全:TIME_WAIT 端口复用(NAT 环境慎用)
- net.ipv4.tcp_tw_reuse = 1
- # conntrack 上限与超时收敛(若启用 iptables/conntrack)
- net.netfilter.nf_conntrack_max = 262144
- net.netfilter.nf_conntrack_tcp_timeout_established = 1200
- EOF
- sysctl --system && sysctl -a | grep -E 'somaxconn|syn_backlog|port_range'
复制代码
别忘了应用侧这一半——这是最容易漏的:MySQL 的 back_log 参数(即 listen backlog)默认只有 80 左右,内核队列再大,实际生效的是 min(backlog, somaxconn):
- SHOW VARIABLES LIKE 'back_log';
- -- 修改 my.cnf:back_log = 2048,重启实例生效
复制代码
步骤 5:压测复现验证
- # 用并发建连工具模拟连接风暴(示例:mysqlslap 纯建连)
- mysqlslap --host=<db_ip> --user=test -p --concurrency=500 --iterations=3 --auto-generate-sql
- # 同时在服务端观察 Recv-Q 峰值与丢弃计数是否再增长
- watch -n 1 'ss -lnt | grep 3306; netstat -s | grep -i overflow'
复制代码
验收标准:压测期间 Recv-Q 峰值低于 Send-Q,overflow/discarded 计数停止增长,客户端建连耗时平稳。
四、实操检查清单
- [ ] ss -lnt 确认监听端口的 Recv-Q 长期远低于 Send-Q;
- [ ] netstat -s 的 listen queue overflows / request sockets discarded 两个计数纳入监控,出现环比增长即告警;
- [ ] somaxconn 与 tcp_max_syn_backlog 已调整,且 sysctl -a 验证生效(不是只写了文件);
- [ ] MySQL back_log、PostgreSQL listen_addresses 对应的 backlog 已与内核值对齐;
- [ ] conntrack 机器:nf_conntrack_count/max 占比告警线设在 80%;
- [ ] 短连接客户端:临时端口范围放宽 + 连接池化改造,客户端监控 TIME_WAIT 总量;
- [ ] tcp_abort_on_overflow 仅在诊断期临时开启,用完关闭;
- [ ] 任何内核参数变更前记录基线,变更后压测回归。
几个容易踩的坑
- 队列大小取小不取大:内核 somaxconn 调到 4096,应用 backlog 还是 80,实际生效的是 80。两边必须同时调,MySQL 是 back_log,其他中间件各有对应参数。
- TIME_WAIT 数量大不等于有病:单机几万个 TIME_WAIT 是主动关闭的正常代价。先确认是哪一端主动关闭、临时端口是否够用,再决定动不动 tcp_tw_reuse;tcp_tw_recycle 在 4.12 内核后已删除,NAT 环境下是历史事故之源,不要再写进文档里传播。
- syncookie 不是万能药:tcp_syncookies=1 能在半连接洪水下保护服务端,但 syncookie 本身不带状态、会牺牲部分 TCP 选项,数据库主机靠"调大半连接队列 + 应对 accept 慢"才是正解。
- 把连接超时当数据库慢来治:加 max_connections、重启实例都是无效动作。先分清"建连失败"和"已建连查询慢",两类问题根因链完全不同。
- 容器/K8s 环境多一层:Pod 里的 somaxconn 是宿主机与 netns 两层都要看,istio/envoy sidecar 的 accept 线程也可能成为新瓶颈,排查时别只看数据库容器。
治理前后对比
| 指标 | 治理前 | 治理后 | | 建连超时告警 | 每周 3~5 次 | 0 次(连续 60 天) | | accept 队列溢出计数 | 事故时段 10 秒 +1200 | 长期 0 增长 | | 建连耗时 P99 | 5.8 秒 | 12ms | | listen backlog(实际生效) | 80 | 2048 | | conntrack 表占用峰值 | 97% | 31% | | 排查一次同类故障耗时 | 半天~一天 | 15 分钟内定位 |
一句话收尾:新建连接超时、存量查询正常,先怀疑内核队列与 conntrack,再看数据库。把 ss -lnt、netstat -s 的两个溢出计数放进日常监控,这类故障在业务报障之前就能被你先看见。 |