Nginx通过非阻塞I/O与事件循环实现高并发:每个worker将socket设为O_NONBLOCK,recv/send立即返回EAGAIN而非阻塞,结合epoll仅处理就绪事件,使单线程可高效调度数万连接而不卡顿。

关键不在“同时挂起”,而在于“不为任一连接阻塞”。Nginx 的单线程 worker 并不真正“挂起”连接,而是把每个连接的 socket 设为 O_NONBLOCK,让所有读写操作都立即返回——无论数据是否就绪、缓冲区是否已满。
非阻塞读写如何避免卡住事件循环
当 socket 处于非阻塞模式时:
-
recv() 若当前无数据可读,立刻返回
EAGAIN或EWOULDBLOCK,而不是等数据到来 -
send() 若内核发送缓冲区已满,也立刻返回
EAGAIN,而不是干等缓冲区腾出空间 - worker 不会因此停在某次调用上,而是马上转向处理下一个就绪事件
- 后续只要该 socket 变成“可读”或“可写”,epoll 就会再次通知,worker 继续推进对应逻辑
连接状态由事件驱动,而非线程等待
每个连接在内存中只保留一个轻量结构(ngx_connection_t),记录当前处于哪个阶段:
- 刚建立:注册读事件,等待 HTTP 请求头
- 收到部分 header:解析后发现需要 body,继续注册读事件;若 Content-Length 已知,则等待完整 body 到达
- 准备发响应:调用 send(),若返回 EAGAIN,就暂停发送,转而注册写事件,等 epoll 通知“可写”再继续
- 长连接空闲时:仅保留在 epoll 监听列表中,不消耗 CPU,也不占栈空间
一次连接的生命周期完全异步分段执行
以一个 keepalive 下的 HTTP 请求为例,整个过程被拆成多个可中断的步骤,全部跑在同一个线程里:
- accept 新连接 → 注册读事件
- epoll 通知可读 → recv header → 解析 method/path → 决定是否需要 body
- 若需 body 且未收全 → 继续注册读事件,等待下一次通知
- 读完后查文件或转发 upstream → 若是静态文件,用
sendfile()零拷贝发起发送 - sendfile 返回 EAGAIN → 注册写事件 → 等下次 epoll 通知可写 → 继续发送剩余字节
- 响应发完 → 若 keepalive 开启,重置状态,回到“等待新请求头”阶段
为什么数万连接不会压垮单线程
因为绝大多数连接在任意时刻都是“安静”的:
- 它们只是被注册进 epoll,占用一个 fd 和约 2.5 KB 内存,不参与调度
- worker 每次
epoll_wait()只拿到当前活跃的几十或几百个 fd,处理完就结束本轮循环 - 没有线程创建、栈分配、上下文切换、锁竞争这些开销
- CPU 始终在做有效工作:响应就绪事件、推进请求状态、释放资源


















