来源《netty权威指南》
1、netty线程模型
一般在讨论netty的线程模型的时候,我们会考虑到经典的Reactor线程模型,下面分别说明下经典的Reactor线程模型
1、1 Reactor单线程模型
这个线程模型指的是所有的nio操作都是在一个线程中去完成的。nio线程的职责如下:
作为nio服务端,接收客户端的tcp连接;
作为no客户端,向服务端发起tcp连接;
读取通信对端的请求或是应答消息;
向通信对端发送消息或者应答消息。
其单线程模型如下图所示:
由于Reactor模式是异步非阻塞i/o,所有的i/o操作都不会导致阻塞。理论上一个线程可以独立处理所有的i/o相关的操作。例如,通过Acceptor接收客户端的连接,当链路建立完成后通过Dispatch将对应的ByteBuffer派发到指定的handler上,进行消息解码。用户线程消息编码后通过nio线程将消息发送给客户端。
在一些小容量的应用场景下,可以使用单线程模型,但对于高负载,大并发的应用场景确不合适,主要原因如下:
一个nio线程同时处理成百上千的链路,性能无法满足,即便nio线程的cpu达到100%,也无法满足海量的消息编码、解码、读取和发送。
nio线程负载过重,处理速度变慢,这会导致大量的客户端的连接超时,超时之后往往会进行重发,这更加加重了nio线程的负载,最终导致大量消息积压和处理超时,成为性能瓶颈。
可靠性问题:一旦nio线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
1、2 Reactor多线程模型
它与单线程模型最大的区别就是多了一组nio线程池来处理io操作,其模型如下:
特点如下:
有一个nio线程负责处理客户端的连接;
增加了一组nio线程池来处理网络io操作;
在绝大数情况下,Reactor多线程模型可以满足性能需求。但是,在个别特殊场景中,一个nio线程负责监听和处理所有客户端的连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下Acceptor线程会存在性能不足的问题,为了解决这个问题,产生了第三种模型,主从Reactor模型;
1、3 Reactor主从多线程模型
Reactor主从多线程模型,用于监听客户端的连接不在是一个Nio线程了,它是一个nio线程池进行监听客户端的连接包括安全认证,当链路建立后就将网络读取的操作放在另外一个线程去进行读取。其模型如下:
利用多线程模型可以有效处理一个线程无法处理多个客户端连接请求的情况,在netty官方Demo中,推荐使用该线程模型。
1、4 Netty线程模型
netty的线程模型不是一层不变的,它取决于用户的启动参数配置。通过设置不同的启动参数,netty可以同时支持Reactor单线程模型、多线程模型、主从Reactor线程模型。
netty的线程模型如下:
netty服务端启动代码如下:
- /配置服务端的nio线程组,
- //EventLoopGroup是一个线程组,它包含了一组nio线程,专门用于网络事件的处理,实际上他们就是Reactor线程组
- //这里创建2个的原因是一个用于服务端接受客户的连接,另一个用于SockentChannel的网络读写。
- 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();
- }
如上看到,这里定义了2个NioEventLoopGroup,实际上就是2个线程池,一个是用于监听客户端的连接,一个用户处理网络io,或者执行系统task等。
用于接收客户端请求的线程池职责如下:
1)接收客户端tcp请求,初始化Channel参数。
2)将链路状态变更事件通知给ChannelPipeline。
用于处理io操作的Reactor线程池职责如下:
1)异步读取通信端的数据,发送读事件到ChannelPipeline;
2)异步发送消息到通信对端,调用ChannelPipeline的消息发送接口;
3)执行系统调用Task;
4)执行定时任务Task,例如链路空闲状态监测定时任务;
2、NioEventLoop设计原理
NioEventLoop的设计并不是一个单纯的io读写,还兼顾处理了以下2类任务:
系统task
:通过调用NioEventLoop的execute(Runnable task)方法实现,Netty有很多系统task,创建他们的主要原因是:当io线程和用户线程同时操作网络资源的时候,为了防止并发操作导致的锁竞争,将用户线程的操作封装成Task放入消息队列中,由i/o线程负责执行,这样就实现了局部无锁化。
定时任务:执行schedule()方法
其类图的结构如下:
NioEventLoopGroup是NioEventLoop的组合,用于管理NioEventLoop。
nioEventLoop需要处理网络的读写,因此它必须会有一个多路复用器对象。下面是Selector的定义:
Selector selector;
private SelectedSelectionKeySet selectedKeys;
private final SelectorProvider provider;
它的初始化非常简单直接是Selector.open方法进行初始化。
网络的读取操作是在run方法中去执行的,首先看有没有未执行的任务,有的话直接执行,否则就去轮训看是否有就绪的Channel,如下:
@Override
protected void run() {
for (;;) {
oldWakenUp = wakenUp.getAndSet(false);
try {
if (hasTasks()) {
selectNow();
} else {
select();
// 'wakenUp.compareAndSet(false, true)' is always evaluated
// before calling 'selector.wakeup()' to reduce the wake-up
// overhead. (Selector.wakeup() is an expensive operation.)
//
// However, there is a race condition in this approach.
// The race condition is triggered when 'wakenUp' is set to
// true too early.
//
// 'wakenUp' is set to true too early if:
// 1) Selector is waken up between 'wakenUp.set(false)' and
// 'selector.select(...)'. (BAD)
// 2) Selector is waken up between 'selector.select(...)' and
// 'if (wakenUp.get()) { ... }'. (OK)
//
// In the first case, 'wakenUp' is set to true and the
// following 'selector.select(...)' will wake up immediately.
// Until 'wakenUp' is set to false again in the next round,
// 'wakenUp.compareAndSet(false, true)' will fail, and therefore
// any attempt to wake up the Selector will fail, too, causing
// the following 'selector.select(...)' call to block
// unnecessarily.
//
// To fix this problem, we wake up the selector again if wakenUp
// is true immediately after selector.select(...).
// It is inefficient in that it wakes up the selector for both
// the first case (BAD - wake-up required) and the second case
// (OK - no wake-up required).
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.
}
}
}
}
在进行轮训的时候,可能为空,也没有wakeup操作或是最新的消息处理,则说明本次轮训是一个空轮训,此时会触发jdk的epoll bug,它会导致Selector进行空轮训,使i/o线程处于100%。为了避免这个bug。需要对selector进行统计:
1)对selector操作周期进行统计
2)每完成一次轮训进行一次计数
3)当在某个周期内超过一定次数说明触发了bug,此时需要进行重新建立Selector,并赋值新值,将原来的进行关闭。
调用rebuildSelector方法。
当轮训到有就绪的Channel时,就进行网络的读写操作,其代码如下:
此时会去调用processSelectedKeysPlain方法,默认没有开启SelectedKey的优化方法。这里执行的方法如下:
- 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.
- }
- }
- }
- private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
- // check if the set is empty and if so just return to not create garbage by
- // creating a new Iterator every time even if there is nothing to process.
- // See https://github.com/netty/netty/issues/597
- if (selectedKeys.isEmpty()) {
- return;
- }
- Iterator<SelectionKey> i = selectedKeys.iterator();
- for (;;) {
- final SelectionKey k = i.next();
- final Object a = k.attachment();
- i.remove();
- if (a instanceof AbstractNioChannel) {
- processSelectedKey(k, (AbstractNioChannel) a);
- } else {
- @SuppressWarnings("unchecked")
- NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
- processSelectedKey(k, task);
- }
- if (!i.hasNext()) {
- break;
- }
- if (needsToSelectAgain) {
- selectAgain();
- selectedKeys = selector.selectedKeys();
- // Create the iterator again to avoid ConcurrentModificationException
- if (selectedKeys.isEmpty()) {
- break;
- } else {
- i = selectedKeys.iterator();
- }
- }
- }
- }
- 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());
- }
- }
- @Override
- protected int doReadMessages(List<Object> buf) throws Exception {
- SocketChannel ch = javaChannel().accept();
- try {
- if (ch != null) {
- buf.add(new NioSocketChannel(this, childEventLoopGroup().next(), ch));
- return 1;
- }
- } catch (Throwable t) {
- logger.warn("Failed to create a new channel from an accepted socket.", t);
- try {
- ch.close();
- } catch (Throwable t2) {
- logger.warn("Failed to close a socket.", t2);
- }
- }
- return 0;
- }
- @Override
- protected int doReadBytes(ByteBuf byteBuf) throws Exception {
- return byteBuf.writeBytes(javaChannel(), byteBuf.writableBytes());
- }
后面的如果网络操作位为连接状态,则需要对连接结果进行判断。
处理完网络的io后,Eventloop要执行一些非io的系统task和定时任务,代码如下:
- final long ioTime = System.nanoTime() - ioStartTime;
- final int ioRatio = this.ioRatio;
- runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
由于要同时执行io和非io的操作,为了充分使用cpu,会按一定的比例去进行执行,如果io的任务大于定时任务和task,则可以将io比例调大。反之调小,默认是50%,其执行方法如下:
- protected boolean runAllTasks(long timeoutNanos) {
- fetchFromDelayedQueue();
- Runnable task = pollTask();
- if (task == null) {
- return false;
- }
- final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
- long runTasks = 0;
- long lastExecutionTime;
- for (;;) {
- try {
- task.run();
- } catch (Throwable t) {
- logger.warn("A task raised an exception.", t);
- }
- runTasks ++;
- // Check timeout every 64 tasks because nanoTime() is relatively expensive.
- // XXX: Hard-coded value - will make it configurable if it is really a problem.
- if ((runTasks & 0x3F) == 0) {
- lastExecutionTime = ScheduledFutureTask.nanoTime();
- if (lastExecutionTime >= deadline) {
- break;
- }
- }
- task = pollTask();
- if (task == null) {
- lastExecutionTime = ScheduledFutureTask.nanoTime();
- break;
- }
- }
- this.lastExecutionTime = lastExecutionTime;
- return true;
- }
最后eventloop的run方法,会判断是否优雅关闭,如果是优雅关闭会执行closeAll方法,如下:
- private void closeAll() {
- selectAgain();
- Set<SelectionKey> keys = selector.keys();
- Collection<AbstractNioChannel> channels = new ArrayList<AbstractNioChannel>(keys.size());
- for (SelectionKey k: keys) {
- Object a = k.attachment();
- if (a instanceof AbstractNioChannel) {
- channels.add((AbstractNioChannel) a);
- } else {
- k.cancel();
- @SuppressWarnings("unchecked")
- NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
- invokeChannelUnregistered(task, k, null);
- }
- }
- for (AbstractNioChannel ch: channels) {
- ch.unsafe().close(ch.unsafe().voidPromise());
- }
- }