惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
F
Fortinet All Blogs
博客园_首页
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
D
DataBreaches.Net
aimingoo的专栏
aimingoo的专栏
爱范儿
爱范儿
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
D
Docker
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
博客园 - 三生石上(FineUI控件)
量子位
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
The Blog of Author Tim Ferriss
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件
Cloudbric
Cloudbric
博客园 - 司徒正美
H
Help Net Security
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
L
LangChain Blog
Latest news
Latest news
M
MIT News - Artificial intelligence
T
Threat Research - Cisco Blogs
博客园 - Franky
S
Security Affairs
W
WeLiveSecurity
F
Full Disclosure
Know Your Adversary
Know Your Adversary
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
Cyberwarzone
Cyberwarzone
美团技术团队
PCI Perspectives
PCI Perspectives
C
Check Point Blog
Spread Privacy
Spread Privacy

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 排序基准测试:用数据说话
【Linux 网络子系统深度拆解】多队列与流量分发:RSS/RPS/RFS/XPS
2026-04-27 · via 土法炼钢兴趣小组的算法知识备份

一台 64 核服务器配了一张支持 32 队列的万兆网卡,但 sar -n DEV 显示只有 4 个 CPU 在处理网络中断,其余 60 个核空闲——这不是网卡的问题,是流量分发策略没配好。

多队列网卡把入站流量拆分到多个硬件队列,每个队列绑定一个 CPU 中断。但光有硬件队列不够:RSS 决定哪些流量进哪个队列、RPS 在没有硬件 RSS 时提供软件替代、RFS 让包被处理它的应用所在的 CPU 接收、XPS 让发送队列和 CPU 对齐、aRFS 把这一切下沉到硬件。

本文从内核源码拆解这五个机制的实现。

RSS / RPS / RFS / XPS 流量分发全景

一、多队列基础设施

1.1 netdev_queue 与队列数组

每个 net_device 维护 RX 和 TX 两组队列数组:

// include/linux/netdevice.h
struct net_device {
    // ...
    struct netdev_queue    *_tx;              // TX 队列数组
    unsigned int           num_tx_queues;     // 总 TX 队列数
    unsigned int           real_num_tx_queues;// 实际活跃 TX 队列数

    struct netdev_rx_queue *_rx;              // RX 队列数组
    unsigned int           num_rx_queues;     // 总 RX 队列数
    unsigned int           real_num_rx_queues;// 实际活跃 RX 队列数
    // ...
};

TX 队列结构:

// include/linux/netdevice.h
struct netdev_queue {
    struct net_device       *dev;
    struct Qdisc __rcu      *qdisc;        // 绑定的排队规则
    spinlock_t              _xmit_lock;     // 发送锁
    unsigned long           trans_start;    // 最后发送时间戳
    unsigned long           state;          // 队列状态标志
    // BQL (Byte Queue Limits) 相关字段
    struct dql              dql;
};

RX 队列结构:

// include/linux/netdev_rx_queue.h
struct netdev_rx_queue {
    struct xdp_rxq_info             xdp_rxq;
    struct rps_map __rcu            *rps_map;        // RPS CPU 映射
    struct rps_dev_flow_table __rcu *rps_flow_table;  // RFS 流表
    struct napi_struct              *napi;            // 绑定的 NAPI 实例
};

关键的是 real_num_*_queues——它可以小于 num_*_queuesethtool -L eth0 combined 16 修改的就是活跃队列数。未使用的队列不分配中断,不消耗 CPU。

1.2 队列与中断的绑定

每个 RX 队列对应一个 MSI-X 中断向量。中断亲和性通过 /proc/irq/<N>/smp_affinity 设置,决定哪个 CPU 处理该队列的 NAPI 轮询。

# 查看队列数
ethtool -l eth0

# 查看中断分布
cat /proc/interrupts | grep eth0

# 设置中断亲和性
echo 2 > /proc/irq/42/smp_affinity  # 绑定到 CPU 1

irqbalance 守护进程自动平衡中断分布,但在高性能场景中通常手动配置以避免抖动。

RSS(Receive Side Scaling)是网卡硬件实现的多队列流量分发。网卡对每个入站包计算哈希值,通过查表决定送入哪个 RX 队列。

2.1 Toeplitz 哈希

RSS 标准使用 Toeplitz 哈希算法。输入是包的 N 元组(通常是四元组:源 IP + 目的 IP + 源端口 + 目的端口),密钥是 40 字节(320 位)的哈希键。

算法原理:将输入的每一位与哈希键做异或移位运算,产生 32 位哈希值。相同流量的包总是产生相同的哈希值,因此始终被分到同一个队列——这保证了同一 TCP 连接的包在同一 CPU 上顺序处理。

// include/linux/ethtool.h
struct ethtool_rxfh_param {
    u8   hfunc;           // 哈希函数(ETH_RSS_HASH_TOP = Toeplitz)
    u32  indir_size;      // 间接表大小
    u32  *indir;          // 间接表:hash_value % size → queue_index
    u32  key_size;        // 哈希键大小(通常 40 字节)
    u8   *key;            // Toeplitz 哈希键
    u32  rss_context;     // RSS 上下文 ID
    u8   input_xfrm;      // 输入变换方法
};

2.2 间接表

哈希值不直接映射到队列——中间有一个间接表(indirection table)。表的每个条目存储一个队列编号:

Hash(pkt) = 0x3A7B1C2D
Index = 0x3A7B1C2D % 128 = 45
Queue = indirection_table[45] = 7
→ 包送入 RX Queue 7

间接表的大小通常是 128 或 256。通过修改间接表,可以不改变哈希算法的情况下重新分配流量。比如下线一个 CPU 时,只需要把间接表中指向该队列的条目改指其他队列。

# 查看 RSS 配置
ethtool -x eth0

# 修改间接表和哈希键
ethtool -X eth0 equal 16    # 平均分配到 16 个队列
ethtool -X eth0 weight 1 0 1 0  # 只用奇数队列

# 查看/修改哈希输入字段
ethtool -n eth0 rx-flow-hash tcp4  # 查看 TCP 哈希使用哪些字段
ethtool -N eth0 rx-flow-hash tcp4 sdfn  # src/dst IP + src/dst port

Linux 6.x 支持多个 RSS 上下文(rss_context),允许不同的流量使用不同的间接表。配合 ethtool 的 flow steering 规则,可以把特定流量(比如 VXLAN 内层)定向到专用队列集合。

三、RPS:软件接收包导向

RPS(Receive Packet Steering)是 RSS 的软件实现。当网卡不支持多队列、或队列数少于 CPU 数时,RPS 在 netif_receive_skb() 路径中把包转发到其他 CPU 处理。

3.1 核心数据结构

// include/linux/netdevice.h
struct rps_map {
    unsigned int    len;    // CPU 数量
    struct rcu_head rcu;
    u16             cpus[]; // 候选 CPU 列表
};

struct rps_dev_flow {
    u16             cpu;       // 该流量当前绑定的 CPU
    u16             filter;    // 硬件过滤器 ID(aRFS 用)
    unsigned int    last_qtail;// 上次排队时的队列尾指针
};

struct rps_dev_flow_table {
    unsigned int    mask;      // 哈希掩码
    struct rcu_head rcu;
    struct rps_dev_flow flows[];
};

每个 RX 队列有自己的 rps_map(候选 CPU 列表)和 rps_dev_flow_table(流表)。

3.2 get_rps_cpu():CPU 选择逻辑

当 NAPI 收包后调用 netif_receive_skb(),如果当前 RX 队列配置了 RPS,内核调用 get_rps_cpu() 选择目标 CPU:

// net/core/dev.c(简化)
static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
                       struct rps_dev_flow **rflowp)
{
    struct rps_map *map;
    struct rps_dev_flow_table *flow_table;
    u32 hash;
    int cpu = -1;

    map = rcu_dereference(rxqueue->rps_map);
    flow_table = rcu_dereference(rxqueue->rps_flow_table);

    // 1. 计算包的哈希值(skb_get_hash)
    hash = skb_get_hash(skb);

    // 2. 如果配置了 RFS,先查 rps_sock_flow_table
    if (sock_flow_table) {
        u32 ident = sock_flow_table->ents[hash & sock_flow_table->mask];
        // 检查应用进程在哪个 CPU 上运行
        if ((ident ^ hash) & ~rps_cpu_mask == 0) {
            next_cpu = ident & rps_cpu_mask;
            // RFS 覆盖 RPS 选择
        }
    }

    // 3. 如果没有 RFS 命中,使用 RPS 映射
    if (cpu < 0 && map) {
        cpu = map->cpus[reciprocal_scale(hash, map->len)];
    }

    return cpu;
}

如果选出的 CPU 不是当前 CPU,内核将 skb 放入目标 CPU 的 per-CPU backlog 队列(sd->input_pkt_queue),然后触发该 CPU 的 NET_RX_SOFTIRQ。目标 CPU 在自己的软中断中处理这些包。

3.3 IPI 与 backlog

RPS 的核心开销是跨 CPU 投递:

  1. 将 skb 放入目标 CPU 的 input_pkt_queue
  2. 如果目标 CPU 还没被调度,发送 IPI(Inter-Processor Interrupt)唤醒它
  3. 目标 CPU 在 process_backlog() 中处理包

IPI 的代价约 1-2 微秒。如果流量已经在正确的 CPU 上(比如 RSS 已经做了正确的分发),RPS 的 IPI 反而是额外开销。因此 RPS 主要用于:

  • 网卡队列数少于 CPU 数(比如只有 1-2 个队列的虚拟网卡)
  • 虚拟化环境中 veth 等不支持 RSS 的设备

3.4 配置

# 为 rx-0 队列启用 RPS,分发到 CPU 0-7
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# 设置 backlog 队列长度
sysctl -w net.core.netdev_budget=600
sysctl -w net.core.netdev_max_backlog=10000

四、RFS:应用感知的流量导向

RPS 把包分到哪个 CPU 是基于哈希——但哈希选出的 CPU 可能不是运行目标应用的 CPU。RFS(Receive Flow Steering)解决这个问题:让包被送到应用进程正在运行的 CPU 上,最大化缓存局部性。

4.1 rps_sock_flow_table

RFS 维护一个全局流表 rps_sock_flow_table,记录每个流量的应用进程当前在哪个 CPU 上:

// include/linux/netdevice.h
struct rps_sock_flow_table {
    u32     mask;
    u32     ents[] ____cacheline_aligned_in_smp;
};

每个条目是 32 位值,高位存储哈希验证(防止冲突),低位存储 CPU 编号。

4.2 记录时机

当应用程序调用 recvmsg()sendmsg() 等 socket 操作时,内核调用 rps_record_sock_flow() 更新流表:

// include/linux/netdevice.h
static inline void rps_record_sock_flow(struct rps_sock_flow_table *table,
                                        u32 hash)
{
    unsigned int index = hash & table->mask;
    u32 val = hash & ~rps_cpu_mask;
    val |= raw_smp_processor_id();  // 当前 CPU = 应用所在 CPU
    if (READ_ONCE(table->ents[index]) != val)
        WRITE_ONCE(table->ents[index], val);
}

每次应用在 CPU X 上调用 recv(),流表就更新为”这个流的应用在 CPU X 上”。下次该流的包到来时,RPS 会优先把包送到 CPU X。

4.3 与 RPS 的协同

RFS 不是独立工作的——它覆盖 RPS 的 CPU 选择。在 get_rps_cpu() 中:

  1. 先查 rps_sock_flow_table,看该流的应用在哪个 CPU
  2. 如果查到了,且该 CPU 在 rps_map 的候选列表中,就使用它
  3. 如果没查到(流表中没有该流的记录),回退到 RPS 的哈希选择

RFS 的前提是应用进程有相对稳定的 CPU 亲和性。如果进程频繁在不同 CPU 之间迁移,RFS 的流表会不断更新,反而增加了缓存失效。

4.4 配置

# 设置全局 RFS 流表大小(建议 = 期望并发连接数)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# 设置每个 RX 队列的流表大小
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

五、XPS:发送端队列选择

XPS(Transmit Packet Steering)解决发送方向的队列选择问题。没有 XPS 时,netdev_pick_tx() 可能选择一个与当前 CPU 不相关的 TX 队列,导致跨 CPU 竞争发送锁。

5.1 两种映射模式

// include/linux/netdevice.h
enum xps_map_type {
    XPS_CPUS = 0,    // CPU → TX 队列映射
    XPS_RXQS,        // RX 队列 → TX 队列映射
    XPS_MAPS_MAX,
};

struct xps_map {
    unsigned int    len;
    unsigned int    alloc_len;
    struct rcu_head rcu;
    u16             queues[];   // 候选 TX 队列列表
};

struct xps_dev_maps {
    struct rcu_head rcu;
    unsigned int    nr_ids;     // CPU 数或 RX 队列数
    s16             num_tc;     // 流量类别数
    struct xps_map __rcu *attr_map[];
};

CPU 模式XPS_CPUS):每个 CPU 映射到一组 TX 队列。CPU 3 发包时,直接选择与 CPU 3 关联的 TX 队列,不需要竞争其他 CPU 的锁。

RX 队列模式XPS_RXQS):将 RX 队列映射到 TX 队列。这在请求-响应模式中特别有用——应答包的发送队列与请求包的接收队列对齐,保持 NUMA 局部性。

5.2 netdev_pick_tx():发送队列选择

// net/core/dev.c(简化)
u16 netdev_pick_tx(struct net_device *dev, struct sk_buff *skb,
                   struct net_device *sb_dev)
{
    // 1. 如果驱动实现了 ndo_select_queue,使用驱动的选择
    if (ops->ndo_select_queue)
        return ops->ndo_select_queue(dev, skb, sb_dev);

    // 2. 查 XPS 映射
    int new_index = get_xps_queue(dev, sb_dev, skb);

    // 3. 如果 XPS 没有命中,使用 skb 哈希
    if (new_index < 0)
        new_index = skb_tx_hash(dev, sb_dev, skb);

    // 4. 缓存选择结果到 skb->queue_mapping
    return new_index;
}

5.3 配置

# CPU 模式:CPU 0-3 使用 TX 队列 0,CPU 4-7 使用 TX 队列 1
echo f > /sys/class/net/eth0/queues/tx-0/xps_cpus   # CPU 0-3
echo f0 > /sys/class/net/eth0/queues/tx-1/xps_cpus  # CPU 4-7

# RX 队列模式:RX 队列 N 的回复走 TX 队列 N
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_rxqs
echo 2 > /sys/class/net/eth0/queues/tx-1/xps_rxqs

最简单的最佳实践:一对一映射——CPU N → TX Queue N → RX Queue N。这保证了收发路径在同一 CPU 上,最大化 L1/L2 缓存命中率。

六、aRFS:硬件加速的流表

aRFS(Accelerated RFS)把 RFS 的逻辑下沉到网卡硬件。网卡的 Flow Director 功能直接根据五元组把包送到正确的 RX 队列——不需要软件 RPS 的 IPI 开销。

6.1 驱动回调

// include/linux/netdevice.h
struct net_device_ops {
    // ...
#ifdef CONFIG_RFS_ACCEL
    int (*ndo_rx_flow_steer)(struct net_device *dev,
                             const struct sk_buff *skb,
                             u16 rxq_index,
                             u32 flow_id);
#endif
};

当 RFS 检测到一个流应该被分到不同的 RX 队列时,内核调用 ndo_rx_flow_steer(),请求网卡硬件创建或更新 Flow Director 规则:

  • skb:示例包(包含五元组信息)
  • rxq_index:目标 RX 队列(应用所在 CPU 对应的队列)
  • flow_id:流标识符

网卡硬件收到请求后,将该五元组的规则写入 Flow Director 表。后续该流的包直接被硬件送到目标队列,无需 RPS 的软件重定向。

6.2 流表过期

// include/linux/netdevice.h
bool rps_may_expire_flow(struct net_device *dev, u16 rxq_index,
                         u32 flow_id, u16 filter_id);

当流量停止或应用迁移到其他 CPU 时,rps_may_expire_flow() 检查硬件流表规则是否可以清除。这防止 Flow Director 表被过期规则填满。

6.3 配置前提

aRFS 需要:

  1. 网卡驱动实现 ndo_rx_flow_steer(Intel ixgbe、i40e、mlx5 等支持)
  2. 系统启用了 RPS 和 RFS
  3. 网卡固件支持 Flow Director 或 N-tuple 过滤
# 检查是否支持 ntuple(aRFS 前提)
ethtool -k eth0 | grep ntuple
ntuple-filters: on

# 启用 ntuple(如果关闭)
ethtool -K eth0 ntuple on

七、完整数据流

把五个机制串联起来,一个包的完整 CPU 分配流程:

                    ┌─ 硬件 ─────────────────────────┐
NIC 收包            │                                │
    ↓               │  RSS: Toeplitz(5-tuple)        │
    ↓               │  → indirection_table[hash%N]   │
    ↓               │  → RX Queue K                  │
    ↓               │                                │
    ↓               │  aRFS: Flow Director           │
    ↓               │  → 五元组精确匹配 → RX Queue M │
                    └────────────────────────────────┘
    ↓
NAPI 轮询(CPU = IRQ 绑定的 CPU)
    ↓
netif_receive_skb()
    ↓
    ├─ RPS 启用?
    │   ↓ 是
    │   get_rps_cpu()
    │   ├─ RFS 命中?→ 使用应用所在 CPU
    │   └─ 否 → 使用 rps_map 哈希选择
    │   ↓
    │   目标 CPU != 当前 CPU?
    │   ├─ 是 → enqueue_to_backlog() + IPI
    │   └─ 否 → 本地处理
    ↓
协议栈处理 → socket → 应用 recv()
    ↓
rps_record_sock_flow()  ← 更新 RFS 流表
    ↓
应用 send()
    ↓
netdev_pick_tx()
    ├─ XPS 命中?→ 使用 CPU/RXQ 关联的 TX 队列
    └─ 否 → skb_tx_hash() 哈希选择
    ↓
TX Queue → NIC 发送

八、NUMA 感知与性能调优

8.1 NUMA 拓扑对齐

在 NUMA 系统中,网卡通过 PCIe 连接到某个 NUMA 节点。最优配置是:

  • 网卡中断绑定到同一 NUMA 节点的 CPU
  • RSS 队列映射到同一 NUMA 节点的 CPU
  • XPS 同样映射到同一 NUMA 节点

跨 NUMA 访问内存的延迟约 100ns,相比本地访问的 30-40ns 多出 2-3 倍。在 10Gbps 线速处理小包时,这个差异足以成为瓶颈。

# 查看网卡的 NUMA 节点
cat /sys/class/net/eth0/device/numa_node

# 查看 NUMA 拓扑
numactl --hardware

# 将网卡中断绑定到本地 NUMA 节点的 CPU
# 假设网卡在 NUMA 0,CPU 0-15 属于 NUMA 0
for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    echo ffff > /proc/irq/$irq/smp_affinity
done

8.2 irqbalance 与手动配置

irqbalance 是 Linux 默认的中断负载均衡守护进程。它会周期性地重新分配中断到不同 CPU,对于通用服务器是合理的默认值。

但对于网络密集型应用,irqbalance 的重新分配会破坏缓存局部性。最佳实践是:

  1. 关闭 irqbalancesystemctl stop irqbalance
  2. 手动设置中断亲和性
  3. 配置 RSS + XPS 一对一映射
  4. 如果应用线程数固定,启用 RFS

8.3 常见调优参数

参数 路径 默认值 建议
RX 队列数 ethtool -L 取决于网卡 = CPU 数(同 NUMA 节点)
RSS 间接表 ethtool -X 均匀分配 按需调整
RPS CPU 掩码 rx-N/rps_cpus 0(关闭) 虚拟网卡场景启用
RFS 流表大小 rps_sock_flow_entries 0(关闭) 期望并发连接数
XPS CPU 映射 tx-N/xps_cpus 一对一映射
backlog 队列 netdev_max_backlog 1000 10Gbps+ 增大到 10000
NAPI budget netdev_budget 300 高 PPS 增大到 600

九、可观测性

9.1 查看当前分发状态

# 查看每个 CPU 的软中断处理量
cat /proc/softirqs | grep NET_RX

# 查看每个 RX 队列的包计数
ethtool -S eth0 | grep rx_queue

# 查看 RPS 统计
cat /proc/net/softnet_stat
# 每行对应一个 CPU:processed, dropped, time_squeeze, ...

9.2 bpftrace 追踪

追踪 RPS CPU 选择:

bpftrace -e '
kprobe:get_rps_cpu {
    printf("rps_select: dev=%s\n",
           ((struct net_device *)arg0)->name);
}
kretprobe:get_rps_cpu /retval >= 0/ {
    printf("  → cpu=%d\n", retval);
}'

追踪 XPS 队列选择:

bpftrace -e '
kretprobe:netdev_pick_tx {
    printf("tx_queue=%d cpu=%d\n", retval, cpu);
}'

追踪跨 CPU 投递(IPI):

bpftrace -e '
kprobe:enqueue_to_backlog {
    printf("backlog: skb→cpu=%d from cpu=%d\n",
           arg1, cpu);
}'

9.3 softnet_stat 解读

cat /proc/net/softnet_stat

每行是一个 CPU,字段含义:

含义 警报阈值
1 处理的包总数 不均匀说明分发不平衡
2 backlog 队列满丢包数 >0 需增大 netdev_max_backlog
3 time_squeeze 次数 >0 说明 budget 不够

如果第 2 列在某些 CPU 上增长,说明该 CPU 的 backlog 满了——可能是 RPS 把太多流量导到了同一个 CPU。

十、参考文献

  • Linux 6.6 内核源码 include/linux/netdevice.hnet/core/dev.c
  • Linux 6.6 内核源码 include/linux/netdev_rx_queue.h
  • Linux 内核文档 Documentation/networking/scaling.rst
  • Tom Herbert, Dave Taht,《Understanding the Linux Networking Stack》
  • Intel,《Receive Side Scaling on Intel Network Adapters》

上一篇隧道协议内核实现:VXLAN、IPIP、GRE 与 WireGuard

下一篇分段卸载:GRO/GSO/TSO 的内核实现与陷阱

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-23 · linux / networking

【Linux 网络子系统深度拆解】Traffic Control 深度拆解:qdisc、class 与 filter

dev_queue_xmit() 不是直接把包交给网卡——中间还有一层 Traffic Control。本文从 Linux 6.6 内核源码拆解 TC 框架的完整实现:struct Qdisc 与 Qdisc_ops 操作表、pfifo_fast/fq_codel/HTB/TBF 的内核实现差异、TCQ_F_CAN_BYPASS 快路径、TCQ_F_NOLOCK 无锁排队、EDT(Earliest Departure Time)调度模型、TC BPF direct-action 模式,以及 MQ 多队列根 qdisc 与 netdev_queue 的关系。