写点什么

Nginx 架构赏析

用户头像
旺旺
关注
发布于: 2021 年 01 月 24 日
Nginx架构赏析

淘宝的某位大佬曾经做过测试,在一台 24G 内存的机器上,Nginx 的最大并发连接数达到了 200 万。同学们听到这个结论后,是不是被 Nginx 的超高性能深深折服了,它内部的架构设计究竟是怎么样的呢?这篇文章就带同学们来认识一下 Nginx 的架构设计吧。


本文主要参考了淘宝技术团队写的 Nginx 文章,将会从以下个方面去进行分享:

  • Nginx 进程模型

  • Nginx 事件模型

Nginx 进程模型

Nginx 默认以多进程的方式启动运行,当然 Nginx 也是支持多线程的方式的,只是我们主流的方式还是多进程的方式,也是 Nginx 的默认方式。Nginx 采用多进程的方式有很多的好处,这一点与我们大部分同学们的认知发生冲突了。大部分同学会说进程比线程重,进程更耗费资源等等,总之可以说出一大堆线程优于进程的证明。如果有同学恰恰是这样想的,那就得扩充下认知啦。比如我们大名鼎鼎的 Redis,Oracle 里边都是有多进程概念的 ,而这些软件无一例外都具有超高性能。所以,我们今天就主要来学习下 Nginx 的多进程模式。


Nginx 的进程模型长个什么样子,废话不多说,直接上图。


Nginx架构图


Nginx 在启动后,会有一个 master 进程和多个 worker 进程。master 进程主要用来管理 worker 进程,包含:接收来自操作系统的信号,向各 worker 进程发送信号,监控 worker 进程的运行状态,当 worker 进程退出后(异常情况下),会自动重新启动新的 worker 进程。而基本的网络事件,则是放在 worker 进程中来处理了。多个 worker 进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个 worker 进程中处理,一个 worker 进程,不可能处理其它 worker 进程的请求。worker 进程的个数是可以设置的,一般设置与机器 cpu 核数一致,这里面的原因与 Nginx 的进程模型以及事件处理模型是分不开的。


Nginx 的多进程模型给它带来了一大优点: 优雅重启,服务不间断。具体它是怎么做到呢?从上面我们已经得知,master 管理 worker 进程,所以我们只需要与 master 进程通信就行了。master 进程会接收来自操作系统发来的信号,再根据信号做不同的事情。所以我们要控制 Nginx,只需要通过操作系统提供的 kill 命令向 master 进程发送信号就行了。比如使用 kill -HUP pid 重启 Nginx,master 进程在接收到 HUP 信号后是怎么做的呢?首先 master 进程在接到信号后,会先重新加载配置文件,然后再启动新的 worker 进程,并向所有老的 worker 进程发送信号,告诉他们可以光荣退休了。新的 worker 在启动后,就开始接收新的请求,而老的 worker 在收到来自 master 的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后,再退出。


我们知道了在操作 Nginx 的时候,Nginx 内部做了些什么事情,那么,worker 进程又是如何客户端处理请求的呢?我们前面有提到,worker 进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供 80 端口的 http 服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?首先,每个 worker 进程都是从 master 进程 fork 过来,在 master 进程里面,先创建好 ServerSocket,开启监听,然后再 fork 出多个 worker 进程。所有 worker 进程都将和 master 进程持有同一份 socket 句柄,并且句柄会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有 worker 进程在注册句柄读事件前抢 accept_mutex,抢到互斥锁的那个进程注册句柄读事件,在读事件里调用 accept 接受该连接。当一个 worker 进程在 accept 这个连接之后,就生成了一个新的 socket,通过这个 socket 开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,一个完整的请求就是这样玩完了。我们可以看到,一个请求,完全由 worker 进程来处理,而且只在一个 worker 进程中处理。


那么,Nginx 采用这种进程模型有什么好处呢?实在是太多了,重点说上几条。首先,对于每个 worker 进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在编程以及问题查找时,也会方便很多。其次,采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master 进程则很快启动新的 worker 进程。当然,worker 进程的异常退出,肯定是程序有 bug 了,异常退出,会导致当前 worker 上的所有请求失败,不过不会影响到所有请求,所以降低了风险。当然,好处还有很多,同学们可以慢慢体会。

Nginx 事件模型

Nginx 采用了 NIO 的方式来处理请求,这是 Nginx 可以同时处理成千上万个请求的根本原因。想想低版本 Tomcat 的 IO 模型(高版本已支持 NIO),每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对操作系统来说,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的 cpu 开销很大,自然性能就上不去了,而这些开销完全是没有意义的。


Nginx 为什么要使用 NIO 呢?NIO 到底是怎么回事呢?IO 的本质就是读写事件,而当读写事件没有准备好时,必然不可操作,如果不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,工作线程再继续吧。阻塞调用会进入内核等待,cpu 就会让出去给别人用了,工作线程只能傻傻的睡觉等待,对单线程的 worker 来说,显然不合适,当网络事件越多时,大家都在等待呢,cpu 空闲下来没人用,cpu 利用率自然上不去了,更别谈高并发了。所以,为了高性能,必须使用 NIO。关于 NIO,我们这里就是一笔带过,想详细学习的同学可以去看我的 NIO 源码分析文章。


接下来,我们使用一段伪代码来总结一下 Nginx 的事件处理模型吧。

Date now;	// 表示当前时间while (true) {    // 处理任务队列里的所有任务    for task in tasks:        task.handler();
flushTime(now); // 刷新当前时间 timeout = initValue; // 超时时间 // waitTasks可以理解为注册到epoll里的所有有效任务 for task in waitTasks: // 列表里的第一个task的超时时间是最短的,如果它已经超时了,就调用超时处理函数 if (task.time <= now) { task.timeoutHandler(); } else { // 更新超时时间 timeout = t.time - now; break; } // 通过epoll拿到已经就绪的事件 nevents = epoll(events, timeout); // 挨个遍历处理事件 for i in nevents: Task task; // 读事件 if (events[i].type == READ) { task.handler = readHandler; } else { // 写事件 task.handler = writeHandler; } // 防到一个专门的执行队列里 tasks(task);}
复制代码


好的,文章到这里就结束啦。希望 Nginx 的进程模型和事件模型的设计思想,能对朋友们以后的系统设计有所帮助。最后,喜欢我文章的同学们,欢迎关注我的公众号“小瑾守护线程”,不错过任何有价值的干货。

小瑾守护线程


发布于: 2021 年 01 月 24 日阅读数: 43
用户头像

旺旺

关注

一个深爱技术的程序猿。 2020.12.29 加入

对技术保持强烈好奇心。

评论

发布
暂无评论
Nginx架构赏析