恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
网络协议栈优化实战:定位软中断与内核参数瓶颈,QPS从8千提升到2万
首页
资讯中心
/
网络协议栈优化实战:定位软中断与内核参数瓶颈,QPS从8千提升到2万
网络协议栈优化实战:定位软中断与内核参数瓶颈,QPS从8千提升到2万
发布时间:2026/10/22 8:05:28
今天想聊聊网络协议栈优化这件事。半年前我接手了一个内部网关项目的性能问题排查单机压测QPS卡在八千左右上不去CPU的sys占用却已经彪到35%软中断占比接近18%怎么加机器都感觉不划算。后来沿着中断、队列、socket参数、内核内存这几层一层层拆最后把单机吞吐拉到两万以上p99延迟还降了40%。整个过程让我对“网络协议栈优化”有了更系统的认识它不是一个sysctl参数列表而是一套先定位热点、再有针对性地调整的工程方法。这篇文章我会用这个模拟项目X的真实排查过程来串起全部内容覆盖业务侧socket层优化、内核协议栈参数调优、网卡队列和软中断处理以及进阶的用户态协议栈思路。无论你现在是在调高并发服务、网关还是中间件只要你觉得机器CPU很高、压测上不去、连接老超时这篇内容应该能帮你理清排查方向。1. 先把瓶颈聊清楚协议栈到底在忙什么1.1 别急着调参数先看数据拍到哪一环节很多人一上来就搜“网络优化参数”然后照着网上的配置一通改。这种做法我不太推荐因为没有基线改完你也不知道哪个参数起了作用甚至可能越调越差。正确姿势是先量化瓶颈。我当时用的工具组合很简单top看进程CPU、mpstat -P ALL 1看每核软中断、pidstat看线程级占用、perf top看内核函数热点。压测命令是wrk模拟项目X的短连接场景一轮压测下来记录了几个关键指标应用进程CPUusr约28%sys约35%软中断si约18%并且集中在两个核上netstat -s里listen queue overflow在增长本机吞吐到8000 QPS后延迟开始从5ms直接跳到30ms以上这个组合说明什么usr占比不高说明业务代码不是瓶颈sys高、软中断集中、accept队列溢出说明瓶颈就是内核协议栈处理路径本身——从网卡中断、软中断处理到socket层分发整个链路在高压下开始排队。这时候再去调协议栈方向就对了。1.2 一次数据包的网络“旅行”里藏着哪些优化机会想精准优化得先理解一个数据包从网线到业务代码要经过哪些关卡。以TCP收包为例大概是这样的路径网卡收到数据帧通过DMA写入内存环形队列网卡触发硬中断通知CPU有包到了内核进入NAPI轮询关硬中断、开软中断批量处理包软中断里做协议解析IP层、TCP层处理查找socket数据拷贝到socket接收缓冲区唤醒等待中的进程应用通过read/recv从socket缓冲区拿数据这六步里每一步都有对应的优化空间。硬中断可以靠多队列RSS分散到多个核NAPI轮询可以调整权重和合并参数TCP层可以优化内存、拥塞控制、连接表socket层可以调整backlog、复用端口、关闭Nagle。关键是要根据热点来决定动哪里。如果你的瓶颈在步骤1和4那调socket参数没什么用如果瓶颈在步骤6那内核网络参数即使改得再漂亮也帮不上忙。这就是为什么要先数据、后动刀。2. 优化前的基本功像体检一样先建立性能基线2.1 压测场景设计长短连接要分开测协议栈优化最怕笼统。连接建立过程涉及SYN队列、accept队列、TIME_WAIT状态这些只在短连接场景下才是热点而长连接大量读写场景下热点会转移到send/recv路径、内存拷贝和锁竞争。所以我当时把压测拆成了两个场景短连接场景模拟客户端每次请求都新建TCP连接主要考验三次握手、accept分发、四次挥手的处理能力长连接场景连接建立后持续发送小包主要考验数据收发路径、缓冲区管理、CPU亲和性每个场景至少压15分钟取稳定段数据而不是只看峰值。峰值只能反映几秒钟的突发能力对真实容量规划意义不大。稳定段数据中要记录QPS、平均延迟、p99延迟、sys占用、软中断占比、收包丢包计数。2.2 几个必须看懂的内核计数基线建立过程中我建议重点盯这几个文件它们比压测工具的统计更接近内核真实状态指标查看方式含义需要注意的异常值软中断分布mpstat -P ALL 1各CPU的si占比集中在一个核说明分发不均收包队列drop/proc/net/softnet_stat每行第三列是softnet backlog溢出计数持续增长说明NAPI处理不过来accept队列溢出netstat -sgrep ListenOverflows三次握手完成但应用没accept客户端connect偶发超时重传率netstat -sgrep retransTCP重传次数高重传往往伴随延迟抖动socket内存压力ss -m和cat /proc/net/sockstat各socket内存占用tcp mem持续顶到上限TIME_WAIT数量ss -s主动关闭连接产生的状态几万以上会占内存这些指标要压测前、压测中、压测后各记录一轮形成对比。我第一次记录时softnet_stat的dropped列在压测过程中每10秒涨几千这就让我很确定网卡中断和NAPI处理已经忙不过来了而不是业务代码在拖后腿。3. 应用层与socket层优化改改代码就能捡回不少性能3.1 SO_REUSEPORT让多进程分摊accept锁竞争模拟项目X的服务端最早是单进程accept模型后来改成了多进程但多个进程共用一个监听socketaccept的时候还是要抢同一把锁。压测时能看到某个核的sys特别高说明锁竞争真实存在。解决方案是把监听socket改成SO_REUSEPORT方式每个worker进程都独立创建监听socket设置相同的地址端口由内核根据四元组哈希自动把新连接分发到不同进程。这样每个进程有自己的accept队列和锁基本消除accept竞争。int fd socket(AF_INET, SOCK_STREAM, 0); int reuse 1; setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, reuse, sizeof(reuse)); bind(fd, ...); listen(fd, 128);几个注意点Linux 3.9之后才支持生产环境基本都满足内核分发连接时是按四元组哈希同一个连接固定落在同一个进程上省掉了跨进程迁移的开销如果业务要负载均衡可以开启eBPF或者配合其他LB单纯依赖REUSEPORT的话短连接场景下各进程的负载比较均匀但长连接场景受哈希分布影响可能不均衡每次修改worker数量后要观察一段时间不能频繁启停否则会有一段TIME_WAIT堆积实测效果挺明显QPS从8000提到了12000左右sys占用降了一截。3.2 TCP_NODELAY与延迟ACK的取舍内部服务大多是小包交互如果开了Nagle小包会在缓冲区里等着凑满MSS再发延迟很容易到40ms级别。同时内核的延迟ACK机制最多可等待40ms两者叠加一次交互的浪费非常夸张。模拟项目X的RPC调用是典型的请求-响应模型我直接在服务端和客户端都设置了TCP_NODELAYint flag 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, flag, sizeof(flag));关闭Nagle之后每个小包立即发出延迟明显下降。代价是网络包数量上升小包占比高的场景会略微增加CPU开销。对于延迟敏感且包小的业务来说这个取舍完全值得。除了Nagle还可以留意TCP_QUICKACK它能让内核在收到数据后立即回ACK不用等延迟ACK计时器。设置方式类似但要注意它是一次性的read之后可能被清除长连接可能需要周期性设置。3.3 backlog和somaxconnaccept队列打满的隐形杀手前面提到压测时listen queue overflow在增长我第一反应就是accept队列不够。查了代码发现listen传的是128net.ipv4.tcp_max_somaxconn系统参数是4096看起来没毛病。但这里有个坑内核实际取的accept队列长度是min(应用listen backlog, tcp_max_somaxconn)。如果应用只传了128那系统参数再大也没用。所以我把代码里的backlog直接提到了1024同时把net.core.somaxconn保持4096。调整后再用ss -lnt查看监听端口的Send-Q列它会显示实际accept队列长度。溢出不再增长连接超时问题大部分消失。这个细节如果不现场看数据很容易被忽略——很多文章只告诉你调somaxconn却没告诉你应用层的listen参数同样关键。4. 内核与驱动层调优把队列、软中断和内存都理顺4.1 网卡多队列与RSS让每个核都有自己的收包队列软中断集中在两个核这个问题靠应用层参数解决不了得看网卡多队列。通过ethtool -l eth0查看网卡队列数默认只有4个队列但机器有32个物理核这意味着只有4个核在承担所有收包中断其他核都在围观。用ethtool -L eth0 combined 16把队列扩到16个再配合RSS哈希让不同连接分散到不同队列和CPU。这里要注意如果网卡队列数超过实际CPU核数或者跨NUMA节点反而会导致内存访问变慢。我当时的处理是每个NUMA节点对应的核心组各自处理本节点的队列用smp_affinity把队列中断绑到指定CPU范围。操作过程中有个小技巧绑定中断时先看cat /proc/interrupts里各队列的中断号然后用echo CPU掩码 /proc/irq/中断号/smp_affinity来绑。比如6号中断绑到CPU2-3掩码就是0xC。绑定之后软中断分布均匀了很多从两个核打满变成八个核平滑分担单核瓶颈解除。整个系统的吞吐直接迈过了一个台阶到了15000 QPS左右。4.2 RPS/RFS没有多队列时的软件分发补充如果网卡本身不支持多队列或者队列数不够可以用RPSReceive Packet Steering来做软件层面的收包分发。RPS会把收到的包按哈希分散到指定CPU的backlog队列里相当于在软中断层做负载均衡。RPS相关配置在/sys/class/net/eth0/queues/rx-0/rps_cpus和rps_flow_cnt里。比如要让CPU2到CPU7处理收包echo fc /sys/class/net/eth0/queues/rx-0/rps_cpus echo 4096 /sys/class/net/eth0/queues/rx-0/rps_flow_cntRPS只是解决了“哪个CPU处理包”的问题但如果应用进程和收包CPU不在同一个核上缓存命中率会很差。RFS则更进一步它会尽量让处理某条流的CPU和应用读取这条流的CPU一致减少跨核访问。注意RPS/RFS会额外消耗CPU因为包要在软中断里做哈希和队列分发。如果本身网卡多队列已经够用不建议再开RPS否则就是重复劳动。我当时是网卡扩队列后RPS就关了避免两套逻辑互相干扰。4.3 中断合并与NAPI预算延迟和吞吐的平衡网卡驱动支持中断合并也就是攒一批包再通知CPU能显著降低硬中断次数。这个参数是一把双刃剑合并多了吞吐上去了但单包延迟也会增加。查看当前合并参数ethtool -c eth0调整示例ethtool -C eth0 rx-usecs 20 rx-frames 8rx-usecs 20表示最多等20微秒再上报rx-frames 8表示攒够8个包立即上报。对于模拟项目X这种延迟敏感服务我选的是偏保守的组合rx-usecs 8、rx-frames 4保证延迟的同时硬中断次数也能降不少。NAPI的轮询预算在/sys/class/net/eth0/gro_flush_timeout和内核参数里也能调但对普通业务来说默认值够用不用刻意动。想进一步低延迟可以试试busy poll不过会额外占满CPU我建议只在某些专门的高频交易类场景用常规服务性价比不高。4.4 TCP缓冲区与内存参数不要让它成为丢包元凶TCP接收/发送缓冲区太小会导致吞吐上不去太大又会导致内存浪费和突发延迟。合理做法是根据业务带宽延迟积来估算。公式很简单带宽延迟积 带宽 × RTT。假设服务需要跑满2GbpsRTT约0.5ms那单个连接的接收窗口至少需要2Gbps × 0.0005s ≈ 125KB。我基于这个估算设置了net.ipv4.tcp_rmem 4096 262144 6291456 net.ipv4.tcp_wmem 4096 262144 6291456这里三个数分别是最小值、默认值、最大值。最小值太低会导致小包场景性能受限默认值按业务平均并发来设即可。tcp_mem控制整个系统的TCP内存上限按页为单位net.ipv4.tcp_mem 393216 524288 786432这三个数分别是low、pressure、max。低于low不回收超过pressure开始收缩到max后系统会拒绝新的TCP内存分配表现为丢包或者连接异常。这个参数需要结合服务器物理内存来设我当时的机器是64GB内存设置如上正常工作压力下不会触碰pressure。还有几个连接表相关参数net.core.somaxconn 4096 net.ipv4.tcp_max_syn_backlog 8192 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_tw_reuse 1tcp_tw_reuse允许客户端复用TIME_WAIT连接对主动发起大量短连接的服务端有效。但注意不要开tcp_tw_recycle它在NAT环境下会误判时间戳导致丢包而且新版内核已经移除这个坑很多人还在网上传。5. 进阶路线当常规优化到顶可以看什么5.1 协议栈旁路从内核态到用户态如果常规参数调到极限sys占用还是高转发路径还是长那就得上更硬核的方案绕过内核协议栈直接在用户态处理网络包。典型做法是用DPDK这类技术把网卡收包直接映射到用户态内存应用自己实现或借助用户态协议栈完成TCP处理。这个方向能带来巨大的性能提升但代价也高业务代码改动量大需要自己处理连接状态、重传、拥塞运维团队需要新的技能栈调试难度也上了一个级别。模拟项目X评估后没有选这条路因为大多数业务场景下把常规优化做到位已经能拿到80%以上的收益用户态协议栈的复杂度得不偿失。如果未来对单机吞吐要求到几十万甚至百万QPS且业务逻辑极简、连接生命周期短那用户态协议栈会是值得考虑的方向。5.2 从TCP走向RDMA和新型传输另一种绕开协议栈瓶颈的思路是换传输层协议比如RDMA。RDMA允许数据直接从应用内存到网卡绕过内核协议栈和内存拷贝延迟和CPU占用都非常低。但它对网络设备有硬性要求需要专门的网卡和交换机支持网络拓扑也得配合。对于跑在通用云环境里的业务基本不具备落地条件。我的建议是先把TCP协议栈调明白如果还不够再评估业务是否真的要上RDMA。很多项目其实卡在应用层锁、序列化、GC这些问题上并不是TCP协议栈不行。6. 常见问题与排查技巧实录6.1 症状到原因对照表下面这张表是我在多个项目里沉淀出来的快速定位表遇到类似症状可以直接对号入座症状可能瓶颈排查命令解决方向sys高、单核打满中断/软中断不均衡mpstat -P ALL 1网卡多队列、RPS、绑核connect偶发超时accept队列溢出netstat -sListenOverflows调大listen backlog/somaxconnp99延迟抖动Nagle延迟ACK抓包看TCP时间戳开TCP_NODELAY/QUICKACK重传率高缓冲区不足/丢包netstat -sretrans调整tcp_rmem/wmem、排查链路内存飙升TIME_WAIT过多ss -s开tw_reuse、调整连接复用QPS上不去但CPU不高锁或应用瓶颈perf top排查用户态锁等待这里要特别提醒表里的“解决方向”不是单一操作每一条都要结合压测数据交叉验证。比如重传率高既要看socket缓冲区也要看网卡是否有drop还要确认对端负载不能只调内核参数。6.2 三个我踩过的坑坑一盲目调大tcp_rmem。我第一次把rmem最大值调到了16MB想着大一点总没错。结果高并发下内存占用爆涨部分连接内存超限反而触发内核回收延迟抖动更严重了。后来按带宽延迟积算出需要的值控制在合理范围内问题消失。坑二只调somaxconn忘了应用backlog。前文说过两次参数会取最小值应用层不配合系统层参数等于白改。这类问题的排查方法是ss -lnt看Send-Q列不要只看系统配置。坑三同时开网卡多队列、RPS、irqbalance。三套机制同时工作时中断和软中断的分布反而乱了部分队列空转、部分队列排队。最后明确分工硬中断绑核由网卡队列和smp_affinity负责软件分发交给RPS二者只保留一套。6.3 优化结果复盘模拟项目X经过这一轮优化后单机短连接场景QPS从最初的8000提升到20000p99延迟从30ms降到18mssys占比降到22%软中断从集中在2个核变成均匀分布在8个核上。长连接场景的提升更明显单连接小包吞吐增加了约70%。整个过程中我最大的体会是网络协议栈优化不是背参数而是建立“数据→瓶颈→方案→验证”的闭环。每次只改一个可变项记录前后数据避免一次改多个参数导致结论说不清。如果你也是在处理类似的性能问题建议先动手记录软中断分布和accept队列状态大概率能找到下一步的方向。