You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

124 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 34 | Nginx高性能的Web服务器
经过前面几大模块的学习你已经完全掌握了HTTP的所有知识那么接下来请收拾一下行囊整理一下装备跟我一起去探索HTTP之外的广阔天地。
现在的互联网非常发达用户越来越多网速越来越快HTTPS的安全加密、HTTP/2的多路复用等特性都对Web服务器提出了非常高的要求。一个好的Web服务器必须要具备稳定、快速、易扩展、易维护等特性才能够让网站“立于不败之地”。
那么,在搭建网站的时候,应该选择什么样的服务器软件呢?
在开头的几讲里我也提到过Web服务器就那么几款目前市面上主流的只有两个Apache和Nginx两者合计占据了近90%的市场份额。
今天我要说的就是其中的Nginx它是Web服务器的“后起之秀”虽然比Apache小了10岁但增长速度十分迅猛已经达到了与Apache“平起平坐”的地位而在“Top Million”网站中更是超过了Apache拥有超过50%的用户([参考数据](https://w3techs.com/technologies/cross/web_server/ranking))。
![unpreview](https://static001.geekbang.org/resource/image/c5/0b/c5df0592cc8aef91ba961f7fab5a4a0b.png)
在这里必须要说一下Nginx的正确发音它应该读成“Engine X”但我个人感觉“X”念起来太“拗口”还是比较倾向于读做“Engine ks”这也与UNIX、Linux的发音一致。
作为一个Web服务器Nginx的功能非常完善完美支持HTTP/1、HTTPS和HTTP/2而且还在不断进步。当前的主线版本已经发展到了1.17正在进行HTTP/3的研发或许一年之后就能在Nginx上跑HTTP/3了。
Nginx也是我个人的主要研究领域我也写过相关的书按理来说今天的课程应该是“手拿把攥”但真正动笔的时候还是有些犹豫的很多要点都已经在书里写过了这次的专栏如果再重复相同的内容就不免有“骗稿费”的嫌疑应该有些“不一样的东西”。
所以我决定抛开书本换个角度结合HTTP协议来讲Nginx带你窥视一下HTTP处理的内幕看看Web服务器的工作原理。
## 进程池
你也许听说过Nginx是个“轻量级”的Web服务器那么这个所谓的“轻量级”是什么意思呢
“轻量级”是相对于“重量级”而言的。“重量级”就是指服务器进程很“重”占用很多资源当处理HTTP请求时会消耗大量的CPU和内存受到这些资源的限制很难提高性能。
而Nginx作为“轻量级”的服务器它的CPU、内存占用都非常少同样的资源配置下就能够为更多的用户提供服务其奥秘在于它独特的工作模式。
![](https://static001.geekbang.org/resource/image/3e/c1/3e94fbd78ed043e88c443f6416f99dc1.png)
在Nginx之前Web服务器的工作模式大多是“Per-Process”或者“Per-Thread”对每一个请求使用单独的进程或者线程处理。这就存在创建进程或线程的成本还会有进程、线程“上下文切换”的额外开销。如果请求数量很多CPU就会在多个进程、线程之间切换时“疲于奔命”平白地浪费了计算时间。
Nginx则完全不同“一反惯例”地没有使用多线程而是使用了“**进程池+单线程**”的工作模式。
Nginx在启动的时候会预先创建好固定数量的worker进程在之后的运行过程中不会再fork出新进程这就是进程池而且可以自动把进程“绑定”到独立的CPU上这样就完全消除了进程创建和切换的成本能够充分利用多核CPU的计算能力。
在进程池之上还有一个“master”进程专门用来管理进程池。它的作用有点像是supervisor一个用Python编写的进程管理工具用来监控进程自动恢复发生异常的worker保持进程池的稳定和服务能力。
不过master进程完全是Nginx自行用C语言实现的这就摆脱了外部的依赖简化了Nginx的部署和配置。
## I/O多路复用
如果你用Java、C等语言写过程序一定很熟悉“多线程”的概念使用多线程能够很容易实现并发处理。
但多线程也有一些缺点,除了刚才说到的“上下文切换”成本,还有编程模型复杂、数据竞争、同步等问题,写出正确、快速的多线程程序并不是一件容易的事情。
所以Nginx就选择了单线程的方式带来的好处就是开发简单没有互斥锁的成本减少系统消耗。
那么疑问也就产生了为什么单线程的Nginx处理能力却能够超越其他多线程的服务器呢
这要归功于Nginx利用了Linux内核里的一件“神兵利器”**I/O多路复用接口**“大名鼎鼎”的epoll。
“多路复用”这个词我们已经在之前的HTTP/2、HTTP/3里遇到过好几次如果你理解了那里的“多路复用”那么面对Nginx的epoll“多路复用”也就好办了。
Web服务器从根本上来说是“I/O密集型”而不是“CPU密集型”处理能力的关键在于网络收发而不是CPU计算这里暂时不考虑HTTPS的加解密而网络I/O会因为各式各样的原因不得不等待比如数据还没到达、对端没有响应、缓冲区满发不出去等等。
这种情形就有点像是HTTP里的“队头阻塞”。对于一般的单线程来说CPU就会“停下来”造成浪费。而多线程的解决思路有点类似“并发连接”虽然有的线程可能阻塞但由于多个线程并行总体上看阻塞的情况就不会太严重了。
Nginx里使用的epoll就好像是HTTP/2里的“多路复用”技术它把多个HTTP请求处理打散成碎片都“复用”到一个单线程里不按照先来后到的顺序处理而是只当连接上真正可读、可写的时候才处理如果可能发生阻塞就立刻切换出去处理其他的请求。
通过这种方式Nginx就完全消除了I/O阻塞把CPU利用得“满满当当”又因为网络收发并不会消耗太多CPU计算能力也不需要切换进程、线程所以整体的CPU负载是相当低的。
这里我画了一张Nginx“I/O多路复用”的示意图你可以看到它的形式与HTTP/2的流非常相似每个请求处理单独来看是分散、阻塞的但因为都复用到了一个线程里所以资源的利用率非常高。
![](https://static001.geekbang.org/resource/image/4c/59/4c6832cdce34133c9ed89237fb9d5059.png)
epoll还有一个特点大量的连接管理工作都是在操作系统内核里做的这就减轻了应用程序的负担所以Nginx可以为每个连接只分配很小的内存维护状态即使有几万、几十万的并发连接也只会消耗几百M内存而其他的Web服务器这个时候早就“Memory not enough”了。
## 多阶段处理
有了“进程池”和“I/O多路复用”Nginx是如何处理HTTP请求的呢
Nginx在内部也采用的是“**化整为零**”的思路把整个Web服务器分解成了多个“功能模块”就好像是乐高积木可以在配置文件里任意拼接搭建从而实现了高度的灵活性和扩展性。
Nginx的HTTP处理有四大类模块
1. handler模块直接处理HTTP请求
2. filter模块不直接处理请求而是加工过滤响应报文
3. upstream模块实现反向代理功能转发请求到其他服务器
4. balance模块实现反向代理时的负载均衡算法。
因为upstream模块和balance模块实现的是代理功能Nginx作为“中间人”运行机制比较复杂所以我今天只讲handler模块和filter模块。
不知道你有没有了解过“设计模式”这方面的知识,其中有一个非常有用的模式叫做“**职责链**”。它就好像是工厂里的流水线,原料从一头流入,线上有许多工人会进行各种加工处理,最后从另一头出来的就是完整的产品。
Nginx里的handler模块和filter模块就是按照“职责链”模式设计和组织的HTTP请求报文就是“原材料”各种模块就是工厂里的工人走完模块构成的“流水线”出来的就是处理完成的响应报文。
下面的这张图显示了Nginx的“流水线”在Nginx里的术语叫“阶段式处理”Phases一共有11个阶段每个阶段里又有许多各司其职的模块。
![](https://static001.geekbang.org/resource/image/41/30/41318c867fda8a536d0e3db6f9987030.png)
我简单列几个与我们的课程相关的模块吧:
* charset模块实现了字符集编码转换[第15讲](https://time.geekbang.org/column/article/104024)
* chunked模块实现了响应数据的分块传输[第16讲](https://time.geekbang.org/column/article/104456)
* range模块实现了范围请求只返回数据的一部分[第16讲](https://time.geekbang.org/column/article/104456)
* rewrite模块实现了重定向和跳转还可以使用内置变量自定义跳转的URI[第18讲](https://time.geekbang.org/column/article/105614)
* not\_modified模块检查头字段“if-Modified-Since”和“If-None-Match”处理条件请求[第20讲](https://time.geekbang.org/column/article/106804)
* realip模块处理“X-Real-IP”“X-Forwarded-For”等字段获取客户端的真实IP地址[第21讲](https://time.geekbang.org/column/article/107577)
* ssl模块实现了SSL/TLS协议支持读取磁盘上的证书和私钥实现TLS握手和SNI、ALPN等扩展功能[安全篇](https://time.geekbang.org/column/article/108643)
* http\_v2模块实现了完整的HTTP/2协议。[飞翔篇](https://time.geekbang.org/column/article/112036)
在这张图里你还可以看到limit\_conn、limit\_req、access、log等其他模块它们实现的是限流限速、访问控制、日志等功能不在HTTP协议规定之内但对于运行在现实世界的Web服务器却是必备的。
如果你有C语言基础感兴趣的话可以下载Nginx的源码在代码级别仔细看看HTTP的处理过程。
## 小结
1. Nginx是一个高性能的Web服务器它非常的轻量级消耗的CPU、内存很少
2. Nginx采用“master/workers”进程池架构不使用多线程消除了进程、线程切换的成本
3. Nginx基于epoll实现了“I/O多路复用”不会阻塞所以性能很高
4. Nginx使用了“职责链”模式多个模块分工合作自由组合以流水线的方式处理HTTP请求。
## 课下作业
1. 你是怎么理解进程、线程上下文切换时的成本的为什么Nginx要尽量避免
2. 试着自己描述一下Nginx用进程、epoll、模块流水线处理HTTP请求的过程。
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
![unpreview](https://static001.geekbang.org/resource/image/4c/3d/4c7bceb80a8027389705e9d6ec9eb43d.png)