4. 高性能网络库剖析

聊到高并发网络编程,有两个模式是绕不开的:Reactor 和 Proactor。说白了,它们就是解决「如何高效处理海量连接」这个问题的两种思路。

我刚开始做网络编程时,总觉得能写个 select/poll 循环就挺厉害了。后来线上一个服务连接数刚过千就卡死,我才意识到——嗯,模式选错了,后面全是坑。

Reactor 模式:事件驱动的核心思想

Reactor 模式,你可以把它理解成一个「事件分发中心」。它不主动干活,而是等着 I/O 事件发生,然后通知对应的处理器去处理。

核心流程就三步:

  1. 注册感兴趣的事件(读、写、连接等)到 Reactor
  2. Reactor 循环调用多路复用器(select/poll/epoll)等待事件
  3. 事件到来,Reactor 分发给对应的 Handler

一句话总结:Reactor 是「来了活再干」,不是「轮询问有没有活」。

我习惯把 Reactor 比作一个餐厅的前台。客人(连接)来了,前台(Reactor)登记一下,然后喊服务员(Handler)去接待。前台不负责端菜,只负责喊人。

单线程 Reactor:简单但有限制

所有事情都在一个线程里完成:事件监听、事件分发、业务处理。你想想看,这就像前台、服务员、厨师全是一个人。

// 伪代码示意
while (true) {
    events = selector.select();  // 阻塞等待事件
    for (event : events) {
        handler = getHandler(event);
        handler.process(event);  // 同步处理
    }
}

优点很明显:没有锁,没有上下文切换,代码简单到极致。

缺点也很致命:任何一个 Handler 阻塞了,整个服务就停了。我在项目中遇到过,一个慢查询把整个网关拖垮,就是因为用了单线程 Reactor,一个请求卡住,所有连接都等死。

避坑指南:我曾经在一个 IoT 网关项目里用了单线程 Reactor,设备连接数 500 以内时一切正常。后来客户接入量涨到 2000,CPU 占用率不到 30%,但响应时间从 2ms 飙升到 5s。查了半天,发现是某个设备的握手协议实现有问题,每次握手要等 3 秒超时。单线程下,一个慢设备拖死所有正常设备。

适用场景:连接数少(几百以内)、业务处理极快(纯内存计算)、没有阻塞操作。比如 Redis 的早期版本就是单线程 Reactor,但它所有操作都是内存级的,所以扛得住。

多线程 Reactor:把业务处理扔出去

核心思路:Reactor 线程只负责事件监听和分发,业务处理交给工作线程池。

// 伪代码示意
while (true) {
    events = selector.select();
    for (event : events) {
        handler = getHandler(event);
        threadPool.execute(() -> handler.process(event));  // 异步处理
    }
}

这样一来,Reactor 线程永远不会被阻塞。它只管「喊人」,不管「干活」。

我个人的习惯是,工作线程数设置为 CPU 核心数的 2 倍左右。为什么?因为网络 I/O 处理中,很多操作是短暂的 CPU 计算,线程太多反而增加上下文切换开销。

对比项 单线程 Reactor 多线程 Reactor
事件监听 主线程 主线程
业务处理 主线程 工作线程池
阻塞风险 高 低
编码复杂度 低 中
适用连接数 < 1000 < 10000

小技巧:多线程 Reactor 里,Reactor 线程和业务线程之间用无锁队列传递任务。我一般用 disruptor 或者 Java 的 LinkedBlockingQueue,注意队列不能无界,否则内存会爆。

主从 Reactor:多路复用器的终极形态

当连接数到了几万甚至几十万,一个 Reactor 线程处理事件分发也会成为瓶颈。为什么?因为 select/epoll 的返回、事件的分发、新连接的 accept,全挤在一个线程里。

主从 Reactor 的思路是:

  • 主 Reactor(Main Reactor):只负责 accept 新连接,然后把连接注册到从 Reactor
  • 从 Reactor(Sub Reactor):每个从 Reactor 有自己的 selector,负责已连接 socket 的读写事件

你想想看,这就像机场的值机柜台。主 Reactor 是引导员,只负责把乘客分流到不同的柜台;从 Reactor 是柜台工作人员,负责具体办理业务。

// 主 Reactor 伪代码
class MainReactor {
    ServerSocketChannel serverSocket;
    SubReactor[] subReactors;
    
    void run() {
        while (true) {
            SocketChannel client = serverSocket.accept();
            SubReactor sub = subReactors[nextIndex];  // 轮询选择
            sub.register(client);  // 注册到从 Reactor
        }
    }
}

// 从 Reactor 伪代码
class SubReactor {
    Selector selector;
    
    void run() {
        while (true) {
            selector.select();
            for (SelectionKey key : selector.selectedKeys()) {
                dispatch(key);  // 处理读写事件
            }
        }
    }
}

我在 Netty 的源码里看到过这种实现。Netty 的 EventLoopGroup 本质上就是主从 Reactor——BossGroup 是主 Reactor,WorkerGroup 是从 Reactor。

关键点:主 Reactor 的 accept 操作本身是轻量的,但如果不分离,当大量连接同时涌入时,accept 会阻塞 selector 的 select 调用,导致已有连接的读写事件被延迟处理。

适用场景:连接数万级以上、连接频繁建立和断开、需要高吞吐量的场景。比如即时通讯服务器、游戏网关、API 网关。

Proactor 模式与异步 I/O

Reactor 模式有一个本质特点:它是「同步非阻塞」的。什么意思?就是事件来了,你还是要自己去读数据、写数据。虽然不阻塞,但读写的操作还是由你的线程完成。

Proactor 模式则更进一步:它是「异步」的。你告诉操作系统:「帮我读 1024 字节,读完了放这个缓冲区里,然后通知我。」操作系统真的帮你把数据读好了,你直接拿来用就行。

说白了,Reactor 是「我来通知你,你自己去拿」;Proactor 是「我帮你拿好,通知你来取」。

对比项 Reactor Proactor
I/O 操作 应用程序自己读写 操作系统完成读写
通知方式 事件就绪通知 操作完成通知
系统调用 select/epoll(就绪通知) IOCP(完成通知)
编程模型 同步非阻塞 异步
典型实现 Netty(Linux epoll) Windows IOCP、Boost.Asio

Proactor 模式在 Windows 上表现很好,因为 Windows 的 IOCP(I/O Completion Port)是原生支持的。但在 Linux 上,异步 I/O(AIO)的实现一直不太成熟,性能甚至不如 epoll 的 Reactor 模式。

避坑指南:我曾经在一个跨平台项目中尝试用 Linux AIO 实现 Proactor,结果发现:

  • 文件 AIO 还行,但 socket AIO 在 2.6 内核上 bug 很多
  • 即使到了 4.x 内核,AIO 的上下文切换开销也比 epoll 大
  • 很多 Linux 发行版默认不开启 AIO 支持

最后老老实实切回 epoll 的 Reactor 模式,性能反而提升了 30%。

我如何选择模式

这个问题没有标准答案,但我可以分享我的选择逻辑:

  1. 看操作系统:Windows 优先考虑 Proactor(IOCP),Linux 优先考虑 Reactor(epoll)
  2. 看连接数:几千以内用多线程 Reactor,几万以上用主从 Reactor
  3. 看业务复杂度:纯计算型用单线程 Reactor 就够了,有阻塞操作必须用多线程或主从
  4. 看团队能力:Proactor 的编程模型更复杂,调试难度更大。如果团队对异步编程不熟,Reactor 更稳妥

我的个人习惯:99% 的场景我用主从 Reactor。为什么?因为它在 Linux 上性能足够好,代码可读性强,出了问题好排查。Proactor 我只在 Windows 平台、且对延迟有极致要求时才用。

下面这张图可以帮你快速理解三种 Reactor 和 Proactor 的核心区别:

Reactor 与 Proactor 模式对比 单线程 Reactor Reactor 线程(监听+分发+处理) select/epoll 等待事件 Handler 同步处理业务 多线程 Reactor Reactor 线程(仅监听+分发) 工作线程池(异步处理业务) Handler 在工作线程执行 主从 Reactor 主 Reactor(仅 accept) 从 Reactor 1(读写事件) 从 Reactor 2(读写事件) 从 Reactor N(读写事件) Proactor 模式(异步 I/O) 发起异步读/写请求 操作系统执行 I/O 完成通知 + 数据就绪 直接处理数据 核心区别总结 • 单线程 Reactor:一个人干所有事,简单但脆弱 • 多线程 Reactor:监听和业务分离,适合大多数场景 • 主从 Reactor:多路复用器分离,适合海量连接 • Proactor:操作系统帮你读写,适合 Windows 平台

最后说一句:不要为了用模式而用模式。我见过有人用主从 Reactor 写了个只处理 100 个连接的服务,纯属杀鸡用牛刀。选模式之前,先搞清楚你的瓶颈在哪——是连接数?是 CPU?还是 I/O?

嗯,模式选对了,后面的事就顺了。