Skip to content

第 7 章 · 实战应用:Redis / Nginx / Netty 是怎么用 epoll 的

学完本章你将知道:业界最有代表性的三个高并发系统——Redis、Nginx、Netty——是怎么把 epoll 这个原始 syscall 包装成自己事件循环的;它们各自踩过什么坑、做过什么权衡;面试时谈到这些是绝对加分项


1. 一句话开场

没有任何一个生产系统会让你"裸用"epoll 三剑客——它们都会在 epoll 之上构建事件循环(Event Loop)Reactor 模式,把"哪个 fd 就绪 → 调用对应回调"这件事抽象起来。

看完这章你会发现:Redis、Nginx、Netty 三家的 epoll 用法各有不同——但底层套路都是一样的


2. Reactor 模式:所有事件循环的共同祖宗

2.1 什么是 Reactor

Reactor 模式:一个事件分派器(Reactor)阻塞等待事件 → 事件来了根据类型分派给对应处理器

核心思想:Reactor = epoll_wait 循环 + 事件 → 回调的映射表。

2.2 三种 Reactor 部署形态

text
① Single Reactor + Single Thread (单线程)
    ┌────────────────────────────────┐
    │  Reactor (一个 epoll_wait 循环) │
    │  ↓  ↓  ↓                       │
    │  Acceptor / Reader / Writer    │
    │  (全部在主线程同步执行)          │
    └────────────────────────────────┘
    优点: 简单, 无锁
    缺点: 一个慢请求拖死全部
    代表: Redis (除了 6.0+ 的 IO threads)

② Single Reactor + Worker Thread Pool
    ┌────────────────────┐    ┌──────────────┐
    │ Reactor 主线程      │ →  │ Worker Pool  │
    │ (只 accept + read)  │    │ (业务处理)    │
    └────────────────────┘    └──────────────┘
    优点: 业务逻辑可以耗时
    缺点: Reactor 仍是单点

③ Multi-Reactor (主从 / 多 Reactor)
    ┌──────────┐
    │ Main     │ ← 只负责 accept, 派给 sub
    │ Reactor  │
    └────┬─────┘
         ↓ 分发新 fd
    ┌─────────┬─────────┬─────────┐
    │  Sub    │  Sub    │  Sub    │ ← 各自有自己的 epoll
    │ Reactor │ Reactor │ Reactor │
    └─────────┴─────────┴─────────┘
    优点: 多核充分利用
    代表: Nginx (master + workers), Netty

3. Redis 的事件循环 (ae.c) ⭐

3.1 为什么 Redis 单线程也能扛 10 万 QPS?

关键:Redis 的"单线程"是指命令执行单线程——网络 I/O 是 epoll 多路复用的。

text
Redis 事件循环 (ae.c)

    └─→ aeMain() {
          while (!stop) {
              aeProcessEvents();    // 一次 epoll_wait
          }
        }

aeProcessEvents() {
    // 1. epoll_wait 等事件
    numevents = epoll_wait(...);
    
    // 2. 派发到对应回调
    for each event in events:
        if (mask & READABLE) fe->rfileProc(...)    // readQueryFromClient
        if (mask & WRITABLE) fe->wfileProc(...)    // sendReplyToClient
}

3.2 Redis 的 epoll 封装(ae_epoll.c

Redis 把 epoll 封装成跨平台的 4 个函数:

c
aeApiCreate    → epoll_create
aeApiAddEvent  → epoll_ctl(ADD/MOD)
aeApiDelEvent  → epoll_ctl(DEL/MOD)
aeApiPoll      → epoll_wait

设计巧妙之处:在 macOS 编译时这些函数会被替换成 kqueue 的对应实现,在 Solaris 上是 evport——业务代码不需要改一行

3.3 为什么 Redis 用 LT 而不是 ET?

维度Redis 选择 LT 的理由
单线程一次没读完下次再读,没有竞争问题
命令简单RESP 协议小包,一次 read 通常就够了
代码可读性LT 写法直观,不容易出错
性能够用Redis 瓶颈在 CPU 单线程,不在 epoll

3.4 Redis 6.0+ 的 IO 线程

Redis 6.0 引入了 多线程 I/O——但仍只有一个事件循环,I/O 线程只做"读 socket / 写 socket"的网络拷贝,命令执行还是单线程

text
[主线程]              [I/O Threads]
epoll_wait

   ├─ 把 read 任务派给 IO 线程 ──→ 并行 read
   │                                  │
   │ ←─ 收齐所有 read 结果 ←─────────┘

   ├─ 主线程串行执行命令 (无锁,简单)

   ├─ 把 write 任务派给 IO 线程 ──→ 并行 write
   └─ 收齐写完成

设计哲学:把"瓶颈中的瓶颈"(网络拷贝)多线程化,但不动数据结构访问,避免引入复杂的锁。


4. Nginx 的多 worker + epoll ⭐

4.1 整体架构

text
                    Master Process
                  (管理, 不处理请求)

        ┌─────────┬──────┴──────┬─────────┐
        ↓         ↓             ↓         ↓
    Worker 1  Worker 2     Worker 3   Worker 4
    (epoll)   (epoll)      (epoll)    (epoll)
        ↑         ↑             ↑         ↑
        └─────────┴──── 接收客户端 ─┴─────────┘

4.2 Nginx 用 ET 还是 LT?

Nginx 默认用 ET 模式 + 非阻塞 socket——它的核心代码 ngx_epoll_module.c 这样注册:

c
ee.events = EPOLLIN | EPOLLRDHUP | EPOLLET;

为什么敢用 ET?因为 Nginx 的代码极度严谨——每个 read/write 都有循环到 EAGAIN 的处理,没有任何遗漏。

4.3 Nginx 怎么解决惊群?

方案 A:accept_mutex(默认)

多个 worker 都监听同一个 listen_fd——但 Nginx 给它套了一个进程间互斥锁

text
worker1: lock(mutex)? → 拿到 → epoll_ctl(ADD listen_fd) → epoll_wait
worker2: lock(mutex)? → 拿不到 → 不监听 listen_fd, 只处理已有连接
worker3: lock(mutex)? → 拿不到 → 同上

→ 同一时刻只有 1 个 worker 在等新连接,避免惊群

方案 B:SO_REUSEPORT(Linux 3.9+,推荐)

每个 worker 都 bind 同一个端口——内核做负载均衡:

c
int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
text
内核维护 N 个独立 listen 队列,新连接哈希分到其中一个
→ 每个 worker 只处理自己的队列,零竞争

业界数据:SO_REUSEPORT 比 accept_mutex 在多核机器上 QPS 高 30%+。

4.4 Nginx 的事件分发器抽象

c
ngx_event_actions_t  ngx_event_actions = {
    ngx_epoll_add_event,        // 增加 fd
    ngx_epoll_del_event,        // 删除 fd
    ngx_epoll_add_connection,
    ngx_epoll_del_connection,
    NULL,                       // notify (不用)
    ngx_epoll_process_events,   // epoll_wait 循环
    ngx_epoll_init,
    ngx_epoll_done
};

同 Redis 一样,Nginx 也通过这层抽象实现跨平台——FreeBSD 上自动换成 kqueue 实现。


5. Netty 的 EventLoop(Java NIO) ⭐

5.1 Java 默认有什么?

Java NIO 提供了 Selector(它是 select/poll/epoll 的跨平台封装)——但默认实现性能不够好,Netty 自己写了一套基于 epoll 的 JNI 直调。

5.2 Netty 的两种 EventLoop 实现

底层适用场景
NioEventLoopJava Selector(跨平台)默认,跨 OS 通用
EpollEventLoop直接 JNI 调 epoll(仅 Linux)性能极致,Linux 推荐

5.3 Netty 的主从 Reactor

java
EventLoopGroup boss = new EpollEventLoopGroup(1);          // 主 Reactor,只 accept
EventLoopGroup worker = new EpollEventLoopGroup(8);        // 从 Reactor,处理 IO

ServerBootstrap b = new ServerBootstrap();
b.group(boss, worker)
 .channel(EpollServerSocketChannel.class)
 .childHandler(new MyHandler());
text
                 EpollServerSocketChannel (listen fd)

                       Boss EventLoop (1)
                       (epoll_wait, accept)

                            ↓ 派发新连接
            ┌───────────────┼───────────────┐
            ↓               ↓               ↓
    Worker EL 1     Worker EL 2  ...   Worker EL N
    (epoll_wait)    (epoll_wait)        (epoll_wait)
    (有自己的 epfd) (有自己的 epfd)

5.4 Netty 的 ET 模式

Netty 的 EpollEventLoop 默认也用 ET + 非阻塞——通过 Java JNI 直接调 epoll,并实现了完整的"循环读到 EAGAIN"逻辑。


6. 实战架构对比表

维度RedisNginxNetty
事件循环数1 主 + 多 IO 线程 (6.0+)N worker,每个独立1 boss + N worker
触发模式LTETET
惊群方案不存在(单实例)accept_mutex / SO_REUSEPORTSO_REUSEPORT
平台抽象ae.c (epoll/kqueue/evport)ngx_event_actions (epoll/kqueue)EventLoopGroup (epoll/NIO/kqueue)
典型用途内存数据库Web 反向代理通信框架

7. 自己怎么写一个 Reactor?

7.1 最小骨架

c
typedef void (*event_handler_t)(int fd, void *ctx);

typedef struct {
    int epfd;
    event_handler_t handlers[MAX_FDS];
    void *contexts[MAX_FDS];
} reactor_t;

void reactor_register(reactor_t *r, int fd, event_handler_t cb, void *ctx) {
    r->handlers[fd] = cb;
    r->contexts[fd] = ctx;
    struct epoll_event ev = {.events = EPOLLIN | EPOLLET, .data.fd = fd};
    epoll_ctl(r->epfd, EPOLL_CTL_ADD, fd, &ev);
}

void reactor_run(reactor_t *r) {
    struct epoll_event events[64];
    while (1) {
        int n = epoll_wait(r->epfd, events, 64, -1);
        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            r->handlers[fd](fd, r->contexts[fd]);
        }
    }
}

7.2 用法

c
reactor_t r;
r.epfd = epoll_create1(0);

reactor_register(&r, listen_fd, on_accept, NULL);
reactor_run(&r);

// on_accept 里:
void on_accept(int fd, void *ctx) {
    int conn = accept(fd, NULL, NULL);
    set_nonblock(conn);
    reactor_register(&r, conn, on_read, NULL);
}

void on_read(int fd, void *ctx) {
    char buf[1024];
    while (read(fd, buf, sizeof(buf)) > 0) { /* echo */ }
}

短短几十行,就有了 Redis / Nginx 同款的事件分派架构。


8. 业界最佳实践清单

实践说明
优先用现成的事件库libuv (Node.js)、libevent、boost.asio——别自己造轮子
Linux 上用 epoll,跨平台用 selector 抽象不要写"if Linux else select"
生产环境用 SO_REUSEPORT比 mutex 锁优雅、性能好
ET 模式必须循环到 EAGAIN漏一次就丢事件
永远监听 EPOLLRDHUP否则可能一直 epoll 出来同一个已关闭的 fd
maxevents 设 64-1024太小饥饿,太大延迟
配 ulimit -n 65535否则 fd 用尽
多 worker 时关注 NUMA 亲和taskset 把 worker 绑核

9. 本章小结

text
工业级 epoll 用法精要
─────────────────────────────────
✅ 都用 Reactor 模式 (epoll_wait + 回调表)
✅ Redis: 1 主 + IO 线程, LT 模式
✅ Nginx: master + N worker, ET 模式 + SO_REUSEPORT
✅ Netty: 主从 Reactor, EpollEventLoop, ET 模式
✅ 跨平台: ae.c / ngx_event / NioEventLoop 抽象层
✅ 性能极限: 单机百万连接, 几十万 QPS

10. 真实面试题(系统设计常考)

Q1:Redis 单线程为什么能这么快?

A:① 数据全在内存;② 数据结构 O(1) 设计(hash / skiplist / ziplist);③ 基于 epoll 的 I/O 多路复用——一个线程靠 epoll_wait 同时管理上万连接;④ 避免线程切换 + 锁竞争。6.0+ 引入 I/O 线程优化网络 IO,但命令执行仍是单线程。

Q2:Nginx 为什么用多进程而不是多线程?

A:① 进程隔离——一个 worker 崩了不影响其他;② 多核利用——一个 worker 独占一个核,配合 SO_REUSEPORT 内核负载均衡;③ 避免锁——每个 worker 内部全异步无锁,相比多线程少一层心智负担;④ cgroup / 资源控制更友好

Q3:什么是 Reactor 模式?什么是 Proactor?

A:Reactor:事件分派器等到事件后通知用户线程"该读了"——用户自己 read(同步 I/O)。多路复用类(epoll)是 Reactor。 Proactor:内核自己完成 I/O,结果好了通知用户——用户拿到的就是数据。Windows IOCP / Linux io_uring 是 Proactor。

Q4:Netty 的 boss 和 worker 各做什么?

A:boss(通常 1 个)只负责 accept 新连接,把 SocketChannel 注册到某个 worker 的 EventLoop;worker(CPU 核数 × 2)跑各自的 epoll_wait,处理已建立连接的 read / write。这是经典的主从 Reactor 模式

Q5:什么是 SO_REUSEPORT?解决了什么问题?

A:Linux 3.9+ 引入的 socket 选项——允许多个进程/线程 bind 同一个端口,内核为每个进程维护独立的 accept 队列,按 5 元组哈希做负载均衡。解决了惊群问题,且比应用层加锁性能好 30%+。Nginx、HAProxy、Envoy 都默认开。

Q6:业务低并发场景该用 epoll 吗?

A:不一定。如果连接数 < 100、业务逻辑简单:① 一连接一线程的阻塞 I/O 写起来更直观;② 异步代码的"颜色传染"反而拖累开发效率。epoll 是为高并发服务的,不是越多越好


下一站第 8 章:面试题集锦,把全系列高频面试题集中起来——含追问、陷阱、加分项

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗