Nginx 设计原理

· 10 min read

HTTP服务器

想要让本地的浏览器,获取到放在远端服务器上的 HTML 文件。

那很简单,我们可以在远端服务器启动一个进程,这个进程对外提供 HTTP 服务,说白了就是提供了个 URL。用户在浏览器中输入这个 URL, 回车,浏览器就会向 这个进程发起 HTTP 请求,进程收到浏览器的请求后,就将 HTML 文件发给浏览器,浏览器完成解析和展示,完美。

而像这种根据浏览器请求,返回 HTML 文件的服务进程,其实就叫 HTTP 服务器。 有了它,前端写的各种 HTML 文件就能部署到远端服务器上,对外提供网页服务了。

反向代理

但一个完整产品往往不止有前端页面,还有后端服务,假设现在前端页面已经被加载到浏览器中,浏览器会按页面里写好的代码逻辑,向后端商品服务发起请求,获取数据,流量小的时候没什么问题,流量变大后,后端服务器扛不住的话,就需要增加商品服务的个数,服务变多后,每个都有对应的 ip 和端口,浏览器就不知道该访问哪个服务了。

所以我们还需要在这几个后端服务前面加一个进程,对外提供一个 URL 域名,请求来了,由这个进程均匀转发给背后的几个服务,让每个服务都能处理上请求,也就实现了所谓的负载均衡

像这种,屏蔽掉背后具体有哪些服务器的代理方式,就是我们常说的反向代理

有了反向代理,我们对外就可以只提供一个URL域名,背后根据需要, 随时扩缩容服务。 这个反向代理的功能,正好可以加到前面放 HTML 文件的进程上。

那现在这个进程就很灵性了,既可以为 前端 HTML 文件提供 HTTP 服务器的功能,当 HTML 文件被加载到浏览器,并向后端发起请求的时候,这个进程还能为后端服务器提供反向代理的功能。

模块化网关

既然是中间层,所有网络流量都要经过进程,那它高低也算个网关了。

于是我们就可以顺理成章的在它上面加入一些通用网关能力,比如加个日志,记录每次调用的结果,方便后续排查问题,又比如加个对输入输出的内容进行压缩的功能,减小网络带宽消耗,又或者是对某个 IP 进行限流或封禁,甚至还可以修改输入输出的内容。能实现的功能实在太多,想象空间很大,于是将这部分功能设计为开放接口,让用户通过自定义模块来实现特定功能。

这还不够,现在这个网关只支持HTTP,我们其实还能扩展下,让它支持 tcp,udp,HTTP2和websocket。本来不支持的,自会有人通过自定义模块帮我支持。

配置能力

前面提到那么多种能力,用户肯定不会全用上,所以需要有个地方让人选择用哪些能力,于是我们可以加个配置文件,也就是 Nginx.conf ,用户想用什么能力,就在配置文件上声明清楚就行,非常方便。

单线程与 IO 多路复用

现在这个网关进程的主要任务就是跟上下游建立网络连接,顺便内部做下处理。多个客户端请求通过网络进入到一个进程,如果用多线程并发处理,那就需要考虑并发问题,同时影响性能。怎么办呢?

很简单!外部不管有多少有个网络连接,网关进程收到客户端请求后,都统一塞到一个线程上,在一个线程上处理客户端请求,什么并发问题线程切换开销,完全不存在!

多 Worker 进程

但单个进程要单线程处理那么多流量,哪怕再快,压力也不小,于是可以将单个进程改成多个进程,我们管它们叫 Worker 进程。进程之间互相独立,一个 Worker 挂了不影响另外一个 Worker 进程。

让多个 Worker 进程同时监听一个 ip 地址+端口。只要一有流量进来,操作系统就会随机给到其中一个进程处理。将进程数量设置为跟操作系统 cpu核数 一致,那每个进程都能得到一个核,开足马力猛猛干。

为什么多个进程同时监听一个端口不会出现端口冲突(port is already in use)?

 因为 Nginx 的 Worker 进程是通过 fork 方式创建的,Worker 子进程会继承 Master父进程中已绑定、已监听的 socket 文件描述符,而不是各自独立去 bind 端口。

共享内存

但多 Worker 进程的情况下,同一个客户端的多个请求会随机打到某个 Worker ,对于限流这种需要计数的场景,就会被分散到多个 Worker 上单独计数,那还怎么限流,所以还需要给这些 Worker 进程 分配一个共享内存区域,方便多个进程之间共用同一份数据做逻辑,确保系统数据一致性

proxy cache

作为网关,它在收到前端网页请求后,会转发给后端,并将后端处理结果中转给前端。如果它能将响应结果缓存起来,这样下次收到同样的请求,直接将缓存里的数据返回给前端,从而减少响应时间和网络负载

那这个数据是放在共享内存里吗?内存贵,不合适,我们可以维护些磁盘文件,用于在前端请求后端的过程中,暂存后端响应的结果,后面再有相同请求,就可以将磁盘里的数据返回。

这又是经典的空间换时间,用廉价的磁盘空间换取网络传输和 CPU 计算耗时。对于后端响应较慢或重复请求较多的场景,读写磁盘总归比直接将请求打到后端来得快。这些用于缓存响应数据的磁盘文件,就是 所谓的 proxy cache 。

Master 进程

现在每个 Worker 都会分走一部分流量,如果功能更新,所有 Worker 同时一起重启,上面的网络连接就会全部断掉。更好的方式是创建 Worker 和关闭 Worker 挨个陆续执行,这样前端网页连接断开后还能去连另外一个Worker,保证任意时间一直有Worker在工作。也就是所谓的滚动升级。因此还需要一个新的进程协调各个 Worker 谁先谁后,这个协调进程,就是所谓的 Master 进程。让Master 读取前面提到的 Nginx.conf 配置,统一管理多个Worker。

Nginx是什么

一个支持动态配置多种通用网关能力和多种网络协议,单 Master 多 Worker 架构、多个Worker 进程之间共享内存和 proxy cache,对外提供一个 IP+端口,支持 HTTP 服务器和反向代理的高性能网关服务。

说到底,Nginx 其实也只是某台服务器上的多个进程,一旦服务器挂了,Nginx 也就挂了,存在单点问题。那怎么解决 Nginx 的单点问题呢?Nginx 有集群模式吗?

核心思路:用两台(或多台)Nginx,对外只暴露一个虚拟 IP。这样任何一台挂了,另一台自动顶上,用户毫无感知。如果上了云,直接用云厂商的 SLB/ALB,连两台服务器都不用自己管了。

有,而且非常成熟。 虽然 Nginx 本身没有内置的集群管理功能,但通过 Keepalived 或云负载均衡器,可以轻松构建高可用的 Nginx 集群。

参考资料

Nginx 是什么?Nginx高并发架构拆解指南 | golang全栈指南