Code前端首页关于Code前端联系我们

两者都是事件驱动的,为什么NGINX的性能比Redis高?如何配合请求?您使用的是多核CPU吗?

terry 2年前 (2023-09-28) 阅读数 56 #未命名
同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

对于Redis缓存,在描述其性能时,我们会说:支持万个并发连接,几万的QPS。当我们描述Nginx的高性能时,我们说它支持C10M(千万级并发连接)和百万级QPS。 Redis和Nginx都使用事件驱动、异步调用和Epoll机制。为什么Nginx的并发连接数这么高? (本文不讨论Redis的分布式集群)

这个其实是由进程的架构决定的。为了让进程充分占用CPU的计算能力,Nginx充分利用了分时操作系统的特性,比如增加CPU的时间片,提高CPU秒的命中率——一级缓存,使用异步。 IO 和线程池以避免阻塞磁盘上的读取操作等。等等,只有了解 Nginx 技巧才能最大限度地提高 Nginx 性能。

为了维持Worker进程之间的负载平衡,Nginx在1.9.1版本之前使用互斥锁来维持基于七个八位字节阈值的简单高效的基本平衡。之后,使用操作系统提供的更多ACCEPT队列,Nginx可以获得更高的吞吐量。

本文沿着高性能主线介绍Nginx Master/Worker进程架构,包括进程间的流量共享以及多线程模式的运行,默认是关闭的。本文也是Nginx开源社区基础培训系列6月18日晚第三期直播课第一季的部分文字总结。


如何充分利用多核CPU?

由于散热问题,CPU频率已经十几年没有提升了。下图显示了1970年至2018年间CPU性能的变化。可以看出,表示频率的绿线自2005年以来就没有增加。大多数服务器使用 2.x GHz CPU,功耗更经济: 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?(图片来源):https://www.karlrupp.net/2018/02/42-years-of-microprocessor-trend-data/ )

我们知道CPU的频率决定了指令的执行速度。频率的提升已经到了瓶颈,这意味着所有单线程、单线程软件的性能都无法通过CPU升级来提升,包括本文开头介绍的Redis服务。如果您熟悉 JavaScript,您可能使用过 NodeJS Web 服务。虽然是高并发服务的代表,但同时受制于单线程和单线程架构,无法充分利用CPU资源。

CPU厂商解决这个问题的办法是向多核横向发展,所以上图中表示核心数量的黑线在2005年之后迅速增加。由于操作系统使用的最小单位是一个CPU核心,一个线程,为了同时使用所有CPU核心,软件必须支持多线程。当然,进程比线程具有更大的调度粒度,因此像 Nginx 这样的多线程软件是可能的。

下图是Nginx的进程架构图。可以看到,包含4类进程:1个主supervisor进程、若干个Worker进程、1个Cache Loader缓存加载进程、1个Cache Manager缓存清除进程。 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

其中Master就是管理进程。它长时间处于休眠状态,不参与请求的处理,因此几乎不消耗服务器的IT资源。此外,缓存加载器和缓存管理器进程仅在启用 HTTP 缓存后才存在。当Nginx启动时加载磁盘上的缓存文件时,Cache Loader进程也会自动退出。后面介绍Nginx缓存时我会详细阐述这两个缓存过程。

Worker进程负责处理用户请求。只有当Worker进程能够充分利用多核CPU时,Nginx的QPS才能达到最大值。因此,工作进程的数量必须等于或大于CPU核心的数量。由于Nginx采用事件驱动、非阻塞的架构,Worker进程在繁忙时会一直处于Running状态。因此,一个Worker进程可以完全占据一个CPU核心的全部计算能力。如果工作进程的数量超过了CPU核心的数量,一些工作进程就会因为抢不到CPU而进入休眠状态。因此,Nginx 通过以下代码自动获取处理器核心数:

ngx_ncpu = sysconf(_SC_NPROCESSORS_ONLN);

如果将以下配置行添加到 nginx.conf 文件中

worker_processes auto;

Nginx 会自动将 Worker 进程数设置为 CPU 核心数

if (ngx_strcmp(value[1].data, "auto") == 0) {
    ccf->worker_processes = ngx_ncpu;
  return NGX_CONF_OK;
}

为了让服务器上的其他进程不占用过多的CPU,可以给Worker进程一个较高的静态优先级。 Linux作为一个分时操作系统,将CPU的执行时间分成许多块,每个进程轮流执行。这些时间片可长可短,范围从5毫秒到800毫秒。当内核分配其长度时,它将基于静态和动态优先级。其中,动态优先级是由内核根据进程类型自动确定的。例如,CPU类型的进程可以获得比IO类型进程更长的时间片,而静态优先级可以通过setpriority函数设置。

Linux 有 40 个静态优先级,范围从 -20 到 +19,其中 -20 是最高优先级。默认进程优先级为0,因此您可以更改优先级以使Worker进程能够访问更多CPU资源。在nginx.conf文件中,worker_priority配置可以设置静态优先级,例如:

worker_priority -10;

因为每个CPU核心都有一级缓存和二级缓存(Intel Smart Cache的三级缓存是所有核心共享的) ),为了提高两级缓存的命中率,Worker进程也可以绑定到CPU核心上。由于CPU缓存距离计算单元更近,并且使用更快的数据介质,因此二级缓存的访问速度不超过10纳秒。相应地,主存访问速度至少为60纳秒,因此命中频繁。 CPU缓存可以提高Nginx语句的执行速度。在nginx.conf文件中,可以通过如下配置行来绑定CPU:

worker_cpu_affinity auto;

Nginx的多线程架构已经支持C10M级别的高并行度,那么Nginx的多线程又如何呢?这从对 Linux 文件系统的非阻塞调用开始。

Worker进程包含数万个并行连接,在连接处理过程中会发生大量的上下文切换。由于一次内核切换的成本约为 5 微秒,因此随着并发连接数的增加,该成本呈指数级增长。因此,只有大部分切换是在用户态完成,才能实现高并发。做到这一点的唯一方法是使用完全非阻塞的系统调用。非阻塞套接字对于网络消息的传输是完全可行的。然而,在Linux上读取磁盘文件是一个大问题。

我们知道在机械硬盘上查找文件非常耗时。由于磁盘速度很难提升(服务器磁盘速度只有10000转),定位操作大约需要8毫秒,这是一个非常高的数字。写入文件时,还可以使用Page Cache磁盘缓存的回写功能,先写入内存,然后异步恢复到磁盘。没有什么好的方法来读取文件。尽管Linux提供了本机异步IO系统调用,但是当内存紧张时,异步AIO会回退到阻塞API(FreeBSD OS AIO没有这个问题)。

为了减少阻塞 API 的影响,Nginx 允许在单独的线程池中执行文件读取操作。例如,您可以通过以下两种配置来启用HTTP静态资源服务线程池:

thread_pool name threads=number [max_queue=number];
aio threads[=pool];

该线程池运行在Worker进程中,并传递一个任务队列(max_queue设置队列的最大长度)。生产者/消费者模型与主线程交换数据如下图所示: 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

在极端场景(活跃数据占满内存)下,线程池可以将静态资源服务的性能提升9倍。具体测试请参见这篇文章:Nginx with threadpools的性能的九倍。

您可能有一个疑问:多进程、多线程。为什么 Nginx 不更简单并使用多线程解决方案?这主要是由2个原因决定的:

  • 首先,作为高性能的负载分配,稳定性非常重要。由于多个线程共享相同的地址空间,一旦出现内存错误,所有线程都会被内核强制终止,降低系统可用性;
  • 其次,Nginx 的模块化架构允许将第三方代码嵌入到核心执行流程中。虽然它极大丰富了 Nginx 生态系统,但也带来了风险。

因此,Nginx更喜欢多线程模式来使用多核CPU,多线程模式仅作为补充。


工作进程在处理请求时如何协同工作?

如果某个进程侦听端口 80,则其他进程无法连接到该端口。通常会显示“地址已在使用”消息。那么所有Worker进程如何同时监听80或443端口呢?

如果使用netstat命令,可以看到只有进程ID为2758的Master进程在监听该端口: 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

Worker进程是Master的子进程。使用 ps 命令,可以看到下图中有 2,2758 个。标识符为 3188 和 3189 的 Worker 进程: 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

由于子进程自然继承了父进程打开的端口,因此 Worker 进程还监听 80 和 443 端口。您可以使用 lsof 命令看到这一点: 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

我们知道 TCP 三向握手是由操作系统执行的。其中,成功建立连接的socket被放入ACCEPT队列中,如下图所示: 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

这样,当Worker进程通过epoll_wait read事件获取新连接时(Master进程不会执行epoll_wait函数),内核选择1个Worker进程处理新连接。早期的Linux内核选择算法非常糟糕。特别是,当建立新的连接时,内核会唤醒所有阻塞在epoll_wait函数中的工作进程。然而,只有Worker进程可以通过accept函数获取新的连接。其他进程如果获取失败会再次进入睡眠状态。这就是曾经广为人知的“雷霆羊群”现象。然而,这很容易导致工作进程之间的负载不平衡。由于每个Worker进程都绑定了一个CPU核心,如果某些Worker进程中并发的TCP连接太少,这意味着CPU的计算能力处于空闲状态,这也会降低系统吞吐量。

Nginx早期通过accept_mutex锁应用层解决了这个问题。在1.11.3版本之前,默认开启:

accept_mutex on;

这个锁的工作原理如下:同时,通过粘上accept_mutex锁,只有持有该锁的单个Worker进程才能将监听socket添加到epoll :

if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
    ngx_listening_t* ls = cycle->listening.elts;

    for (ngx_uint_ti = 0; i < cycle->listening.nelts; i++) {
        ngx_connection_t * c = ls[i].connection;
        if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) {
            return NGX_ERROR;
        }
    }
}

这解决了“冲击组”问题。我们来看看负载均衡功能是如何实现的。在 nginx.conf 文件中,您可以通过以下配置行设置每个 Worker 进程可以处理的最大并发连接数:

worker_connections number;

如果空闲连接数小于总连接数的八分之一,则 Worker进程将显着减少它接收到的accept_mutex的数量。锁定概率:

ngx_int_tngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
if (ngx_accept_disabled > 0) {
    ngx_accept_disabled--;
} else {
    if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
        return;
    }
    ...
}

我们还可以通过accept_mutex_delay配置来控制负载均衡的执行频率。默认值为500毫秒,即最多500毫秒后,并发连接数较少的Worker进程会尝试处理新连接:

accept_mutex_delay 500ms;

当然,1.11.3版本之后,Nginx默认关闭accept_mutex锁定。这是因为操作系统提供了一个更好的解决方案,称为reuseport(该功能仅在Linux 3.9之后可用)。首先我们看一下处理新连接的性能对比表: 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

上图中,横轴默认元素开启了accept_mutex锁。可以看到,使用reuseport后,QPS吞吐量提升了3倍,处理延迟明显下降。特别是延迟波动(蓝色散点线)已显着减少。

Reuseport 能达到如此高的性能,是因为内核为每个 Worker 进程创建了一个单独的 ACCEPT 队列,并且内核根据负载情况将创建的连接分发到各个队列中,如下图所示: 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

这样,Worker进程的执行效率要高很多!如果要开启reuseport功能,只需要在listen命令后添加reuseport选项即可: 同是事件驱动,为什么NGINX性能远高于Redis?如何协同处理请求?使用多核CPU?

当然,Master/Worker进程架构的优势包括热加载和热升级。在这篇文章https://www.nginx-cn.net/article/70中我将详细向您展示这个过程。


总结

最后总结一下文章的内容。

在材料、散热等基础技术没有重大突破之前,CPU频率很难提升。 Redis、NodeJS等单进程、单线程、高并发的服务只能向分布式集群演进,才能不断提升性能。通过Master/Worker多线程架构,Nginx可以充分利用服务器的数百个CPU核心来访问C10M。

为了压榨多核CPU的价值,Nginx想尽一切办法:通过CPU绑定提高二级缓存命中率、通过静态优先级扩大时间片、平衡Worker之间的负载。使用各种工具进行处理。将阻塞IO操作隔离在单独的线程池中等可以看到高性能不仅来自架构,还来自细节。

原作者:陶辉
转载来源:NGINX开源社区

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门