Nginx 能扛高并发,不是因为快,是因为它不肯为每个连接开一个线程。 它自己不带 RTMP,要靠外挂模块补。
第 9 章的 EasyDarwin 是「流媒体服务器」,Nginx 是「通用 Web 服务器加模块」。 这一节讲它的底子,10.2 讲怎么装,10.3 讲怎么补 RTMP。
一、它是怎么扛住并发的
传统做法是一个连接一个线程。 一万个连接就是一万个线程,光切换就耗掉大半 CPU。 Nginx 反过来:少量 worker,每个 worker 用事件循环管成千上万个连接。
这就是「事件驱动」的全部含义: 不让线程去等,而是让一个线程看住很多个连接, 谁有数据了就处理谁。 等待这件事交给操作系统,不占 CPU。
代价是代码不好写: 所有逻辑都被切成一段一段的回调, 一件事做到一半要记住自己做到哪了。 所以你去看 Nginx 源码,大量状态机式的写法,都是为这件事付出的成本。
二、master 与 worker 的分工
Nginx 一启动就有两类进程: ① master:读配置、开端口、管 worker。 它自己不处理请求。 ② worker:真正干活的,每个都是独立的事件循环。
这个分工带来两个好处: 一是「配置热加载」——master 读完新配置, 起一批新 worker,再让老 worker 处理完手上请求自己退出,连接不断; 二是「worker 挂了不影响别人」,master 会再补一个。
所以 reload 和 restart 的差别是本质的:
reload 是换 worker,restart 是连 master 一起换。
直播业务上永远优先用前者,因为正在传的流不该断。
三、它自己不会推流
这点必须说清楚:裸装的 Nginx 就是一个 Web 服务器。 它能吐静态文件、能做反向代理,但它不懂 RTMP。
流媒体能力全靠外挂模块: nginx-rtmp-module:给它 RTMP 收发与 HLS 切片。
nginx-http-flv-module:在上一者基础上再加 HTTP-FLV 输出。
这两个模块不是官方维护的,是社区项目。 所以它们的更新节奏、配置语法、坑,都和 Nginx 本体不完全同步。 遇到问题时要分清是「Nginx 的问题」还是「模块的问题」——两者的报错长得不一样,官网上也查不到模块的文档。
四、动手 Demo:Nginx 简介——一个 master,一堆 worker 抢连接
最小版 master-worker:一个监听套接字,多个线程抢 accept。 同一份负载,worker 从 1 加到 4,看它怎么变。
import socket, threading, time
RESP = (b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n"
b"Connection: close\r\n\r\nhello")
srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
srv.listen(64)
srv.settimeout(0.05)
addr = srv.getsockname()
stop = threading.Event()
def worker():
while not stop.is_set():
try:
c, _ = srv.accept()
except socket.timeout:
continue
try:
c.recv(4096)
c.sendall(RESP)
finally:
c.close()
ws = [threading.Thread(target=worker) for _ in range(NW)]
for w in ws:
w.start()
def client():
for _ in range(25):
s = socket.create_connection(addr, timeout=3)
s.sendall(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n")
while s.recv(4096):
pass
s.close()
t0 = time.perf_counter()
cs = [threading.Thread(target=client) for _ in range(8)]
for c in cs:
c.start()
for c in cs:
c.join()
el = (time.perf_counter() - t0) * 1000
print("%d 个 worker:200 个请求用了 %.0f 毫秒,每秒 %.0f 个"
% (NW, el, 200 / (el / 1000)))
实跑输出:
--------------------------------------------------------------------
Nginx 简介——一个 master,一堆 worker 抢连接
--------------------------------------------------------------------
**① 它凭什么能扛住高并发**
**不是因为它快,是因为它「不肯为每个连接开一个线程」。**
**传统服务器一个连接一个线程,一万个连接就是一万个线程,**
**光切换就耗光了 CPU。**
**Nginx 反过来:少量 worker,每个 worker 用事件循环管成千上万个连接。**
模型 连接数上限 切换成本 代表
------------------------------------------------------------------
一连接一线程 受线程数限制 高 早期 Apache
事件循环 受内存与句柄限制 几乎没有 Nginx
**② 真跑:worker 数不同,抢连接的方式也不同**
**一个监听套接字,多个线程同时 accept,这就是最小版的 master-worker。**
**下面测同一份负载,worker 从 1 加到 4。**
worker 数 完成请求 耗时毫秒 每秒请求
----------------------------------------------------
1 200 223 895
2 200 128 1558
4 200 110 1823
**最好的那档比最差的那档快 2.0 倍。**
**但注意它不是线性涨的:worker 从 1 加到 4,快的倍数远不到 4。**
**因为客户端只有 8 个,瓶颈已经不在 accept 上了。**
**worker 数一旦超过并发数,再往上加就只剩争抢。**
**所以 Nginx 默认 `worker_processes auto`,跟着 CPU 核数走,不瞎加。**
**下面这些数字只能看趋势,不能当性能结论:**
**Python 的线程受 GIL 限制,而且这里每次请求都新建连接,**
**真 Nginx 会复用连接,开销低得多。**
**③ worker 数怎么定**
**两条经验,都是算出来的:**
参数 怎么定 理由
-------------------------------------------------------------------------
worker_processes 等于 CPU 核数 再多也不会有更多 CPU 可用
worker_connections 按内存与句柄反推 每个连接占一个句柄
**理论最大连接数是两者相乘,但那是理论上限:**
**真实跑到一半就该考虑加机器,因为还要留给反向代理的上游连接。**
**Nginx 做反向代理时,一个客户端连接会占用两个连接的资源**——
**一个对客户端,一个对后端。**
1 个 worker 也能跑满 200 个请求,只是慢一些。 加到 4 个快了一倍多,但远不到 4 倍。 因为客户端只有 8 个,,瓶颈已经不在 accept 上了。 这些数字只能看趋势,Python 线程受 GIL 限制,不能当性能结论。
30 秒实验
- 把客户端数从 8 改成 32,看多 worker 还有没有优势——优势会更明显,因为争抢变多了。
- 把 worker 数改成 16,看还会不会更快——不会,超过并发数之后就只剩争抢。
- 把
Connection: close去掉、改成保持连接,想想会怎样——请求数不变但建连次数少了,耗时会明显下降。
常见误区
- 「Nginx 快是因为代码优化得好」——是模型选对了,不是代码快。
- 「worker 越多越好」——超过并发数就只剩争抢。
- 「Nginx 自带 RTMP」——裸装没有,全靠外挂模块。
自测三问(点开对答案)
1. Nginx 靠什么扛高并发? —— **事件驱动加少量 worker**,不是一连接一线程。 2. `reload` 与 `restart` 的本质差别? —— **reload 只换 worker,不断连接;restart 连 master 一起换。** 3. 裸装 Nginx 能推 RTMP 吗? —— **不能**,要装 nginx-rtmp-module 这类外挂模块。- ☐ (本节完,下一节 10.2.1 Windows 10 下安装 Nginx)