
1. 高并发压测时 CPU 只跑到 60% 却上不去吞吐瓶颈到底在哪如果你在做 Linux 服务器高并发压测大概率见过这种画面top里 CPU 总利用率卡在 60% 左右但吞吐量死活上不去si软中断那一列却高得离谱netstat -s里pruned或者drop在涨。这时候很多人第一反应是应用层代码有问题去翻 epoll 逻辑、查线程池结果折腾半天发现代码没毛病——问题出在 Linux 网络协议栈本身。我先把结论摆出来这类现象通常不是单一原因而是三个环节叠加的结果。第一是硬中断分配不均所有网卡队列的中断都挤到 CPU 0导致单核软中断过载其余核心在围观第二是 TCP 收发缓冲区TCP Buffer没匹配上带宽延迟积单连接吞吐被窗口卡死第三是 epoll 唤醒和连接队列参数不合理突发连接时 SYN 队列溢出。这三个环节对应的是数据包从网卡到用户态的完整路径任何一段堵住整条链路都跑不满。这篇文章面向的是正在做 Linux 网络性能调优的运维和后端同学尤其是手里有 32 核以上物理机、跑高并发 TCP 服务的场景。我会从网卡中断亲和性绑定讲起到软中断分配RPS/RFS再到 TCP Buffer 参数配置每一层都给出可复制的ethtool、sysctl配置片段和压测验证步骤。你不需要一开始就全量套用可以按层定位、逐项验证。需要说明的是调优不是参数越大越好。我在生产环境踩过的坑里至少一半是因为照搬了网上所谓的高性能配置把tcp_rmem的 max 设到 64MB结果并发一上来内存压力检测反而把 Buffer 收缩得更狠吞吐出现断崖式波动。所以下面每个参数我都会说清楚它的适用边界和副作用。另外如果你在调优过程中需要快速验证某个模型服务或 API 的响应表现可以用 TaoToken 模型对话 做对照测试把网络层调优前后的请求延迟数据拉出来对比比单纯看wrk的吞吐数字更直观。2. 数据包从网卡到用户态的完整路径与瓶颈定位方法在动手改参数之前你得先知道数据包到底走了哪些路。很多调优失败的原因就是不知道自己在改哪一段。完整路径大致是这样网卡收到数据包DMA 写入 Ring Buffer触发硬中断 IRQIRQ 被路由到某个 CPU 核心硬中断处理程序启动 NAPI 轮询然后触发软中断NET_RX_SOFTIRQ内核协议栈处理 IP/TCP 层新连接走三次握手进连接跟踪表已建立连接的数据进 TCP 接收队列epoll 收到就绪事件唤醒用户态应用recv/read把数据取走。这条链路里有三个高频瓶颈点IRQ 路由决定软中断分布、软中断处理影响协议栈吞吐、接收缓冲区限制单连接性能。定位方法很直接先看/proc/softirqs里NET_RX在各 CPU 上的分布。如果某个核心的数字是其他核心的十几倍那中断亲和肯定有问题。再看ss -tnm里Recv-Q是否持续堆积堆积说明应用取数速度跟不上或者 Buffer 不够。最后用perf top -g看热点函数如果大量时间花在__netif_receive_skb或tcp_v4_rcv说明协议栈处理是瓶颈。2.1 中断亲和性绑定与 irqbalance 的取舍现代网卡基本都支持多队列RSS每个队列对应独立的硬中断号。你可以通过cat /proc/interrupts | grep eth0看到类似eth0-TxRx-0到eth0-TxRx-15的中断条目。默认情况下irqbalance守护进程会自动分配但它的均衡策略偏保守在高并发场景下经常把多个队列塞到同一个核心。手动绑定的做法是往/proc/irq/{irq}/smp_affinity写十六进制位掩码。比如要把 IRQ 128 绑到 CPU 3掩码就是8二进制 1000。批量绑定时用模运算轮转分配确保每个队列落到不同核心。这里有个关键动作绑完必须停掉irqbalance否则它过一会儿就把你的设置覆盖了。命令是systemctl stop irqbalance systemctl disable irqbalance。对于虚拟化环境里不支持 RSS 的网卡就得靠 RPSReceive Packet Steering在软件层分发。RPS 通过/sys/class/net/eth0/queues/rx-0/rps_cpus设置把接收处理分散到多个 CPU。RFS 是 RPS 的增强版它会把数据包路由到处理该连接的应用线程所在 CPU提升缓存命中率。实测下来 RFS 相比纯 RPS 能降低 15% 到 20% 的处理延迟但前提是应用线程绑定了固定 CPU否则线程频繁迁移会让 RFS 路由目标乱跳缓存反而失效。2.2 TCP Buffer 与带宽延迟积的计算TCP 接收缓冲区大小直接决定单连接吞吐上限。理论最优值由带宽延迟积BDP决定BDP 带宽 × RTT。举个例子10Gbps 链路、1ms RTTBDP 约 1.2MB。如果你的tcp_rmem最大值低于这个数吞吐量永远达不到链路容量因为接收窗口开不大。但这里有个陷阱内核的自动调优tcp_moderate_rcvbuf在高并发下会失效。当并发连接超过数千时内存压力检测机制会过度收缩 Buffer导致吞吐断崖式下降。所以高并发场景建议手动设定tcp_rmem和tcp_wmem的 min/default/max 三档值而不是完全交给内核自动调。max 值要按 BDP 留 2 倍余量同时结合物理内存和预期并发数反推避免内存被 Buffer 吃光。3. 可复制的 sysctl 与 ethtool 全链路调优配置片段下面这套配置是我在 64 核物理机、万兆网卡、高并发短连接场景下验证过的。你可以把它拆成三部分逐项应用不要一次性全量刷上去。先备份当前配置sysctl -a /root/sysctl-backup-$(date %F).conf。第一部分是中断亲和绑定脚本。把网卡名替换成你的实际名称比如eth0或ens1f0#!/bin/bash set -euo pipefail NIC${1:-eth0} CPU_COUNT$(nproc) # 收集该网卡的所有 RSS 队列中断号 IRQ_LIST() while IFS: read -r irq desc; do if [[ $desc *$NIC*-TxRx* ]] || [[ $desc *$NIC*-queue* ]]; then IRQ_LIST($irq) fi done /proc/interrupts if [[ ${#IRQ_LIST[]} -eq 0 ]]; then echo [WARN] 未检测到 $NIC 的 RSS 队列跳过中断亲和 exit 0 fi # 轮转分配到各 CPU 核心 for i in ${!IRQ_LIST[]}; do irq${IRQ_LIST[$i]} cpu$((i % CPU_COUNT)) mask$((1 cpu)) hex_mask$(printf %x $mask) echo $hex_mask /proc/irq/$irq/smp_affinity echo [INFO] IRQ $irq - CPU $cpu (mask0x$hex_mask) done # 停掉 irqbalance 防止覆盖 if systemctl is-active --quiet irqbalance 2/dev/null; then systemctl stop irqbalance systemctl disable irqbalance echo [INFO] 已停止并禁用 irqbalance fi第二部分是 RPS/RFS 配置适用于无 RSS 的虚拟网卡。注意rps_sock_flow_entries必须大于等于所有队列rps_flow_cnt之和#!/bin/bash set -euo pipefail NIC${1:-eth0} CPU_COUNT$(nproc) # 计算 RPS 位掩码 if [[ $CPU_COUNT -le 32 ]]; then RPS_MASK$(printf %x $((2**CPU_COUNT - 1))) else RPS_MASKffffffff,$(printf %x $((2**(CPU_COUNT-32) - 1))) fi for q in /sys/class/net/$NIC/queues/rx-*/rps_cpus; do [[ -f $q ]] echo $RPS_MASK $q done # RFS 流表每核 4096 条流 FLOW_ENTRIES$((CPU_COUNT * 4096)) QUEUE_COUNT$(ls -d /sys/class/net/$NIC/queues/rx-*/ 2/dev/null | wc -l) for f in /sys/class/net/$NIC/queues/rx-*/rps_flow_cnt; do [[ -f $f ]] echo $FLOW_ENTRIES $f done echo $((FLOW_ENTRIES * QUEUE_COUNT)) /proc/sys/net/core/rps_sock_flow_entries echo [INFO] RPS/RFS 完成: rps_cpus0x$RPS_MASK, flow_entries$FLOW_ENTRIES第三部分是 TCP 协议栈参数写入/etc/sysctl.d/99-network-tuning.conf持久化。这里用 TOML 风格的键值对照说明每个参数的设计意图# /etc/sysctl.d/99-network-tuning.conf # 接收缓冲区 min/default/max字节max 需 BDP net.ipv4.tcp_rmem 4096 131072 25165824 # 发送缓冲区逻辑同上 net.ipv4.tcp_wmem 4096 16384 25165824 # 全局接收缓冲区上限必须 tcp_rmem 的 max net.core.rmem_max 25165824 # 全局发送缓冲区上限 net.core.wmem_max 25165824 # 窗口缩放启用后窗口可超 64KB net.ipv4.tcp_window_scaling 1 # TIME_WAIT 复用仅短连接高并发场景启用 net.ipv4.tcp_tw_reuse 1 # SYN 队列长度应对突发连接 net.ipv4.tcp_max_syn_backlog 65536 # 连接跟踪表大小 net.netfilter.nf_conntrack_max 1048576 # listen() 全连接队列上限 net.core.somaxconn 65535 # TCP Fast Open减少一次 RTT net.ipv4.tcp_fastopen 3应用命令是sysctl -p /etc/sysctl.d/99-network-tuning.conf。如果你用的是 Cline MCP 或者 Codex 这类工具做自动化运维记得在配置里写全三件套Base URL、Key、Model ID否则工具连不上后端会报local proxy failed。具体来说Base URL 填https://taotoken.net/apiKey 在 TaoToken API Keys 页面生成Model ID 按你实际调用的模型填。这三项缺一不可很多人只填了 Key 忘了 Model ID结果请求一直 401。4. 压测验证用 wrk 和 ss 确认调优是否真的生效改完参数不验证等于没改。验证分三步走每步都有明确的观察指标。第一步验证中断分布。执行cat /proc/softirqs | grep NET_RX看各 CPU 的NET_RX计数是否大致均匀。如果某个核心还是明显偏高说明中断亲和没生效回去检查irqbalance是否真的停了以及smp_affinity写入是否成功有些内核需要写smp_affinity_list。这一步的预期结果是各核心计数差异在 20% 以内。第二步验证 TCP Buffer 实际使用。用ss -tnm查看已建立连接的skmem信息重点看rb接收缓冲区已用和tb发送缓冲区已用。如果rb长期贴着tcp_rmem的 max 值说明 Buffer 还是不够需要继续调大如果rb很小但吞吐上不去那瓶颈不在 Buffer回去看软中断。同时用netstat -s | grep -i prune确认没有因为 Buffer 不足被裁剪的数据包。第三步用wrk做基准压测。命令示例wrk -t16 -c2000 -d60s --latency http://your-server:8080/。重点对比调优前后的两个数字吞吐量Requests/sec和 P99 延迟。我实测下来中断亲和优化通常能带来 2 到 4 倍吞吐提升这是投入产出比最高的一步。Buffer 调优在长肥管道高 RTT场景效果明显短连接场景提升有限。压测时同步开一个终端跑perf top -g观察热点是否从__netif_receive_skb转移到了应用层函数如果是说明协议栈瓶颈已经消除。验证过程中如果遇到reading choices这类报错通常是你调用的模型服务返回格式不对检查一下请求体里的model字段和messages结构。如果是 OAuth 相关报错说明鉴权方式选错了TaoToken 的 API 用的是 Key 鉴权不需要走 OAuth 流程。这些报错在 TaoToken 接入文档 里都有对照说明遇到不确定的先查文档再改配置。5. 调优常见报错排查401、local proxy failed 与参数冲突调优过程中遇到的报错分两类一类是工具链的鉴权/连接报错一类是内核参数本身的冲突。分开说。工具链报错里最常见的是 401。这个基本就是 Key 没填对或者过期了去 TaoToken API Keys 重新生成一个注意复制时不要带空格。第二个是local proxy failed这个通常出现在你用 Cline MCP 或类似工具时原因是 Base URL 配错了。正确写法是https://taotoken.net/api不要加多余的路径后缀也不要加 UTM 参数。第三个是reading choices报错说明返回的 JSON 结构里没有choices字段大概率是 Model ID 填错了模型名要和后端实际部署的一致。内核参数冲突这块我踩过最深的坑是中断亲和与 NUMA 亲和的冲突。多 NUMA 节点服务器上如果把网卡中断分配到远端 NUMA 节点的 CPU会引入跨节点内存访问延迟大约增加 40 到 60ns。正确做法是把中断限制在网卡所在 NUMA 节点的 CPU 上哪怕该节点负载更高。跨 NUMA 的延迟惩罚远大于负载不均的影响。你可以用cat /sys/class/net/eth0/device/numa_node查看网卡所在节点然后用lscpu确认该节点的 CPU 范围。第二个坑是tcp_tw_reuse的隐蔽风险。启用后允许复用 TIME_WAIT 连接短连接场景能快速回收端口。但在 NAT 环境中复用连接可能被中间设备误判为连接劫持导致连接重置。如果对端还持有旧连接状态复用会触发 RST。所以这个参数只在纯内网短连接场景开有 NAT 网关的链路建议关掉。第三个坑是 TCP Buffer 过大导致的内存压力反噬。tcp_rmem的 max 设 24MB意味着最坏情况下每个连接接收缓冲区可能占 24MB。1000 并发就是 24GB128GB 内存看着够但并发到 10000 时仅 Buffer 就要 240GB远超物理内存。内核内存压力检测会强制收缩 Buffer反而导致更剧烈的吞吐波动。所以 max 值必须按并发数 × 单连接 Buffer反推留 30% 内存余量给系统和应用。第四个坑是 RFS 的 CPU 迁移开销。RFS 把数据包路由到应用线程所在 CPU但如果应用线程被 CFS 调度器频繁迁移RFS 路由目标就跟着乱跳CPU 缓存频繁失效。解决方案是用taskset或 cgroups 把应用线程绑到固定 CPU配合 RFS 才能发挥效果。命令示例taskset -cp 4-7 $(pgrep -f your-app)。6. 长期编码与 Agent 场景下的网络层持续调优思路网络协议栈调优不是一次性动作尤其是当你跑长期编码任务或者 Agent 工作流时连接模式会随时间变化。比如白天短连接密集夜间长连接为主Buffer 和连接队列的最优值就不一样。我的做法是把调优参数做成可切换的 profile用脚本按时间段应用不同配置。对于需要持续跑编码任务的场景建议用 TaoToken Coding Plan 配合本地网络调优把 API 请求的 P99 延迟压下来。具体操作是在 Coding Plan 里配置好 Base URL 和 Key然后在本地用wrk对 API 端点做基准测试对比调优前后的延迟变化。如果延迟从 200ms 降到 80ms那说明网络层优化确实生效了。持续调优的核心是建立监控闭环。用sar -n DEV 1看网卡吞吐用cat /proc/softirqs看软中断分布用ss -s看连接状态统计。这三个指标每 5 分钟采一次存到时序数据库里出现异常时能快速定位是哪一层的问题。我一般会在/etc/cron.d/net-monitor里放一个采集脚本把数据推到本地 Prometheus。最后说一个实用技巧调优参数不要写死在脚本里用环境变量或者配置文件管理。这样换机器、换场景时只改配置不改代码。比如把tcp_rmem的 max 值抽成TCP_RMEM_MAX变量在/etc/sysctl.d/里用include引入。这样你可以在测试环境用一套值生产环境用另一套互不干扰。调优的本质是让参数匹配场景而不是追求某个最优解。