扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

Nginx | CPU 层面性能优化秘诀:时间片拉满 + 内核级负载均衡,让性能直接“狂飙”!

18小时前
165
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是 WeiyiGeek,一名深耕安全运维开发(SecOpsDev)领域的技术从业者,致力于探索DevOps与安全的融合(DevSecOps),自动化运维工具开发与实践,企业网络安全防护,欢迎各位道友一起学习交流、一起进步 ,若此文对你有帮助,一定记得点个关注⭐与小红星❤️或加入到作者知识星球『 全栈工程师修炼指南』,转发收藏学习不迷路  。

话接上文《Nginx | CPU 层面性能优化秘诀:Worker 进程绑定、减少上下文切换,彻底榨干每一颗核心!》,继续讲解针对 Nginx 中间件CPU 层面优化。

温馨提示:若文章代码块中存在乱码或不能复制,请联系作者,也可通过文末的阅读原文链接,加入知识星球中阅读,原文链接:https://articles.zsxq.com/id_c5gt26jx4zcp.html

1.1 CPU层面优化

如何调整CPU时间片的大小?

Linux 中进程的优先级由 静态优先级(nice 值 -20 ~ 20)和动态优先级(PR 值 0 ~ 139)共同决定。

静态优先级范围从 -20(最高)到 +20(最低),默认值为 0;数值越小,优先级越高,例如:nice = -20 表示"不友好",抢占更多CPU 动态优先级在不同的内核版本中略有不同,例如:

    • 在 2.6.23 版本前:基于O(1)调度器,PR = nice + 120,动态偏移 ±5,时间片范围 5ms–800ms,针对不同IO密集型进程受奖励(缩短时间片),CPU密集型受惩罚(延长至800ms)。• 在 2.6.23 版本后:基于CFS调度器,PR = nice + 20,时间片由虚拟运行时间(vruntime)动态计算。

在 Nginx 中可通过 worker_priority 指令设置 worker 进程的调度优先级,例如:默认值为 0,生产环境推荐设为 -20,以最大化单次时间片长度。

Syntax:	worker_priority number;
Default: worker_priority 0;
Context: main

# 示例配置
worker_priority -20;

同样,使用 top 命令查看 Nginx 进程的优先级,例如:

top -c -p $(ps aux | grep "nginx" | grep -E "(master process|worker process)" | sed -e 's#[ ][ ]*# #g' | cut -d " " -f 2 | paste -s -d ',') -n 1

weiyigeek.top-查看 Nginx 进程优先级情况图

为理清多核CPU间负载均衡机制,需深入讲解CPU缓存、CPU架构等内核底层知识,本小节将讲述多核CPU环境下提升Nginx性能的负载均衡关键技术,包括 accept_mutex 机制、reuseport 内核特性、多队列网卡RSS/RPS/RFS)、CPU 缓存局部性(worker_cpu_affinity)及NUMA架构优化原理与实测指标分析。

多核 CPU 间 Worker 进程负载均衡方案有那些?

在旧版 Linux Kernel 内核中,由于实现缺陷导致多个 worker 被同时唤醒(epoll_wait返回),未获请求者随即休眠,造成CPU空转与上下文切换开销。所以在早期的 Nginx 版本中默认是 accept_mutex on 指令,主要解决前面所述的惊群效应(Thundering Herd)问题,即在多个 worker 进程在监听套接字时,同一时刻仅需一个进程处理新连接,Nginx 开发者在应用层面引入了 accept_mutex 机制,即在应用层(NGINX)加锁,确保任意时刻仅有一个worker进程执行 accept() 系统调用,其余进程阻塞等待,从而避免惊群。

当前 Linux 内核在 3.9+ 版本之后已经解决了惊群问题,因此,所以在最新版本的 Nginx 中默认关闭了 accept_mutex 功能,缺省为 accept_mutex off,可改为使用 Linux 内核的 SO_REUSEPORT 套接字选项,该机制允许多个 worker 同时监听同一端口,内核根据哈希算法会自动分配连接至不同worker,无需应用层锁,完全消除惊群。

温馨提示:SO_REUSEPORT 功能在 Linux 中没有对应的全局内核参数(如 /etc/sysctl.conf 中的设置),它完全是一个套接字级别的选项,需要在应用程序代码中显式设置才能生效。

从下图中 accept_mutex (应用级) 与 reuseport (SO_REUSEPORT 套接字选项) 性能对比测试结果可知;其从三个维度进行对比吞吐量(Requests/sec)、时延(Latency)和时延标准差(Latency STDEV),进行了详尽的对比测试。

• Requests/sec:accept_mutex on 性能最差;accept_mutex off 略有提升;启用reuseport 后吞吐量巨幅提升。

• Latency: accept_mutex off 时延升高(红色曲线),因负载不均导致部分 worker 过载、请求排队。

• 时延标准差(Latency STDEV):accept_mutex off 时标准差最大,反映请求处理时间波动剧烈;reuseport 显著降低方差,提升服务稳定性。

weiyigeek.top-负载均衡方案性能对比图

那么如何查看当前系统是否支持 SO_REUSEPORT 套接字选项?可通过编写一个简单的 C 语言程序进行测试。

// 代码示例:test_reuseport.c
tee test_reuseport.c <<EOF
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<errno.h>

intmain() {
// 1. 创建一个套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
        perror("socket");
exit(EXIT_FAILURE);
    }

// 2. 尝试设置 SO_REUSEPORT 选项
int opt = 1;
// SOL_SOCKET 是选项的级别, SO_REUSEPORT 是我们要测试的选项名
if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) == 0) {
printf("成功: 系统支持 SO_REUSEPORT。n");
    } else {
// 如果 setsockopt 调用失败,打印错误原因
        perror("setsockopt 失败");
printf("结论: 系统不支持 SO_REUSEPORT (或内核版本过旧)。n");
    }

    close(fd);
return0;
}
EOF

// 编译和运行:
gcc -o test_reuseport test_reuseport.c
./test_reuseport
// 成功: 系统支持 SO_REUSEPORT。

结果解读:

• 若输出 成功: 系统支持 SO_REUSEPORT:则表明你的内核和头文件都支持该选项。通常情况下,这意味着你的内核版本高于 3.9。

• 若输出 ·setsockopt 失败: Protocol not available· 或类似错误,则表示系统不支持 SO_REUSEPORT,可能的原因是你的内核版本过低,或者内核在编译时没有开启 CONFIG_NET_SOCKET_REUSEPORT 选项。

综上所示,在 Linux 3.9+ 内核版本中,推荐使用 SO_REUSEPORT 套接字选项代替 accept_mutex,以获得全面性能优势,例如:http 和 stream 模块均可启用。

# ngx_http_core_module
Syntax:	listen address[:port] [default_server] [ssl] [http2 | quic] [proxy_protocol] [setfib=number] [fastopen=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
listen port [default_server] [ssl] [http2 | quic] [proxy_protocol] [setfib=number] [fastopen=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
listen unix:path [default_server] [ssl] [http2 | quic] [proxy_protocol] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
Default:	
listen *:80 | *:8000;
Context:	server

# ngx_stream_core_module
Syntax:	listen address:port [default_server] [ssl] [udp] [proxy_protocol] [setfib=number] [fastopen=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
Default:	—
Context:	server

# 配置示例
server {
  listen 80 reuseport backlog=4096;  # 关键点 reuseport backlog=4096
  server_name test.weiyigeek.top;
  charset utf-8;
  default_type text/html;

# 启用 HTTP/2 支持
  http2 on;

# 日志文件
  access_log /var/log/nginx/test.log main;
  error_log /var/log/nginx/test.err.log debug;

# 静态资源 location 配置
  location / {
    root  /usr/local/nginx/html;
    index index.html;
# 开启缓存,最大缓存1000个文件描述符,在单位时间 60s 内未被访问将被移除
    open_file_cache          max=1000 inactive=60s;
# 30秒内检查缓存文件句柄的有效性,确保获取最新的文件信息
    open_file_cache_valid    30s;
# 60秒内最少使用2次后才留继续在缓存中。
    open_file_cache_min_uses 2;
# 开启错误信息缓存
    open_file_cache_errors   on;
  }
}

配置完毕后,重启 Nginx 服务并使用 ss -tlnp | grep :80 命令查看端口监听状态,可用发现有多少个 Nginx Worker 进程就有多少个PID监听同端口。

# 查看 Nginx worker 进程数量
$ ps aux | grep "nginx: worker process" | grep -v "grep" | wc -l
16

# 查看启用 reuseport 端口监听效果
$ ss -tlnp | grep ":80 "
LISTEN 0      4096         0.0.0.0:80         0.0.0.0:*    users:(("nginx",pid=48151,fd=36),("nginx",pid=48150,fd=36),("nginx",pid=48149,fd=36),("nginx",pid=48148,fd=36),("nginx",pid=48147,fd=36),("nginx",pid=48146,fd=36),("nginx",pid=48145,fd=36),("nginx",pid=48144,fd=36),("nginx",pid=48143,fd=36),("nginx",pid=48142,fd=36),("nginx",pid=48141,fd=36),("nginx",pid=48140,fd=36),("nginx",pid=48139,fd=36),("nginx",pid=48138,fd=36),("nginx",pid=48137,fd=36),("nginx",pid=48136,fd=36),("nginx",pid=3752,fd=36))
LISTEN 0      4096         0.0.0.0:80         0.0.0.0:*    users:(("nginx",pid=48151,fd=35),("nginx",pid=48150,fd=35),("nginx",pid=48149,fd=35),("nginx",pid=48148,fd=35),("nginx",pid=48147,fd=35),("nginx",pid=48146,fd=35),("nginx",pid=48145,fd=35),("nginx",pid=48144,fd=35),("nginx",pid=48143,fd=35),("nginx",pid=48142,fd=35),("nginx",pid=48141,fd=35),("nginx",pid=48140,fd=35),("nginx",pid=48139,fd=35),("nginx",pid=48138,fd=35),("nginx",pid=48137,fd=35),("nginx",pid=48136,fd=35),("nginx",pid=3752,fd=35))
.....

weiyigeek.top-启用内核级端口重用负载均衡图

之后,还可使用 ab 或 wrk 等压力测试工具进行性能压测,验证负载均衡效果,下面作者以 wrk 工具为例,它是一种高性能 HTTP 压测工具,其特点是 使用多线程 + epoll 网络模型,能够以极少的系统资源产生高并发压力。本次测试结果显示出被压测的服务具有 极佳的性能表现,适合高并发在线业务场景。

# 安装 wrk
yum install -y epel-release
yum install -y wrk

# 测试示例
wrk -t 10 -c 400 -d 30s --latency https://server.weiyigeek.top
## 参数说明
输出延时分布情况:--latency
线程数 (Threads): 10
并发连接数 (Connections): 400
测试持续时间 (Duration): 30 秒
测试目标地址: http://localhost/

## 执行结果
Running 30s test @ https://server.weiyigeek.top
  10 threads and 400 connections

## 1. 线程统计 (Thread Stats)
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.12ms    0.93ms 103.35ms   95.77%
# 延迟 (Latency)
# 平均值 (Avg): 1.12 毫秒
# 标准差 (Stdev): 0.93 毫秒 → 延迟波动较小,性能稳定
# 最大值 (Max): 103.35 毫秒 → 极端情况下延迟较高,但属少数
# 分布比率 (+/- Stdev): 95.77% 的请求延迟落在 1个标准差范围内 (即约在 0.19ms ~ 2.05ms),表示延迟非常集中,服务响应一致性很好
    Req/Sec    33.94k     4.55k   54.41k    78.13%
# 每秒请求数 (Req/Sec)
# 平均值 (Avg): 33,940 请求/秒
# 标准差 (Stdev): 4.55k → 吞吐量有一定波动
# 最大值 (Max): 54.41k → 峰值吞吐量可达 5.4 万请求/秒
# 分布比率 (+/- Stdev): 78.13% 的采样点吞吐量落在 1 个标准差范围内 (即约 29.39k ~ 38.49k 请求/秒),吞吐量表现高且相对稳定

## 2. 延时分布情况
Latency Distribution
     50%    1.10ms
     75%    1.33ms
     90%    1.60ms
     99%    3.97ms

## 3. 总请求与数据传输
  10137464 requests in 30.05s, 5.28GB read
# 总请求数: 1013 万次请求
# 测试时间: 30.05 秒
# 总读取数据量: 5.28 GB
# 平均每个请求约 5.28 GB ÷ 10,137,464 ≈ 545 字节
Requests/sec: 337376.93    # 平均吞吐量: 33.7 万 QPS (每秒请求数)
Transfer/sec:    179.86MB  # 平均传输速率: 179.86 MB/s

接下来我们看另外一个跟多核的负载均衡有关的一个话题,叫做多队列网卡。对多核 CPU 与中断负载均衡的特性有 RSS/RPS/RFS,即接收侧散列(Receive Side Scaling)、接收包分发(Receive Packet Steering)和接收过滤(Receive Filtering),当前绝大部分网卡都支持这一个特性。

在之前我们可能把网络报文的硬中断放在一个 CPU 上执行,有了多队列网卡后,它可以在每个CPU上创建一些队列,并将网络报文分发到多个CPU上执行,从而实现硬中断的并行化。

• RSS(Receive Side Scaling) :网卡硬件支持,将网络报文按哈希分发至不同CPU的接收队列,实现硬中断并行化。

• RPS(Receive Packet Steering):软件层面模拟RSS,在无硬件RSS支持时,由软中断将报文分发至多CPU。

• RFS(Remote Receive Flow Steering): 基于RPS的增强策略,结合应用层进程位置信息,将报文导向正在处理该流的CPU,提升缓存命中率。

weiyigeek.top-多队列网卡对多核CPU的优化图

特别注意,RPS/RFS虽提升并行度,但消耗额外CPU资源;需依实际场景(如CPU空闲度)评估启用必要性,不可盲目开启。

如何提供 CPU 缓存命中率?

同样,学习过作者前面《个人计算机硬件设备配置介绍与选型参考》笔记的话,应该知道 CPU 缓存分为三级:L1、L2 和 L3 缓存,其中L1(指令/数据各32KB)、L2(256KB)、L3(20MB共享)三级缓存,访问延迟逐级增大(L1≈1ns, L2≈3ns, L3≈>3ns),用以解决CPU与内存之间的速度不匹配问题。

# 查看当前16核的服务器 CPU 缓存大小
# 指令缓存大小
$ cat /sys/devices/system/cpu/cpu15/cache/index0/size
32K
# 数据缓存大小
$ cat /sys/devices/system/cpu/cpu15/cache/index1/size
32K
# 二级缓存大小
$ cat /sys/devices/system/cpu/cpu15/cache/index2/size
4096K

# 三级缓存大小(共享) 
$ cat /sys/devices/system/cpu/cpu15/cache/index3/size
16384K
# 查看三级缓存共享的CPU核心列表
$ cat /sys/devices/system/cpu/cpu0/cache/index3/shared_cpu_list
0-7
$ cat /sys/devices/system/cpu/cpu15/cache/index3/shared_cpu_list
8-15

weiyigeek.top-服务器 CPU 缓存大小图

在多核CPU环境下,进程在不同CPU核心间迁移会导致其专属缓存(尤其是L1/L2)失效,降低缓存命中率,增加内存访问延迟。所以为了提高缓存命中率,通常会将同一个进程的多个线程绑定到同一核心上执行,这样可以减少不同核心之间的缓存竞争,从而提高性能。

weiyigeek.top-CPU缓存架构图

如上文提到的 worker_cpu_affinity 指令,可以将 worker 进程绑定到特定 CPU 上执行,该指令通过CPU掩码手动绑定,或设为 auto 使每个 worker 独占一核,从而减少不同核心之间的缓存竞争,保障缓存局部性。

什么是NUMA架构及其优化?

随着计算机的发展 CPU 核心数越来越多,例如 16核、32核、64核、128核甚至更多,由于内存总线只有一条,而多核CPU可以并发访问,访问的时候就会导致冲突,从而访问内存的时候就受限了。所以为了解决由于内存访问延迟和缓存局部性问题导致性能瓶颈问题,引入了非统一内存架构(Non-Uniform Memory Access, NUMA)的服务器设计。

如下图所示,多核CPU下单一内存总线成为瓶颈,NUMA将CPU与本地内存配对(如2个CPU节点,各配32GB内存),访问本地内存(Local Node)快,访问远端内存(Remote Node)慢3–4倍,为了缩短访问延迟,你总会在服务器主机板说明书上看到CPU插槽分别与多个内存插槽对应,这样可以减少不同节点之间的内存访问延迟。

weiyigeek.top-NUMA架构图

使用 numactl 命令可以查看 NUMA 架构下的内存分布情况,并通过 -m 查看节点信息,通过 -l 查看本地性(locality)信息。

# 安装 numactl
$ yum install numactl

# 查看 NUMA 节点信息
$ lscpu | grep "NUMA"
NUMA node(s):                         2
NUMA node0 CPU(s):                    0-23,48-71
NUMA node1 CPU(s):                    24-47,72-95

# 更加详细的查看 NUMA 信息
$ numactl --hardware
available: 2 nodes (0-1)  # 两个节点
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71  # 节点0的CPU核心
node 0 size: 128542 MB
node 0 free: 112165 MB
node 1 cpus: 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95  # 节点1的CPU核心
node 1 size: 129001 MB
node 1 free: 117421 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

# 查看 NUMA 统计信息
$ numastat
                           node0           node1
numa_hit                63794544        79609341  # 本地内存命中次数 (关键指标)
numa_miss                      0               0  # 本地内存未命中次数(关键指标)
numa_foreign                   0               0  # 远端内存访问次数
interleave_hit               112             119  # 交错内存命中次数
local_node              63782470        79582096  # 本地内存访问次数
other_node                 12074           27245  # 远端内存访问次数

若 numa_miss 高则表明存在跨节点内存访问问题,常用优化手段有在BIOS中禁用NUMA以回归UMA模式或通过 numactl --membind 等工具强制进程使用本地内存,禁用远程访问

好了,本小节讲解了 CPU 层面中对 Nginx 性能优化的几个方面,包括 CPU 亲和性、内核级端口重用、多队列网卡和多核CPU负载均衡等,最后通过不同的角度来看了多核间的负载均衡对于我们实际的服务有怎样的一个影响。

最后有需要新手朋友们,可加入到作者知识星球中,完整系统的学习 Nginx 高性能Web服务器从入门到生产时间,后续也将持续更新 OpenResty 系列文章,感谢大家支持。

加入:作者【全栈工程师修炼指南】知识星球

相关推荐