§ Essay
Nginx 设计原理
从 HTTP 服务器到反向代理,剖析 Nginx 的模块化网关设计、单线程 IO 多路复用、多 Worker 进程架构等核心设计原理。
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 集群。