的一些浅见:从Linux kernel中断到轮询的调优实践与TaoToken接入)
1. 从一次网关丢包说起NAPI 到底是什么、能做什么、适合谁如果你在 Linux 上跑过 New API 这类网关服务大概率遇到过一种很别扭的现象CPU 看着不高top里si软中断却时不时飙一下接口偶发超时ss -s里SYN重传悄悄涨。我第一次碰到时以为是 New API 的并发配置写小了把 worker 从 4 调到 16结果只是把问题从「偶发」变成「更频繁的偶发」。后来顺着perf top看下去热点落在net_rx_action和网卡驱动的poll上才意识到问题不在应用层而在内核收包路径——也就是 NAPI。NAPI 全称 New API是 Linux 内核里用于网络设备的中断缓解技术。名字确实起得随意但机制本身很讲究。它的核心不是「用了轮询」而是在纯中断和轮询之间动态切换数据量小的时候走中断来一个包处理一个数据量大的时候关掉网卡中断改成软中断里批量收割避免 CPU 被 IRQ 反复打断。这个「什么时候切、切过去待多久」的取舍直接决定了网关在高 PPS 场景下是稳还是抖。这篇文章适合三类人一是用 New API 做统一模型网关、被收包性能困扰的后端同学二是想搞懂netdev_budget、weight、time_squeeze这些参数到底在调什么的运维三是准备把 TaoToken 这类统一 Key/API 通道接进自建网关、需要确认转发链路行为符合预期的开发者。下面我会从硬中断触发讲到软中断轮询的完整链路给出可复制的内核参数和 New API 配置片段最后用 TaoToken 的 API 通道跑一次真实请求验证收包与转发是否如预期。先把结论放前面NAPI 的调优不是把netdev_budget调大就完事它和网卡weight、软中断时间片、ksoftirqd调度是联动的。理解链路之后你才知道该动哪个旋钮。2. NAPI 完整链路拆解从硬中断到软中断轮询的切换逻辑要调优先得把链路走一遍。NAPI 的骨架由三个 API 和两个数据结构撑起来。三个 API 分别是netif_napi_add驱动告诉内核「我要用 NAPI」注册poll回调并设定weightnapi_schedule驱动在硬中断里告诉内核「有数据了安排一次轮询」napi_complete驱动在poll里告诉内核「这轮收完了可以重新开中断了」。真正执行轮询的是软中断NET_RX_SOFTIRQ注册的回调net_rx_action。两个关键结构softnet_data是 per-CPU 的每个 CPU 一份里面的poll_list挂着所有待轮询的napi_structnapi_struct是最小调度单位它的weight就是单次poll最多收割的 skb 数量默认 64千兆网卡常见值。完整流程是这样的第一步网卡 DMA 把数据搬进内存后触发硬中断进入驱动注册的 IRQ handler。handler 里通常先关掉并清除网卡中断然后调用napi_schedule。这一步做的事很轻把napi_struct挂进当前 CPU 的poll_list然后置起NET_RX_SOFTIRQ软中断标志。第二步硬中断上半部结束内核在合适的时机执行软中断net_rx_action被调用。它从poll_list头部取出一个napi_struct调用驱动的poll回调传入weight作为本次最多处理的包数。第三步驱动的poll从 DMA ring 里取数据。这里就是切换的关键点如果 ring 里的数据不足weight驱动认为收完了调用napi_complete把napi_struct从poll_list摘掉并重新打开网卡中断回到纯中断模式。如果 ring 里数据达到或超过weight驱动只取weight个就返回不摘除、不开中断net_rx_action把这个napi_struct移到poll_list尾部等下一轮继续。这就是切到了轮询模式。net_rx_action自身还有一层预算控制。它有一个netdev_budget默认 300表示一次软中断执行中所有poll累计能处理的包数上限还有一个 2 jiffies 的时间上限。任一条件触发就跳出循环重新置起NET_RX_SOFTIRQ把剩余工作留给下一次软中断或ksoftirqd。这里有个容易被忽略的点netdev_budget是全局单次软中断的预算weight是单个网卡单次 poll的预算。按weight64算300 的预算大约够 5 个网卡轮次。如果你的机器上有多张网卡或者多队列预算会被瓜分单队列能分到的轮次更少time_squeeze就容易涨。time_squeeze是softnet_data里的统计值记录「预算或时间用完被迫退出」的次数。它藏在/proc/net/softnet_stat的第三列每一行对应一个 CPU。这个值持续增长说明软中断处理跟不上收包速度是判断瓶颈最直接的信号。把链路串起来看NAPI 的精髓就是那个「游走」中断频繁且数据量大时poll一直返回满weightnapi_struct在poll_list里循环中断保持关闭纯轮询数据量掉下来某次poll收不满napi_complete一调中断重新打开回到中断模式。切换点由「ring 里数据是否够 weight」隐式决定不需要你手动干预。3. 可复制的内核参数与 New API 配置片段理解链路之后调优就有据可依了。下面这些参数和配置可以直接抄但请先在自己的测试环境验证别直接上生产。先看内核侧。netdev_budget支持运行时修改# 临时调整重启失效 sudo sysctl -w net.core.netdev_budget600 # 持久化写入配置文件 echo net.core.netdev_budget600 | sudo tee -a /etc/sysctl.conf sudo sysctl -pnetdev_budget_usecs控制软中断单次执行的时间上限单位微秒默认 2000即 2mssudo sysctl -w net.core.netdev_budget_usecs4000netdev_max_backlog是每个 CPU 的收包队列上限队列满了会丢包高 PPS 场景可以适当调大sudo sysctl -w net.core.netdev_max_backlog5000网卡侧的weight和队列数通常由驱动决定可以用ethtool查看和调整多队列# 查看网卡队列和中断情况 ethtool -l eth0 ethtool -S eth0 | grep -i drop # 调整队列数需网卡支持 sudo ethtool -L eth0 combined 8再看 New API 侧。New API 作为网关收包之后要做协议解析和上游转发它的并发模型和内核收包是两回事但配置要匹配。下面是一份可复制的docker-compose片段重点是资源限制和网络模式version: 3.8 services: new-api: image: calciumion/new-api:latest container_name: new-api restart: always ports: - 3000:3000 environment: - TZAsia/Shanghai - SQL_DSNroot:passwordtcp(mysql:3306)/new-api - REDIS_CONN_STRINGredis://redis:6379 - SESSION_SECRETreplace_with_your_own_secret - SYNC_FREQUENCY60 ulimits: nofile: soft: 65535 hard: 65535 deploy: resources: limits: cpus: 4.0 memory: 4G networks: - gateway-net networks: gateway-net: driver: bridge如果你用 systemd 直接跑二进制/etc/systemd/system/new-api.service里可以加软中断相关的环境约束[Service] LimitNOFILE65535 LimitNPROC65535 # 绑定到特定 CPU减少跨核软中断 CPUAffinity2 3 4 5这里有个取舍要讲清楚netdev_budget调大单次软中断能处理更多包吞吐上去了但单次软中断占用 CPU 时间变长可能影响同核上的其他任务延迟抖动变大。netdev_budget_usecs同理。我的经验是网关这类对延迟敏感的服务netdev_budget保持在 300 到 600 之间netdev_budget_usecs不超过 4000配合多队列把中断分散到不同 CPU比单纯堆预算更稳。另外weight一般不建议改它由驱动在netif_napi_add时设定改它要动驱动代码。真要调优先调队列数和netdev_budget。4. 用 TaoToken 通道验证收包与转发行为配置改完得验证链路是否真的按预期工作。我用 TaoToken 的统一 API 通道跑了一次请求顺便观察收包和转发。TaoToken 的 API 地址是https://taotoken.net/api它提供统一的 Key 和 API 通道兼容 OpenAI 风格的接口。先拿一个 Key在控制台创建即可然后构造请求curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-your-taotoken-key \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: ping}], max_tokens: 16 }请求发出去的同时在另一台机器上盯内核统计# 每 1 秒刷新一次观察第三列 time_squeeze watch -n 1 cat /proc/net/softnet_stat | awk {print \$1, \$2, \$3} # 观察软中断 CPU 占用 mpstat -P ALL 1实测下来单次请求的包量很小time_squeeze基本不动si占用可以忽略。真正能压出差异的是并发场景。用wrk或hey打一轮hey -n 20000 -c 200 -m POST \ -H Authorization: Bearer sk-your-taotoken-key \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:hi}],max_tokens:8} \ https://taotoken.net/api/v1/chat/completions这时候再看softnet_stat如果第三列开始涨说明软中断预算不够可以按第 3 节的参数往上调一档再压。如果si高但time_squeeze不涨多半是单队列中断集中在一个 CPU用ethtool -L加队列、配合irqbalance或手动绑中断亲和性。验证转发行为是否符合预期可以对比 New API 网关的日志和 TaoToken 返回的响应。New API 侧开启请求日志确认请求确实经过了网关转发TaoToken 侧返回 200 且响应体正常说明整条链路通了。如果 New API 日志有记录但 TaoToken 没收到问题在网关到上游的出方向和 NAPI 收包无关别混在一起排查。这一步的意义在于把「内核收包」和「应用转发」两段分开验证。NAPI 调优只影响收包段转发段的瓶颈通常在连接池、DNS 解析或上游限流。分清楚才不会调错方向。5. 常见报错排查401、local proxy failed、reading choices、OAuth调优和接入过程中报错往往比性能问题更让人抓狂。下面几个是我和身边人踩过的按现象、原因、处理列出来。401 Unauthorized。最常见Key 不对或没带。检查Authorization头是不是Bearer sk-xxx格式Key 有没有多余空格是不是用了控制台里已删除的 Key。TaoToken 的 Key 在控制台创建注意区分不同项目的 Key。local proxy failed / connection refused。这个报错通常出现在 New API 网关转发到上游时。先确认 New API 容器能不能解析并访问taotoken.netdocker exec进去curl -v https://taotoken.net/api/v1/models试一下。如果是 DNS 问题检查容器的resolv.conf如果是网络策略问题检查安全组和出方向规则。注意这里说的是容器网络连通性不是任何形式的网络规避手段。reading choices 相关报错比如panic: runtime error: index out of range或解析响应时choices为空。这多半是上游返回了非预期结构比如限流返回了错误 JSON而网关代码直接取choices[0]。处理办法是在 New API 侧加响应校验或者看 TaoToken 返回的原始 body 确认是不是限流。用curl -v看完整响应最直接。OAuth 相关报错。如果你用 Claude Code 或类似工具接入可能碰到 OAuth token 过期或 scope 不足。这类问题要回到对应工具的配置确认 Base URL、Key、Model ID 三件套是否齐全。以 Claude Code 为例配置里需要明确ANTHROPIC_BASE_URL、ANTHROPIC_API_KEY和模型 ID缺一个都会报鉴权或路由错误。排查顺序建议固定下来先看客户端报错原文再看网关日志最后看上游返回。三段对不上就逐段用curl隔离。别一上来就改内核参数收包问题很少表现为 401。6. 继续深入把 NAPI 调优和网关接入串起来走到这里链路、参数、验证、排障都过了一遍。最后说几个实操里容易忽略的点。time_squeeze要长期观察不要只看一次压测。它的增长速率比绝对值更有意义。如果平时为 0大促时缓慢增长说明预算刚好够用如果平时就在涨说明基线配置就有问题先加队列再调预算。ksoftirqd的 CPU 占用要单独看。软中断预算用完会唤醒ksoftirqd它和普通进程抢 CPU。用ps -eo pid,comm,pcpu | grep ksoftirqd看如果某个ksoftirqd长期高占用说明该 CPU 的收包压力过大考虑把中断亲和性分散开。New API 网关的 worker 数和内核收包能力要匹配。worker 开太多应用层抢 CPU反而挤压软中断worker 太少转发成瓶颈。一般按 CPU 核数的 1 到 2 倍起步再根据si和转发延迟微调。TaoToken 的接入文档里有完整的接口说明和示例模型对话、Coding Plan、控制台、API Keys 都有对应入口。如果你要把统一 Key 通道接进自建网关建议先用模型对话页面确认 Key 可用再在网关里配置最后用压测验证整条链路。这样出问题时你能快速定位是 Key、网关还是内核收包。NAPI 这个名字确实起得随意但它的设计思路值得反复琢磨不是非此即彼而是在两种模式间找平衡。网关调优也一样内核参数、应用配置、上游通道每一段都有它的预算和取舍串起来看才知道该动哪里。