1. Netty 线程模型概述
1.1 为什么需要 EventLoop / EventLoopGroup
问题背景:
-
传统阻塞 I/O(BIO)一连接一线程,线程上下文切换与阻塞导致吞吐低、内存/FD 占用高。
-
JDK NIO 引入非阻塞
SelectableChannel
与Selector
,但编写正确高效的多路复用循环仍不易(跨平台差异、空轮询、任务与 I/O 的编排)。
Netty 的答案:
-
EventLoop:抽象出“事件循环线程”,一个
EventLoop
固定绑定若干Channel
,串行处理它们的 I/O 事件、用户任务与定时任务。 -
EventLoopGroup:
EventLoop
的集合,提供线程的生命周期管理与负载分配(如next()
选择一个EventLoop
)。 -
目标:在不牺牲单线程串行安全语义的前提下,把连接分布到多个
EventLoop
,从而水平扩展吞吐。
类比:一个仓库有多条分拣线(EventLoop),每条分拣线上的包裹(Channel)只在这条线上处理,避免“多人同时拆同一包裹”的并发问题。多开几条线(EventLoopGroup 的规模)即可扩容。
1.2 核心设计目标与哲学
Netty 的设计哲学可概括为:事件驱动 + 非阻塞 I/O + 线程亲和(affinity) + 串行化保证:
-
事件驱动:I/O 就绪、用户提交任务、定时器触发皆视为事件,驱动状态机前进。
-
非阻塞:尽量避免阻塞系统调用;在 Linux 上使用
epoll
(EpollEventLoop
),跨平台使用Selector
(NioEventLoop
)。 -
线程亲和:一个
Channel
自始至终交给一个EventLoop
处理,提升缓存命中率、降低锁竞争。 -
串行化:Pipeline 中
ChannelHandler
的channelRead()
、write()
、flush()
等回调由同一EventLoop
顺序执行,默认无需额外同步。 -
可配置:
EventLoopGroup
的大小、选择器实现(NIO/Epoll/KQueue)、任务队列策略等均可按场景调优。
1.3 与 JDK NIO 的关系:Selector、Channel 与事件驱动
-
SelectableChannel
:如SocketChannel
、ServerSocketChannel
,可配置非阻塞模式。 -
Selector
:注册多个 Channel 的关注事件(OP_ACCEPT、OP_READ、OP_WRITE、OP_CONNECT),通过select()
/selectNow()
找到就绪键集合。 -
Netty 封装:
-
NioEventLoop
持有一个Selector
,在其run()
循环中执行 select-处理-执行任务的三段式逻辑。 -
Netty 对
SelectionKey
做了优化(如SelectedSelectionKeySet
),减少遍历与装箱开销。 -
通过
ChannelPipeline
把 I/O 事件转化为 handler 调用,统一“事件驱动”编程范式。
-
记忆要点:JDK NIO 是基础设施,Netty 是在其之上提供更高层次的事件调度与编解码框架,并处理了大量坑位(如空轮询 bug 的规避、唤醒策略、任务与 I/O 的协作)。
1.4 Netty 线程模型家族与演进
-
OIO(旧称 BIO):阻塞 I/O,已不推荐。
-
NIO:跨平台,默认使用
Selector
;核心类:NioEventLoop
、NioEventLoopGroup
。 -
Epoll(Linux):基于
epoll
的本地实现,核心类:EpollEventLoop
、EpollEventLoopGroup
,性能通常优于 NIO。 -
KQueue(macOS/BSD):
KQueueEventLoop
系列。 -
线程模型:
-
单线程模型:一个
EventLoop
处理所有连接(教育/测试环境可行)。 -
多线程模型:多个
EventLoop
并行处理不同连接。 -
主从 Reactor(Boss/Worker):一个(或若干)
EventLoop
专管OP_ACCEPT
(Boss),其余EventLoop
处理读写(Worker)。Netty 默认ServerBootstrap
采用该模型。
-
后续第 6 章将系统对比三种模型的优缺点与适用场景。
1.5 组件角色速览:Channel、Pipeline、EventLoop、EventLoopGroup
-
Channel:面向连接(或面向数据报)的抽象,承载 I/O 操作;
NioSocketChannel
/NioServerSocketChannel
等。 -
ChannelPipeline:Handler 链,负责事件在入站/出站方向上的传播。
-
EventLoop:单线程事件循环,负责驱动某个子集 Channel 的 I/O、用户任务与定时任务。
-
EventLoopGroup:EventLoop 的集合与管理者,提供
next()
分配与线程生命周期管理。
关键关系:
-
一个
Channel
在注册到EventLoop
后,生命周期内始终绑定同一个EventLoop
; -
ChannelPipeline
的回调在同一 EventLoop 线程里串行调用; -
EventLoopGroup#next()
用于选择一个EventLoop
为新连接服务(Worker 侧)。
1.6 一眼看懂:Boss/Worker 与单线程串行化的保障
-
Boss:接受新连接(
OP_ACCEPT
),把SocketChannel
注册到某个 WorkerEventLoop
。 -
Worker:处理
OP_READ/OP_WRITE/OP_CONNECT
,执行业务ChannelHandler
。 -
串行化保障:Worker 内的所有 handler 回调与用户提交任务(
eventLoop.execute(...)
)都在同一条线程执行,避免并发冲突。
思考:为什么 Netty 强调“不要在 EventLoop 里做阻塞或耗时任务”?
因为阻塞该线程 = 阻塞该线程上所有连接的事件处理,形成全局排队,吞吐立刻塌陷。正确做法见第 8、9 章。
1.7 “Hello, EventLoopGroup”:最小可运行示例(Netty 4.1.28.Final)
目标:
1)创建 Boss/Worker 两个NioEventLoopGroup
;
2)绑定端口,接收连接;
3)在channelRead
中回应,并演示向EventLoop
提交普通任务与定时任务。
// 版本:Netty 4.1.28.Final
// 文件:QuickStartServer.java
// 功能:最小化的 Echo 服务器,演示 EventLoopGroup 的使用与任务提交。
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
import java.util.concurrent.TimeUnit;
public class QuickStartServer {
public static void main(String[] args) throws InterruptedException {
// 1) Boss 只处理 OP_ACCEPT;Worker 处理读写与业务逻辑
// 常见配置:boss 1 线程,worker = CPU * 2(默认值)
EventLoopGroup boss = new NioEventLoopGroup(1); // Boss
EventLoopGroup worker = new NioEventLoopGroup(); // Worker(默认 = processors * 2)
try {
ServerBootstrap b = new ServerBootstrap();
b.group(boss, worker)
.channel(NioServerSocketChannel.class)
// childHandler 作用于每个被接受的 SocketChannel
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
// 打印读取线程,验证“同一连接固定在同一 EventLoop 线程”
System.out.println("channelRead thread = " + Thread.currentThread().getName()
+ ", from " + ctx.channel().remoteAddress());
// 业务:回显
ctx.write(msg.retainedDuplicate());
// 2) 演示向当前连接的 EventLoop 提交“普通任务”
// 注意:该 execute() 任务与 pipeline 回调一样,仍在同一 EventLoop 线程执行
ctx.channel().eventLoop().execute(() -> {
System.out.println("execute() running in " + Thread.currentThread().getName());
});
// 3) 演示定时任务:1 秒后发送一条消息
ctx.channel().eventLoop().schedule(() -> {
ByteBuf buf = Unpooled.copiedBuffer("timer tick\n", CharsetUtil.UTF_8);
ctx.writeAndFlush(buf);
}, 1, TimeUnit.SECONDS);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
});
}
})
// 一些常见的 server 选项(此处简化)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 4) 端口绑定
ChannelFuture f = b.bind(8080).sync();
System.out.println("Server started on 0.0.0.0:8080");
// 5) 关闭钩子
f.channel().closeFuture().sync();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
运行结果(示例):
Server started on 0.0.0.0:8080
channelRead thread = nioEventLoopGroup-3-1, from /127.0.0.1:60234
execute() running in nioEventLoopGroup-3-1
-
观察点:
channelRead
与execute()
的输出线程名一致,说明普通任务与 I/O 回调在同一 EventLoop 线程执行。 -
客户端在 1 秒后收到
timer tick
,验证了schedule()
定时任务正常触发。
小贴士:把
worker
的线程数改为 1,再用多个客户端并发连接,你会看到所有连接共享同一 EventLoop 线程,这非常直观地展示了“单线程串行化”的特性。
1.8 线程安全语义与可见性保证(inEventLoop 的意义)
-
EventExecutor#inEventLoop()
:判断当前线程是否就是该EventLoop
的线程。常用于:
1)若当前就在EventLoop
线程,直接执行任务(避免多余的队列入队与唤醒);
2)若不在,使用execute()
把任务安全地切换到EventLoop
线程执行。 -
为何重要:
-
保证有序:所有对
ChannelPipeline
的操作在同一线程顺序执行; -
避免锁:大多数 handler 内无需显式同步;
-
可见性:由于在同一线程上顺序执行,内存可见性由 Java 线程语义与任务发布(happens-before)保障,无需显式
volatile
/锁(除非跨线程共享状态)。
-
示例:在业务线程(非 EventLoop)里安全写入 Channel。
// 版本:Netty 4.1.28.Final
public static void safeWrite(Channel ch, Object msg) {
EventLoop loop = ch.eventLoop();
if (loop.inEventLoop()) {
ch.writeAndFlush(msg); // 已在 EventLoop 线程,直接执行
} else {
loop.execute(() -> ch.writeAndFlush(msg)); // 切换到 EventLoop 线程
}
}
规则:凡是可能触发 Pipeline 回调或访问 Channel 内部状态的操作,都应在 EventLoop 线程内进行。这几乎是使用 Netty 的第一守则。
1.9 常见误区速查
-
在 EventLoop 里做阻塞/大计算
-
现象:吞吐突降、RT 尖刺、延迟抖动加剧。
-
解决:将耗时操作(DB、RPC、IO、压缩/加密)放到业务线程池,结果再
execute()
回到 EventLoop 更新状态或写回客户端。
-
-
跨线程直接操作 Channel / Pipeline
-
现象:偶发并发 bug、
IllegalStateException
。 -
解决:用
inEventLoop()
+execute()
切换。
-
-
错误配置线程数
-
现象:过多线程导致上下文切换与竞争;过少导致队列排队。
-
经验:以
CPU核心数 * 2
为基准,依据 I/O 密集程度与 tail latency 再微调(详见第 8 章)。
-
-
忽略定时任务的成本
-
现象:大量
schedule()
导致时间轮/优先队列膨胀;run()
循环被任务处理占据,I/O 延迟升高。 -
解决:合并定时任务、降频、将周期性任务移出 EventLoop(如专门的调度器)。
-
-
把“线程池”当作 EventLoop 的等价物
-
事实:
EventLoop
≠ 通用线程池。EventLoop 有固定线程亲和、I/O 感知与任务顺序保证;JDK 线程池不具备这些特性(见第 10 章详解)。
-
1.10 小结与自测
本章要点
-
EventLoop = “事件循环 + 固定线程 + Channel 绑定 + 串行化处理”;
-
EventLoopGroup = “EventLoop 的集合与生命周期管理者”,负责分配与伸缩;
-
Boss/Worker 主从 Reactor 是默认的服务端结构;
-
inEventLoop()
是安全切换与避免不必要队列化的关键; -
切忌在 EventLoop 里执行阻塞/重 CPU 的任务。
2. EventLoop 的核心原理与生命周期
2.1 EventLoop 的定位与继承体系
Netty 中的 EventLoop
不是孤立设计,而是嵌套在一系列抽象接口里,逐层扩展功能:
// Netty 4.1.28.Final 源码片段
public interface EventExecutor extends ScheduledExecutorService, EventExecutorGroup {
EventExecutor next();
EventExecutorGroup parent();
boolean inEventLoop();
boolean inEventLoop(Thread thread);
<E extends EventExecutor> Set<E> children();
}
public interface EventLoop extends OrderedEventExecutor, EventLoopGroup {
@Override
EventLoopGroup parent();
@Override
EventLoop next();
ChannelFuture register(Channel channel);
ChannelFuture register(ChannelPromise promise);
ChannelFuture register(Channel channel, ChannelPromise promise);
}
-
关键点:
-
EventExecutor
= 带定时任务的执行器(扩展了ScheduledExecutorService
)。 -
EventLoop
=EventExecutor
+EventLoopGroup
,既能当执行器,又能当分组里的一个成员。 -
register()
系列方法:将 Channel 绑定到这个 EventLoop。
-
类比:EventLoop 像是“既是员工也是班组长”。它自己就是一个线程执行体(员工),同时作为
EventLoopGroup
的一部分(班组)。
2.2 生命周期的四个阶段
一个 EventLoop
的生命周期,可以拆解为 创建 → 运行 → 提交任务 → 关闭 四个阶段:
-
创建
-
由
EventLoopGroup
初始化,分配一个线程,持有Selector
(NIO)或epoll fd
(Epoll)。 -
初始化任务队列与定时任务队列。
-
-
运行(
run()
循环)-
不断轮询 I/O 事件(
select()
/epoll_wait()
)。 -
处理就绪事件 → 调用 Pipeline → Handler 回调。
-
执行任务队列中的普通任务与定时任务。
-
-
提交任务
-
用户线程调用
eventLoop.execute(Runnable)
。 -
若当前线程就是 EventLoop 线程 → 直接执行;否则 → 放入队列,唤醒 Selector。
-
-
关闭
-
调用
shutdownGracefully()
,进入优雅关闭状态。 -
等待任务与定时任务完成,再释放资源(Selector、线程)。
-
2.3 SingleThreadEventLoop:核心实现
SingleThreadEventLoop
是 EventLoop 的抽象基类,它规定了 任务队列 与 线程单一性。
源码关键(简化后):
// Netty 4.1.28.Final
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor
implements EventExecutor {
private volatile Thread thread;
private final Queue<Runnable> taskQueue;
@Override
public void execute(Runnable task) {
if (task == null) throw new NullPointerException("task");
addTask(task);
if (!inEventLoop()) {
startThread(); // 如果线程未启动则启动
}
}
private void addTask(Runnable task) {
taskQueue.add(task);
}
protected boolean inEventLoop() {
return Thread.currentThread() == thread;
}
// runAllTasks() 在事件循环中被调用
protected boolean runAllTasks() {
Runnable task = taskQueue.poll();
while (task != null) {
task.run();
task = taskQueue.poll();
}
return true;
}
}
-
要点:
-
单线程:
thread
字段标记当前唯一 EventLoop 线程。 -
任务队列:
taskQueue
存储Runnable
,包括用户任务与部分系统任务。 -
execute():如果调用线程不是 EventLoop 线程,就将任务入队,并唤醒 EventLoop。
-
runAllTasks():由
run()
循环调用,逐个执行队列里的任务。
-
2.4 NioEventLoop 的 run()
主循环
NioEventLoop
是最常用的实现,它继承自 SingleThreadEventLoop
。核心是 run()
方法:
// Netty 4.1.28.Final (节选)
@Override
protected void run() {
for (;;) {
try {
int ready = selector.select();
if (ready > 0) {
processSelectedKeys();
}
// 处理普通任务 + 定时任务
runAllTasks();
} catch (Throwable t) {
handleLoopException(t);
}
if (isShuttingDown()) {
closeAll();
break;
}
}
}
-
结构:I/O 轮询 → 处理就绪 key → 执行任务队列 → 检查关闭状态。
-
关键点:
-
selector.select()
可能被唤醒(因为有任务提交)。 -
processSelectedKeys()
调用 Channel 的unsafe.read()
等方法,最终触发 Handler。 -
runAllTasks()
负责执行用户提交任务。
-
这就是 Netty 的“事件循环”:一条线程,不停轮询 I/O,就绪时分发事件,同时顺带执行任务队列。
2.5 生命周期示例:打印 EventLoop 的启动与关闭
代码演示(基于 Netty 4.1.28.Final):
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
public class EventLoopLifeCycle {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup(1);
group.execute(() -> {
System.out.println("Task running in: " + Thread.currentThread().getName());
});
Thread.sleep(1000);
// 优雅关闭
group.shutdownGracefully().sync();
System.out.println("EventLoopGroup shutdown complete.");
}
}
输出示例:
Task running in: nioEventLoopGroup-2-1
EventLoopGroup shutdown complete.
说明:
-
execute()
提交任务时,EventLoop 线程会被懒加载启动。 -
shutdownGracefully()
会等待任务结束,确保安全退出。
2.6 本章小结
-
EventLoop 的本质 = 单线程 + 无限循环 + I/O 多路复用 + 任务队列。
-
任务执行模型:
-
I/O 事件 → Pipeline 回调。
-
用户任务 → 入队 → 在 EventLoop 线程串行执行。
-
定时任务 → 时间轮或优先队列调度。
-
-
生命周期四阶段:创建、运行、任务提交、关闭。
-
核心类:
SingleThreadEventLoop
、NioEventLoop
。
第 3 章:EventLoopGroup 的实现机制与线程池管理
3.1 EventLoopGroup 的职责
在 Netty 的抽象中:
-
EventLoop:单线程事件循环,负责处理某个 Channel 上的所有 I/O 事件。
-
EventLoopGroup:多个 EventLoop 的集合,负责将 Channel 分配给某个 EventLoop,并统一管理线程池。
这样做的好处是:
-
单线程保证了线程安全:一个 Channel 始终与一个 EventLoop 绑定,避免了并发读写问题。
-
多线程保证了并发处理能力:EventLoopGroup 内部维护多个 EventLoop,可以并行处理多个 Channel 的 I/O 事件。
3.2 MultithreadEventLoopGroup 的结构
Netty 中常见的 EventLoopGroup(如 NioEventLoopGroup、EpollEventLoopGroup)都继承自 MultithreadEventLoopGroup。
核心逻辑如下:
public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup
implements EventLoopGroup {
public MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads, executor, args);
}
}
这里有两个关键点:
-
MultithreadEventExecutorGroup 是真正管理线程和分配逻辑的父类。
-
EventLoopGroup 只是接口,定义了对外的方法(如
next()
、register(Channel)
)。
因此,MultithreadEventExecutorGroup 是理解线程池机制的核心。
3.3 MultithreadEventExecutorGroup 的线程池设计
构造函数大致逻辑:
public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {
private final EventExecutor[] children; // 保存多个 EventLoop
private final AtomicInteger childIndex = new AtomicInteger();
protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i++) {
children[i] = newChild(executor, args); // 创建每个 EventLoop
}
}
}
关键要点:
-
children 数组:保存多个 EventLoop。
-
newChild:工厂方法,由子类实现,决定创建哪种 EventLoop(如
NioEventLoop
)。 -
executor:线程池,保证每个 EventLoop 在单独线程中运行。
3.4 Channel 与 EventLoop 的绑定
当一个 Channel 注册到 EventLoopGroup 时,Netty 会调用:
EventLoop nextEventLoop = eventLoopGroup.next();
nextEventLoop.register(channel);
-
next() 负责选择一个 EventLoop。
-
register() 将 Channel 绑定到该 EventLoop,之后所有 I/O 都由该 EventLoop 的线程处理。
3.5 next() 的轮询算法
最关键的逻辑在于 next()
,它决定了 哪个线程处理新的 Channel。
源码如下:
@Override
public EventExecutor next() {
return chooser.next();
}
3.5.1 chooser 的初始化
chooser
是一个 EventExecutorChooser,在构造函数里初始化:
chooser = chooserFactory.newChooser(children);
Netty 提供了不同的选择器实现:
-
PowerOfTwoEventExecutorChooser:当线程数是 2 的幂时,效率更高(位运算代替取模)。
-
GenericEventExecutorChooser:普通情况,用
%
取模实现轮询。
3.5.2 PowerOfTwoEventExecutorChooser
public final EventExecutor next() {
return children[idx.getAndIncrement() & children.length - 1];
}
-
idx.getAndIncrement()
:自增计数器。 -
& children.length - 1
:利用位运算快速取模,要求线程数是 2 的幂。
3.5.3 GenericEventExecutorChooser
public final EventExecutor next() {
return children[Math.abs(idx.getAndIncrement() % children.length)];
}
-
普通的
%
运算,保证轮询。
3.6 线程分配策略的特点
-
简单高效:采用轮询策略,保证负载均衡。
-
避免锁竞争:只用一个原子递增计数器,不需要复杂的并发控制。
-
固定绑定:一旦 Channel 分配给某个 EventLoop,就不会再切换,保证线程安全。
3.7 小结
-
EventLoopGroup 管理多个 EventLoop。
-
MultithreadEventLoopGroup 内部维护一个
children[]
数组保存 EventLoop。 -
next() 方法 通过
chooser
实现轮询算法,分配 Channel。 -
策略优化:如果线程数是 2 的幂,用位运算代替
%
,提升性能。
第四章:EventLoop 与 Channel 的绑定过程
4.1 背景与核心原则
在 Netty 的线程模型中,一个 Channel 在整个生命周期内,只能绑定到一个固定的 EventLoop。
这样做的原因是:
-
避免了跨线程的竞争和锁开销(线程安全由“单线程串行化”来保证)。
-
每个 Channel 的 IO 事件(read、write、connect、close 等)始终由同一个线程执行,保证了有序性。
-
提高性能:不需要在多个线程间切换上下文。
因此,Netty 在设计时就规定了:
-
Channel → EventLoop:一对一绑定
-
EventLoop → 多个 Channel:一对多绑定
4.2 Channel 注册流程概述
Channel 的注册是通过 AbstractBootstrap#initAndRegister()
完成的,大致过程如下:
-
创建 Channel 实例(例如 NioSocketChannel)。
-
调用
group().register(channel)
将该 Channel 注册到某个 EventLoopGroup。 -
EventLoopGroup 再从内部挑选一个 EventLoop,并调用
EventLoop.register(channel)
。 -
最终
Channel
与选定的EventLoop
绑定,并注册到 Selector(NIO 模型下)。
代码关键入口:
// AbstractBootstrap
final ChannelFuture initAndRegister() {
Channel channel = channelFactory.newChannel();
group().register(channel); // 交给 EventLoopGroup
return channel.newPromise();
}
4.3 EventLoopGroup 分配 EventLoop 的策略
当调用 group().register(channel)
时,实质上会进入 MultithreadEventLoopGroup
的 register()
:
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
关键点在 next()
—— 选择一个 EventLoop 来绑定 Channel。
-
采用 轮询(Round-Robin)算法:每次递增一个计数器,取模得到下一个 EventLoop。
-
确保负载在所有 EventLoop 线程之间相对均衡。
源码(简化):
private final AtomicInteger idx = new AtomicInteger();
@Override
public EventLoop next() {
return children[Math.abs(idx.getAndIncrement() % children.length)];
}
4.4 EventLoop 完成 Channel 的绑定
选定 EventLoop 后,执行 SingleThreadEventLoop.register(channel)
:
@Override
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
这里关键的动作是:
-
将 Channel 内部的
eventLoop
字段设置为当前 EventLoop。 -
确保 后续所有的 IO 事件分发,都通过这个 EventLoop 处理。
绑定代码片段:
@Override
public void register(ChannelPromise promise) {
channel.unsafe().register(this, promise);
}
再深入 AbstractUnsafe.register(...)
:
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
this.eventLoop = eventLoop; // ✅ 绑定完成
eventLoop.execute(() -> {
// 真正将 Channel 注册到 Selector
doRegister();
});
}
4.5 Channel 生命周期与 EventLoop 的绑定关系
一旦绑定完成:
-
Channel 的所有读写操作都通过该 EventLoop 的线程执行。
-
Pipeline 中的 Handler 回调也都会在该线程中被触发。
-
Channel 关闭后,EventLoop 不会立即解绑,直到资源释放才解除关联。
这种设计保证了:
-
线程安全性:避免 Channel 在多个线程中被并发操作。
-
顺序性:保证事件的触发顺序与实际 IO 顺序一致。
-
性能优化:单线程串行化处理,无需锁。
4.6 小结
-
绑定过程核心链路:
Bootstrap.initAndRegister → EventLoopGroup.register → EventLoop.next → EventLoop.register → AbstractUnsafe.register → Channel.eventLoop=xxx
。 -
设计原则:Channel 在生命周期内与 EventLoop 一对一绑定,EventLoop 负责处理其所有事件。
-
策略:轮询分配 EventLoop,保证负载均衡。
-
意义:实现线程安全、提升性能,并简化并发编程模型。
第五章:任务队列与定时任务的处理
在 Netty 的线程模型中,SingleThreadEventLoop 是核心执行单元,它既要负责 I/O 事件 的高效处理,又要支持 定时任务 与 普通任务 的调度。为了实现高性能与低延迟,EventLoop 在一个事件循环周期(Event Loop Cycle)中,会根据任务的性质和优先级,按照严格的顺序进行有序调度。
整个执行顺序大致可以概括为:
I/O 任务 → 定时任务 → 普通任务 → 尾部任务。
下面分别深入解析这四类任务的调度与执行机制。
1. I/O 任务优先:Selector 驱动的核心
I/O 任务通常包括 Channel 的读写事件、连接建立/断开、异常处理 等,由底层 Selector 统一管理。
-
触发条件:当底层网络 Socket 有数据可读、可写或状态变化时,Selector 会感知并唤醒。
-
执行时机:在事件循环的开始阶段,SingleThreadEventLoop 会优先检查 Selector 是否有待处理的 I/O 事件,如果有则立即执行。
-
优先级原因:I/O 任务直接关系到 网络通信的实时性,如果延迟处理,可能导致数据堆积、心跳超时等问题,因此它们的优先级最高。
执行顺序:
-
事件循环启动 → Selector 轮询
-
如果存在 I/O 事件 → 立即分派给对应的 ChannelPipeline 处理
-
完成后才会进入下一个任务阶段
2. 定时任务(scheduledTaskQueue):按时间点调度
为了支持 延时执行 或 周期性任务,Netty 提供了 scheduledTaskQueue
来存放定时任务。
-
数据结构:一般是一个 最小堆(min-heap),按照任务的 到期时间 排序。
-
执行时机:在处理完 I/O 任务后,EventLoop 会检查队列中是否有已经到期的任务,如果到期则立即执行。
-
优先级定位:高于普通任务,但低于 I/O 任务。
执行顺序:
-
检查
scheduledTaskQueue
-
取出所有到期的定时任务(可能有多个)
-
按照时间顺序依次执行,直到没有到期任务
这种机制保证了 定时任务的时效性,同时不会打断 I/O 事件的优先响应。
3. 普通任务(taskQueue):FIFO 顺序处理
普通任务主要来自用户逻辑,例如:
-
提交到 EventLoop 的 Runnable/Callable
-
ChannelPipeline 中的业务处理回调
-
用户主动调用的
eventLoop.execute()
方法 -
存储位置:
taskQueue
(一个无界或有界的 FIFO 队列)。 -
执行时机:在 I/O 任务和定时任务之后,按顺序执行。
-
执行策略:
-
普通任务是 FIFO 顺序 执行的,保证提交的顺序性。
-
为避免 I/O 任务被长时间阻塞,Netty 提供了 ioRatio 配置,默认情况下 I/O 与普通任务会按照 50:50 的比例分配 CPU 时间。
-
执行顺序:
-
检查
taskQueue
-
按 FIFO 出队
-
执行任务,直到队列为空或达到 ioRatio 限制
4. 尾部任务(tailTasks):循环结束时的收尾工作
Netty 还设计了一类 尾部任务(tailTasks),它们通常是轻量级的收尾逻辑,例如:
-
状态清理
-
执行一些回调钩子
-
异步操作的最后确认
-
存储方式:通常在
tailTasks
队列中 -
执行时机:在当前事件循环的 最后阶段 执行
-
执行要求:必须是非阻塞、快速完成的逻辑,否则会影响下一个事件循环
执行顺序:
-
所有 I/O、定时任务、普通任务执行完成
-
执行 tailTasks
-
本轮事件循环结束,进入下一轮
5 ioRatio 的深度说明(如何计算 & 如何重设)
1)ioRatio 是什么?(语义 & 默认值)
ioRatio
表示 期望在一次事件循环周期内,EventLoop 用于 I/O 的时间占比(百分数)。Netty 会据此“给普通任务分配时间预算”,从而避免业务任务把 I/O 饿死。
官方文档说明:默认值为 50,取值范围 1~100;设置为 100 会关闭配额限制(即不再限制普通任务的执行时长)。
直观理解:
ioRatio = 50
→ 理想上 I/O : 非 I/O ≈ 1 : 1
ioRatio = 80
→ 理想上 I/O : 非 I/O ≈ 4 : 1(更偏向 I/O)
ioRatio = 100
→ 不限制非 I/O(配额逻辑被禁用),仅受队列里有无任务影响。
2)Netty 内部如何“计算”非 I/O 任务的时间预算?
下面是 NioEventLoop(Netty 4.1.x)内部的时间配额逻辑(等价伪代码,便于你在脑中建立模型):
// 伪代码,等价于 Netty 4.1.x 的 run() 主循环片段(以 4.1.28.Final 思路为例)
for (;;) {
// 1)处理 I/O(含 select + processSelectedKeys)
long ioStart = System.nanoTime();
selectAndProcessIO(); // 轮询 + 分发 I/O 事件
long ioTime = System.nanoTime() - ioStart;
// 2)计算普通任务(含定时任务已到期部分)的可用时间预算
if (ioRatio == 100) {
runAllTasksUnbounded(); // 不限时:直接把队列里的任务都跑了
} else {
long nonIoBudgetNanos = ioTime * (100 - ioRatio) / ioRatio;
runAllTasksWithDeadline(nonIoBudgetNanos); // 在预算内尽量多跑一些任务,不够就下轮
}
// 尾部任务(tailTasks)通常在本轮末尾轻量收尾
}
关键公式:
nonIoBudgetNanos = ioTime * (100 - ioRatio) / ioRatio
含义:普通任务的“本轮可用时间” 会被限制到与 ioRatio
匹配的比例。例如:
-
若本轮 I/O 花了
ioTime = 5ms
,ioRatio = 50
,则普通任务预算= 5ms * (50/50) = 5ms
; -
若
ioRatio = 80
,同样ioTime = 5ms
,预算= 5ms * (20/80) = 1.25ms
(更偏向 I/O); -
若
ioRatio = 100
,配额禁用,普通任务不限时(这一行为在官方讨论中也被明确解释过)。
注意:定时任务(到期的部分)也算进“非 I/O”执行阶段,只是定时任务会先于普通任务被处理(同一阶段内部再按到期时间优先)。配额限制的目标是“别让非 I/O 过重,拖慢下次 I/O 轮询”。
第六章:Reactor 线程模型的实现(单线程、多线程、主从模型)
基于 Netty 4.1.28.Final
本章目标:把 Reactor 模式 在 Netty 中的三种典型落地形态讲透:单线程、多线程、主从(Master–Slave)。我们不仅讲概念,还会穿插 源码落点、可运行示例、线程绑定与调度路径、适用场景与性能取舍,并给出“如何选型”的实战建议。
6.1 Reactor 模式回顾与在 Netty 的映射
Reactor 模式的本质是“事件多路复用(Demultiplexing)+ 事件分发(Dispatching)”。在 JDK NIO 下,它体现在 Selector
负责就绪事件的收集,而 事件循环线程 负责把就绪事件分发到各个 ChannelPipeline
/ ChannelHandler
。
Netty 映射:
-
多路复用器:
Selector
(或 Linux 下epoll
,对应EpollEventLoopGroup
)。 -
事件循环:
EventLoop
(单线程),持续select → process → runAllTasks
。 -
分发目标:
ChannelPipeline
→ChannelHandler#channelRead/…
。 -
线程绑定:一个
Channel
只绑定到一个EventLoop
,串行处理,避免锁。
接下来,我们把三种落地形态逐个拆开。
6.2 单线程 Reactor:一把梭的极简实现
6.2.1 原理与数据流
拓扑:一个 NioEventLoopGroup(1)
同时负责 接受新连接(Accept) 与 已建立连接的 I/O(Read/Write),所有工作在 一条线程 上串行完成。
数据流:
-
ServerSocketChannel
被注册到同一个 EventLoop → 负责 Accept。 -
新的
SocketChannel
也绑定到同一个 EventLoop → 负责 Read/Write。 -
业务任务(
execute()
/schedule()
)也在这条线程执行。
优点:实现简单、上下文切换为零、单核资源利用率较高。
缺点:吞吐与延迟强烈受 单线程 约束,一个阻塞点即可放大风险。
6.2.2 关键源码位置(Netty 4.1.28.Final)
-
ServerBootstrap#group(EventLoopGroup group)
:把同一个组设为bossGroup
和workerGroup
。 -
MultithreadEventLoopGroup#next()
:Channel
分配到 EventLoop(这里只有 1 个)。
6.2.3 可运行示例(Java / Netty 4.1.28.Final)
// 基于 Netty 4.1.28.Final
public final class SingleReactorEchoServer {
public static void main(String[] args) throws InterruptedException {
// 单线程 EventLoopGroup:Accept + Read/Write 全在这一条线程
NioEventLoopGroup group = new NioEventLoopGroup(1);
try {
ServerBootstrap b = new ServerBootstrap()
.group(group) // == group(group, group)
.channel(io.netty.channel.socket.nio.NioServerSocketChannel.class)
.localAddress(8080)
.childHandler(new ChannelInitializer<io.netty.channel.socket.SocketChannel>() {
@Override
protected void initChannel(io.netty.channel.socket.SocketChannel ch) {
ch.pipeline().addLast("echo", new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
// 回显
ctx.writeAndFlush(msg.retain());
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("[single] active on " +
Thread.currentThread().getName());
}
});
}
})
// 减轻“长时间阻塞”风险的水位线配置(可选)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK,
new WriteBufferWaterMark(32 * 1024, 64 * 1024));
ChannelFuture f = b.bind().sync();
System.out.println("Single-reactor echo server started at 8080, thread = 1");
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
6.2.4 运行日志示例 & 适用场景
示例日志(可能输出类似):
Single-reactor echo server started at 8080, thread = 1
[single] active on nioEventLoopGroup-2-1
适用:
-
资源受限、连接数不高(<1k)、消息短小的嵌入式/边缘节点。
-
Demo、功能验证、教学环境。
不适用:
-
高并发、突发流量、长耗时业务逻辑(任何阻塞都会“卡死全局”)。
6.3 多线程 Reactor:单组多 EventLoop 的横向扩展
6.3.1 原理与数据流
拓扑:一个 NioEventLoopGroup(N)
同时承担 Accept 与 I/O,但组内有 N 条 EventLoop 线程。
-
ServerSocketChannel
绑定到其中 一个 EventLoop(用于 Accept)。 -
新建的
SocketChannel
会通过childGroup.next()
(此处同组)轮询分配到不同的 EventLoop,形成并行处理能力。 -
所有业务任务仍在各自 Channel 所属的 EventLoop 内 串行 执行,互不干扰。
6.3.2 关键源码位置
-
ServerBootstrap#group(EventLoopGroup group)
(单参重载):把 同一组 作为 Boss 与 Worker。 -
MultithreadEventExecutorGroup#children[]
:保存所有 EventLoop。 -
EventExecutorChooser
:next()
轮询选择 EventLoop(2 的幂使用位运算)。
6.3.3 可运行示例(Java / Netty 4.1.28.Final)
// 基于 Netty 4.1.28.Final
public final class MultiReactorEchoServerOneGroup {
public static void main(String[] args) throws InterruptedException {
// 使用默认线程数(通常是 2 * CPU 核心)
NioEventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap()
.group(group) // 一个组既做 boss 又做 worker
.channel(NioServerSocketChannel.class)
.localAddress(8081)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast("log", new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast("echo", new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
ctx.writeAndFlush(msg.retain());
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("[multi-one] active on " +
Thread.currentThread().getName());
}
});
}
})
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.TCP_NODELAY, true);
ChannelFuture f = b.bind().sync();
System.out.println("Multi-reactor (one group) started at 8081, threads = default");
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
运行日志示例(可能输出多个不同的 EventLoop 线程名):
Multi-reactor (one group) started at 8081, threads = default
[multi-one] active on nioEventLoopGroup-3-1
[multi-one] active on nioEventLoopGroup-3-3
[multi-one] active on nioEventLoopGroup-3-2
6.3.4 性能特点与踩坑
-
优点:简单;单组共享线程,
Channel
之间通过不同 EventLoop 并行处理;无需跨组线程迁移。 -
缺点:
-
Accept 与 I/O 共享同一组,极端高并发时 Accept 事件可能被 I/O 占用时间片“饿”到。
-
一旦业务 Handler 中有阻塞,仍会阻塞该 Channel 所属 EventLoop 的所有 I/O。
-
-
建议:
-
负载较大时,优先选择 主从(两组);
-
重任务下沉到业务池:
DefaultEventExecutorGroup
,避免在 I/O 线程做重活。
-
6.4 主从(Master–Slave)Reactor:Server 端的“标准姿势”
6.4.1 原理与线程角色(Boss / Worker)
拓扑:
-
BossGroup(通常线程数很少,1–2):只处理
ServerSocketChannel
的 Accept。 -
WorkerGroup(线程数较多):处理 已建立连接 的 Read / Write、用户任务与定时任务。
-
新连接从 Boss 线程中被接受后,通过
ServerBootstrapAcceptor
注册到 WorkerGroup 的某个 EventLoop,并保持绑定到关闭。
优势:职责清晰,Accept 高优先级不受工作线程繁忙影响;更稳的 延迟 与 吞吐 表现。
6.4.2 源码落点(Netty 4.1.28.Final)
-
ServerBootstrap#group(EventLoopGroup boss, EventLoopGroup worker)
:两组传入。 -
AbstractBootstrap#initAndRegister()
:创建NioServerSocketChannel
并注册到 Boss。 -
ServerBootstrap#init(Channel ch)
:为 Server 管道安装ServerBootstrapAcceptor
。 -
ServerBootstrapAcceptor#channelRead
(简化思路):-
接受子
SocketChannel
。 -
childGroup.register(child)
—— 把子连接注册到 WorkerGroup 的某个EventLoop
(通过next()
轮询)。 -
安装用户的 child pipeline(childHandler)。
-
伪代码片段(按真实源码逻辑改写注释,非逐字):
// 基于 Netty 4.1.28.Final 的逻辑整理 final class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter { private final EventLoopGroup childGroup; private final ChannelHandler childHandler; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { final Channel child = (Channel) msg; // 新的 SocketChannel // 安装 child pipeline child.pipeline().addLast(childHandler); // 注册到 WorkerGroup 的某个 EventLoop(next() 轮询) childGroup.register(child).addListener((ChannelFuture f) -> { if (!f.isSuccess()) { forceClose(child, f.cause()); } }); } }
6.4.3 可运行示例:高并发友好配置(Java / Netty 4.1.28.Final)
public final class MasterSlaveEchoServer {
public static void main(String[] args) throws InterruptedException {
// 1 个 boss 接受连接;默认个数 worker 处理 I/O(可按机器核数调大)
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
// 建议:Linux 环境可切换为 EpollEventLoopGroup 进一步降低延迟
// EventLoopGroup boss = new EpollEventLoopGroup(1);
// EventLoopGroup worker = new EpollEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap()
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 2048) // 结合操作系统 backlog 调整
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_REUSEADDR, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// I/O 线程中只做轻活,重活下沉到业务线程池
DefaultEventExecutorGroup bizGroup =
new DefaultEventExecutorGroup(Math.max(2,
Runtime.getRuntime().availableProcessors()));
p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(bizGroup, "biz", new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
// 模拟轻量处理
ctx.writeAndFlush(msg.retain());
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("[master-slave] active on " +
Thread.currentThread().getName());
}
});
}
})
// ioRatio 适配(如果 I/O 很密集,适当提高)
;
ChannelFuture f = b.bind(8082).sync();
System.out.println("Master–Slave reactor echo server started at 8082.");
f.channel().closeFuture().sync();
} finally {
boss.shutdownGracefully().sync();
worker.shutdownGracefully().sync();
}
}
}
6.4.4 运行日志示例 & 适用场景
日志(观测到 Boss 与 Worker 线程分工):
Master–Slave reactor echo server started at 8082.
[master-slave] active on nioEventLoopGroup-5-3
[master-slave] active on nioEventLoopGroup-5-1
适用:
-
互联网长连接网关、IM、MQ 代理、游戏服网关、推送服务等 大量连接 + 高频短消息 场景。
-
追求低尾延(P99/99.9)与稳态吞吐的服务端。
不适用:极度轻量的小项目(会比单组多线程多一组线程资源开销)。
6.5 三种模型如何选:参数、CPU 亲和、IO 比例(ioRatio)配合
6.5.1 选择建议(实战简表)
维度 | 单线程 Reactor | 多线程 Reactor(单组) | 主从 Reactor |
---|---|---|---|
Accept 与 I/O | 同一线程 | 同一组不同线程 | 分离(Boss/Worker) |
延迟稳定性 | 弱 | 中 | 强 |
吞吐上限 | 低 | 中–高 | 高 |
复杂度 | 最低 | 低 | 中 |
典型用途 | Demo / 嵌入式 | 小中等规模服务 | 互联网网关/核心服务 |
6.5.2 线程数与亲和
-
默认线程数:
NioEventLoopGroup()
缺省常为2 * CPU 核
。可按压测结果做微调。 -
亲和/绑核:在延迟敏感场景(尤其
epoll
)可考虑把 Worker 线程绑核,减少抖动(进阶优化)。 -
业务重活隔离:用
DefaultEventExecutorGroup
把 CPU 密集型或阻塞操作从 I/O 线程移出。
6.5.3 与 ioRatio 配合
-
I/O 峰值期可调高
ioRatio
(如 70–90),保证select + processSelectedKeys
更及时; -
业务队列堆积且 I/O 稳定,可调低
ioRatio
(如 30–50); -
若业务重活已下沉,通常维持默认或略高(50–80)即可。
详见第 5 章对
ioRatio
的深度说明与动态调参示例。
6.6 与传统容器(如 Tomcat)线程模型对比
-
Tomcat(NIO Connector):通常是 Acceptor 线程 + Poller 线程(负责 Selector)+ 工作线程池(Executor)。业务处理多用 线程池并发,需要更多同步开销(Handler 设计以“任务并发”为中心)。
-
Netty(Reactor):以 EventLoop 串行化 保证线程安全,一个
Channel
绑定一个 EventLoop,不鼓励在 I/O 线程做重活,而是建议显式下沉到业务线程池。 -
对比:Netty 的“每连接一线程(逻辑上的,实为每连接绑定 EventLoop)+ 事件驱动”更利于把 I/O 与业务执行的边界 切清楚,在高并发下减少锁竞争与上下文切换;Tomcat 对“请求/任务并发”更友好,迁移传统阻塞式编程较自然。
6.7 小结与自测题
小结
-
单线程 Reactor:一条线程处理 Accept + I/O + 任务,极简但上限低。
-
多线程 Reactor(单组):同一
NioEventLoopGroup
内多线程并行,简洁易用;高负载下 Accept 可能被 I/O“饿”。 -
主从 Reactor:Boss 专职 Accept,Worker 专职 I/O,最常用、性能与稳定性最佳的服务端拓扑。
-
三种模型的 线程选择、ioRatio 配合、重活下沉(DefaultEventExecutorGroup) 是性能优化三板斧。
-
源码关键在
ServerBootstrapAcceptor
、MultithreadEventLoopGroup#next()
、NioEventLoop#run()
的协同。
自测题
-
为什么单线程 Reactor 模型下,一个阻塞 Handler 会“拖慢全局”?
-
多线程 Reactor(单组)与主从 Reactor 的本质差异是什么?
-
在主从 Reactor 中,新的
SocketChannel
是如何被分配到 Worker 的某个 EventLoop 上的?涉及哪些关键类/方法? -
如果你的服务 Accept TPS 很高且 P99 延迟波动明显,你会如何在三种模型之间做选择?还会调整哪些参数(如
ioRatio
、线程数)?
第 7 章:源码深度解析(NioEventLoop 的 run() 方法)
在 Netty 中,NioEventLoop
继承了 SingleThreadEventLoop
,它的 run()
方法就是 事件循环主循环,负责不断地轮询 Selector,执行 I/O 事件、定时任务、普通任务和尾部任务。
我们先给出一个核心源码(删减简化版,保留主干逻辑):
@Override
protected void run() {
for (;;) {
try {
int strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
// 100% 用来执行 I/O
processSelectedKeys();
runAllTasks();
} else {
final long ioStartTime = System.nanoTime();
processSelectedKeys(); // 执行 I/O 事件
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
7.1 主循环的设计思路
-
无限循环
for(;;)
-
Netty 的事件循环是一个典型的 reactor 模式实现。
-
每个
EventLoop
永远运行在自己的专属线程上,从而避免了锁竞争问题。
-
-
selectStrategy 决策模型
-
selectStrategy
会根据当前是否有待执行任务,决定是否立即selectNow()
(非阻塞检查)、还是执行select(timeout)
阻塞等待。 -
目的:降低空轮询开销,同时在任务繁忙时提升吞吐。
-
-
执行 I/O + 非 I/O 的分工机制
-
I/O 操作通过
Selector
驱动。 -
非 I/O 任务则通过
taskQueue
、scheduledTaskQueue
等存储。 -
核心变量
ioRatio
控制两者的执行比例(前面章节有详细分析)。
-
7.2 I/O 事件处理 —— processSelectedKeys()
-
Selector
返回的selectedKeys
集合中保存了所有准备就绪的 Channel。 -
processSelectedKeys()
会遍历这些SelectionKey
,根据 OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT 等不同事件,调用对应的 Channel 处理逻辑。 -
每个 Channel 都已经与某个
ChannelPipeline
绑定,最终会把 I/O 事件分发给用户自定义的 Handler。
例如:
-
OP_READ → 从 Socket 中读取数据 → fireChannelRead → pipeline → handler。
-
OP_WRITE → 尝试写数据 → flush → pipeline → handler。
7.3 普通任务与定时任务的执行 —— runAllTasks()
-
执行顺序
-
I/O → 定时任务 → 普通任务 → 尾部任务。
-
runAllTasks() 内部会先检查
scheduledTaskQueue
,将到期的定时任务转移到taskQueue
中,然后按 FIFO 顺序执行。
-
-
受 ioRatio 控制
-
若
ioRatio = 100
,则不限制普通任务,全部执行。 -
若
ioRatio < 100
,则普通任务执行的时长上限为:ioTime * (100 - ioRatio) / ioRatio -
这样,I/O 密集时,普通任务会被压缩执行,保证网络吞吐。
-
7.4 shutdown 流程
-
当 EventLoop 被关闭时,
run()
会进入 graceful shutdown 流程:-
清理所有任务
-
关闭所有 Channel
-
释放 Selector
-
最终退出循环,线程结束。
-
7.5 小结
从源码来看,NioEventLoop.run()
体现了 Netty 的高性能设计:
-
单线程模型 → 避免锁竞争。
-
ioRatio 动态控制 → 平衡 I/O 与任务执行。
-
多队列调度 → I/O 任务、定时任务、普通任务、尾部任务各司其职。
-
selectStrategy 策略模式 → 兼顾低延迟和高吞吐。
可以说,run()
方法是 Netty Reactor 模型的核心循环,它既保证了 I/O 的实时性,又通过 ioRatio
实现了 任务执行的灵活调度。
第八章:Netty 线程模型的应用与调优实践
8.1 Netty 线程模型的典型应用场景
Netty 的线程模型在不同类型的系统中有着不同的应用重点:
-
高并发长连接系统(IM、推送服务)
-
特点:连接数极多,每个连接流量不大,但要求实时性。
-
线程模型:大量的
EventLoop
负责管理连接,保证 I/O 事件优先,普通任务通过ioRatio
控制避免过度占用 CPU。 -
调优点:减少业务逻辑在 I/O 线程中执行,更多通过异步回调或业务线程池分担。
-
-
高吞吐短连接系统(HTTP、RPC)
-
特点:请求数量大、生命周期短。
-
线程模型:
bossGroup
负责快速接受新连接,workerGroup
注重处理读写和编解码。 -
调优点:适当增加
workerGroup
的线程数,避免单线程阻塞影响整体吞吐。
-
-
低延迟金融交易/实时计算
-
特点:延迟敏感。
-
线程模型:对
ioRatio
调节敏感,需要保证 I/O 处理的绝对优先性。 -
调优点:设置较高的
ioRatio
(如 80~90),减少普通任务干扰;同时用独立线程池执行耗时业务逻辑。
-
8.2 bossGroup 与 workerGroup 的配置策略
Netty 提供的默认配置是:
-
bossGroup
默认 1 个线程; -
workerGroup
默认CPU * 2
个线程。
但在不同应用场景中,可以按以下策略调整:
-
连接数远大于并发数(如 IM):
workerGroup
可以适当减少,避免线程上下文切换浪费。 -
并发数远大于连接数(如 HTTP 短连接):
workerGroup
应设置为CPU * 2~4
,提升并发吞吐能力。 -
低延迟场景:控制线程数与 CPU 核心数一致,减少调度抖动。
8.3 ioRatio 的调优实践
-
默认值 50:表示 I/O 与普通任务各占一半时间。
-
I/O 密集型场景:提高到 70~90,保证网络事件快速响应。
-
业务逻辑复杂场景:保持默认值 50 或调低,避免普通任务堆积过多。
-
观测方式:
-
观察 eventLoop 执行延迟,如果 I/O 延迟明显升高 → 提高 ioRatio。
-
观察 taskQueue 堆积,如果任务执行不及时 → 降低 ioRatio 或迁移任务到独立业务线程池。
-
8.4 避免阻塞 EventLoop 的设计原则
-
不要在 Handler 中执行耗时操作(如 DB 查询、复杂计算)。
-
不要直接在 I/O 线程执行同步阻塞调用(如同步文件读写、锁竞争)。
-
正确做法:
-
使用
DefaultEventExecutorGroup
将耗时操作派发到业务线程池。 -
对接外部系统时,采用异步调用,减少 I/O 线程阻塞。
-
8.5 任务队列与定时任务调优
-
定时任务:避免长耗时逻辑,应尽量使用轻量级逻辑,必要时丢给业务线程池。
-
普通任务:建议拆分为小任务,避免一次任务执行过久卡死 I/O。
-
尾部任务:适合做轻量的收尾操作(如统计、回调触发),不要写耗时逻辑。
8.6 与操作系统层面的结合
-
epoll(Linux):推荐使用
EpollEventLoopGroup
,延迟更低。 -
kqueue(MacOS/BSD):在本地开发环境中可用,但生产一般还是 Linux。
-
多 Reactor 模式:bossGroup 专注于
accept
,workerGroup 专注于 I/O 与业务任务,符合现代多核 CPU 的利用方式。
8.7 调优实战经验
-
监控指标
-
EventLoop 队列长度
-
I/O 延迟(如 read/write 处理耗时)
-
CPU 使用率分布
-
-
常见问题与解决方案
-
问题:I/O 延迟上升,普通任务积压
→ 提高 ioRatio 或拆分任务到业务线程池。 -
问题:CPU 飙高但吞吐不升
→ 检查 workerGroup 是否过多线程切换,适当减少线程数。 -
问题:定时任务不准时
→ 说明 I/O 占用过多,或任务阻塞 eventLoop,需要优化任务分配。
-
✅ 总结
第八章从 应用场景 → 线程池配置 → ioRatio 调优 → 阻塞规避 → 系统结合 全面展开,帮助理解如何把 Netty 的线程模型真正应用到生产环境中。调优的核心思想是:
-
保证 I/O 响应优先级
-
避免 EventLoop 阻塞
-
结合业务特点动态调整 ioRatio 和线程数