没想到吧,最后的瓶颈居然在网络层——TCP握手超时、SYN队列打满、网卡软中断爆掉,这些你平时不怎么看的数据,真正出问题时能让你排查一整天。这篇笔记就从一个线上告警说起,整理一套我从"参数调优"到"请求分析"的完整排查路径。涉及内核参数调整、监控工具实测、抓包定位这几个核心环节,适合正在维护线上接口的运维和开发同学参考,也是一份可以直接复用的排查清单。如果你现在主要在用云主机,下面的内容同样适用,底层还是那套 Linux 协议栈。
1. 先搞清楚Linux网络优化的本质
1.1 网络瓶颈不只是"带宽小"这么简单
大多数人一想到网络性能,第一反应是带宽不够、机房线路质量差。但接触过真实线上系统后你会发现,大部分性能问题根本轮不到运营商出手。内部自建的接口网关、微服务调用、数据库连接池、缓存集群同步,这些场景下真正拖垮系统的往往是 TCP 协议栈参数不匹配、连接状态机异常、网卡队列处理不过来。带宽只是"道路宽度",协议栈是"交通规则",应用是"车流"。很多情况下道路够宽,车也够多,但红绿灯配比不对,照样堵死。
日常衡量网络性能,主要看这几个指标:吞吐量、延迟、丢包率、重传率、新建连接速率、并发连接数。很多人只盯着带宽,忽略了连接速率。举个例子,一个接口平均响应 50ms,QPS 2000,那么每秒需要新建连接可能只有几十个(长连接),但如果负载均衡策略或者客户端连接池配置不合理,每次请求都新建连接,那么系统可能在建立连接阶段就先把 CPU 和内存耗光了。这时候你去看带宽,可能连 1% 都没跑满,但接口已经变慢。
回到后端服务本身,Linux 网络性能优化的本质其实是"在正确的位置做正确的取舍"。系统默认参数往往是面向通用场景的,它不会知道你服务的特点是长连接多还是短连接多、请求包大还是小、是延迟敏感还是吞吐敏感。所以优化的第一步不是急着改参数,而是先把当前的网络状态摸清楚,知道系统正处于什么状态、哪个环节最吃力,然后再针对性地动刀。
1.2 没有数据支撑的调优就是乱调
我在处理线上问题的时候有一条铁律:先监控,后调优,最后才是改配置。在没有监控数据的情况下直接改内核参数,就像闭着眼睛修车,运气好能蒙对,运气差直接引发故障。而且不少参数的调整对线上是有全局影响的,改了之后不仅解决不了手头的问题,还可能连带影响其他正常业务。
一个完整的排查周期应该长这样:先通过监控系统看基础指标,划分问题边界,然后深入定位,最后才是针对性调整。具体到网络这块,基础指标包括网卡流量、TCP 连接状态分布、SYN 重传次数、TIME_WAIT 连接数、重传率、软中断占比,等等。拿到这些数据后,你基本能判断瓶颈是在网卡层、协议栈层还是应用层。
提示:如果你还没有搭建监控系统,那至少把系统自带的工具用起来,
sar、ss、top、cat /proc/net/netstat这些完全能帮你完成 80% 的初判工作。
2. 内核网络参数调优:动手前先搞清楚原理
2.1 TCP三次握手与SYN队列:高并发第一道坎
先说一个我实际遇到过的情况:某外包团队负责的业务网关在活动大促前突然出现大量连接超时,客户端报"连接建立失败",但服务端 CPU、内存都正常,带宽也很空闲。当时第一反应是后端服务线程满了,但查了一圈发现根本不是。后来用ss -s一看,发现 SYN 队列溢出计数暴涨,问题就出在内核接受半连接的能力上。
Linux 中 TCP 三次握手建立连接的过程,服务端会先把连接放到 SYN 队列(半连接队列),完成握手后移到 accept 队列。这两个队列的容量分别受tcp_max_syn_backlog、net.core.somaxconn影响。如果 SYN 队列太小,内核就会丢弃新的 SYN 包,客户端那边表现为连接超时或者直接失败。此时服务端 CPU、内存看着都正常,只有协议栈在默默丢包。
# 查看当前SYN队列相关参数 sysctl net.ipv4.tcp_max_syn_backlog sysctl net.core.somaxconn # 临时调整 sysctl -w net.ipv4.tcp_max_syn_backlog=10240 sysctl -w net.core.somaxconn=4096关于这两个参数,我的推荐做法是:高并发短连接类型服务,tcp_max_syn_backlog建议至少 10240 起步;如果服务包含 Web 服务(比如 Nginx 之类),somaxconn同理至少要 4096,而且要注意应用层 listen 时传的 backlog 参数不能超过这个值,两者是取较小值的逻辑。另一个值得关注的参数是tcp_syncookies,它用来防范 SYN Flood 攻击。开启后内核会在 SYN 队列快满时用 SYN Cookie 机制绕过半连接队列。但注意,开启 syncookies 会导致部分 TCP 扩展特性失效(例如时间戳等),所以它更适合作为应急保护盾,而不是常规设定。
2.2 TIME_WAIT:被误读最多的状态
很多刚接触网络优化的新手,一看到ss -s里 TIME_WAIT 几万个就慌了,马上搜教程去调tcp_tw_reuse和tcp_tw_recycle。实际上 TIME_WAIT 是 TCP 协议主动关闭方进入的状态,核心目的是确保最后一个 ACK 能被对端收到、同时防止旧连接的报文干扰新连接。它不是"病",而是协议自保机制。
不过在高并发短连接(比如客户端频繁创建和销毁连接)场景下,大量 TIME_WAIT 确实可能引发本地端口耗尽。这里的关键是搞清楚瓶颈到底在服务端还是客户端。服务端主动断开连接时,TIME_WAIT 堆积在服务端;客户端主动断开时,TIME_WAIT 堆积在客户端。如果堆积方是代理或者应用服务自身,而且连接量极大,那才有必要干预。
tcp_tw_reuse只能用于客户端场景,而且必须开启tcp_timestamps,它的作用是在新建连接时安全复用 TIME_WAIT 状态的连接。tcp_tw_recycle则更危险,它依靠时间戳快速回收连接,但由于 NAT 环境下多设备共用一个公网 IP 时时间戳错乱问题极其严重,很容易导致入站连接直接失败。所以我个人的态度是:tcp_tw_recycle在目前的 Linux 内核版本中已经不建议启用,你最好把它当成一个雷区。真正高并发服务端应该优先考虑让客户端复用连接,启用 keep-alive 或者连接池,而不是过度纠结服务端 TIME_WAIT 数量。
# 不建议全局开启tcp_tw_recycle # sysctl -w net.ipv4.tcp_tw_recycle=1 # 踩坑参数,不要用 # 客户端场景可考虑 sysctl -w net.ipv4.tcp_tw_reuse=1 sysctl -w net.ipv4.tcp_fin_timeout=152.3 缓冲区与滑动窗口:带宽和延迟的平衡
TCP 发送和接收缓冲区大小直接影响吞吐。内核接收窗口太小,数据发不过来;发送缓冲太小,数据又排队。这些参数中最核心的是net.ipv4.tcp_rmem和net.ipv4.tcp_wmem,格式是"最小值 默认值 最大值"。很多教程直接建议把默认值拉到 16MB,但这是有副作用的:每个连接都要占用这么多内存,连接数几十万时内存直接爆掉。尤其是接收缓冲,它是需要实际分页分配的。
一个比较合理的方式是先估算带宽延迟积(BDP),即"带宽×延迟",它决定了链路中允许在途的数据量。比如内网带宽 1Gbps,RTT 0.2ms,那么 BDP 就是 1Gbps × 0.0002s ≈ 200Kb ≈ 25KB,不用调大缓冲;但如果跨地域 RTT 是 50ms,BDP 就是 6.25MB,那么默认的 4MB 上限就会限制吞吐。
# 查看当前值 sysctl net.ipv4.tcp_rmem sysctl net.ipv4.tcp_wmem # 示例:延迟较低的内网场景 sysctl -w net.ipv4.tcp_rmem='4096 87380 6291456' sysctl -w net.ipv4.tcp_wmem='4096 16384 4194304'对于绝大多数后端服务,我建议只适当调整上限,不要粗暴拉高默认值。连接数少、延迟高的场景才值得调大默认值,而连接数上万的场景,默认值尽量保持小。
2.4 高频有用的参数速查表
下面这组参数是我在实际项目中验证过、相对安全有效的。注意,这只是速查,所有参数都要结合业务场景决定是否使用。
| 参数 | 作用 | 适用场景 | 不适用场景 |
|---|---|---|---|
net.core.somaxconn | 调整 accept 队列长度 | 高并发短连接、Web服务 | 低并发长连接 |
net.ipv4.tcp_max_syn_backlog | 调整 SYN 半连接队列 | 高并发握手场景 | 连接数极低的服务器 |
net.ipv4.tcp_fin_timeout | 减少 FIN-WAIT-2 等待时间 | 大量主动关闭方 | 依赖TIME_WAIT保护的场景 |
net.ipv4.ip_local_port_range | 本地连接可用端口范围 | 大量出站短连接 | 入站服务场景 |
net.core.netdev_max_backlog | 网卡接收队列长度 | 网卡中断高、突发流量 | 低流量场景 |
net.ipv4.tcp_keepalive_time | keepalive 心跳间隔 | 长连接保活 | 短连接场景 |
net.ipv4.tcp_slow_start_after_idle | 空闲后关闭慢启动 | 长期有流量的连接 | 突发小流量连接 |
我记得有一个印象很深的案例:某平台的推送服务出站连接特别多,本地端口范围默认是32768 60999,也就不到 3 万个端口,出站连接一高就直接报Cannot assign requested address。后来把ip_local_port_range调整到1024 65535,问题立刻缓解。这类问题不调整的人可能永远遇不到,遇到了之后你会发现内核默认值实际上预设的是"够用但不宽裕"。
3. 监控工具实战:用数据指挥行动
3.1 系统自带工具就能解决80%的排查需求
工具这东西,真没必要一开始就上重型的流量分析平台,把系统自带的工具用竞赛,足够你定位大多数问题。首选是ss,它是netstat的替代者,速度快信息也全。比如我想看所有 TCP 连接状态分布,一条ss -s就搞定;想看某个端口上的连接情况,ss -antlp | grep 8080也行。搭配state过滤还能单独挑出 TIME_WAIT 或者 ESTABLISHED,这在判断连接异常时非常有用。
# TCP连接状态汇总 ss -s # 查看端口监听状态 ss -antlp | grep :8080 # 只看ESTABLISHED连接 ss -ant state established | head -20接着看网卡流量,用sar -n DEV 1 10。这个命令每秒钟采集一次网卡收发字节数和包数,连续采 10 次。它会显示rxkB/s、txkB/s、rxcmp/s等字段。注意看rxerrs和txerrs,如果错误包数量不为 0,说明网卡或者链路层就已经有丢包了,这时候调整内核参数是没有意义的,应该先排查网卡驱动、电缆或者光模块问题。
再配合cat /proc/net/netstat | grep TcpExt,可以看协议栈层面的计数。重点关注ListenOverflows和ListenDrops。这两个数值如果一直在涨,说明应用层 accept 队列已经满了,连接会在内核里被丢。然后再看TCPSynRetrans,这是 SYN 重传次数,如果这个值高,说明握手的 SYN 包没有被对方及时确认,可能是对端协议栈问题,也可能是中间网络丢包。
3.2 实时流量与软中断:别忘了看 CPU 的眼睛
有经验的运维都知道,网卡流量高的时候,CPU 的软中断(softirq)往往是先报警的。Linux 收包路径是:网卡收到包 -> 触发硬中断 -> 唤醒 ksoftirqd 或者直接执行软中断 -> 协议栈处理 -> 应用读取。如果某个网卡只有一个队列,所有包都压到一个 CPU 核心上,很容易出现一个核被打满、其他核闲着的情况。
查看软中断占用,用top然后按1看每个核的si字段;更精确的是cat /proc/softirqs,看 NET_TX 和 NET_RX 在每个 CPU 上的分布。如果发现某个核的 NET_RX 值远高于其他核,说明网卡不是多队列或者中断没做亲和性设置。通用解决方案是开启网卡多队列(ethtool -L eth0 combined 4),然后配合 irqbalance 或者手动设置 irq affinity,让中断分散到各个核。
# 查看网卡队列数 ethtool -l eth0 # 查看报错统计 ethtool -S eth0 | grep -E "rx_error|tx_error|drop" # 临时调整队列数(需驱动支持) ethtool -L eth0 combined 4需要留意的是,ethtool -S里的 drop 计数如果不增长,那基本可以排除网卡丢包;如果 drop 一直在涨,多半是环形缓冲区太小,可以用ethtool -G eth0 rx 4096调大 ring buffer。这个操作在突发流量场景下效果相当明显。
3.3 延迟与吞吐:别只看 Ping,iperf3 才是准绳
很多人习惯用 ping 来测网络质量,但 ping 只能说明 ICMP 协议的通达性和 RTT,跟实际的 TCP 请求表现差别很大。想验证两台机器之间到底能跑多大带宽,工具应该用iperf3。例如从机器A做客户端连到机器B做服务端(iperf3 -s),客户端执行iperf3 -c 目标IP -t 30 -P 8,用8个并发连接压 30 秒,就能看到实际聚合带宽。
iperf3 跑出来的结果如果远低于网卡理论带宽,问题可能出现在:两端 MTU 不一致导致分片过多、TCP 缓冲太小、CPU 处理不过来、或者网络路径上有 QoS 限速。这时候回头结合sar -n DEV和top的软中断占比,就能判断到底是内核参数问题,还是物理链路问题。很多"带宽不满"的困惑,最后测下来其实是中间设备限了速,或者对端虚拟化平台的带宽上限设置小了。
# 服务端(机器B) iperf3 -s # 客户端(机器A) iperf3 -c 192.168.1.100 -t 30 -P 84. 请求分析实战:从抓包到定位根因
4.1 tcpdump抓包:在正确的位置抓正确的包
调优和监控做到位以后,剩下的就是深水区:请求分析。这部分的核心工具就是tcpdump加上Wireshark。抓包的目的不是把流量全录下来,而是带着问题去抓,先明确要验证什么。
比如怀疑某个端口上连接建立慢,那就抓指定端口的 SYN 和 SYN-ACK 包。抓包时建议把表达式写精确一点,只抓特定主机和端口。否则高流量环境下抓包文件会暴涨,而且后期分析干扰极大。另外,遇到需要长时间抓包时,一定要设置环形缓冲只保留最近的 N 个文件,避免磁盘写满。
# 抓取指定端口的通信包,保留100MB轮转 tcpdump -i eth0 host 10.0.0.5 and tcp port 8080 -s 0 -C 100 -w /tmp/debug.pcap # 分析重传包 tcpdump -i eth0 tcp port 8080 -t -e 2>/dev/null | grep "retransmission"用 Wireshark 打开抓包文件之后,第一步先启用"分析 -> 专家信息"。这个面板会自动标出异常包,比如重传、乱序、丢包、虚假重传等。凭这些信息你就能快速判断是网络质量差,还是对端策略导致的现象。例如大量重传同时 RTT 平坦,可能是中间设备丢包;重传前有响应延迟,则可能是应用层处理慢导致触发超时重传。这两种情况处理的思路完全不同。
4.2 区分"网络慢"和"应用慢"的黄金法则
很多故障表象上是连接超时、响应延迟,但归根结底要回答一个问题:慢在了网络的哪个环节?我的方法是画一条时间轴:客户端发出请求包 -> 服务端收到 -> 服务端处理 -> 服务端发出响应包 -> 客户端收到响应。哪一段耗时最长,瓶颈就在哪。
在 Wireshark 里看一个完整的 HTTP 请求,找到请求包发出到服务端确认(ACK)的时间,这是网络单向延迟;再找服务端发第一个响应包和最后一个数据包的时间差,这是服务端处理时长。如果服务端处理时间长,那问题在后端应用或者数据库,抓包只能看个表象;如果处理时间很短但客户端看到的总延迟很长,才值得怀疑网络路径。
我印象里有一次排查一个"数据库查询偶尔很慢"的问题,应用日志显示 SQL 执行才 20ms,但接口耗时 600ms。后来抓包发现服务端收到查询请求后隔了 400ms 才回 ACK,这个 ACK 延迟其实是 TCP 延迟确认机制导致的——小包积压到定时器触发才一起确认,而不是真的网络慢。最后通过调整/proc/sys/net/ipv4/tcp_quickack优化掉了这个知,解决了问题。
4.3 请求分析的正确路径:先宏观后微观
概括一下请求分析的整体思路:宏观上先通过ss、nstat判断连接层指标是否异常;然后抓包确认链路层的 TCP 行为;最后结合业务日志定位应用层耗时。三层对应关系可以这么理解:连接层是"门卫",链路层是"道路",应用层是"房间里的服务人员"。先看门卫是否放行、再看道路是否拥堵、最后看房子里办事速度快不快,顺序不能乱。
# 宏观层:快速判断连接状态分布 ss -s cat /proc/net/netstat | grep TcpExt # 中观层:确认TCP行为 tcpdump -i eth0 host 10.0.0.5 and tcp port 8080 # 微观层:查看应用内部耗时 # 比如用jstack查看Java线程状态,或perf记录CPU热点这里有个实操细节:抓包时机怎么选。如果问题能复现,那最好在出问题的时刻抓;如果不能稳定复现,建议落一个长期抓包脚本,配合环形 buffer 保存最近 5 分钟的流量,出问题时立刻停掉抓包进程。这样既不会把磁盘写满,又能保留现场。
5. 常见问题与故障排查实录
5.1 连接数上不去?先看队列再调参数
遇到"连接数上不去"这类问题(服务器明明没挂,但新连接一直失败),我建议按这个顺序排查:ss -lnt看监听队列的 Send-Q(backlog 大小)是否远大于 Recv-Q(当前占用);如果 Send-Q 大于应用层实际设置值,说明应用层 backlog 没调大;再看/proc/net/netstat的ListenOverflows是否持续递增,递增则说明 accept 队列已经打满;最后看dmesg有没有报 "TCP: request_sock_TCP: Possible SYN flooding"。每一步都有对应的处理动作,不要一上来就盲目调net.core.somaxconn。
这个现象最常见的根因是应用层监听参数没跟上。很多人改了内核somaxconn,但应用(比如 Java 的ServerSocket或 Nginx 的listen指令)没透传 backlog。举个例子,Nginx 里要在listen 8080 backlog=4096;显式指定,只看内核参数是不够的。
5.2 带宽打满但吞吐不行?检查软中断和队列
另一个我经常被问到的问题是"明明网卡流量已经跑满,但应用吞吐就是上不去"。这种情况优先怀疑单队列瓶颈。单个网卡队列意味着所有网络包都由同一个 CPU 核心处理软中断,那个核先打满,整体吞吐就受限。检查top的si字段和/proc/softirqs就能看出来,然后想办法开启多队列、做中断亲和。其次,可以看下sar -n DEV里的rxpck/s,如果包量特别大但字节数不大,可能是小包风暴,这时候要结合netstat -st里的收包情况判断是否有协议栈丢包。
5.3 延迟抖动不稳定?检查重传和乱序
接口偶尔超时、但整体又不像挂了,这种感觉最折磨人。先看ss -s里重传段(segments retransmit)占总发送段的比例。如果重传率超过 1%,基本可以说明链路有丢包。再抓包看是乱序还是纯重传。乱序多,往往是链路负载均衡绕路导致;重传多,则可能是中间节点拥塞。针对前者,可以把 TCP 流调度策略调整下;针对后者,检查两端主机是不是有 QoS 标记被清了,或者 MTU 不一致导致分片被丢。
5.4 踩坑汇总速查表
| 现象 | 容易被误导的方向 | 真正的检查点 | 推荐处理 |
|---|---|---|---|
| 连接建立超时 | 以为负载高 | SYN队列溢出计数 | 调大tcp_max_syn_backlog、somaxconn |
| 出站连接报端口耗尽 | 以为是防火墙拦截 | ip_local_port_range耗尽 | 扩大端口范围并复用连接 |
| 带宽跑不满 | 以为网卡配置低 | BDP、MTU、中间限速 | 用 iperf3 测真实吞吐 |
| 接口偶发超时 | 以为代码性能差 | TCP 重传率、RTT 抖动 | 抓包确认是否网络丢包 |
| 一个核被打满 | 以为服务处理慢 | 软中断集中 | 开启网卡多队列 |
| 大量 TIME_WAIT | 以为必须消除 | 连接模型短连接 | 客户端启用连接池复用 |
我个人实际排查中最大的体会是:Linux 网络优化这件事,顺序比努力重要。先确认现象,再看系统当前状态,然后根据状态猜根因,最后才修改参数。很多事故就是在一开始方向判断错了、直接照着网上的"性能调优清单"一顿改,结果越调越乱。把自己用顺手的命令整理成固定的排查脚本,真的遇到问题时能省一半时间。最后再分享一个小技巧:每次调整内核参数前,记得sysctl -a > /tmp/sysctl_before.txt,改完再存一份after版本,出了问题可以快速 diff。这个习惯帮我回滚过好几次误操作。