处理高并发 I/O 的核心设计模式 — Nginx / Redis / Node.js 背后的同一套思想
传统的「一请求一线程」模型:每来一个客户端连接就 fork/create 一个线程去服务它。 线程在等待 I/O(读网络、读磁盘)时会阻塞,什么也干不了。 当并发连接数达到 10 万+ 时,内存和调度开销就会把系统压垮—— 这就是著名的 C10K 问题。
解法的核心思路:不让线程等 I/O,让 I/O 好了之后来通知线程。 Reactor 模式就是这一思路的标准实现。
Reactor 模式是一种基于事件驱动的并发 I/O 设计模式。 它使用一个(或少数几个)线程, 通过操作系统提供的 I/O 多路复用(select / poll / epoll / kqueue) 机制,同时监听大量文件描述符(socket)上的 I/O 事件; 一旦某个描述符就绪,就把对应事件分发给预先注册的 Handler 来处理。
关键词:事件循环(Event Loop)、 事件分发(Dispatch)、 非阻塞 I/O、 Handler 回调。
| 项目 | Reactor 角色 | 底层多路复用 |
|---|---|---|
| Nginx | 每个 worker 进程一个 Reactor | epoll / kqueue |
| Redis | 单线程 Reactor(ae 事件循环) | epoll / kqueue / select |
| Node.js | 单线程 Reactor(libuv) | epoll / kqueue / IOCP |
| Netty | 主从多 Reactor(Boss + Worker) | epoll(Java NIO) |
| Memcached | 多 Reactor(libevent) | epoll |
也叫 Event Loop / Dispatcher。
职责:不断调用 epoll_wait 等待事件,
把就绪事件路由给对应的 Handler。
它是整个模式的调度核心。
就是文件描述符 fd, 代表一个 I/O 资源:监听 socket、连接 socket、管道、定时器 fd 等。 事件都附着在 Handle 上发生。
操作系统提供的多路复用接口:
select / poll / epoll / kqueue。
负责阻塞等待,直到至少一个 Handle 就绪再返回。
应用层注册的回调函数。 不同事件类型(可读、可写、连接、关闭) 对应不同 Handler。Handler 应快速执行, 不做耗时阻塞操作。
特殊的 Handler,专门处理新连接到来事件。 调用 accept() 拿到新 socket fd, 为其创建 Connection Handler 并注册到 Reactor。
处理已建立连接上的读写业务逻辑: 读数据 → 解码 → 执行业务 → 编码 → 写回响应。 可以把耗时部分扔给线程池。
1. Handler 必须非阻塞 —— 若 Handler 内调用了阻塞操作(DB 查询、文件读写), Reactor 线程会被卡住,所有其他连接都饿死。解法:把耗时任务扔进线程池。
2. 注册/注销是动态的 —— Acceptor 建立连接后把 (fd, Handler) 对插入 Reactor 的事件注册表;连接关闭时从表中移除,Reactor 不再监听该 fd。
3. 同一 fd 的读写都由同一 Handler 负责 —— 避免多线程竞争同一连接的状态,连接状态机天然线程安全。
演示:3 个客户端同时连接,Reactor 轮流分发事件,Handler 依次处理请求
✓ 无锁,实现简单
✗ CPU 单核,Handler 阻塞即全挂
适合:单核 CPU、业务极轻、延迟要求不高
✓ 可利用多核 CPU
✗ Reactor 仍单线程,高并发下 accept 成瓶颈
适合:中等并发、业务有 CPU 计算
✓ Main Reactor 专职 accept,Sub Reactor 并行处理 I/O,吞吐量最高
✓ 可完整利用多核,是现代高性能服务器首选
✗ 实现最复杂,需处理线程间任务分发
代表:Netty、Nginx(多 worker 进程版)
# ── 1. 注册表 ────────────────────────────────────────── handlers = {} # fd → handler_func def register(fd, event_type, handler): epoll.ctl(EPOLL_CTL_ADD, fd, event_type) handlers[fd] = handler def unregister(fd): epoll.ctl(EPOLL_CTL_DEL, fd) del handlers[fd] # ── 2. Acceptor ──────────────────────────────────────── def on_accept(listen_fd): conn_fd = accept(listen_fd) # 拿到新连接 fd set_nonblocking(conn_fd) register(conn_fd, EPOLLIN, on_read) # 注册读事件 # ── 3. Handler ───────────────────────────────────────── def on_read(conn_fd): data = recv(conn_fd, 4096) if not data: unregister(conn_fd) close(conn_fd) return response = process(data) # 业务逻辑(必须快!) send(conn_fd, response) # ── 4. Reactor 主循环 ────────────────────────────────── listen_fd = create_tcp_server("0.0.0.0", 8080) set_nonblocking(listen_fd) register(listen_fd, EPOLLIN, on_accept) while True: events = epoll.wait(timeout=-1) # 阻塞直到有事件 for fd, event in events: handler = handlers.get(fd) if handler: handler(fd) # dispatch!
from concurrent.futures import ThreadPoolExecutor pool = ThreadPoolExecutor(max_workers=16) def on_read(conn_fd): data = recv(conn_fd, 4096) # 读数据(非阻塞,快) # ↓ 耗时业务交给线程池,Reactor 立刻返回继续监听 pool.submit(handle_business, conn_fd, data) def handle_business(conn_fd, data): response = slow_db_query(data) # 可以阻塞 # 写回:注意需要线程安全,或 post 写事件到 Reactor send(conn_fd, response)
| 维度 | BIO(一请求一线程) | Reactor(事件驱动) | Proactor(异步 I/O) |
|---|---|---|---|
| I/O 等待 | 线程阻塞等待 | epoll 统一等待,不阻塞业务线程 | 内核完成后回调(IOCP / io_uring) |
| 线程数 | = 连接数(可达数千) | 固定少量(1 ~ CPU 核数) | 固定少量 |
| 数据拷贝时机 | 内核 → 用户(read 完成) | 用户层调用 read(就绪才读) | 内核直接完成 read,通知用户 |
| 实现难度 | 低 | 中 | 高(平台差异大) |
| 跨平台 | ✓ 好 | ✓ 好(抽象层封装) | ✗ 差(Windows IOCP vs Linux io_uring) |
| 典型并发 | 数百 ~ 千 | 十万 ~ 百万 | 十万 ~ 百万 |
| 代表实现 | 传统 Java Servlet | Nginx / Redis / Netty / Node.js | Windows IOCP / io_uring |
① 解耦事件检测与事件处理 —— Reactor 只管"谁就绪",Handler 只管"怎么处理",职责清晰。
② 用少量线程撬动海量并发 —— 线程不在 I/O 上等待,CPU 利用率高,内存占用低。
③ 可扩展 —— 新 I/O 类型只需实现新 Handler 并注册,不改动 Reactor 核心逻辑,符合开闭原则。
④ 注意边界 —— Reactor 不是银弹:Handler 中不能有阻塞操作;CPU 密集型任务仍需线程池; 单 Reactor 在极高 QPS 下 accept 仍可能成瓶颈(升级为主从 Reactor)。