Dawn's Blogs

分享技术 记录成长

0%

Nginx学习 (1) 平台初探

Nginx 是一个高性能的 HTTP 和反向代理服务器,同时也提供了 IMAP/POP3/SMTP 服务,具有高并发的优点。

Nginx 平台初探

Nginx 架构

Nginx 在启动后,在系统中会议 daemon(守护进程)的方式在后台运行,后台进程包含一个 master 和多个 worker 进程。

master 进程主要用来管理 worker 进程,包含:

  • 接收来自外界的信号,向各 worker 进程发送信号。
  • 监控 worker 进程的运行状态,当 worker 进程退出后(异常情况下),会自动重新启动新的 worker 进程。

而基本的网络事件,则是放在 worker 进程中来处理

  • 多个 worker 进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个 worker 进程中处理,一个 worker 进程,不可能处理其它进程的请求。
  • worker 进程的个数是可以设置的,一般我们会设置与机器 cpu 核数一致,这里面的原因与 Nginx 的进程模型以及事件处理模型是分不开的。

img

操作 Nginx

因为 Nginx 是 master-worker 模型,所以如果要操作 Nginx,只需要与 master 通信即可

比如可以用 ./nginx -s reload 来重启 Nginx:此时会启动一个新的 Nginx 进程,在解析到 reload 参数后它会向 master 进程发送信号:

  • 首先 master 进程在接到信号后,会先重新加载配置文件

  • 然后再启动新的 worker 进程,并向所有老的 worker 进程发送信号,告诉他们可以光荣退休了。

  • 新的 worker 在启动后,就开始接收新的请求,而老的 worker 在收到来自 master 的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后,再退出。

处理请求

worker 进程之间是平等的,每个进程,处理请求的机会也是一样的。

  • 首先,每个 worker 进程都是从 master 进程 fork 过来,在 master 进程里面,先建立好需要 listen 的 socket(listenfd)之后,然后再 fork 出多个 worker 进程。
  • 所有 worker 进程在注册 listenfd 读事件前抢 accept_mutex,抢到互斥锁的那个进程注册 listenfd 读事件,在读事件里调用 accept 接受该连接。当一个 worker 进程在 accept 这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接

处理事件

Nginx 采用了异步非阻塞的方式来处理请求,也就是说,Nginx 是可以同时处理成千上万个请求的。

Apache 中(Apache 也有异步非阻塞版本,但因其与自带某些模块冲突,所以不常用),每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对操作系统来说,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的 cpu 开销很大,自然性能就上不去。

请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。具体到系统底层,就是读写事件。阻塞与非阻塞、同步与异步的示意图如下:

image-20230223105009678

在处理网络事件时,以 epoll 为例子:

当事件没准备好时,放到 epoll 里面,事件准备好了就去读写,当读写返回 EAGAIN(事件还没有准备好)时,我们将它再次加入到 epoll 里面。这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在 epoll 里面等着。

这样,我们就可以并发处理大量的并发了,当然,这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个了,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。

推荐设置 worker 的个数为 cpu 的核数,因为更多的 worker 数,只会导致进程来竞争 cpu 资源了,从而带来不必要的上下文切换。Nginx 为了更好的利用多核特性,提供了 cpu 亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来 cache 的失效。

基础概念

connection

在 Nginx 中 connection 就是对 tcp 连接的封装。在 Nginx 中,每个进程会有一个连接数的最大上限,这个上限与系统对 fd 的限制不一样(一个进程所能够打开的 fd 的最大数,即 nofile)。因为每个 socket 连接会占用掉一个 fd,所以这也会限制我们进程的最大连接数,当然也会直接影响到我们程序所能支持的最大并发数,当 fd 用完后,再创建 socket 时,就会失败。

Nginx 通过设置 worker_connectons 来设置每个进程支持的最大连接数。如果该值大于 nofile,那么实际的最大连接数是 nofile,Nginx 会有警告。Nginx 在实现时,是通过一个连接池来管理的,每个 worker 进程都有一个独立的连接池,连接池的大小是 worker_connections。这里的连接池里面保存的其实不是真实的连接,它只是一个 worker_connections 大小的一个 ngx_connection_t 结构的数组。并且,Nginx 会通过一个链表 free_connections 来保存所有的空闲 ngx_connection_t,每次获取一个连接时,就从空闲连接链表中获取一个,用完后,再放回空闲连接链表里面。

worker_connections 表示的是每一个 worker 进程能建立的最大连接数。所以一个 Nginx 能建立的最大连接数,应该是 worker_connections * worker_processes

对于 HTTP 请求本地资源来说,能够支持的最大并发数量worker_connections * worker_processes,而如果是 HTTP 作为反向代理来说,最大并发数量应该是 worker_connections * worker_processes/2。因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接。

公平竞争

一个客户端连接过来后,多个空闲的进程,会竞争这个连接,这种竞争会导致不公平,如果某个进程得到 accept 的机会比较多,它的空闲连接很快就用完了,会导致此 tcp 连接得不到处理,就中止了。Nginx 的解决方法是:

  • 在获取 accept_mutex 之前,会先计算 ngx_accept_disabled 的值,这个值是 Nginx 单进程的所有连接总数的八分之一,减去剩下的空闲连接数量。得到的这个 ngx_accept_disabled 有一个规律,当剩余连接数小于总连接数的八分之一时,其值才大于 0,而且剩余的连接数越小,这个值越大
  • 当 ngx_accept_disabled 大于 0 时,不会去尝试获取 accept_mutex 锁,并且将 ngx_accept_disabled 减 1,于是,每次执行到此处时,都会去减 1,直到小于 0。很显然可以看出,当空余连接越少时,ngx_accept_disable 越大,于是让出的机会就越多,这样其它进程获取锁的机会也就越大。

常用命令

Nginx 常用命令:

  • 查看版本号:./nginx -v
  • 启动 Nginx:./nginx
  • 停止 Nginx:./nginx -s stop
  • 重新加载 Nginx:./nginx -s reload