大部分网络框架的设计都基于 Reactor 模式。
这种模式基于事件驱动,特别适合处理大量的 IO 事件。
根据线程数量,我们可以将 Reactor 模式大致分为以下3种(以服务端实现为例):
单个 Reactor 线程负责对TCP链路读写数据和编解码(包括执行业务逻辑)。
(很多人把该模式称为 “1 - 1”。其实这种称呼并不贴切。)
适合 并发度低、请求处理快 的小应用。
不适合 高并发、高负载的场景。因为:
单线程处理大量并发链路时性能不高,也无法发挥多核计算机的优势。无法满足大量消息的编解码和读写需求。
单线程负载过高后,处理速度变慢,可能导致大量客户端连接超时。超时会引起消息重发,线程负载更重。最终大量消息积压、超时,成为系统的性能瓶颈。
单线程可靠性不足。单个线程意外终止或陷入死循环,会导致整个系统通信瘫痪,无法接受和处理外部请求。
与单线程Reactor模式不同,此模式用一个 NIO线程池 代替原来的单个Reactor线程。
通常,Reactor 线程池中,每个线程可以同时处理 N条链路;
但是一个链路只能由1个线程处理,以防止线程安全问题。
这种模式可以满足绝大多数场景的性能需求。
但是 单线程Acceptor 也可能成为性能瓶颈。客户端连接非常多时,处理客户端请求连接/安全认证等操作也会非常耗性能。
为了解决 单线程Acceptor 的性能问题,又衍生出了第三种模式 —— “主从 多线程 Reactor”。
即,用一个 NIO线程池 专门处理那些与耗时的非业务性操作。
大致过程如下:
开始监听连接:由一个线程作为 Acceptor,绑定监听端口接收客户端连接。
具体实现时,这个Acceptor线程可以是从“主Reactor线程池”中随机选定的一个线程
收到连接 并 分配处理线程:Acceptor 接收到客户端连接,并创建 SocketChannel,并将其注册到 主线程池 的 Reactor 线程上。
执行非业务性操作:主线程池的 Reactor 线程负责 接入认证、IP黑白名单过滤、握手等非业务性操作。
执行业务逻辑:执行完上一步,业务层链路正式建立。SocketChannel 从 主线程池 中线程的多路复用器上摘除,重新注册到 从线程池 的线程上,执行处理后续业务操作。
Netty 的线程模型与上述三种 Reactor 线程模型相似。
Netty 提供了便捷的API来实现相关线程配置。通过 NioEventLoopGroup 的构造方法 和 ServerBootstrap.group() 方法就可以实现相应的线程配置。
可以简单地理解为:
一个 EventLoop 对应一个 Reactor 线程;
EvenLoopGroup 则对应 线程池(ExecutorService)。
如果业务非常简单,执行时间非常短,不需要访问外部资源(如,网络、数据库、磁盘等)时,可以直接在 ChannelHandler 中执行业务。
这样实现简单,避免线程上下文切换,也不会有线程安全问题。
可以将这类业务封装成 Task,投递到后端的业务线程池处理。
因为过多的 业务ChannelHandler 会降低开发效率,增加维护成本。
不要把 Netty 当作业务容器。
(从某种角度而言,这算是上一条的延伸。)
业务通常是多线程模型处理的,如果业务线程直接操作 ChannelHandler,就需要处理线程安全问题。
可以参照Netty自身的做法,将操作封装成独立的 Task 由 NioEventLoopGroup 统一调度。
我们可以在Netty源码中找很多类似如下的代码:
Netty ChunkedWriteHandler 中的 resumeTransfer() 方法:
Java代码
public void resumeTransfer() {
...
if (ctx.executor.inEventLoop()) {
resumeTransfer0(ctx);
} else {
// let the transfer resume on the next event loop round
ctx.executor.execute(() -> resumeTransfer0(ctx));
}
}