Netty——I/O 线程模型
文章目录
- 1. I/O 线程模型基本分类
- 2. 传统阻塞 I/O 模型
- 2.1 模型示意图
- 2.2 特点
- 2.3 问题
- 3. Reactor 模式
- 3.1 单 Reactor 单线程 I/O 模型
- 3.1.1 模型示意图
- 3.1.2 优点
- 3.1.3 缺点
- 3.2 单 Reactor 多线程 I/O 模型
- 3.2.1 模型示意图
- 3.2.2 优点
- 3.2.3 缺点
- 3.3 主从 Reactor 多线程 I/O 模型
- 3.3.1 模型示意图
- 3.3.2 优点
- 3.3.3 缺点
- 4. Redis 使用的 I/O 线程模型
- 4.1 Redis 的核心模型
- 4.2 Redis 6.0 后的优化:多线程 I/O 处理
- 5. Netty 使用的 I/O 线程模型
- 5.1 主 Reactor——BossGroup
- 5.2 从 Reactor——WorkerGroup
- 5.3 Worker 线程池(可选)
- 6. 总结
1. I/O 线程模型基本分类
常见的 I/O 线程模型有:
- 传统阻塞 I/O 模型
- Reactor 模式:
- 单 Reactor 单线程 I/O 模型
- 单 Reactor 多线程 I/O 模型
- 主从 Reactor 多线程 I/O 模型
2. 传统阻塞 I/O 模型
2.1 模型示意图
注:紫色框代表一个线程,绿色框代表线程池,框中的内容就是这个线程需要执行的任务。
2.2 特点
- 同步阻塞:线程发起 I/O 操作后,必须等待操作完成才能继续执行后续代码。
- 单线程处理单连接:每个客户端连接需要分配一个独立的线程处理,线程负责完整的 I/O 操作(如读取请求、处理业务、返回响应)。
2.3 问题
- 线程资源浪费:
- 线程数量爆炸:每个连接需要独立线程,当并发连接数达到数千甚至数万时,线程的创建、销毁和上下文切换会消耗大量 CPU 和内存资源。
- 线程闲置:在等待 I/O 操作完成时,线程处于阻塞状态,无法执行其他任务,导致 CPU 空闲。
- 扩展性差:
- 硬性资源限制:高并发场景下创建太多线程会触发资源耗尽,因为每个线程都需要占用一定的空间。
- 长尾延迟:若某个连接的 I/O 操作耗时较长(如网络延迟),会导致其他连接的请求排队,整体吞吐量下降。
3. Reactor 模式
Reactor 模式的核心思想是 将 事件的 监听 与 处理 分离。
3.1 单 Reactor 单线程 I/O 模型
3.1.1 模型示意图
说明:Reactor 监控客户端请求事件,收到事件后进行判断。如果是 Accept 事件(建立连接请求事件),则由 Acceptor 处理,然后创建一个 Handler 处理连接完成后的后续业务处理;否则由 Dispatcher 将请求事件分发给 Channel 对应的 Handler(一个 Channel 对应一个 Handler),由这个 Handler 处理事件。整个过程在 单线程 中完成。
3.1.2 优点
- 资源消耗低:单线程处理所有 I/O 和业务,无需线程切换,内存占用少。
- 无锁设计:天然线程安全,避免多线程竞争问题。
3.1.3 缺点
- 性能瓶颈:所有操作均在单线程内完成,若某次操作耗时较长,会拖累整个系统响应。
3.2 单 Reactor 多线程 I/O 模型
3.2.1 模型示意图
说明:单 Reactor 多线程 I/O 模型 是 单 Reactor 单线程 I/O 模型 的演进,核心改进在于 Handler 不需要再处理耗时的业务逻辑,而是将其提交给 Worker 线程池,Handler 只需要处理 I/O 事件即可。
3.2.2 优点
- 保持 I/O 高效性:Reactor 单线程处理非阻塞 I/O,避免多线程竞争锁的问题。
- 提升吞吐量:耗时业务由线程池并行处理,避免阻塞 Reactor 线程。
3.2.3 缺点
- 线程安全问题:需确保业务逻辑无共享状态,或通过线程安全数据结构(如队列)传递结果。
- 上下文切换开销:线程池规模过大会增加 CPU 调度成本。
3.3 主从 Reactor 多线程 I/O 模型
3.3.1 模型示意图
说明:在这个模型中,主 Reactor(通常只有一个)负责监听并处理 Accept 事件,处理完毕后,将连接的 Channel 注册到从 Reactor(可以有多个)中,然后从 Reactor 就可以监听这个 Channel 的 I/O 事件。当从 Reactor 监听到 Channel 的 I/O 事件后,将其通过 Dispatcher 分发给 Channel 对应的 Handler,Handler 将耗时的业务逻辑处理交给 Worker 线程池进行处理。
3.3.2 优点
- 高性能:
- 主 Reactor 仅处理连接建立,避免高并发下的 Accept 瓶颈。
- 从 Reactor 多线程并行处理 I/O,充分利用多核 CPU。
- 弹性扩展:从 Reactor 数量和线程池规模可按需调整。
- 资源隔离:从 Reactor 之间无共享状态,避免锁竞争。
3.3.3 缺点
- 复杂度极高:需管理主从 Reactor 协作、负载均衡、线程间通信。
- 上下文切换开销:从 Reactor 线程过多可能导致 CPU 调度开销。
4. Redis 使用的 I/O 线程模型
4.1 Redis 的核心模型
Redis 的核心采用 单 Reactor 单线程模型:
- Reactor 角色:主线程作为唯一的 Reactor,负责监听所有客户端连接的事件(如连接建立、读写请求)。
- 线程模型:所有 网络请求 和 命令处理 均在主线程中串行执行,避免了线程切换开销。
这种设计适合 Redis 的内存数据库特性,因为命令执行速度极快(内存操作),单线程不会成为瓶颈。
4.2 Redis 6.0 后的优化:多线程 I/O 处理
对于 Redis 来说,内存操作速度极快,性能瓶颈只能是 网络 I/O 了。Redis 从 6.0 版本开始引入多线程处理网络 I/O,但多线程仅用于网络 I/O,命令处理仍在主线程中串行执行。
- I/O 线程池:通过配置
io-threads
参数,主线程将网络 I/O 任务(如读取请求、发送响应)分配给多个子线程并行处理。 - Reactor 模型变化:主线程仍负责事件分发,但部分 I/O 操作由多线程分担,属于 单 Reactor 多线程模型 的变种。
5. Netty 使用的 I/O 线程模型
Netty 使用的 I/O 线程模型是 主从 Reactor 多线程 I/O 模型,角色划分如下:
5.1 主 Reactor——BossGroup
- 职责:
- 接收客户端连接:监听服务器端口,处理
OP_ACCEPT
事件(连接建立事件)。 - 分配连接到 Worker Group:将新建立的连接注册到 WorkerGroup 中的某个线程。
- 接收客户端连接:监听服务器端口,处理
- 实现类:
NioEventLoopGroup
。 - 线程数:通过构造函数
NioEventLoopGroup(1)
显式设置,线程数为1
。
注意:通常根据服务器性能调整,但无需过多线程,因为连接建立是轻量级操作。
5.2 从 Reactor——WorkerGroup
- 职责:
- 处理已连接通道的读写事件:监听
OP_READ/OP_WRITE
事件,完成数据的读取和发送。 - 分发事件到
ChannelHandler
:将读取的数据传递给用户自定义的ChannelPipeline
中的处理器链。
- 处理已连接通道的读写事件:监听
- 实现类:
NioEventLoopGroup
。 - 线程数:对于 I/O 密集型任务,为了充分利用多核 CPU,通常将线程数设置为
Runtime.getRuntime().availableProcessors() * 2
,这个也是NioEventLoopGroup
的空参构造器的默认线程数。
注意:虽然对于 I/O 密集型任务可以将线程数设置为 CPU 核数的 2 倍,但实际上为了发挥服务器的最大性能,仍 需要通过压力测试来得到性能最优的线程数。
5.3 Worker 线程池(可选)
- 职责:处理阻塞业务逻辑,若业务逻辑涉及数据库查询、文件读写等阻塞操作,将任务提交到线程池,避免阻塞 WorkerGroup 中的线程。
- 实现类:通常使用
DefaultEventExecutorGroup
或ThreadPoolExecutor
。
注意:只有在
ChannelInitialzer
初始化通道时给ChannelHandler
设置了别的线程池,ChannelHandler
处理任务时才会使用其他线程(如下代码所示),否则ChannelHandler
使用的是 WorkerGourp 的线程。如果ChannelHandler
处理任务需要花费大量的时间,可能会导致 WorkerGroup 的线程无法处理其他 I/O 事件。所以 对于耗时较长的ChannelHandler
,在初始化通道时需要给它设置线程池。
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 给 ChannelHandler 的实现类 MyServerHandler 设置线程池
ch.pipeline().addLast(new DefaultEventExecutorGroup(10), new MyServerHandler());
}
6. 总结
早期的 I/O 线程模型是 传统阻塞 I/O 模型,每个客户端的连接对应一个线程,这个线程需要监听并处理这个客户端的 I/O 事件。在客户端很多的情况下,大量申请线程资源可能会导致内存溢出 OOM。
之后出现了 Reactor 模式,核心思想是 分离事件的监听与处理,使用一个线程监听多个客户端连接的 I/O 事件,监听到之后将其分发到对应的 Handler,让这个 Handler 进行处理。
Reactor 模式 最简单的实现方式是 单 Reactor 单线程模型,它的缺点是 Reactor 线程还需要处理业务逻辑,可能导致其他客户端连接的 I/O 事件无法被及时处理。
对 单 Reactor 单线程模型 进行优化,于是就得到了 单 Reactor 多线程模型,这种模型外置了一个 Worker 线程池,用于处理业务逻辑。当 Worker 线程处理完后,Handler 再将数据返回。
随着互联网民的增多,单 Reactor 多线程模型 已经无法处理超多客户端连接的场景,此时就需要使用 主从 Reactor 多线程模型。主 Reactor 负责监听并处理客户端的连接事件,一旦连接成功,就将这个连接注册到某个从 Reactor 上。多个从 Reactor 负责监听客户端的 I/O 事件,如果有 I/O 事件,则会将其分发给对应的 Handler,Handler 只需要处理 I/O 事件。至于耗时很长的业务逻辑,可以委托给 Worker 线程池。
Redis 是一个高性能的缓存数据库,它使用的是 单 Reactor 单线程模型。由于内存操作是极快的,网络 I/O 成为了性能的瓶颈,所以 Redis 6.0 引入了多线程来处理网络 I/O 事件,核心的命令处理仍只能被主线程串行执行。
Netty 是 Java 的一个高性能网络框架,它采用了 主从 Reactor 模型,有着 BossGroup 处理连接事件,WorkerGroup 处理 I/O 事件的设计,如果有耗时较长的业务逻辑,可以交给额外的线程池处理。