写在前面
最近心血来潮学了Netty,并手搓了一个简单的im系统(WeTalk: 基于netty + springboot + react的简易im系统),对这门技术有不少心得体会,所以打算写一个系列,系统且详细的整理相关知识点。
在Java网络编程领域,Netty几乎已经成为事实标准。无论是Dubbo、RocketMQ这样的中间件,还是各类游戏服务器、即时通讯系统,Netty都是首选的网络框架。但很多人在使用Netty时,往往停留在"会用"的层面------知道怎么启动服务端、怎么写Handler,却对底层架构设计知之甚少。
本文将从架构设计的角度,深入剖析Netty的核心------Reactor线程模型。理解了这一点,才能真正明白Netty为什么快、为什么稳定。
一、从BIO到NIO:网络编程的演进
在讨论Reactor之前,有必要回顾一下Java网络编程的演进历程,这能帮助我们理解为什么需要Reactor模型。
1.1 传统BIO模型的困境
早期的Java网络编程采用BIO(Blocking I/O)模型,典型代码如下:
java
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待连接
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
while (true) {
int len = in.read(buffer); // 阻塞等待数据
if (len == -1) break;
// 处理数据...
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
这种模型的问题显而易见:
- 线程资源浪费:每个连接需要一个线程,1000个连接就需要1000个线程
- 上下文切换开销:大量线程在CPU上频繁切换,性能急剧下降
- 内存占用高:每个线程默认栈空间约1MB,1000个线程就是1GB
当并发连接数达到几千时,服务器基本就扛不住了。
1.2 NIO的改进与不足
JDK 1.4引入了NIO(Non-blocking I/O),核心是Selector多路复用器:
java
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞等待就绪事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 处理连接
} else if (key.isReadable()) {
// 处理读事件
}
}
}
NIO用单线程就能处理多个连接,理论上解决了BIO的线程资源问题。但JDK的NIO API存在几个致命缺陷:
缺陷一:API设计过于底层
Selector、SelectionKey、ByteBuffer这些概念对于普通开发者来说太底层了。光是正确处理ByteBuffer的flip、clear、compact就够让人头疼的。
缺陷二:Epoll空轮询Bug
这是JDK NIO最著名的问题。在Linux下,Selector在某些情况下会出现空轮询,select()方法明明没有事件却立即返回,导致CPU飙升到100%。这个问题在JDK很多版本中都没有彻底解决。
缺陷三:缺乏高级功能
断线重连、心跳检测、消息编解码、半包粘包处理......这些网络编程中常见的需求,JDK NIO都没有提供,需要开发者自己实现。
Netty正是为了解决这些问题而生。
二、Reactor模式:高性能服务器的基石
2.1 什么是Reactor模式
Reactor模式是一种事件驱动的设计模式,核心思想是:将事件的监听和事件的处理分离。
打个比方,Reactor就像餐厅的服务员:
- 传统BIO模式:每个顾客配一个服务员,服务员全程守在桌边等顾客点菜
- Reactor模式:一个服务员负责多张桌子,哪桌有需求就响应哪桌
Reactor模式包含两个核心角色:
- Reactor:负责监听和分发事件,相当于"事件循环"
- Handler:负责处理具体事件,相当于"事件处理器"
2.2 三种Reactor模型
根据Reactor数量和线程模型的不同,有三种经典实现:
单Reactor单线程模型
┌─────────────────────────────────────────────────────┐
│ Reactor Thread │
│ ┌─────────────────────────────────────────────┐ │
│ │ Selector (多路复用器) │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Dispatch (事件分发) │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Acceptor │ │ Handler1 │ │ Handler2 │ │
│ │ (连接处理) │ │ (业务处理) │ │ (业务处理) │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────┘
所有I/O操作都在一个线程内完成。优点是简单、无线程切换开销;缺点是业务处理耗时会影响后续请求,无法利用多核CPU。
Redis就是采用这种模型,因为Redis的操作都是内存操作,速度极快,单线程足以应对。
单Reactor多线程模型
┌─────────────────────────────────────────────────────┐
│ Reactor Thread │
│ ┌─────────────────────────────────────────────┐ │
│ │ Selector (多路复用器) │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Dispatch (事件分发) │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ │
│ │ Acceptor │ │ Handler │ │
│ │ (连接处理) │ │ (读/写) │ │
│ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────┘
│
▼ (提交任务)
┌───────────────────────┐
│ ThreadPool │
│ ┌─────┬─────┬─────┐ │
│ │ T1 │ T2 │ T3 │ │
│ └─────┴─────┴─────┘ │
│ (业务处理线程池) │
└───────────────────────┘
Reactor线程只负责I/O操作,业务处理交给线程池。优点是充分利用多核CPU;缺点是Reactor线程仍然要处理所有I/O,高并发时可能成为瓶颈。
主从Reactor多线程模型
┌─────────────────────────────────────────────────────────────────┐
│ mainReactor │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Selector (连接监听) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Acceptor │ │
│ │ (建立连接) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ 分配连接
▼
┌─────────────────────────────────────────────────────────────────┐
│ subReactor (多个) │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ Selector 1 │ │ Selector 2 │ │ Selector N │ │
│ │ (I/O读写) │ │ (I/O读写) │ │ (I/O读写) │ │
│ └──────────────────┘ └──────────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ Handler │ │ Handler │ │ Handler │ │
│ │ (I/O处理) │ │ (I/O处理) │ │ (I/O处理) │ │
│ └──────────────────┘ └──────────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
这是最完善的模型。mainReactor只负责连接建立,建立后将Channel分配给subReactor处理I/O读写。subReactor可以有多个,每个对应一个线程。
这种模型的优势:
- 职责分离:连接建立和I/O处理由不同线程负责
- 水平扩展:subReactor数量可以根据CPU核心数灵活调整
- 高吞吐:多线程并行处理I/O,充分利用多核
Netty默认采用的就是这种模型。
三、Netty中的Reactor实现
3.1 EventLoopGroup与EventLoop
Netty中,Reactor的角色由EventLoop承担,多个EventLoop组成EventLoopGroup。
java
// Netty服务端启动代码
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // mainReactor
EventLoopGroup workerGroup = new NioEventLoopGroup(); // subReactor
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 配置Pipeline
}
});
这里的bossGroup对应mainReactor,workerGroup对应subReactor。
EventLoopGroup的构造:
java
public NioEventLoopGroup() {
this(0); // 默认线程数为0,实际会设置为CPU核心数*2
}
public NioEventLoopGroup(int nThreads) {
this(nThreads, (Executor) null);
}
// 实际创建逻辑
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory,
Object... args) {
if (nThreads <= 0) {
nThreads = NettyRuntime.availableProcessors() * 2;
}
// 创建EventLoop数组
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i++) {
children[i] = newChild(executor, args);
}
// 创建选择器(用于分配EventLoop)
chooser = chooserFactory.newChooser(children);
}
EventLoop的继承体系:
EventLoop
└── OrderedEventExecutor
└── EventExecutor
└── EventExecutorGroup
└── ScheduledExecutorService (JDK)
EventLoop本质上是一个单线程的ScheduledExecutorService,具备定时任务执行能力。
3.2 Channel与EventLoop的绑定
当一个连接建立时,ServerSocketChannel会将其分配给某个EventLoop:
java
// 服务端Channel注册到bossGroup的EventLoop
ChannelFuture regFuture = group().register(channel);
// 新连接建立后,分配给workerGroup的EventLoop
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
// 选择一个EventLoop并注册
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
}
EventLoop选择策略:
java
// EventLoopChooser有两种实现
public final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
@Override
public EventExecutor next() {
// 位运算取模,效率更高
return executors[idx.getAndIncrement() & executors.length - 1];
}
}
public final class GenericEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
@Override
public EventExecutor next() {
// 普通取模
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
}
Netty会根据EventLoop数量是否为2的幂次方,选择不同的Chooser实现。当数量是2的幂次方时,使用位运算代替取模,性能更优。
3.3 EventLoop的工作流程
EventLoop的核心是一个无限循环,不断处理I/O事件和任务队列:
java
// SingleThreadEventLoop的核心逻辑
@Override
protected void run() {
for (;;) {
try {
try {
// 1. 计算select策略
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
// 执行select,可能阻塞
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
}
} catch (IOException e) {
rebuildSelector0();
handleLoopException(e);
continue;
}
cancelledKeys = 0;
needsToSelectAgain = false;
// 2. 处理I/O事件
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
// 3. 执行所有任务
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
// 根据ioRatio控制I/O和任务的时间比例
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
工作流程图:
┌─────────────────────────────────────────────────────────────┐
│ EventLoop │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 无限循环 │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ 1. select() - 等待I/O事件就绪 │ │ │
│ │ │ (有任务时非阻塞,无任务时阻塞) │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ 2. processSelectedKeys() - 处理I/O事件 │ │ │
│ │ │ (Accept、Read、Write等) │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ 3. runAllTasks() - 执行任务队列 │ │ │
│ │ │ (定时任务、用户提交任务) │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Task Queue (任务队列) │ │
│ │ ┌─────┬─────┬─────┬─────┬─────┬─────┐ │ │
│ │ │Task1│Task2│Task3│Task4│ ... │ │ │ │
│ │ └─────┴─────┴─────┴─────┴─────┴─────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
3.4 ioRatio的作用
ioRatio控制I/O处理时间和任务处理时间的比例:
java
// 默认ioRatio为50,表示I/O和任务各占一半时间
private volatile int ioRatio = 50;
// 假设I/O处理花了10ms
// 则任务处理时间 = 10 * (100 - 50) / 50 = 10ms
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
这个设计很巧妙:
- ioRatio = 100:先处理完所有I/O事件,再处理所有任务
- ioRatio = 50:I/O和任务时间各占一半
- ioRatio越小,任务处理时间占比越大
通过调整ioRatio,可以在I/O密集型和计算密集型场景之间取得平衡。
四、Netty如何解决JDK NIO的问题
4.1 解决Epoll空轮询Bug
Netty通过检测空轮询次数来规避这个问题:
java
// NioEventLoop中的处理
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
int selectedKeys = selector.select(timeoutMillis);
selectCnt++;
// 检测空轮询
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// 正常情况,select阻塞了足够长时间
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// 空轮询次数超过阈值,重建Selector
selector = selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
// ...
}
} catch (CancelledKeyException e) {
// ...
}
}
默认阈值是512次,如果select在没有事件的情况下连续返回512次,Netty会重建Selector,将所有Channel重新注册到新的Selector上。
4.2 提供更友好的API
Netty对JDK NIO的API进行了全面封装:
|---------------------|-----------------|----------------|
| JDK NIO | Netty | 说明 |
| ByteBuffer | ByteBuf | 支持动态扩容、引用计数、池化 |
| Selector | EventLoop | 自动处理空轮询,支持任务调度 |
| SelectionKey | Channel | 丰富的属性存储、异步操作 |
| ServerSocketChannel | ServerBootstrap | 链式配置,开箱即用 |
五、实战:如何选择线程数
EventLoopGroup的线程数配置是性能调优的关键。
5.1 Boss线程数
Boss线程只负责处理连接建立,工作非常轻量。通常设置为1即可:
java
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
即使有上万QPS的连接请求,单个Boss线程也完全够用,因为连接建立是内核完成的,Boss线程只需要accept()然后分配给Worker。
5.2 Worker线程数
Worker线程处理I/O读写,需要根据场景调整:
I/O密集型场景(如代理服务器、网关):
java
// 线程数 = CPU核心数 * 2
int workerThreads = Runtime.getRuntime().availableProcessors() * 2;
EventLoopGroup workerGroup = new NioEventLoopGroup(workerThreads);
I/O密集型任务大部分时间在等待网络,CPU利用率低,可以配置更多线程。
计算密集型场景(如消息序列化、业务逻辑):
java
// 线程数 = CPU核心数 + 1
int workerThreads = Runtime.getRuntime().availableProcessors() + 1;
EventLoopGroup workerGroup = new NioEventLoopGroup(workerThreads);
计算密集型任务CPU利用率高,线程数不宜过多,否则线程切换开销会抵消并行收益。
混合场景(推荐做法):
java
// Netty默认值:CPU核心数 * 2
EventLoopGroup workerGroup = new NioEventLoopGroup();
Netty默认值是一个比较平衡的选择。实际项目中,建议通过压测确定最优值。
六、总结
Reactor线程模型是Netty高性能的基石。通过主从Reactor架构,Netty实现了:
- 职责分离:连接建立和I/O处理由不同线程负责
- 水平扩展:Worker线程数可根据CPU核心数灵活调整
- 高效调度:单线程EventLoop避免了锁竞争,任务队列实现了异步处理
理解Reactor模型,不仅有助于更好地使用Netty,也能帮助我们设计其他高性能服务器。下一篇,我们将深入分析Channel与EventLoop的交互机制,揭示Netty如何实现高效的I/O处理。