干了这么多年Linux运维,我见过太多人一听到“高并发”就冲过去改sysctl,把网卡队列、文件描述符、TIME_WAIT一顿调,结果压测一跑,比没调之前还慢,甚至直接把线上搞挂。说实话,Linux网络参数调优不是玄学,也不是堆数值越大越好,关键是搞清楚你的业务到底卡在哪一层,然后再动手。这篇文章我就结合自己处理过的几个高并发场景,把网络参数调优的思路、关键参数背后的原理、可复制的配置项,以及踩过的坑一次说清楚。
内容面向的是搞后端开发、运维、SRE,以及看了一堆“调优大法”却不知道怎么落地的朋友。我会从瓶颈排查开始讲,再到具体参数怎么改、为什么这么改,最后给一套我实测过的高并发参数配置,附上前后压测数据对比和踩坑记录。跟着走一遍,你应该能自己判断:“我的服务到底需不需要调网络参数,调哪些有效果,调完怎么确认有效果。”
1. 别急着改参数:先确认你的瓶颈到底在哪
高并发场景下服务反应慢,原因可能五花八门:CPU不够、数据库扛不住、应用层锁竞争、内存换页、磁盘IO慢,网络参数只是其中一环。如果连瓶颈都没定位就动sysctl,等于头痛医脚,调完只能靠运气。
1.1 高并发“卡”在哪一层?
我自己判断瓶颈有个三板斧的顺序:先看CPU和负载,再看网络连接状态,最后才翻内核统计。
- 如果CPU跑满,优先怀疑应用本身,而不是网络
- 如果CPU不高但请求大量超时,再看连接队列和丢包
- 如果CPU、内存都不高,但ab/wrk压测QPS上不去,才重点查网络参数
特别是nginx、网关这类接入层服务,很多时候瓶颈是epoll事件处理逻辑或者worker进程数不够,网络参数的锅并不大。我遇到过一台8核机器,压测时CPU直接烧到95%,同事还在拼命调somaxconn,结果真正的瓶颈是nginx worker进程只配了1个。这个坑很典型。
1.2 快速定位手段:几条命令看穿问题
先列几条我每次排查都会敲的命令,全都能在线上安全执行,放心用:
# 系统整体负载和CPU top # TCP连接状态统计,重点关注TIME_WAIT、SYN_RECV、ESTABLISHED ss -s ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn # 查看全连接队列溢出情况(Recv-Q大于0且数值大,说明accept队列塞满了) ss -lnt # 看丢包、超时等协议栈统计 netstat -s # 中断和网卡处理能力 mpstat -P ALL 1还有一个很容易被忽略的命令是dmesg。如果出现nf_conntrack: table full, dropping packet,说明连接跟踪表满了,这种情况下你调什么tcp_rmem都没用,该调的是nf_conntrack_max。我在OpenStack环境里就栽过这个跟头,搞了半天才在dmesg里看到丢包原因。
ss -lnt的Recv-Q列很有意思。如果你在nginx机器上执行后看到某个监听端口的Recv-Q长期不为0,说明accept队列满了,客户端连进来但nginx还没来得及accept。这就是典型的应用层处理不过来,而非网卡或内核的锅。这时候调大backlog只是治标,真正要做的是提升应用accept能力,或者加worker进程。
1.3 不同瓶颈对应不同参数组合
把这套逻辑整理成表格,大家排查时可以直接对号入座:
| 现象 | 大概率瓶颈 | 该动的参数 |
|---|---|---|
| CPU跑满,QPS上不去 | 应用层/worker数/锁 | 改代码、调nginx worker,别动网络参数 |
| 大量TIME_WAIT,端口耗尽 | 短连接太多/连接回收慢 | 端口范围、tw_reuse、长连接改造 |
| SYN_RECV堆积,握手慢 | SYN队列不够/半连接丢包 | tcp_max_syn_backlog、somaxconn、syn_cookies |
| Recv-Q堆积在监听端口 | accept队列满/应用accept慢 | backlog、somaxconn、应用层并发模型 |
| dmesg报conntrack丢包 | 连接跟踪表满 | nf_conntrack相关参数,而非TCP参数 |
| 带宽高延迟大,传输慢 | TCP窗口/拥塞控制 | rmem/wmem、拥塞控制算法 |
看清自己的问题属于哪一类,再往下翻参数,才不会白调。接下来我要讲的参数,基本都是针对“连接多、短连接频繁、队列积压”这几个典型高并发痛点。
2. 文件描述符、全连接队列与TIME_WAIT:第一组见效最快的配置
先说结论:在高并发场景下,文件描述符、accept队列和TIME_WAIT是三个最先捅破窗户纸的地方,也是最容易调出肉眼可见效果的地方。
2.1 文件描述符限制:第一个必须加大
Linux一切皆文件,socket也是文件描述符。默认情况下一台机器通常允许单个进程打开1024个文件描述符,这个数对高并发毫无意义。假设你的进程要维持1万个长连接,再加上日志、配置文件、临时文件占用,1024根本不够用。
调整个进程限制有两种方式。临时生效用ulimit:
ulimit -n 1048576注意这个只对当前shell及其子进程有效,重启后失效。生产环境我建议改在systemd服务文件里,因为现在大部分服务都由systemd托管:
[Service] LimitNOFILE=1048576同时还要确认内核级限制:
sysctl -w fs.file-max=2097152这里有段踩坑经历:有一次我只改了nginx的worker_rlimit_nofile,没有看systemd的LimitNOFILE,结果nginx启动后文件描述符上限还是1024(因为systemd会先约束进程资源限制再让它继承)。后来把LimitNOFILE也设成1048576才生效。所以,检查顺序别反了:先看systemd/进程的limit,再看内核fs.file-max。
fs.file-max是内核级别的全局上限,如果跑的是容器,还可能受cgroup限制影响,需要看/sys/fs/cgroup/pids.max或者/sys/fs/cgroup/.../pids.current确认是否撞到容器上限。
2.2 backlog与somaxconn:accept队列才是隐藏乡下
很多人调完文件描述符依然大量超时,问题出在accept队列。当客户端发起TCP连接,内核完成三次握手后,这个连接不是立刻交给应用层,而是先放进监听socket的accept队列,应用调用accept()后才会真正拿到连接。如果这个队列满了,新连接直接被内核丢掉,客户端表现为连接超时或者被重置。
内核里有两个参数控制这个队列:net.core.somaxconn和net.ipv4.tcp_max_syn_backlog。严格来说,listen时传入的backlog参数会被内核和somaxconn取较小值。也就是说,即使你写代码时传入backlog=1024,如果somaxconn是128,最终队列长度还是128。
nginx默认listen时backlog是511,redis默认是511,这些都依赖somaxconn支持。所以我通常直接把somaxconn调到1024以上:
sysctl -w net.core.somaxconn=2048同时把nginx配置里的backlog也写清楚:
listen 80 backlog=2048;验证方法很简单,压测的时候看ss -lnt:
ss -lnt | grep :80如果Recv-Q长期占用很高,说明队列确实在排队,调大backlog有帮助;如果Recv-Q本身一直是0或者很小,那问题根本不在队列。有个特别容易混淆的概念:这里说的accept队列是“已完成三次握手、等待应用accept”的队列,而SYN队列是“还没完成握手、等待SYN ACK”的半连接队列。tcp_max_syn_backlog管的是后者,在SYN Flood攻击或握手大量堆积时才有意义,普通高并发下基本不用优先动它。
2.3 TIME_WAIT与端口回收:调了可能踩坑的重灾区
TIME_WAIT是TCP四次挥手后主动关闭连接的一方进入的状态,会持续大约2倍的MSL,很多资料会说默认60秒。如果业务短连接多(比如Java服务直连MySQL、HTTP api频繁短连),TIME_WAIT会积压到几万个,多到占用端口耗尽。
ss -s里如果TIME_WAIT数量长期超过几万,先别急着搜“怎么快速干掉TIME_WAIT”,先想想为什么会有这么多主动关闭的连接。如果是服务端主动断开,很可能是KeepAlive超时设置太短、连接池配置不合理;如果是客户端还好,调整频率即可。
实在需要回收,有几个参数组合值得慎重。
net.ipv4.tcp_tw_reuse允许内核在TIME_WAIT状态还没结束时就复用该端口发起新的连接,前提是tcp_timestamps开启(默认开启)。这个参数对客户端主动发连接特别有效。但注意它只对outbound连接生效,且不能保证完全可靠,如果连接四元组撞车且时间戳校验出问题,偶发连接异常。
旧内核还有一个tcp_tw_recycle,这个参数在NAT环境下有大坑,会导致不同客户端被NAT成同一IP后,因时间戳递增判断异常而互相影响,出现随机性连接失败。幸运的是Linux 4.12及以后内核直接把它移除了,所以如果你用较新内核,不用纠结这个参数。如果你还在用老内核,建议直接别开。
还想从根本上减少TIME_WAIT,可以调整MSL相关的tcp_fin_timeout:
sysctl -w net.ipv4.tcp_fin_timeout=15这是一把双刃剑:调小可以加快TIME_WAIT释放,但如果网络上还有迟到的数据包,可能出现老连接数据串到新连接上的风险。好在现在的业务大多走TCP且上层有校验,风险整体可控。我会强调一点:别把tcp_fin_timeout调得太极端,比如小于5,我在生产上试过10~15比较平衡。
端口资源也是隐蔽瓶颈。客户端发起大量短连接时,需要占用本地端口,默认范围是32768~60999,大概不到3万个端口。如果TIME_WAIT又占着端口,很快就耗尽。可以适当扩大:
sysctl -w net.ipv4.ip_local_port_range=1024 65000但别把起始端口设成1024以下,那里有特权端口和一堆系统服务占用,容易冲突。同时也别指望靠扩大端口范围解决一切,因为端口总数就那么多,核心还是让连接复用和变成长连接。
3. TCP缓冲区、窗口与拥塞控制:在内核协议栈里抠带宽
连接数搞定之后,下一个性能瓶颈往往是吞吐量。这部分和上面完全两码事:连接多不一定慢,但传输慢会让你觉得“系统好像卡了”。TCP传输性能主要由缓冲区大小、窗口缩放和拥塞控制算法三件事决定。
3.1 缓冲区大小不是越大越好:BDP才是依据
TCP的发送和接收缓冲区决定了单条TCP连接能缓冲多少未确认数据。如果缓冲区太小,即使带宽很大,发送方也得停下来等ACK,等同于把高带宽链路跑成了低带宽。这里的核心概念是BDP(Bandwidth-Delay Product,带宽延迟积)。
BDP = 带宽 × RTT
举个例子,假设你的业务是10Gbps内网,RTT是0.5ms,那么BDP = 10Gbps × 0.0005s = 5Mbit = 0.625MB。如果是跨机房场景,带宽1Gbps,RTT=50ms,BDP = 1Gbps × 0.05s = 50Mbit = 6.25MB。也就是说,单条TCP连接的缓冲区要能装下6.25MB,才能把1Gbps带宽跑满。
Linux默认的net.ipv4.tcp_rmem和net.ipv4.tcp_wmem是三个值(最小值、默认值、最大值),单位是字节:
sysctl net.ipv4.tcp_rmem # 通常输出类似 4096 131072 6291456中间那个默认值会自动调节,但如果要跑大流量、长肥网络,建议把最大值调大。我常用的配置是:
sysctl -w net.ipv4.tcp_rmem='4096 87380 16777216' sysctl -w net.ipv4.tcp_wmem='4096 65536 16777216'注意,tcp_rmem的最大值还受到net.core.rmem_max限制,tcp_wmem受net.core.wmem_max限制,需要一并调大,否则写了也不生效。
另外一个容易忘的事:接收窗口扩大要靠net.ipv4.tcp_window_scaling,默认是1(开启),一般不用动,但老内核或某些裁剪内核可能为0,那就得打开。
这里必须提醒一句:缓冲区不是越大越好。如果单条连接的缓冲区设置到16MB,1万个连接意味着最大可能占用160GB内存,直接把机器搞OOM。所以在比值调优时一定要结合连接数估算内存,别光看大数字爽。
3.2 拥塞控制算法:CUBIC够用,BBR给网络情况差的人惊喜
拥塞控制决定TCP在链路出现丢包和延迟时怎么调整发送速率。传统CUBIC对通用网络友好,是默认选择。如果链路高带宽高延迟,且偶发丢包,CUBIC遇到丢包后退避太激进,带宽利用率下降;这时候Google的BBR实测有奇效。
查看和切换算法:
# 查看当前可用算法 sysctl net.ipv4.tcp_available_congestion_control # 切换成BBR(需要内核4.9+,且内核编译了BBR模块) sysctl -w net.ipv4.tcp_congestion_control=bbr要持久化,写进/etc/sysctl.d/99-network-tuning.conf:
net.ipv4.tcp_congestion_control=bbr我实测的一个场景是公网跨地域数据传输,带宽约200Mbps,RTT约100ms,中间偶尔丢包。默认CUBIC时吞吐只有带宽的三分之一,切到BBR后几乎跑满,延迟还有改善。而在低延迟数据中心内部,CUBIC和BBR差距很小,此时没必要折腾。
如果你的内核没有编译BBR,升级内核或者使用发行版自带的内核模块(如tcp_bbr,可以用modprobe tcp_bbr加载)后再说。别在没模块的情况下硬写配置,不生效还误导判断。
3.3 杂项参数:keepalive、SYN重试与netdev_max_backlog
一些杂项参数单独看不显眼,组合起来效果很明显。我常用的有这几组:
# 连接空闲多久开始探测,以及探测间隔、失败次数 sysctl -w net.ipv4.tcp_keepalive_time=600 sysctl -w net.ipv4.tcp_keepalive_intvl=30 sysctl -w net.ipv4.tcp_keepalive_probes=3 # 半连接/全连接相关 sysctl -w net.ipv4.tcp_max_syn_backlog=4096 sysctl -w net.core.netdev_max_backlog=10000 # 减小握手失败等待时间 sysctl -w net.ipv4.tcp_syn_retries=3 sysctl -w net.ipv4.tcp_synack_retries=3netdev_max_backlog是网卡接收队列的积压上限,突发流量进来时如果协议栈处理不过来,这个队列满就会丢包。调大它对突发流量有缓冲,但也不会神奇地提升稳定QPS。tcp_syn_retries控制SYN重传次数,默认5,对公网客户端可以适当调低,让快速失败的请求尽快结束,减少半连接占用。
这里有个小技巧:keepalive参数为什么要调?因为很多业务层没有心跳,连接又是TCP长连接,如果对端崩溃或者网络闪断,你不调整keepalive,可能很久都发现不了死连接,连接数被死连接占满。我调过一台连接数2万的机器,里面至少有3000个死连接,调低keepalive_time后,僵尸连接被清理,连接数马上降下来。
4. 一套实测过的调优组合:压测前后对比与参考配置
看了这么多理论,咱们落一个完整场景。前阵子有个业务叫我帮忙处理:nginx反向代理到两台后端Tomcat,并发峰值8000,压测2000并发时就大面积超时,TIME_WAIT堆积接近4万,偶尔还有连接被重置。
4.1 测试环境与操作流程
测试机配置:8核16G,网卡千兆,CentOS 7.9,内核3.10。压测工具用wrk,从另一台机器发起,目标URL是一个经过nginx转发的小接口,后端返回JSON约40字节。压测命令:
wrk -t8 -c2000 -d120s http://target/api/ping先采集基线数据,然后逐项调整参数,每改完一组就重新压测,最后汇总对比。
调整参数过程我分了四步,每次只动一组,避免说不清是哪个参数起的作用:
第一步,改文件描述符和accept队列; 第二步,改TIME_WAIT和端口范围; 第三步,改TCP缓冲区和拥塞控制; 第四步,重新压测、观察连接状态。
这里要提一个重要习惯:每改一组参数,立刻压测并记录结果,不要四组一起上。否则线上出了问题你根本不知道是哪一个参数引入的,回滚都无从谈起。
4.2 前后数据对比
基线数据(未调优):QPS约4200,P99延迟220ms,2000并发下错误率6.5%,TIME_WAIT峰值38000,端口耗尽导致connect: Cannot assign requested address报错频繁出现。
第一轮调整后主要是文件描述符、somaxconn、backlog、端口范围,QPS提到5800,错误率降到2.1%,最明显的是TIME_WAIT不再导致端口耗尽了,log里的connect报错消失。
第二轮调整加上tw_reuse、把短连接往keepalive长连接方向改,并放宽TCP缓冲区后,QPS到6700,P99延迟降到95ms,错误率0.3%。
完整参考配置见下方sysctl文件。注意这是针对这台机器和业务压出来的,可以参考,别直接抄到你所有机器上,内存小、连接模式不同的机器需要微调。
4.3 一份可直接落地的参考配置
cat /etc/sysctl.d/99-network-tuning.conf # 内核文件句柄上限 fs.file-max = 2097152 # 单条TCP连接收/发缓冲区(最小值 默认值 最大值) net.ipv4.tcp_rmem = 4096 87380 16777216 net.ipv4.tcp_wmem = 4096 65536 16777216 # 内核层缓冲区上限 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 # 监听队列 net.core.somaxconn = 2048 net.ipv4.tcp_max_syn_backlog = 4096 net.core.netdev_max_backlog = 10000 # TIME_WAIT与端口 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_tw_reuse = 1 net.ipv4.ip_local_port_range = 1024 65000 # keepalive net.ipv4.tcp_keepalive_time = 600 net.ipv4.tcp_keepalive_intvl = 30 net.ipv4.tcp_keepalive_probes = 3 # 握手重试 net.ipv4.tcp_syn_retries = 3 net.ipv4.tcp_synack_retries = 3 # 拥塞控制 net.ipv4.tcp_congestion_control = cubic然后执行:
sysctl --system # 或者老版本用 sysctl -p /etc/sysctl.d/99-network-tuning.conf提醒一句:sysctl --system会把所有配置文件都加载一遍,如果已有文件里有和这个文件相同的键,后加载的生效,优先级要看目录顺序。建议先检查现有配置:
sysctl -a | grep '^net\.'避免改完冲突。这个配置里我把拥塞控制写了cubic,因为我这台机器内网通信,BBR收益不大。如果你跑公网大流量跨地域,按前面讲的那套再换成bbr即可。
4.4 验证效果的正确方式
压测完成后,别只看QPS,还要看TCP连接状态分布。我用这几条命令做持续观察:
# 每秒打印一次tcp状态统计,持续30秒 for i in $(seq 1 30); do ss -s; sleep 1; done # 看特定监听端口队列情况 watch -n 1 'ss -lnt | grep :80' # 看网络协议栈的错误计数 watch -n 1 'netstat -s | grep -E "timewait|listen|overflow|retransmit"'重点观察:TIME_WAIT是否还堆积、ss -lnt的Recv-Q是否一直高位、netstat -s里的listen queue overflow是否持续增加。如果升级参数后,这些数字都降下来了,才叫真正调优成功。
另外,最好结合业务监控看:压测时后端Tomcat的线程池使用率、GC频率、连接池活跃数都要看。网络参数调好了,但如果后端线程池被打满,前端表现还是超时,那就继续往后端排查。参数调优不是独立事件,永远是全链路协作的一部分。
5. 线上踩过坑之后:调优的边界、回滚与监控
最后这部分才是真正区分“背参数”和“会调优”的分界线。我在生产环境里因为乱调网络参数出过几次事故,都记在这里了,希望大家能避开。
5.1 三个真实踩坑记录
第一个坑:盲调tcp_rmem/tcp_wmem导致内存暴涨。当时为了追求单连接拉满带宽,把rmem/wmem最大值都设成了64MB,结果这台机器扛了两万连接,内存直接飙到可用内存的90%,随后OOM killer把主要进程杀了,线上服务中断。后来我估算了一下:两万连接每个都开大缓冲区,最坏情况几百GB内存,完全超出物理内存。
教训:调缓冲区之前先做数学题。公式很简单,最大内存占用 = 连接数 × (tcp_rmem最大 + tcp_wmem最大)。8G内存的机器,如果连接数1万,那缓冲区最大支撑就是8G/10000 ≈ 800KB,再高就是赌并发不会同时开满。合理做法是区分业务:下载/传输型服务才调大缓冲区,短请求型服务保持默认小缓冲区即可。
第二个坑:开启了tcp_tw_recycle在NAT网络下翻车。那台老机器内核3.10,同事看TIME_WAIT太多,直接开了tcp_tw_recycle=1,结果上线后用户随机反馈“部分请求连不上,刷新几次又好了”,排查过程极其痛苦。最后定位到是NAT网关后面多个客户端共用一个出口IP,tw_recycle的时间戳机制误判旧连接包为“过期”,直接丢弃。
教训:新内核无此参数,老内核千万别碰tcp_tw_recycle。tcp_tw_reuse对客户端出方向连接比较安全,能不用尽量不用,先考虑改长连接和调小fin_timeout。
第三个坑:改了net.core.somaxconn=2048之后重启服务发现nginx反复失败。排查了很久才发现,nginx配置里我写了listen 80 backlog=2048,但系统里旧版本nginx的listen指令有最大上限,或者没有这个backlog选项的权限,编译模块有限制。更诡异的是某些应用内部listen传入的backlog参数是硬编码的,根本不读你系统的somaxconn,你外面调再大也无效。
教训:改完核对实际生效值。用ss -lnt看当前监听端口的Send-Q列,这个值显示的就是listen实际使用的backlog(和somaxconn取较小值后的结果),如果Send-Q显示还是128,说明应用传的backlog太小,或者somaxconn改动没生效,得继续往上找原因。
5.2 参数变更流程与回滚方案
运维老手都知道,改sysctl最容易闯祸的地方不是参数本身,而是过程不对。我给自己定了一套固定流程:
- 把要改的参数写进独立的
/etc/sysctl.d/99-tuning.conf,不直接在/etc/sysctl.conf上改,方便回滚时直接删除整个文件 - 执行前用
sysctl -a > /tmp/sysctl-before-$(date +%F).txt做全量备份 - 用
sysctl -w临时改一遍,验证没有副作用后再写配置 - 回滚时直接删除配置文件和
sysctl -w重新设回旧值,或重启机器后自然回到默认
有一点要知道:sysctl -w改的可以立即生效,但重启后恢复默认;写入配置文件的要重启或者执行sysctl --system才生效。所以推荐的做法是先用-w临时验证,确认可行后写文件,再执行sysctl --system永久生效。这样既不用反复重启,也能快速回滚。
回滚还有一个容易被忽视的地方:一次改了很多键,回滚时分不清哪些是上次改的。所以每次变更先在配置文件里写好注释,标注变更日期、原因、预期效果。调优不是一个人的事,后来接手的人看到注释才知道这套参数是为了解决哪一次压测事故,否则下一个人莫名其妙把这些参数当成“默认配置”,出问题也不知道从何查起。
5.3 持续监控的意义:调完不等于结束
参数调完,压测好看,不代表线上就万事大吉。流量模型会变,连接数会涨,内核参数还需要动态观察。我的习惯是压测结束后继续观察至少3天,重点看这几个指标的趋势:
- TCP连接状态分布(TIME_WAIT、ESTABLISHED、SYN_RECV、CLOSE_WAIT)
- 端口使用率(
/proc/sys/net/ipv4/ip_local_port_range范围内已用端口数) - 网卡丢包和重传率(
netstat -s里的retransmit、listen queue overflow) - 内存中用于socket缓冲区的部分(可以用
ss -m看每条连接占用)
把这些指标接到监控系统里,设置告警阈值。如果过了几天TIME_WAIT重新堆起来,说明业务方的连接池配置又变了,或者流量模型变了,需要重新评估。
我平时还会用一条简化命令看端口是否接近耗尽:
cat /proc/net/sockstat # 重点关注 TCP: inuse 数值,和 # /proc/sys/net/ipv4/ip_local_port_range 做减法估算剩余端口/proc/net/sockstat里的inuse是当前正在用的TCP socket数量,虽然不直接等于本地端口占用数,但趋势很有参考价值。配合ss -tan | wc -l,基本可以判断连接数是否逼近上限。
回到最开始那句话:调优最难的从来不是敲那几行sysctl,而是找到值得调的参数、确认它真的生效、并且能在出问题时快速回滚。把这套方法练熟了,以后再遇到高并发网络问题,你至少不会被网上那些“万能调优大法”牵着鼻子走了。
最后再分享一个小技巧:改完任何网络参数,先不要急着上生产全量,找一台低峰期机器先压测观察,确认没有异常后再推全量。这套流程我用了很久,几乎可以杜绝网络参数变更引发的线上事故。