1. 网络性能优化的整体思路与排查框架
1.1 为什么网络优化不能靠“猜”
做Linux性能优化这些年,我最大的体会就是:网络问题最怕凭感觉。CPU飙高你能看到,内存泄漏你能看到,磁盘IO瓶颈你也能看到,唯独网络问题,很多时候你连问题出在哪一层都摸不着头脑。应用层说慢,系统层说包发出去了,网卡说我没丢包,交换机说端口没拥塞——每个环节看起来都正常,但用户就是觉得卡。
这就是网络性能优化最棘手的地方:链路过长,观测点分散,任何一个环节的微小异常都可能在端到端体验上被放大。所以我在实际工作中总结出来的第一条原则就是:不要猜,要建立分层排查的框架。
这个框架说白了就是把网络通信拆成几个可观测的层次,从下往上或者从上往下逐层排除。具体来说,我通常分成这么几层:
- 物理层与链路层:网卡状态、协商速率、双工模式、链路聚合配置
- 网络层:路由表、ARP缓存、ICMP行为、MTU与分片
- 传输层:TCP/UDP连接状态、重传率、拥塞窗口、端口耗尽
- 应用层:协议效率、请求模式、连接复用、超时配置
- 系统资源层:中断分布、软中断负载、socket缓冲区、conntrack表
每一层都有对应的观测工具和调优参数。关键在于,你得知道先看什么、后看什么,而不是一上来就改内核参数。
1.2 分层排查的实操顺序
我一般的排查顺序是这样的:先看整体吞吐和延迟指标,确定问题是带宽受限、延迟过高还是丢包严重;然后根据症状定位到具体层次;最后在该层内做细粒度的观测和调优。
举个例子,如果用户反馈“文件传输慢”,我会先跑一个基准测试看实际吞吐量。假设千兆网卡环境下只有30MB/s,那明显不正常。接下来我会看网卡的协商速率是不是跑在了百兆模式,这是最常见也最容易被忽略的问题。确认链路没问题之后,再看TCP重传率和拥塞窗口,判断是不是网络质量导致的降速。最后才去检查应用层的读写逻辑和缓冲区配置。
这个顺序的核心逻辑是:从粗到细,从外到内,先排除低级错误再深入高级调优。我见过太多人一上来就调内核参数,结果折腾半天发现是网线插在了百兆口上。
注意:每次只改一个变量,改完立刻验证效果。网络调优涉及的因素太多,同时改多个参数你根本不知道是哪个起了作用,出了问题也无法回滚定位。
1.3 建立性能基线的必要性
在开始任何优化之前,你必须知道“正常”是什么样子。这就是性能基线的作用。没有基线,你连“慢”和“快”都无法量化判断。
建立基线的方法不复杂,但需要坚持。我通常会在业务低峰期采集一组数据,包括:网络吞吐量(用iperf3或类似工具)、TCP连接建立时间、典型请求的端到端延迟、系统软中断分布、网卡丢包统计。这些数据记录下来,作为后续对比的参照。
基线的价值在于,当问题出现时,你能快速判断是“一直这么慢”还是“突然变慢”。前者可能是配置问题或容量不足,后者更可能是突发流量、硬件故障或配置变更导致的。两种情况的排查方向完全不同。
2. 核心观测工具与关键指标解读
2.1 从ss和netstat看连接状态
ss命令是我日常用得最多的网络观测工具,没有之一。它比netstat快得多,尤其在连接数上万的场景下,netstat基本卡到不能用,而ss依然能秒出结果。
最常用的几个组合:
# 查看所有TCP连接的状态分布 ss -s # 查看处于TIME_WAIT状态的连接数 ss -tan state time-wait | wc -l # 查看监听队列溢出情况 ss -lnt # 按远端地址统计连接数 ss -tan | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20这里面的关键指标是Send-Q和Recv-Q。对于监听状态的socket,Send-Q代表当前已完成三次握手但还没被应用accept的连接数,如果这个值持续大于0,说明应用的accept速度跟不上新连接到达的速度,需要检查应用是否有阻塞或者backlog设置太小。对于已建立的连接,Send-Q持续不为0说明发送缓冲区有数据积压,可能是对端接收慢或者网络拥塞。
TIME_WAIT的数量也是重点观察对象。大量TIME_WAIT本身不是问题,它是TCP正常挥手流程的一部分。但如果TIME_WAIT数量逼近本地端口可用范围,就会导致新连接无法建立。这种情况下可以考虑开启tcp_tw_reuse,但要注意这个参数只对 outgoing 连接有效,而且需要时间戳支持。
2.2 用sar和nstat追踪历史趋势
ss只能看当前状态,要看历史趋势就得靠sar和nstat。sar -n DEV 1可以实时看网卡的收发速率和丢包统计,sar -n TCP,ETCP 1能看TCP层的重传和连接建立情况。
nstat更适合做增量统计。它读取的是内核的SNMP计数器,每次执行会显示自上次执行以来的增量值。比如:
nstat -az | grep -i -E "retrans|drop|error"这个命令能快速告诉你这段时间内发生了多少次重传、丢包和错误。我通常会在排查开始时执行一次,然后间隔一段时间再执行一次,对比两次输出的差值,就能知道问题是否正在发生。
关键指标包括:
- TcpRetransSegs:重传报文段数量,持续增长说明网络质量有问题
- TcpExtTCPLostRetransmit:重传丢失,比普通重传更严重
- TcpExtListenDrops:监听队列溢出导致的丢弃
- TcpExtTCPAbortOnMemory:内存不足导致的连接中止
2.3 用tcpdump做精准抓包分析
抓包是网络排查的终极手段,但也是最容易用错的手段。我见过有人在万兆环境下直接tcpdump -i any不加任何过滤条件,结果抓出来的文件几个GB,分析工具直接打不开。
正确的抓包姿势是这样的:
# 抓取特定主机的特定端口,限制包数量和文件大小 tcpdump -i eth0 -s 128 -C 100 -W 5 -w /tmp/capture.pcap host 192.168.1.100 and port 8080 # 只看TCP标志位,快速判断连接状态 tcpdump -i eth0 -nn -t -c 100 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0'-s 128只抓每个包的前128字节,对于分析TCP握手和标志位足够了,能大幅减小文件体积。-C 100 -W 5表示每个文件100MB,最多写5个文件循环覆盖,避免把磁盘写满。
分析抓包文件时,我推荐用tshark做统计而不是逐包看:
# 统计重传次数 tshark -r capture.pcap -q -z io,stat,1,"tcp.analysis.retransmission" # 统计TCP连接建立时间 tshark -r capture.pcap -q -z expert提示:生产环境抓包一定要加过滤条件,并且限制抓包时长和文件大小。我一般会提前跟相关同事打招呼,避免抓包期间出现性能波动引起不必要的紧张。
2.4 软中断与网卡多队列的观测
网络包的处理最终要落到CPU上,而大部分处理发生在软中断上下文。如果软中断分布不均,就会出现“一核有难、多核围观”的情况。
查看软中断分布:
# 查看每个CPU的软中断统计 cat /proc/softirqs | grep -E "NET_RX|NET_TX" # 用mpstat查看软中断占用的CPU时间 mpstat -P ALL 1如果发现某个CPU的NET_RX软中断数量远高于其他CPU,说明网卡的中断亲和性没有配好,或者网卡不支持多队列。现代网卡基本都支持RSS(接收端缩放),可以通过ethtool -l eth0查看队列数量,通过ethtool -L eth0 combined N调整队列数。
调整中断亲和性的方法是设置/proc/irq/<irq_num>/smp_affinity,把不同的网卡队列中断绑定到不同的CPU核心上。具体操作我会在后面的实操章节详细展开。
3. 内核参数调优的实战配置
3.1 TCP缓冲区的最优设置
TCP缓冲区大小直接决定了单条连接的吞吐能力。这里有一个经典的计算公式:
吞吐量 = 缓冲区大小 / 往返延迟
假设你的网络往返延迟是10ms,想要达到1Gbps的吞吐量,那么缓冲区至少需要:
1Gbps × 10ms = 1000Mbps × 0.01s = 10Mb = 1.25MB
所以缓冲区至少要设置到1.25MB以上才能跑满千兆。如果是跨机房场景,延迟可能到30ms甚至更高,那缓冲区就要相应增大。
内核中相关的参数有三个:
# 最小、默认、最大接收缓冲区 net.ipv4.tcp_rmem = 4096 87380 6291456 # 最小、默认、最大发送缓冲区 net.ipv4.tcp_wmem = 4096 65536 4194304 # 系统级最大缓冲区 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216这里有个容易踩的坑:tcp_rmem和tcp_wmem的第三个值是自动调优的上限,但net.core.rmem_max和net.core.wmem_max是应用通过setsockopt手动设置时的上限。如果应用手动设置了缓冲区大小,那自动调优就不生效了。所以两个都要设,而且core的最大值要大于等于tcp的最大值。
我一般的配置策略是:tcp_rmem最大值设为16MB,tcp_wmem最大值设为8MB,core的rmem_max和wmem_max都设为16MB。这个配置在万兆环境下也能跑满单条连接。
3.2 连接队列与backlog调优
当新连接到达时,如果应用来不及accept,连接会先放在队列里。这个队列分两级:半连接队列(SYN队列)和全连接队列(accept队列)。
半连接队列的大小由tcp_max_syn_backlog控制,全连接队列的大小由somaxconn和应用调用listen()时传入的backlog共同决定,取两者中的较小值。
# 半连接队列 net.ipv4.tcp_max_syn_backlog = 8192 # 全连接队列 net.core.somaxconn = 32768判断队列是否溢出的方法是看ss -lnt输出中的Send-Q列。对于监听socket,Send-Q就是当前全连接队列的长度。如果这个值经常接近somaxconn,说明队列不够用,需要调大。
还有一个相关参数是tcp_abort_on_overflow。默认是0,表示队列满时丢弃新连接但不发RST,让客户端重试。如果设为1,队列满时直接发RST拒绝连接。我建议保持默认值0,因为发RST会让客户端立即感知失败,而重试机制给了服务端喘息的机会。
3.3 TIME_WAIT与端口复用的平衡
TIME_WAIT是TCP协议保证可靠性的重要机制,但它也确实会占用端口资源。在高并发短连接场景下,TIME_WAIT堆积是常见问题。
处理TIME_WAIT有几个方向:
第一,减少TIME_WAIT的产生。最根本的方法是使用长连接代替短连接,或者在应用层做连接池。如果必须用短连接,尽量让客户端主动关闭连接,这样TIME_WAIT就留在客户端而不是服务端。
第二,加快TIME_WAIT的回收。tcp_tw_reuse允许将TIME_WAIT状态的连接重新用于新的outgoing连接,但需要TCP时间戳支持。tcp_tw_recycle在较新的内核中已经被移除,不建议使用。
第三,扩大端口范围。net.ipv4.ip_local_port_range默认是32768到60999,大约28000个端口。可以扩大到1024到65535,增加可用端口数量。
net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_timestamps = 1 net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_max_tw_buckets = 262144注意:
tcp_tw_reuse只在客户端主动发起连接时生效,对服务端的TIME_WAIT没有帮助。服务端的TIME_WAIT只能等2MSL超时自动回收,或者通过减少短连接来从源头控制。
3.4 拥塞控制算法的选择
Linux默认的拥塞控制算法是CUBIC,在大多数场景下表现不错。但在高带宽长延迟的网络中,BBR通常能获得更好的吞吐量和更低的延迟。
查看当前可用的算法:
sysctl net.ipv4.tcp_available_congestion_control切换算法:
# 临时切换 sysctl -w net.ipv4.tcp_congestion_control=bbr # 永久生效 echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.confBBR的优势在于它不依赖丢包来判断拥塞,而是通过测量带宽和往返延迟来动态调整发送速率。这意味着即使在有一定丢包率的链路上,BBR也能保持较高的吞吐量。但BBR也不是万能的,在极度拥塞或者多流竞争的场景下,BBR流可能会抢占过多带宽,影响其他流的公平性。
我的建议是:内网环境优先用BBR,跨公网场景根据实际测试结果选择。切换前后一定要做对比测试,用数据说话。
4. 典型故障场景与排查实录
4.1 场景一:吞吐量上不去但延迟正常
这是最让人困惑的场景之一。ping延迟很低,但文件传输速度就是上不去。
排查思路是这样的:先确认链路协商速率,ethtool eth0看Speed和Duplex。如果是Half duplex,那问题就找到了——半双工会导致冲突和重传,吞吐量直接砍半。这种情况通常是网线质量差或者交换机端口配置问题。
如果链路没问题,接着看TCP窗口。用ss -ti查看连接的详细TCP信息:
ss -ti dst 192.168.1.100输出中的cwnd是拥塞窗口,rwnd是接收窗口,rtt是往返时间。如果cwnd很小但rtt正常,说明拥塞控制算法过于保守,可能是之前有过丢包导致窗口被压缩。如果rwnd很小,说明接收端缓冲区不够,需要调大接收缓冲区。
还有一个容易被忽略的点是中断合并。某些网卡默认开启了中断合并,在高吞吐场景下会导致延迟增加。可以通过ethtool -c eth0查看,用ethtool -C eth0 rx-usecs 0关闭或调小合并参数。
4.2 场景二:大量连接处于SYN_RECV状态
ss -s显示大量连接卡在SYN_RECV状态,这通常意味着SYN队列溢出或者SYN Cookie被触发。
先看netstat -s | grep -i listen的输出,如果有大量的SYNs to LISTEN sockets dropped,说明半连接队列满了。调大tcp_max_syn_backlog可以缓解,但根本原因可能是遭受了SYN Flood攻击,或者应用accept太慢导致队列堆积。
如果是SYN Flood,可以开启SYN Cookie:
net.ipv4.tcp_syncookies = 1SYN Cookie的原理是不在队列中保存半连接状态,而是把状态编码到SYN-ACK的序列号中。当客户端回复ACK时,服务端从序列号中解码出状态信息,直接建立连接。这样就不需要维护半连接队列了,能有效抵御SYN Flood。
但SYN Cookie也有代价:它不支持TCP选项的完整协商,比如窗口缩放、SACK等选项可能无法正常使用。所以它应该作为应急手段,而不是长期方案。
4.3 场景三:网络延迟忽高忽低
延迟抖动大,但平均延迟正常,这种问题最难排查。可能的原因包括:缓冲区膨胀、中断分布不均、CPU频率调节、虚拟机 steal time等。
缓冲区膨胀是常见原因。当网络拥塞时,数据包在缓冲区中排队,导致延迟增加。BBR算法对缓冲区膨胀有一定的抵抗能力,但根本解决方法是启用AQM(主动队列管理),比如fq_codel或CAKE。
# 查看当前队列规则 tc qdisc show dev eth0 # 设置fq_codel tc qdisc replace dev eth0 root fq_codel中断分布不均也会导致延迟抖动。如果所有网络中断都打在CPU0上,当CPU0繁忙时,网络包的处理就会延迟。解决方法是通过RPS(接收包转向)或RFS(接收流转向)把软中断分散到多个CPU。
# 开启RPS,把软中断分散到CPU 0-3 echo "f" > /sys/class/net/eth0/queues/rx-0/rps_cpus # 调整RPS流表大小 echo 32768 > /proc/sys/net/core/rps_sock_flow_entries4.4 常见问题速查表
| 症状 | 可能原因 | 排查命令 | 解决方向 |
|---|---|---|---|
| 吞吐量低但延迟正常 | 链路半双工/窗口太小 | ethtool eth0、ss -ti | 检查链路、调大缓冲区 |
| 大量SYN_RECV | SYN队列溢出 | netstat -s | grep listen | 调大backlog、开SYN Cookie |
| TIME_WAIT过多 | 短连接频繁 | ss -tan state time-wait | wc -l | 连接池、tcp_tw_reuse |
| 延迟抖动大 | 缓冲区膨胀/中断不均 | tc qdisc show、mpstat | fq_codel、RPS |
| 软中断单核跑满 | 中断亲和性未配 | cat /proc/softirqs | 多队列、中断绑定 |
| 连接建立慢 | DNS解析/路由问题 | tcpdump抓包 | 检查DNS、路由表 |
5. 实操心得与避坑经验
5.1 调优前的准备工作
在动手改任何参数之前,有三件事必须做:
第一,记录当前配置。把所有相关参数的当前值保存下来,万一调优后出现问题可以快速回滚。我一般会写一个脚本,把sysctl -a的输出保存到文件,同时记录网卡的ethtool配置和中断亲和性设置。
第二,确认业务低峰期。网络调优可能会短暂影响网络性能,比如重置网卡队列会导致连接中断。一定要在业务低峰期操作,并且提前通知相关方。
第三,准备回滚方案。每个参数的修改都要有对应的回滚命令。对于sysctl参数,回滚就是设回原值;对于网卡配置,回滚就是恢复原来的队列数和中断绑定。
5.2 参数调优的常见误区
我见过太多人把网上抄来的一堆参数直接塞进sysctl.conf,然后重启了事。这种做法非常危险,因为不同业务场景对网络的需求完全不同。
比如tcp_tw_reuse,对于客户端频繁发起短连接的场景是有益的,但对于服务端来说基本没用。再比如tcp_syncookies,开启后能防SYN Flood,但会牺牲TCP选项的协商能力,在需要窗口缩放的场景下反而会降低性能。
还有一个常见误区是盲目调大缓冲区。缓冲区不是越大越好,过大的缓冲区会导致缓冲区膨胀,增加延迟。正确的做法是根据实际的带宽延迟积来计算,然后设置一个合理的上限。
5.3 监控与持续优化
网络调优不是一锤子买卖,而是一个持续的过程。业务在变,流量模式在变,硬件在变,参数也需要跟着调整。
我建议建立一套网络性能监控体系,至少包括以下指标:
- 网卡收发速率和丢包率
- TCP重传率和连接建立成功率
- 软中断分布和CPU占用
- 连接状态分布(ESTABLISHED、TIME_WAIT、SYN_RECV等)
- 关键业务的端到端延迟
这些指标可以用Prometheus加node_exporter来采集,配合Grafana做可视化。设置合理的告警阈值,当指标异常时能第一时间发现。
5.4 一个真实的排查案例
之前遇到过一个问题:某服务的响应时间在业务高峰期会突然从50ms飙升到500ms,但CPU、内存、磁盘看起来都很正常。
用ss -s看连接状态,发现高峰期TIME_WAIT数量会暴增到几万。用nstat看TCP统计,发现重传率并不高。用tcpdump抓包分析,发现大量连接的建立时间很长,有些甚至超过了100ms。
进一步排查发现,问题出在somaxconn设置太小。默认值128在高并发场景下完全不够用,导致全连接队列频繁溢出。客户端发起连接后,服务端因为队列满而丢弃SYN-ACK,客户端只能重试,每次重试间隔1秒,所以延迟就上去了。
把somaxconn调到32768,同时把应用的listen backlog也调大,问题就解决了。这个案例告诉我,网络问题不一定出在网络层,应用层的配置同样关键。
5.5 工具链的合理搭配
最后说一下工具链的搭配。我的习惯是:
- 日常巡检用
sar和nstat,轻量且能看趋势 - 实时排查用
ss和mpstat,快速定位到具体层次 - 深度分析用
tcpdump加tshark,精准到每个包 - 性能测试用
iperf3和netperf,量化吞吐和延迟
工具不在多,在于用对场景。ss能解决的问题不要上tcpdump,nstat能看出的趋势不要靠猜。把每个工具用在它最擅长的地方,排查效率能提升好几倍。
网络性能优化这件事,说到底就是观测、假设、验证、调整的循环。没有一劳永逸的配置,只有持续不断的观察和调整。我到现在也不敢说哪个参数是“最优”的,因为最优永远是相对于当前业务场景而言的。保持敬畏,保持观测,用数据说话,这才是做性能优化该有的态度。