本文为《Netty权威指南》的读书笔记,读书过程中也伴随着一些源码阅读和其他文档阅读,所以内容和《Netty权威指南》会略有不同,请知晓。
Netty Api地址:http://netty.I/O/5.0/api/
一 线程模型
1. Reactor单线程模型
1) 线程模型
所有的I/O操作都在一个NIO线程上完成。NIO线程职责:
- 作为NIO服务端,接收客户端的TCP连接。
- 作为NIO客户端,向服务器发起TCP连接。
- 读取对方的消息。
- 向对方发送消息。
由于Reactor模式使用的是异步非阻塞I/O,所有的I/O操作都不会导致阻塞,理论上一个线程可以独立处理所有的I/O操作。
通过Acceptor接收客户端的TCP连接请求消息,当链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上,进行消息解码。用户线程消息编码后通过NIO线程将消息发送给客户端。
2) 缺点
在小容量场景下,可以使用单线程模型,但是对一个高负载、大并发的应用会存在问题。
一个NIO线程同时处理成百上千的链路,性能存在瓶颈,无法满足海量消息的编码、解码、读取、发送。
当NIO线程负载过高以后,处理速度会降低,这会导致大量客户端连接超时,而发生超时以后往往需要进行重发,这又会加重NIO线程的负载,导致大量消息积压。
一旦NIO线程出现问题或者进入死循环,将导致不能接受和处理外部消息,整个通讯模块不可用,即出现节点故障。
2. Ractor多线程模型
1) 线程模型
Acceptor线程(一个专门的NIO线程)监听服务端、客户端的TCP连接。
读写操作由NIO线程池负责,负责消息的读取,编码、解码、发送。
一个NIO线程对应N个链路,但是一个链路只对应一个NIO线程。
2) 缺点
并发百万级的客户端连接,或者服务端需要对客户端握手进行安全认证(安全认证本身非常消耗性能),这种场景下,一个Acceptor线程可能存在性能不足的问题。
3. 主从Reactor多线程模型
1) 线程模型
服务端通过独立的NIO线程池接收客户端连接。Acceptor接收到客户端TCP连接请求并处理完成后(例如完成耗时的认证操作),将新创建的SocketChannel注册到I/O线程池(sub Reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编码工作。
Acceptor线程池仅仅用于客户端的登录、握手、安全认证,一旦链路建立成功,就将链路注册到sub Reactor线程池的I/O线程上,后续操作交给此I/O线程。
4. Netty线程模型
1) 线程模型
Netty的线程模型不是一成不变,它取决于用户启动参数配置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型、主从线程模型。Netty推荐使用主从Reactor线程池模式。
2) 原理
服务端启动的时候创建的两个NioEventLoopGroup,就是连个独立的Reactor线程池。
a) Accetpor线程池职责
接收客户端TCP连接,初始化Channel参数。
将链路状态变更的事件通知给ChannelPipeline。
b) NIO处理IO操作的线程池职责
异步读取数据报,发送读事件到ChannelPipeline。
异步发送消息,调用ChannelPipeline的消息发送接口。
执行系统调用Task
调用定时任务Task,例如链路空闲状态监测定时任务。
c) 示例代码:
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler());
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
二 源码分析
1. NioEventLoop
1) run
无限循环,直到收到退出指令。
首先将wakeUp设置成false,同时将之前的值存储在oldWakeUp中。
通过hasTask判断当前消息队列中是否有消息尚未处理。
如果有待处理消息,则调用selectNow方法立即进行一次select操作,此时如果有准备就绪的Channel,则返回就绪Channel的集合,否则返回0。选择完成后,再次判断用户是否调用了Selector的wakeup方法,如果调用过,则执行selector.wakeup操作。
如果没有待处理消息,则执行select方法,由Selector多路复用器无限轮询,看是否有就绪的Channel。当出现以下几种情况时,退出无限轮询。
有Channel处于就绪状态,selectedKeys不为0,说明读写事件需要处理。
oldWakeUp改为false
系统或这用户调用了wakeup操作,唤醒当前的多路复用器。
消息队列中有新任务需要处理。
如果本次Selector轮询结果为空,也没有wakeup操作或者是新的消息需要处理,则说明是个空轮询。(示例代码中rebuildSelector相关逻辑主要是解决JDK bug的。)
protected void run() {
for (;;) {
oldWakenUp = wakenUp.getAndSet(false);
try {
if (hasTasks()) {
selectNow();
} else {
select();
if (wakenUp.get()) {
selector.wakeup();
}
}
cancelledKeys = 0;
final long ioStartTime = System.nanoTime();
needsToSelectAgain = false;
if (selectedKeys != null) {
processSelectedKeysOptimized(selectedKeys.flip());
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
final long ioTime = System.nanoTime() - ioStartTime;
final int ioRatio = this.ioRatio;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
break;
}
}
} catch (Throwable t) {
logger.warn("Unexpected exception in the selector loop.", t);
// Prevent possible consecutive immediate failures that lead to
// excessive CPU consumption.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore.
}
}
}
}
void selectNow() throws IOException {
try {
selector.selectNow();
} finally {
// restore wakup state if needed
if (wakenUp.get()) {
selector.wakeup();
}
}
}
private void select() 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;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
int selectedKeys = selector.select(timeoutMillis);
selectCnt ++;
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks()) {
// Selected something,
// waken up by user, or
// the task queue has a pending task.
break;
}
if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// The selector returned prematurely many times in a row.
// Rebuild the selector to work around the problem.
logger.warn(
"Selector.select() returned prematurely {} times in a row; rebuilding selector.",
selectCnt);
rebuildSelector();
selector = this.selector;
// Select again to populate selectedKeys.
selector.selectNow();
selectCnt = 1;
break;
}
currentTimeNanos = System.nanoTime();
}
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row.", selectCnt - 1);
}
}
} catch (CancelledKeyException e) {
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector - JDK bug?", e);
}
// Harmless exception - log anyway
}
}
2) processSelectedKey
首先判断SelectionKey是否可用,如果不可用通过unsafe.close方法释放资源。
然后根据网络操作位readyOps来判断那种事件(read、write、connnect),根据事件执行相应的网络操作。
private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
return;
}
try {
int readyOps = k.readyOps();
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {
// Connection already closed - no need to handle write.
return;
}
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
} catch (CancelledKeyException e) {
unsafe.close(unsafe.voidPromise());
}
}
三 最佳实践
1. 建议
创建两个NioEventLoopGroup,用户逻辑隔离NIO Acceptor和NIO I/O线程。
尽量不要在ChannelHandler中启动用户线程(编码后将POJO消息发送到通信对端的线程除外)。
解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息解码。
如果没有复杂的逻辑计算,没有可能导致线程被阻塞的磁盘操作、DB操作、网络操作等,可以直接在NIO线程上完成业务逻辑,不用切换到用户线程。
如果业务逻辑复杂,不要再NIO线程中完成,以保证NIO线程尽快被释放,处理其他的I/O操作。
2. 推荐的线程数计算公式
线程数 = (线程总时间 / 瓶颈资源时间)x 瓶颈资源的线程并行数
QPS = 1000 / 线程总时间 x 线程数