接好运鸭 发表于 17 小时前

Nginx 为什么能扛上万并发?拆解 C10K 问题、epoll、master‑worker 与反向代理

导语

同样是 Web 服务,Apache 传统 prefork 模式遇到上万条慢连接就内存爆满、新请求直接拒绝;而 Nginx 仅少数几个进程,就可以同时处理上万甚至几十万条 TCP 长连接。很多面试只记住一句 “Nginx 性能很高”,却并不理解底层发生了什么。
Nginx 诞生之初就是为了解决著名的C10K 问题:单台服务器如何高效处理一万条并发连接。本文对比 Apache 传统每连接每进程模型,通俗讲解 epoll I/O 多路复用、非阻塞 IO、master‑worker 进程架构、graceful reload 平滑重启原理,以及反向代理在真实业务架构中的价值,同时补充现代同类开源 Web 服务如 Caddy、Pingora。
提示:本文为后端网络技术科普,参考 Nginx 官方文档、Linux 内核 epoll 相关公开技术资料整理。

一、C10K 问题:一万条并发连接带来的困境

C10K 问题,即单机处理一万并发连接的技术难题。现实业务大量存在慢连接场景:移动端弱网网页、长轮询、WebSocket,连接一直保持,但每隔数秒才会发送少量数据。大部分时间进程都在等待客户端发来数据。
Apache prefork 模式:一连接一进程模型

Apache prefork 处理逻辑:每来一条新连接,就分配一个独立进程专门服务这条连接。哪怕连接大部分时间只是空闲等待数据,进程也不会释放。
实测现象:一万条慢连接,每条 5 秒才发送少量数据。

[*]开启一万个进程,光进程本身就要占用大量内存;
[*]大量进程带来频繁操作系统上下文切换,CPU 开销暴涨;
[*]进程数量达到上限之后,新的用户请求直接被拒绝,无法接入。
核心痛点:空闲连接也要独占完整进程资源,资源开销随连接数量线性膨胀。即便连接什么数据都不传输,只要 TCP 不断开,进程就被持续占用。
二、Nginx:单进程处理上万连接,epoll + 非阻塞 IO

Nginx 的 worker 进程不采用 “一个连接绑定一个进程”。依靠 Linux 的epoll I/O 多路复用 + 非阻塞 Socket实现单进程管理成千上万连接。
select /poll 的旧方案

早期 select、poll 机制:进程每次调用系统调用,需要把全部监视的连接列表交给内核,内核挨个遍历所有连接,找出哪些连接就绪。一万条连接就要遍历一万个条目,哪怕绝大多数连接处于空闲,每次都要完整扫描,连接越多,开销越高。
epoll 工作机制

epoll 只需要提前把所有需要监听的 socket 注册一次到内核。只有当某个 socket 真正收到数据、事件就绪,内核才会把就绪的连接放到就绪列表返回给应用程序。worker 进程只处理真正有事件发生的连接,空闲连接不会带来额外开销。
通俗类比:

[*]select/poll:挨个挨个敲门询问一万个人,“你有没有消息要发送?” 大部分时候都是白跑一趟。
[*]epoll:把所有人登记好,谁有消息主动通知我,我只处理收到通知的人。
搭配非阻塞 IO:当 socket 只收到半份 HTTP 数据,不会阻塞等待,先记录当前状态,立刻转头处理其它就绪连接。等到后续数据完整到达,epoll 再次通知回来继续处理这条连接。
资源差距直观对比(一万空闲长连接)

[*]Apache prefork:占用 GB 级别内存,进程数量巨大;
[*]Nginx:仅占用数 MB 内存,少量 worker 进程即可完成管理,CPU 消耗极低。
注意:一万连接不等于一万同时在干活,绝大多数是空闲 keep‑alive 长连接,这正是互联网线上非常普遍的场景。
三、Nginx Master‑Worker 进程架构

Nginx 分为两类进程:master 主进程 + 多个 worker 工作进程。

[*]Master 主进程:不处理业务请求。负责读取解析配置文件、监听绑定端口、管理 worker 进程生命周期、处理 reload 信号。
[*]Worker 工作进程:真正处理网络事件,执行 epoll 事件循环。CPU 几核,一般就配置几个 worker,做到 CPU 核心一一对应,减少上下文切换。
关键疑问:多个 worker 进程,监听同一个 80 端口为什么不会冲突?

两种工作模式:

[*]传统 fork 模式master 进程提前完成listen绑定 80 端口,之后 fork () 生成各个 worker 子进程。子进程继承已经打开的监听套接字。新连接到来,内核通过锁机制避免惊群效应,唤醒其中一个 worker 来接受连接。
[*]reuseport 模式(Linux3.9 及以上内核支持)配置listen 80 reuseport;,每个 worker 进程独立绑定同一个端口,内核直接做新连接的负载分发,不再依靠用户态锁,性能更好,有效解决惊群问题nginx。
graceful reload 平滑重启(nginx -s reload)

线上修改 Nginx 配置,执行 reload,不会断开正在传输的用户连接,实现零断流更新配置,完整流程:

[*]master 进程读取并校验新配置,如果配置语法错误,直接放弃更新,老配置继续运行;
[*]生成一批使用新配置的 worker 子进程,开始处理新的请求;
[*]向旧 worker 发送信号,旧 worker 不再接收新连接,已经建立的请求继续处理完毕;
[*]旧 worker 全部处理完存量连接之后,自动退出。
注意:长连接 WebSocket 场景,旧 worker 会一直挂到老连接客户端主动断开;生产环境可配置worker_shutdown_timeout设置超时强制退出时间,防止旧进程长期堆积。
如果是完全停止再启动 Nginx,正在下载、传输的请求会直接被切断,而 reload 可以做到下载文件不会中途失败。
四、反向代理:Nginx 放在业务后端前面的价值

很多 Python、PHP 后端服务(例如 gunicorn sync 同步 worker 模式),依然是 “一个请求占用一个 worker” 的模型,遇到大量慢连接,后端进程会迅速被占满,新请求直接超时。
最佳实践:Nginx 作为反向代理放在业务后端前面。

[*]海量慢连接、keep‑alive 全部在 Nginx 这一层消化;
[*]Nginx 完整接收 HTTP 请求之后,再把干净的请求转发给后端应用服务;
[*]后端只需要处理真正的业务逻辑,不需要扛海量空闲长连接。
除此之外反向代理天然附带额外能力:负载均衡、静态资源缓存、SSL 证书卸载、访问限流、黑名单过滤。
gunicorn 官方文档也明确建议生产环境前端部署 Nginx 反向代理。
五、同类现代 Web 服务简单对比


[*]Apache:新版本 event MPM 也实现事件驱动模型,解决 prefork 的缺陷,但历史包袱重,在海量长连接场景资源开销依旧高于 Nginx。
[*]Caddy(Go 语言开发):使用 goroutine,每个连接一个协程,底层依旧依赖操作系统 IO 多路复用,优势是配置极简,自动申请续期 HTTPS 证书。
[*]Pingora(Cloudflare):Rust 开发,Cloudflare 大规模线上使用,高性能代理网关。
六、面试高频认知误区澄清


[*]误区:Nginx 快是因为代码写的比别人快很多。纠正:核心优势来自事件驱动非阻塞 IO 模型 epoll,减少进程 / 线程开销,不是单段代码运算速度碾压对手。
[*]误区:epoll 可以让服务器处理无限多连接。纠正:epoll 解决的是 “空闲连接开销”,文件描述符、内存、操作系统参数依然存在上限。大量连接同时活跃做业务计算,CPU 压力依旧会上来。
[*]误区:reload 可以做到所有连接完全不断开。纠正:正在进行的请求可以跑完;但是老旧长连接(WebSocket)会保留在旧 worker,等待客户端主动断开。
[*]误区:开启 reuseport 所有环境都直接无脑用。纠正:需要 Linux3.9 以上内核,老版本内核不支持该特性。
[*]误区:Nginx 可以替代业务后端程序。纠正:Nginx 擅长网络接入、代理、静态资源;复杂业务逻辑依旧交给后端应用服务处理。
文末总结

Nginx 解决 C10K 问题的核心要点:

[*]抛弃 “一连接一进程”,依靠 Linux epoll + 非阻塞 IO,单 worker 进程管理成千上万空闲 TCP 连接;
[*]master‑worker 进程模型,一核一 worker,reload 平滑更新配置,尽量不打断线上用户流量;
[*]线上架构中,更多是充当反向代理网关,挡在业务后端前面,消化海量慢连接,保护后端应用服务。
它的强大,不是单纯运算速度快,而是解决海量空闲长连接场景下的资源浪费问题。理解慢连接对服务器资源的消耗,才能真正看懂 Nginx 架构设计的价值。


页: [1]
查看完整版本: Nginx 为什么能扛上万并发?拆解 C10K 问题、epoll、master‑worker 与反向代理