WEBKT

突破单核软中断瓶颈:云服务器环境下通过 RPS/RFS 解决 Nginx 丢包实战

4 0 0 0

在公有云环境(如阿里云、腾讯云、AWS 等)中部署高并发、大吞吐量的 Nginx 网关时,你可能会遇到这样一种诡异的现象:系统整体 CPU 利用率并不高(甚至低于 30%),但 Nginx 开始出现随机的连接超时、握手失败或响应丢包;通过 top 观察,发现 CPU0(或某单个核心)的 si(软中断)占比长期处于 90% ~ 100% 满载状态,而其他 CPU 核心却闲得发慌。

这种由于单核网络软中断过高导致的性能瓶颈,在云服务器(尤其是规格较小、不支持硬件多队列网卡,或者多队列未与 vCPU 绑定均衡的 VM)上极为常见。

本文将深入探讨该问题的底层根源,并提供通过调节 Linux 内核 RPS (Receive Packet Steering)RFS (Receive Flow Steering) 参数,将单核软中断负载完美分摊到多核上的完整实战指南。


一、 为什么会发生单核软中断过高?

在传统的物理服务器中,网卡通过 RSS (Receive Side Scaling, 接收端缩放) 硬件功能,利用哈希算法将不同的网络流分发到不同的硬件接收队列(Rx Queue)中,每个队列对应一个独立的 CPU 中断。

但在**云服务器(虚拟机)**环境下,情况会有所不同:

  1. 网卡队列受限:低规格的云服务器通常只分配了单队列(Single Queue)虚拟网卡。
  2. 中断绑定不均:即使云服务器支持多队列,虚拟化层(Hypervisor)分发中断的机制也可能失效,或者网卡中断默认全部绑定到了 CPU0 上。

当大流量涌入时,所有的网卡接收中断(NET_RX)全部由 CPU0 独占处理。一旦 CPU0 的软中断处理能力达到饱和,后续到达的报文就会在内核的接收队列(backlog)中积压,最终被无情丢弃。此时,跑在其他核心上的 Nginx 工作进程由于拿不到报文,只能处于饥饿状态,导致客户端感知到丢包和高延迟。


二、 核心武器:RPS 与 RFS

为了解决硬件/虚拟化层无法多队列分流的问题,Linux 内核在 2.6.35 版本引入了软件模拟的解决方案:

  1. RPS (Receive Packet Steering)

    • 原理:在软件层面模拟多队列网卡。当单个 CPU 核心(如 CPU0)收到网卡中断并把报文从 ring buffer 读入内核后,在软件中断上下文里,根据报文的四元组(源IP、目的IP、源端口、目的端口)计算出哈希值,然后将报文分发到指定的其他 CPU 核心的接收队列中,由这些 CPU 协助处理后续的协议栈解码工作。
    • 作用:将 NET_RX 的 CPU 消耗分摊到多个核心。
  2. RFS (Receive Flow Steering)

    • 原理:RPS 只管哈希分发,不管应用进程在哪运行。这会导致“CPU-A 刚处理完报文协议栈,却要把数据复制给运行在 CPU-B 上的 Nginx 进程”的情况,造成极大的 CPU 缓存缺失(Cache Miss)。RFS 则是 RPS 的智能升级版,它会感知当前应用程序(如 Nginx 进程)运行在哪个 CPU 上,并优先将该连接的网络报文分发到该应用程序所在的 CPU 核心上处理。
    • 作用:提升 CPU 缓存命中率,进一步降低网络延迟和系统开销。

三、 故障诊断与指标观测

在动手优化前,必须先确诊。请依次执行以下命令:

1. 确认软中断分布

使用 mpstat(来自 sysstat 工具包)观察各核心的软中断占比:

mpstat -P ALL 1 3

观察 %soft(或 top 下的 %si)列。如果仅有 CPU0 接近 100%,而其他核心接近 0%,则基本确诊。

也可以直接查看系统软中断计数器的变化:

watch -d cat /proc/softirqs

重点观察 NET_RX(网络接收)这一行在各个 CPU 上的增长速度。

2. 确认 Nginx 是否在丢包

查看内核网络丢包计数器:

netstat -s | grep "packet receive errors"

或者查看网卡底层的丢包统计:

ethtool -S eth0 | grep rx_dropped

如果这些计数器在持续飙升,说明网卡收包缓冲区或内核接收队列已经溢出。

3. 检查网卡多队列支持情况

ethtool -l eth0
  • 如果 Combined 值为 1,说明是单队列网卡,必须开启 RPS/RFS 进行软件分流。
  • 如果 Combined 大于 1 且当前已启用,但中断依然倾斜,说明中断绑定未生效或需要 RPS 辅助。

四、 RPS 与 RFS 优化配置实战

下面以单网卡 eth0、8核云服务器(CPU 编号 0-7)为例进行配置。

第一步:计算并配置 RPS CPU 掩码(Bitmask)

RPS 通过一个十六进制的 CPU 掩码来控制将流量分发到哪些核心。

  • 掩码计算规则
    每一个 CPU 核心对应二进制中的一位(从右往左,CPU0 是最低位)。
    假设我们有 8 核,想让 CPU0-CPU7 全部参与网络收包处理:

    CPU7 CPU6 CPU5 CPU4 CPU3 CPU2 CPU1 CPU0
    1 1 1 1 1 1 1 1

    对应的二进制为 11111111,转换为十六进制即为 ff

    如果你希望避开 CPU0(让 CPU0 只负责物理中断,其他核心负责软中断),则二进制为 11111110,转换为十六进制即为 fe

  • 写入 RPS 配置
    将计算好的掩码写入网卡接收队列的控制文件。若为单队列网卡,对应的路径为 rx-0

    echo "ff" > /sys/class/net/eth0/queues/rx-0/rps_cpus
    

    (注:如果是多队列网卡,如 rx-0rx-1 等,需对所有队列分别写入对应的掩码。)

第二步:配置全局和单队列的 RFS 流表大小

启用 RFS 需要配置两个参数:全局套接字流表大小(rps_sock_flow_entries)和单个队列的流表大小(rps_flow_cnt)。

  • 参数大小估算

    • rps_sock_flow_entries:建议设置为最大预期并发连接数。在高并发 Nginx 网关中,通常设置为 3276865536
    • rps_flow_cnt:单个队列分配的流表大小。计算公式:rps_flow_cnt = rps_sock_flow_entries / 接收队列数量
      在我们的单队列(rx-0)示例中:rps_flow_cnt 直接设为 65536。如果是双队列,则每个设为 32768
  • 写入 RFS 配置

    1. 设置全局 RFS 流表:
      sysctl -w net.core.rps_sock_flow_entries=65536
      
    2. 设置单个队列的 RFS 流表(单队列网卡示例):
      echo 65536 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
      

第三步:调整内核网卡接收队列溢出上限

默认的内核网络驱动队列限制较小,高并发下即使分流也容易瞬间溢出,建议同步放大以下内核参数:

编辑 /etc/sysctl.conf 并追加:

# 每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目
net.core.netdev_max_backlog = 16384

# 允许系统分配的套接字最大缓冲区
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

执行命令使之生效:

sysctl -p

五、 联动优化:Nginx CPU 亲和性(Affinity)

在开启 RFS 后,由于 RFS 会主动追踪 Nginx 工作进程所在的 CPU 核心并将报文定向分发过去,因此锁定 Nginx 进程的 CPU 亲和性能够产生极强的协同效应,最大化发挥 RFS 的就近处理优势。

修改 Nginx 配置文件 /etc/nginx/nginx.conf

user nginx;
# 自动根据 CPU 核心数启动对应数量的 worker 进程
worker_processes auto;

# 开启自动 CPU 亲和性绑定,将 worker 进程一一绑定到不同的 CPU 核心上
worker_cpu_affinity auto;

events {
    # 适当调大单个 worker 的连接限制
    worker_connections 65535;
    use epoll;
}

保存并重载 Nginx 配置:

nginx -s reload

六、 优化效果验证

完成配置后,使用压测工具(如 wrkab)重新发起高并发请求,并观察系统指标变化:

  1. 软中断均衡度
    再次运行 mpstat -P ALL 1,你应当会看到先前一家独大的 CPU0%soft 显著下降,而 CPU1 ~ CPU7%soft 开始均匀上升。

  2. 丢包情况
    持续观察 netstat -s | grep "packet receive errors" 计数器,应当停止增长,Nginx 的连接建立超时报错消失。

  3. 延迟指标
    在客户端测试请求,P99、P999 的响应延迟将会变得更加平滑,不再出现由于包排队导致的尖峰。

七、 配置持久化脚本

为了防止云服务器重启后上述 /sys 下的临时配置丢失,建议将初始化命令写成脚本,并在系统启动时执行。

创建脚本 /etc/rc.local(或通过 systemd 服务调用):

#!/bin/bash
# 激活 eth0 网卡的 RPS,分发至所有 8 个核心
echo "ff" > /sys/class/net/eth0/queues/rx-0/rps_cpus

# 激活 eth0 网卡的 RFS
echo 65536 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

exit 0

确保脚本具有可执行权限:

chmod +x /etc/rc.local

总结

在云原生和虚拟化环境中,受限于物理资源抽象和虚拟网卡能力,单核网络软中断瓶颈是制约高性能网关吞吐量的常见拦路虎。通过启用 RPS 与 RFS,我们在软件层面架设了一座多路分流桥梁,配合 Nginx CPU 绑定,实现了网络报文从驱动层、内核协议栈到应用层进程的“全链路单核就近处理”。该优化不花一分钱,却能极大压榨云服务器的 CPU 潜在网卡处理极限,是每位架构师和 SRE 的必备调优技能。

运维架构师老杰 NginxLinux内核调优软中断

评论点评