操作系统内核调优:中断亲和性、网卡多队列与内核Bypass
各位好,今天我们来聊聊操作系统内核层面的调优。说实话,很多做网络开发的朋友,应用层代码写得飞起,但一到内核层面就有点发怵。我刚开始也是这样,总觉得内核是黑盒,不敢碰。直到有一次线上事故,让我彻底改变了看法。
那次是一个高并发的网关服务,流量一上来CPU就飙到100%,但业务处理能力却上不去。我排查了半天,发现所有中断都打在一个CPU核上,其他核在旁边看热闹。嗯,这就是典型的「中断风暴」问题。从那以后,我意识到:网络性能的瓶颈,往往不在应用层,而在内核的调度机制上。
今天我们就围绕三个核心点展开:中断亲和性、网卡多队列、内核Bypass。这三板斧用好了,延迟能降一个数量级。
核心观点:网络包从网卡到应用,要经过中断处理、内核协议栈、socket缓冲区。每一步都有优化空间。我们的目标就是让数据路径更短、更直、更少竞争。
一、中断亲和性:别让一个核累死
先说说中断亲和性。说白了,就是决定「网卡中断来了,哪个CPU核去处理它」。默认情况下,Linux会把所有中断都扔给CPU0。你想想看,如果每秒几十万个包进来,CPU0忙得冒烟,其他核却在喝茶——这合理吗?
我个人的习惯是,坚决不用默认配置。线上环境一定要做中断绑核。怎么做呢?有两种方式:
- 手动绑核:通过 /proc/irq/{IRQ_NUM}/smp_affinity 文件设置。把中断号对应的CPU位图写进去就行。
- irqbalance 服务:系统自带的守护进程,会自动分配中断。但说实话,在高性能场景下我建议关掉它,自己手动绑更可控。
我的经验:曾经在一个40核的服务器上,默认irqbalance把网卡中断分散到了所有核。结果呢?每个核都在处理中断,L1/L2缓存频繁失效,性能反而下降了。后来我改成只绑前8个核,吞吐量提升了30%。
手动绑核的示例:
# 查看网卡中断号
cat /proc/interrupts | grep eth0
# 假设中断号是 78-85,绑定到 CPU0-7
echo "ff" > /proc/irq/78/smp_affinity
echo "ff" > /proc/irq/79/smp_affinity
# ... 以此类推
注意:smp_affinity 的值是十六进制位图。比如 "ff" 表示 CPU0-7,"01" 表示只绑CPU0。我曾经见过有人写成 "0xff",结果不生效,排查了半天才发现是格式问题。
二、网卡多队列:让硬件帮你分流
中断亲和性解决了「谁来处理」的问题,但还有一个问题没解决:一个队列的包,能不能让多个核并行处理?
这就引出了网卡多队列技术。现代网卡(比如Intel XL710、Mellanox ConnectX系列)都支持多队列。每个队列可以绑定到不同的CPU核,硬件直接根据哈希值把包分发到不同队列。
这里涉及三个概念,我简单梳理一下:
| 技术 | 层级 | 原理 | 适用场景 |
|---|---|---|---|
| RSS | 硬件 | 网卡硬件根据IP/端口哈希分流 | 多队列网卡,性能最高 |
| RPS | 软件 | 内核软件模拟多队列分发 | 单队列网卡,或RSS不够用 |
| RFS | 软件 | 基于RPS,考虑CPU亲和性 | 需要降低缓存缺失的场景 |
我个人建议:能用RSS就用RSS,硬件分流几乎没有开销。如果网卡不支持,再考虑RPS。RFS呢?说实话,我很少用,因为它的维护成本有点高,而且收益不明显。
配置RSS很简单,用ethtool就行:
# 查看当前队列数
ethtool -l eth0
# 设置队列数为8(需要网卡和驱动支持)
ethtool -L eth0 combined 8
# 查看哈希策略
ethtool -x eth0
关键点:RSS的哈希算法默认是异或(XOR),对于某些场景(比如所有流量都来自同一个五元组),哈希可能不均匀。这时候可以调整哈希key,或者改用Toeplitz算法。我在一个CDN项目中就遇到过这个问题,改了哈希key后,队列负载从「一个队列80%,其他20%」变成了「每个队列12.5%」。
三、内核Bypass:绕过协议栈的终极方案
前面说的都是优化,但内核协议栈本身的开销是绕不过去的。系统调用、内存拷贝、锁竞争……这些在极端低延迟场景下都是致命的。
举个例子:一个64字节的小包,从网卡到应用,经过内核协议栈的路径大约是1-2微秒。听起来很快对吧?但在高频交易场景下,这个延迟是不可接受的。他们需要的是纳秒级的处理。
怎么办?答案是:绕过内核。直接让应用接管网卡。
DPDK:用户态驱动
DPDK(Data Plane Development Kit)的思路很暴力:把网卡驱动搬到用户态,应用通过轮询(Polling)直接从网卡收包,完全不走内核。
我参与过一个5G核心网的项目,用DPDK替换了内核协议栈后,单核处理能力从50万pps提升到了1500万pps。你想想看,30倍的差距!
DPDK的基本架构:
# 绑定网卡到DPDK驱动
dpdk-devbind.py -b igb_uio 0000:02:00.0
# 启动DPDK应用时指定内存通道和核数
./build/app/testpmd -l 0-3 -n 4 -- -i
避坑指南:我曾经在DPDK的巨页配置上栽过跟头。默认的2MB巨页够用,但如果你要处理大量流表,建议用1GB巨页。否则TLB缺失会让你欲哭无泪。配置方法:在grub命令行加上 default_hugepagesz=1G hugepagesz=1G hugepages=8。
XDP:内核内的快速路径
XDP(eXpress Data Path)是另一种思路。它不绕过内核,而是在内核的早期阶段(网卡驱动刚收到包时)就介入处理。你可以用BPF程序直接决定:这个包是丢弃、转发、还是交给协议栈。
XDP的好处是:不需要修改应用代码,也不需要用户态驱动。它就像一个「交通警察」,站在网卡门口,看到可疑的包直接拦下来。
一个简单的XDP丢弃程序:
#include <linux/bpf.h>
SEC("xdp")
int drop_all(struct xdp_md *ctx) {
return XDP_DROP;
}
char _license[] SEC("license") = "GPL";
编译后加载:
ip link set dev eth0 xdp obj drop_all.o
注意:XDP有两种模式:native(网卡驱动支持)和generic(通用模式)。Native模式性能好,但需要网卡驱动支持。Generic模式所有网卡都能用,但性能差很多。我建议先确认你的网卡驱动是否支持XDP native,如果不支持,就别折腾XDP了,直接上DPDK吧。
总结一下
这三板斧,其实对应了三个不同的优化层次:
- 中断亲和性:解决「谁来干活」的问题,成本最低,效果立竿见影。
- 网卡多队列:解决「怎么分活」的问题,硬件支持的话,几乎零成本。
- 内核Bypass:解决「要不要内核」的问题,性能提升最大,但改造成本也最高。
我个人的建议是:先做前两步,收益已经很大了。如果还不够,再考虑DPDK或XDP。毕竟,不是每个业务都需要纳秒级的延迟,但每个业务都值得把中断绑核做好。
嗯,今天就聊到这里。这些内容都是我在实际项目中踩过坑、流过汗总结出来的。希望对你有帮助。