十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

网卡中断绑核实战:解决CPU单核打满与网络延迟抖动

网卡中断绑核实战:解决CPU单核打满与网络延迟抖动 干过几年Linux服务器运维和性能调优的人迟早都会碰上这么一个场景业务高峰期CPU单核被打满其他核空闲网卡收包延迟动不动飙到几毫秒甚至几十毫秒。你查了负载、看了TOP、翻遍了应用日志最后才发现瓶颈根本不在业务代码里而是所有网卡中断都挤在了同一个CPU核心上把那个核活活压死了。这篇文章就围绕“网卡中断绑核”这个优化手段把原理、操作、验证、进阶玩法一次性讲透属于直接把工作台搬到你面前的那种分享。网卡中断绑核简单说就是把网卡收发包触发的中断请求IRQ定向分配到指定的CPU核心上处理避免中断全部堆积在单个核上形成热点。它解决的核心问题是高并发网络场景下CPU 0通常是默认处理中断的核负载过高而其它核心空闲导致整体吞吐量上不去、延迟不稳定。适合所有使用Linux物理机或虚拟机跑高流量服务的同学参考——尤其是网关、负载均衡、缓存、数据库、WEB服务这类网络密集型应用。这类优化的价值很容易被低估但我在实际项目中多次靠它把服务端吞吐量拉高了30%以上而且不需要改一行业务代码。下面把整套思路和实操细节完整拆开包括命令、参数、脚本和我踩过的坑。1. 网卡中断绑核到底解决什么问题1.1 一次性能排查把我逼到了中断优化这条路上先讲一个真实案例。某次压测一个网关服务16核的机器流量大概到了80万PPS包每秒的时候CPU使用率看起来并不高才30%左右但延迟曲线已经开始剧烈抖动。打开top后按1看每个核心的使用率问题立刻就暴露了CPU0的使用率接近100%而其余15个核都在10%以下。为什么会出现这种极端不均衡因为默认情况下大多数网卡驱动程序会把所有中断请求都发送到CPU0——这个设计在单核时代合情合理但在多核服务器上就成了明显的瓶颈。更让人头疼的是CPU0除了处理网卡中断还要承担系统时钟中断、调度器的一部分工作以及RCURead-Copy Update回调等杂七杂八的内核任务。多股流量叠加在一起CPU0直接就顶不住了中断处理延迟急剧上升网络吞吐量同步下降。1.2 中断处理的完整链路从网卡到CPU要理解绑核的意义得先看清楚一个网络数据包从网卡到达应用层的完整路径。物理网卡收到数据帧后通过DMA直接内存访问把数据写入内存中的环形缓冲区Ring Buffer完成写入后网卡通过硬件引脚向CPU触发一个中断请求。这时CPU暂停手头的工作跳转到内核注册的中断处理函数ISRInterrupt Service Routine完成收包、校验、协议栈处理等后续工作。整个过程涉及两类中断硬中断Hard IRQ和软中断SoftIRQ。硬中断处理由网卡驱动程序注册的处理函数执行处理完就快速返回而真正消耗大量CPU时间的协议栈处理IP、TCP/UDP和流量分发是在软中断上下文中完成的。硬中断触发后内核会标记对应的软中断等硬中断上下文退出后在当前CPU上继续执行软中断处理。这个设计的关键点在于软中断始终在触发它的那个CPU上运行。也就是说你把硬中断绑定到CPU3那么后续的软中断处理也会在CPU3上执行。所以绑核一次操作实际上同时约束了硬中断和软中断的运行位置这正是它能显著改善CPU负载均衡的原因。理解了这条链路后面所有的排查和优化就都有方向了。2. 动手前的准备工作查清楚你的中断现状2.1 核心工具/proc/interrupts 解读绑核不是拍脑袋随便绑第一步一定是搞清楚当前系统的中断分布情况。Linux把所有的中断信息都暴露在/proc/interrupts这个虚拟文件里用cat就能直接查看cat /proc/interrupts输出大概是这样的CPU0 CPU1 CPU2 CPU3 0: 28 0 0 0 IO-APIC 2-edge timer 8: 1 0 0 0 IO-APIC 1-edge rtc0 24: 9765231 0 0 0 PCI-MSI 524288-edge enp3s0每一行代表一个中断源。第一列是中断号IRQ Number中间几列是各CPU上该中断发生的次数最后一列是中断的设备名称。像enp3s0就是网卡对应的中断PCI-MSI说明这是通过MSIMessage Signaled Interrupts方式传递的中断。判断中断是否扎堆只需要看对应行在各CPU之间的计数是否均衡。如果一行里只有CPU0的计数在不断增长其他核心基本不动那就说明当前中断全压在CPU0上。用下面这条命令可以动态观察每秒刷新一次watch -n1 cat /proc/interrupts重点盯住你要优化那块网卡对应的中断行看它的计数增长是否集中在某一个或某几个CPU上。另外需要说明的是现在主流服务器网卡都支持MSI-X它允许一个网卡申请多个中断号每个队列对应一个中断号。如果你在输出里看到多个PCI-MSI行都指向同一块网卡说明网卡的多队列功能已经启用这个对后面的优化非常有利。2.2 确认网卡是否支持多队列多队列Multi-Queue是网卡高性能收包的基础能力。传统单队列网卡无论流量多大只有一个中断号和一个Ring Buffer所有包都在一条流水线上处理而支持多队列的网卡如Intel的i350/i40e、Mellanox的CX系列可以把Ring Buffer拆分成多个队列每个队列独立申请中断号由驱动把收到的包分散到不同队列里再由不同CPU核心并行处理。检查网卡是否支持多队列可以先看中断列表里是不是有多个同网卡的中断号。更准确的方法是查网卡队列信息ls /sys/class/net/enp3s0/queues/如果输出里有rx-0、rx-1、rx-2、rx-3等多个接收队列还有对应的tx-*发送队列说明多队列已经启动。查看每个队列实际配置的CPU亲和性cat /sys/class/net/enp3s0/queues/rx-0/rps_cpus如果这个文件内容全是0说明当前没有使用RPSReceive Packet Steering软件分发机制中断完全依赖硬件队列和硬中断绑核来处理。对于不支持多队列的老网卡或者驱动没开启多队列的情况KVM虚拟机里常见的virtio-net也支持多队列但需要在宿主机和虚拟机两端都做配置。如果确认网卡不支持多队列后续优化就要依赖RPS/RFS这类软件方案来分发软中断内容我会在第四章展开讲。2.3 多队列与单队列下的绑核策略差异多队列和单队列的网卡绑核策略完全不同下面用一个表来对照说明网卡类型中断特征推荐绑核策略核心目标多队列网卡多个中断号每个队列一个将不同队列绑定到不同CPU核并行处理充分利用多核单队列网卡仅一个中断号绑定到一个专门处理中断的核避免影响其它核心配合RPS多队列高吞吐队列数核数或倍数一队列对应一核合理规划NUMA降低跨核访问开销虚拟化环境队列数取决于virtio配置绑定到同一个NUMA节点内的核减少跨NUMA访存开销单队列网卡即使你把中断绑到一个核上那个核仍然可能成为瓶颈。所以在规划绑核方案前先搞清楚网卡类型不要盲目照搬网上的配置命令。我见过不少案例网卡明明支持多队列但驱动用了老的参数导致只起来一个队列性能翻倍的空间白白浪费了。3. 网卡中断绑核的标准操作流程3.1 确定中断号和目标CPU绑核的第一步是找到目标网卡对应的中断号。前面已经看过/proc/interrupts现在我们用更精确的方式定位。假设网卡名是enp3s0先用ethtool确认网卡信息和队列数ethtool -l enp3s0输出里会列出一个Combined字段显示当前网卡实际启用的收发队列数量。之后通过中断列表找到所有enp3s0相关的行。如果网卡有8个队列通常会有8个对应的中断号可能分布在连续的编号区间里。也可以用脚本直接把网卡对应的中断号整理出来grep -i enp3s0 /proc/interrupts | awk {print $1} | tr -d :拿到中断号之后下一步就是确定你要把这些中断绑定到哪些CPU核心。这里有一个最基本的规则优先绑定到同一NUMA节点内的核心上。查看CPU拓扑lscpu重点关注NUMA node0 CPU(s)和NUMA node1 CPU(s)这两行的信息。如果网卡挂在node0的PCIe总线上那么中断绑定到node0的核上CPU访问网卡DMA映射的内存就在本地内存上延迟最低如果你绑到node1的核上每次中断处理都要跨NUMA访问内存性能反而不如不绑。3.2 通过smp_affinity设置中断亲和性Linux内核为每个中断号都提供了两个亲和性控制文件/proc/irq/中断号/smp_affinity用十六进制掩码表示CPU列表和/proc/irq/中断号/smp_affinity_list用十进制列表表示具体CPU编号。smp_affinity_list更直观操作起来也更容易理解# 将中断号128绑定到CPU0和CPU1 echo 0-1 /proc/irq/128/smp_affinity_list比如echo 0-1表示允许CPU0和CPU1处理该中断实际运行时会根据负载在两者之间分配。如果只想绑定到单个CPUecho 3 /proc/irq/128/smp_affinity_list如果使用smp_affinity需要把CPU编号换算成十六进制位掩码。比如想让CPU3处理中断就把第3位置1得到二进制1000十六进制就是8echo 8 /proc/irq/128/smp_affinity这里特别容易踩坑的是十六进制掩码的计算。有个快速方法先用printf把十进制CPU编号转换成十六进制掩码。比如要绑定CPU5和CPU7CPU列表换算成掩码是(15) | (17) 32128 160十六进制就是a0。初学者建议直接用smp_affinity_list省去换算的麻烦。设置完成后再次查看/proc/interrupts中断会在新绑定的CPU上递增计数。但注意老的计数不会清零你需要观察新增的部分是否落在目标CPU上才能确认绑定生效。3.3 使用irqbalance自动管理还是手动绑核很多Linux发行版默认装了irqbalance服务它会周期性扫描系统中断分布情况并根据CPU负载自动调整中断亲和性。这个服务在普通桌面系统和小型服务器上问题不大但对于追求极致性能的生产环境我建议直接关掉它因为它会打乱你手动设置的任何绑核方案。检查并关闭irqbalance# 查看状态 systemctl status irqbalance # 停止并禁用 systemctl stop irqbalance systemctl disable irqbalance为什么手动绑定更可靠irqbalance只考虑CPU使用率来决定中断迁移策略它不知道你的业务是网络密集型还是计算密集型也不知道哪些网卡是关键路径。它默认的目标是让所有CPU负载相对均匀但这恰恰和高性能网络的优化目标冲突——网络密集型应用需要把中断集中到指定的几个核上让业务线程独占其余核心而不是大家一起分摊。关闭irqbalance后手动设置的smp_affinity才能稳定生效。别忘了检查系统里是否还有其它自动调整中断的机制比如tuned服务如果启用了某些性能调优profile也可能在后台改中断亲和性需要一并留意。3.4 绑定后的验证与效果评估完成绑核后必须用数据说话确认优化真正产生了效果。第一步看中断是否已经均匀分布到目标CPU用top按1键观察各核心的使用率是否趋于均衡或者借用一个采样脚本对比绑定前后各CPU的中断次数增量。更关键的验证指标是网络延迟和吞吐量的变化。我常用的工具是perf和mpstat# 查看各CPU的中断和软中断分布 mpstat -I CPU -P ALL 1输出里的intr/s列代表每秒中断次数各CPU之间应该看到明显差异且分布在你预期的核上。同时用perf监测软中断处理是否均匀perf stat -e irq_vectors:local_timer_entry,softirq:softirq_entry -a sleep 5除了观察系统侧的数据业务侧也要同步对比用压测工具如wrk、iperf3、dpdk-testpmd等跑流量对比绑核前后服务的P99延迟和吞吐量。我常见的效果是绑核后吞吐量提升20%~50%延迟抖动大幅减少CPU0从打满状态降到30%以下。如果这些指标没有明显改善甚至变差了大概率是绑定策略有问题比如跨NUMA绑定或者软中断和业务线程挤在同一个核上后面第五章会讲如何排查。4. 进阶结合网卡多队列、RPS/RSS与numactl的完整优化方案4.1 RSSReceive Side Scaling与网卡多队列的配合RSS是网卡硬件层面的多队列负载均衡机制。启用RSS后网卡会根据数据包的五元组信息源IP、目的IP、源端口、目的端口、协议计算哈希值把哈希结果映射到不同的接收队列。这样同一个TCP连接的所有包都会被分到同一个队列避免乱序不同连接则被分散到不同队列实现并行处理。查看当前网卡的RSS配置ethtool -x enp3s0输出会显示当前网卡的RSS哈希字段和重定向表Indirection Table。如果哈希字段只有src-ip和dst-ip建议开启四元组/五元组哈希让不同TCP连接更容易被分散到不同队列ethtool -N enp3s0 rx-flow-hash tcp4 sdfnsdfn分别代表源IP、目的IP、源端口、目的端口。对于UDP用udp4同理ethtool -N enp3s0 rx-flow-hash udp4 sdfn设置完后检查重定向表ethtool -x enp3s0这时每个队列对应的CPU编号会直观地列出来。通过把网卡队列数设为与CPU核数一致或适当倍数再配合中断绑核可以让每个CPU核处理一个硬件队列的中断实现真正的并行收包。4.2 RPS/RFS 在软件层面分发中断对于单队列网卡或者在虚拟化环境中无法启用硬件多队列的场景Linux内核提供的RPSReceive Packet Steering机制可以在软件层面把收到的包分发到不同CPU核心的软中断队列中。开启RPS的关键就是修改队列目录下的rps_cpus文件。假设你要让rx-0队列的包分发到CPU2到CPU7这6个核上先把CPU掩码算出来。CPU2到CPU7对应位掩码为11111100二进制从高位到低位分别是CPU7到CPU0实际换算按位从CPU0开始十进制为252十六进制为fcecho fc /sys/class/net/enp3s0/queues/rx-0/rps_cpus更直观的写法是直接用十六进制掩码。不太会算的同学可以借助这个思路把CPU编号列表折算成二进制位从CPU0开始从右往左排比如CPU0、CPU1、CPU2对应00000011也就是十六进制03如果你的环境是40核起步的服务器记得掩码要按64位考虑写成fc000000000000之类的形式。RFSReceive Flow Steering是RPS的增强版它能根据应用的运行CPU位置把同一个流的包发送到正在处理该流的CPU上从而提升CPU缓存命中率。开启RFS需要设置/proc/sys/net/core/rps_sock_flow_entries和每个队列的rps_flow_cnt# 假设可能同时有32768个活动流 echo 32768 /proc/sys/net/core/rps_sock_flow_entries echo 4096 /sys/class/net/enp3s0/queues/rx-0/rps_flow_cntRPS/RFS不是银弹它需要消耗额外的CPU来做分发计算如果本来CPU已经吃紧效果会打折扣。但在单队列网卡上这是唯一无需改硬件就能实现中断负载均衡的办法。4.3 结合numactl进行CPU核心的合理规划绑核优化到了一定程度真正的瓶颈往往不再是中断处理本身而是CPU访问内存的路径。NUMA架构下每个CPU核访问本地内存和远端内存在延迟上能差出1.5到2倍。所以绑核方案要配合NUMA规划一起来做。完整规划思路是分层的先确认网卡挂载在哪个NUMA节点上查看PCIe设备对应的NUMA节点编号cat /sys/bus/pci/devices/0000:03:00.0/numa_node03:00.0是网卡的PCI地址可以根据lspci输出找到。假设结果显示网卡在node0那么中断绑核优先使用node0的CPU核比如node0有CPU0~7就把网卡的多个中断分别绑到0~7。网卡中断绑在node0的核上后续运行DPDK或专用收包线程时用numactl --cpunodebind0 --membind0启动进程保证内存分配也在node0上。业务线程则绑到node1的核上避免和中断处理抢CPU。命令示例# 启动一个绑定到node1的CPU8~15上的业务进程内存也优先从node1分配 numactl --cpunodebind1 --membind1 ./your_service这套组合拳打下来中断处理、内存访问、业务计算各占一块地盘互不干扰性能和稳定性都能上一个台阶。4.4 完整优化脚本参考我把自己用惯的一套优化脚本简化了一下放到下面。生产环境使用前请根据自己的网卡名、CPU拓扑、中断号进行修改建议先在测试环境完整验证一遍。#!/bin/bash # 网卡中断绑核与RPS优化脚本 # 请根据实际环境修改变量 # 配置区 IFACEenp3s0 # 用于中断绑定的CPU列表根据NUMA拓扑调整 IRQ_CPUS0 1 2 3 # 用于RPS分发的CPU掩码十六进制按需计算 RPS_CPUS_MASK0f # 期望开启的硬件队列数 QUEUE_COUNT4 # # 1. 停止irqbalance systemctl stop irqbalance 2/dev/null systemctl disable irqbalance 2/dev/null # 2. 设置网卡队列数如果驱动支持 ethtool -L $IFACE combined $QUEUE_COUNT 2/dev/null # 3. 获取网卡对应的所有中断号 IRQS$(grep -i $IFACE /proc/interrupts | awk -F: {print $1} | tr -d ) # 4. 将中断分别绑定到指定CPU i0 for irq in $IRQS; do cpu$(echo $IRQ_CPUS | tr \n | sed -n $((i % $(echo $IRQ_CPUS | wc -w) 1))p) echo $cpu /proc/irq/$irq/smp_affinity_list i$((i 1)) done # 5. 对每个RX队列开启RPS for rxq in /sys/class/net/$IFACE/queues/rx-*; do echo $RPS_CPUS_MASK $rxq/rps_cpus done # 6. 打印当前中断分布确认 echo 当前中断分布 grep -i $IFACE /proc/interrupts脚本的关键点在于把多个中断号轮询分配到指定的CPU列表上确保不出现两个队列的中断扎堆在同一核上的情况。如果你有16个中断号、4个目标核轮询会让每4个中断对应一个核充分利用有限的核资源。5. 常见问题与排查技巧实录5.1 中断号不固定/重启失效怎么办手动改动/proc/irq/下的配置只能临时生效服务器一重启就全部还原。要让绑核配置永久化有几种方案。最直接的办法是把脚本写进systemd服务开机自动执行。创建一个服务文件/etc/systemd/system/irq-affinity.service[Unit] DescriptionSet IRQ CPU Affinity Afternetwork.target [Service] Typeoneshot ExecStart/usr/local/sbin/irq-affinity.sh RemainAfterExityes [Install] WantedBymulti-user.target然后执行chmod x /usr/local/sbin/irq-affinity.sh systemctl daemon-reload systemctl enable irq-affinity.service另一种思路是用irqbalance的配置文件保留服务并让它只管理你指定的中断。但如前所述生产环境我建议彻底关掉irqbalance手动脚本更可控。需要注意的是有些系统有irqaffinity内核启动参数可以在GRUB配置里指定默认中断亲和性。例如编辑/etc/default/grub在GRUB_CMDLINE_LINUX中加入irqaffinity0-3然后运行update-grub并重启。这个方式对所有中断统一生效但对不同网卡需要不同绑定策略的场景来说不够灵活我一般只在紧急兜底时使用。5.2 绑核后反而性能下降排查思路有时做完绑核吞吐量不升反降延迟还变高了。这种情况通常不是绑核本身错了而是方案和当前的业务模型不匹配。按下面三个方向逐一排查。第一检查是否发生了跨NUMA访问。如果你的网卡在node0却把中断绑到了node1的核上每次DMA数据拷贝和协议栈访问都会产生远端内存访问性能自然劣化。用numastat查看内存分配是否跨节点严重失配。第二确认软中断处理核是否和业务线程的核冲突。如果中断绑在CPU2~3而你的应用主线程也恰好跑在CPU2~3上两边抢CPU资源延迟照样飙高。中断绑核的核心思想是“让专门的核处理中断”所以要保证业务线程分散到其它核上必要时用taskset -c给业务线程指定CPU。第三确认是否有RPS配置叠加导致重复分发。如果网卡本身支持多队列且已开启RSS同时又设置了RPS两层分发机制可能互相干扰增加额外的计算开销。通常硬件多队列启用时不需要再开RPS二者择一即可。5.3 如何确认软中断softirq是否均匀分布在目标CPU上只盯着/proc/interrupts看硬中断是不够的还要确认软中断确实在预期核上执行。查看各CPU的软中断统计cat /proc/softirqs输出中的NET_RX和NET_TX两行是网卡收发包软中断的关键分别观察各CPU的计数增长情况。正常情况下增长应集中在你绑定的目标核上。如果发现NET_RX计数增长在未绑定的核上原因通常是网络数据没有走硬件多队列而是通过RPS分发到了其它CPU。这时需要调整rps_cpus配置或者检查是否配置了flow steering相关的规则。更细的观察可以用perf top在内核层面看哪个函数在哪个CPU上消耗资源perf top -C 2,3这里的-C参数指定查看CPU2和CPU3上正在执行的热点函数。如果看到process_backlog、net_rx_action这类函数在目标核上占比很高说明收包软中断确实在按预期方式运行。5.4 实战排查命令速查表把整个排查过程中最常用的命令整理成一张表方便直接复制使用目标命令说明查看中断分布cat /proc/interrupts看各CPU中断计数查看软中断分布cat /proc/softirqs看NET_RX/NET_TX分布查看网卡队列数ethtool -l enp3s0确认Combined队列数查看RSS配置ethtool -x enp3s0看哈希字段和重定向表修改RSS哈希ethtool -N enp3s0 rx-flow-hash tcp4 sdfn开启四元组哈希修改中断亲和性echo N /proc/irq/irq/smp_affinity_list直接指定CPU编号查看NUMA拓扑lscpu看各NUMA节点包含的CPU查看设备NUMA节点cat /sys/bus/pci/devices/0000:03:00.0/numa_node确认网卡所在节点监控各CPU中断mpstat -I CPU -P ALL 1每秒刷新设置RPS掩码echo 0f /sys/class/net/enp3s0/queues/rx-0/rps_cpus软件分发软中断查看软中断热点perf top -C 2,3分析指定核上的内核函数这套命令组合足以覆盖从问题发现、原因定位、方案实施到效果验证的完整链路。实际生产环境里我通常先用cat /proc/interrupts扫一遍再用mpstat盯几秒钟基本就能判断出该不该做绑核优化。5.5 绑核优化里最容易被忽略的CPU核隔离问题最后补充一个很多人都会忽略的细节绑核优化要配合CPU隔离才能真正发挥全部性能。Linux内核本身的调度器、内核线程、定时器等仍然可能在你绑定的核心上运行干扰中断处理和软中断执行的实时性。把一部分CPU从通用调度器中隔离出来可以使用内核启动参数isolcpus。例如把CPU4~7隔离isolcpus4,5,6,7 nohz_full4,5,6,7 rcu_nocbs4,5,6,7nohz_full可以减少隔离核上的时钟中断频率rcu_nocbs则让RCU回调不在这些核上执行。配合irq affinity这些隔离核就能近乎专一地处理网卡中断性能表现更稳定。隔离CPU是双刃剑隔离后普通用户态进程默认无法调度到这些核上如果忘记配置合适的亲和性业务线程会少掉一大部分计算资源。规划时要全局考虑中断处理、业务线程、内核进程三方的CPU资源分配不能顾此失彼。我在实际项目里常用的划分方式是16核的机器CPU0保留给系统内核和其它杂项任务CPU1~7给业务线程CPU8~15做中断绑定前提是网卡挂在对应NUMA节点上并且网卡有足够的队列数。如果网卡只有4个队列就只绑定其中4个核剩下的作为软中断处理的余量。这种“隔离绑核”的组合方案在高负载网关场景下实测可以把P99延迟降低50%以上效果非常明显。另外在做完绑核和CPU隔离之后重启服务之前一定要检查一下systemd的CPUAffinity设置因为systemd可能默认把服务限制在特定CPU列表跟你的绑核方案冲突。在服务文件里显式设置CPUAffinity和NUMAMask能避免很多“明明绑了却没生效”的怪问题。这套网卡中断绑核的优化方法我先后在多家公司的生产环境里实践过从几百台规模到几千台规模都验证过效果。操作本身不复杂核心是理解背后的中断处理链路并且每一步都要用数据验证。如果你正在被CPU0打满、网络延迟抖动、吞吐量上不去这些问题困扰按照文章里的步骤试一遍大概率能找到一个之前被忽略的优化空间。
返回列表