🎬 第十章 Nginx搭建直播平台
📄 一句话概括

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 秒实验

  1. 把客户端数从 8 改成 32,看多 worker 还有没有优势——优势会更明显,因为争抢变多了。
  2. 把 worker 数改成 16,看还会不会更快——不会,超过并发数之后就只剩争抢。
  3. 把 Connection: close 去掉、改成保持连接,想想会怎样——请求数不变但建连次数少了,耗时会明显下降。

常见误区

  1. 「Nginx 快是因为代码优化得好」——是模型选对了,不是代码快。
  2. 「worker 越多越好」——超过并发数就只剩争抢。
  3. 「Nginx 自带 RTMP」——裸装没有,全靠外挂模块。
自测三问(点开对答案) 1. Nginx 靠什么扛高并发? —— **事件驱动加少量 worker**,不是一连接一线程。 2. `reload` 与 `restart` 的本质差别? —— **reload 只换 worker,不断连接;restart 连 master 一起换。** 3. 裸装 Nginx 能推 RTMP 吗? —— **不能**,要装 nginx-rtmp-module 这类外挂模块。

相关笔记