上文说到基于NioEventloop注册channel时,会创建一线程并持有其引用,线程内部调用的就是NioEventLoop#run方法。 我们看下这个方法都干了些啥。
NioEventLoop#run
protected void run() {
for (;;) {
try {
//hasTask就是判断taskQuene 或者 tailTasks是否为空
//calculateStrategy方法就是根据有无任务 无任务就返回SelectStrategy.SELECT
//有任务就执行selector.selectNow(); 这是非阻塞的 会直接返回已准备好的通道数
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
//阻塞直到一个通道就绪 或者有其他线程唤醒
//select之前会将wakenUp状态修改为false,表示可能线程进入阻塞状态
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
// fallthrough
}
cancelledKeys = 0;
//select执行完毕后 将needsToSelectAgain 为false 为何 ?
//我们知道 当channel注册到selector上时会返回一个selectionKey,
//当selectionKey取消的时候,也就是channel从selector上移除的时候,cancelledKeys计数会+1,
//当cancelledKeys >= 256时,会将 needsToSelectAgain置为true,同时 cancelledKeys重置为0
//这么做的寓意在下方 if (needsToSelectAgain) { 可以看到
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
//Ensure we always run tasks.
runAllTasks();
}
} else {
//默认ioRatio设置为50
final long ioStartTime = System.nanoTime();
try {
//2.处理产生io事件的channel
processSelectedKeys();
} finally {
//3.处理任务队列 执行时间由处理io事件的时间动态算得,默认是1:1
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
....
}
}
从run方法中可以看到3块
- select调用
- 处理select产生的io事件
- 处理任务队列
1.NioEventLoop#select
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
//delayNanos 判断 scheduledTaskQueue有无定时任务 没有的话默认返回1s 有则返回最近的定时任务时间
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
//只有500000/1000000 = 0.5取整是为0的,只有当最近的定时任务延迟时间大于0.5ms,timeoutMillis才会>=1
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
//定时任务截止时间快到了 低于0.5ms 中断本次轮询
if (timeoutMillis <= 0) {
//如果没有进行过select操作就就调用一次非阻塞的selectNow
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
//有任务加入 中断本次轮询
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}
//阻塞 select,假设 timeoutMillis时间非常宽裕,则有可能长时间阻塞,但只有NioEventloop中有任务加入,就有可能进行唤醒
int selectedKeys = selector.select(timeoutMillis);
//每“阻塞”式执行完一次就+1
selectCnt ++;
//唤醒之后 判断是否满足,如满足就中断轮询
a. 有io事件
b. 被唤醒
c. 有任务
d. 第一个定时任务将要被执行
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}
if (Thread.interrupted()) {
..
selectCnt = 1;
break;
}
//解决jdk selector 空轮询bug
long time = System.nanoTime();
//如果阻塞的时间大于等于timeoutMillis 那么就是一次有效的轮询
//否则相当于没有阻塞 一旦有io事件 在上面的selectedKeys != 0就已经break
//这里只有可能是触发了空轮询bug 或者被其他线程添加任务唤醒了
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
selectCnt = 1;
} else if
//默认值是512 如果空轮询的次数超过512次 那么需要重建selector
(SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
1.1 重建selector
rebuildSelector();
selector = this.selector;
// Select again to populate selectedKeys.
selector.selectNow();
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
}
} catch (CancelledKeyException e) {
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
selector, e);
}
// Harmless exception - log anyway
}
}
1.1 重建selector rebuildSelector
NioEventLoop#rebuildSelector
public void rebuildSelector() {
...
final Selector oldSelector = selector;
final Selector newSelector;
if (oldSelector == null) {
return;
}
try {
newSelector = openSelector();
} catch (Exception e) {
logger.warn("Failed to create a new Selector.", e);
return;
}
// Register all channels to the new Selector.
int nChannels = 0;
for (;;) {
try {
for (SelectionKey key: oldSelector.keys()) {
//前文有说过 注册的时候netty的 socketChannel会作为attchment绑定起来
Object a = key.attachment();
try {
//已经转移到新的selector
if (!key.isValid() || key.channel().keyFor(newSelector) != null) {
continue;
}
int interestOps = key.interestOps();
//取消关系
key.cancel();
//注册到新的selector
SelectionKey newKey = key.channel().register(newSelector, interestOps, a);
if (a instanceof AbstractNioChannel) {
//引用更新
((AbstractNioChannel) a).selectionKey = newKey;
}
nChannels ++;
} catch (Exception e) {
...
}
}
} catch (ConcurrentModificationException e) {
//故障重试
continue;
}
break;
}
selector = newSelector;
try {
// time to close the old selector as everything else is registered to the new one
oldSelector.close();
} catch (Throwable t) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to close the old Selector.", t);
}
}
}
1.2 jdk selector空轮询bug
以我在 netty前序 -io模式 中代码举例
public class SelectServerSockect {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true){
//阻塞直到一个通道就绪,SelectionKey中的ready集合将会更新,返回值为就绪的通道数
//当出现轮询bug时 num返回就是0 ,常规情况下它是会阻塞直至有io事件发生的
//空轮询bug就是快速响应,然后一直调用select,导致cpu被打满,没时间处理其他的任务了
int num = selector.select();
if(num == 0){
continue;
}
System.out.println(num);
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey next = iterator.next();
if(next.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) next.channel();
SocketChannel accept = channel.accept();
accept.configureBlocking(false);
//注册到selector上 感兴趣的操作是读
accept.register(selector,SelectionKey.OP_READ);
}
//ServerSocketChannel SocketChannel都注册到了一个Selector上 所有会有多种Ops
if(next.isReadable()){
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(channel.read(byteBuffer) > 0){
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
channel.write(byteBuffer);
}
byteBuffer.clear();
}
channel.close();
}
//selectedKeys 需要手动移除
iterator.remove();
}
}
}
}
此bug可能存在的场景
1.服务端等待连接
2.客户端发起连接,发送消息
3.服务端接受连接,并注册监听通道的OP_READ
4.服务端读取消息,从感兴趣事件集合中移除OP_READ
5.客户端关闭连接
6.服务端给客户端发送消息
7.服务端select方法不再阻塞,无限被唤醒并且返回值为0.
可能的解释
问题产生于linux的epoll。如果一个socket文件描述符,注册的事件集合码为0,然后连接突然被对端中断,那么epoll会被POLLHUP或者有可能是POLLERR事件给唤醒,并返回到事件集中去。这意味着,Selector会被唤醒,即使对应的channel兴趣事件集是0,并且返回的events事件集合也是0。
可解决的方案
1.取消对应的key,立即调用selector.selectNow刷新一次selector。
2.如果注册到selector兴趣事件集为0,则直接取消注册。 如果注册到selector兴趣事件集不为0,则需要将linux epoll事件POLLHUP/POLLERR转化为OP_READ 或者OP_WRITE。。
(netty采用的方案)
3.重新new一个selector 。
重新构造selector,需要重新注册channnel到其上,并注册感兴趣事件,重新注册的过程中有机会检测channel的可用性
总结: 可以看到select操作根据taskQueue普通任务队列是否有任务进行非阻塞/阻塞调用。同时还会根据定时任务队列最近到期的任务时间来决定阻塞时间。阻塞期间,外部线程添加任务时可唤醒阻塞状态。netty还根据计数空轮询次数,如果超过512次就重建selector来修复空轮询bug。
2.处理io事件 processSelectedKeys
NioEventLoop#processSelectedKeys
private void processSelectedKeys() {
if (selectedKeys != null) {
//前文讲创建NioEventloop时 openSelector netty有调整selectedKeys的数据结构,
//并将SelectedSelectionKeySet引用指向到了NioEventloop,
//这里只要selectedKeys不为空,那就是进入这个分支了,获取所有就绪的通道进行处理
processSelectedKeysOptimized(selectedKeys.flip());
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}
NioEventLoop#processSelectedKeysOptimized
private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
for (int i = 0;; i ++) {
final SelectionKey k = selectedKeys[i];
if (k == null) {
break;
}
//清除引用 防止内存泄露 SelectionKey的attachement关联channel,
//channel消亡时所有的引用都应清除,不然无法回收
//jdk selectedKeys是set结构,netty改造成了数组,添加/遍历更快,remove相当于空方法,这里需要手动移除
selectedKeys[i] = null;
//获取io事件对应的channel
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
//处理通道
processSelectedKey(k, (AbstractNioChannel) a);
} else {
//attachment除了可能为channel外 还有可能为NioTask 基本没有使用
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
//是否需要再次轮询
if (needsToSelectAgain) {
//每256个连接断开 清理下selectedKeys。 因为SelectedSelectionKeySet重写了remove方法,
//remove方法并不会对数组进行改变,因为需要依赖null进行边界判定,且数组频繁重排也会影响影响性能,
//而当连接取消,调用selectionKey.cancel 加入到cancelledKeys时。
//jdk的selector在select前后都会校验cancelledKeys中是否有值,
//如果有,调用selectedKeys的remove方法进行移除。 这里没有移除,用的是上方 selectedKeys[i] = null;
//下方2.1中 if (!k.isValid()) 判断,以及此处的批量移除来防止内存泄漏和性能优化。
for (;;) {
i++;
//到达边界
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
}
//将needsToSelectAgain 置为false,调用selectNow
selectAgain();
selectedKeys = this.selectedKeys.flip();
//重置下标 重新开始遍历
i = -1;
}
}
}
2.1 NioEventLoop#processSelectedKey
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
return;
}
if (eventLoop != this || eventLoop == null) {
return;
}
//关闭通道 这个key已经无效了
unsafe.close(unsafe.voidPromise());
return;
}
try {
int readyOps = k.readyOps();
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
//有连接进入 2.1.1 处理连接请求 主从reactor线程其实都是NioEventloop,
//所以他们的区别实质是接下来的维持的unsafe实例的区别
unsafe.read();
if (!ch.isOpen()) {
return;
}
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
2.1.1 处理连接请求 boss read
这里称之为boss,因为是AbstractNioMessageChannel(NioServerSocketChannel),专门负责请求分发 AbstractNioMessageChannel.NioMessageUnsafe#read
public void read() {
assert eventLoop().inEventLoop();
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config);
boolean closed = false;
Throwable exception = null;
try {
...
//readBuf 是一个 List<Object> 结构 保留下面创建的NioSocketChannel的引用
//2.1.1.1 接收连接信息 创建NioSocketChannel
doReadMessages(readBuf);
...
int size = readBuf.size();
//可能多个连接同时接入
for (int i = 0; i < size; i ++) {
readPending = false;
//将NioSocketChannel作为msg传入,从head节点开始 挨个调用所有ChannelInboundHandler
//的channelRead方法 初始化 NioSocketChannel并注册其相关的SocketChannel(jdk)
//其中关键性的一个节点ServerBootstrapAcceptor下方有介绍
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
if (exception != null) {
closed = closeOnReadError(exception);
pipeline.fireExceptionCaught(exception);
}
if (closed) {
inputShutdown = true;
if (isOpen()) {
close(voidPromise());
}
}
} finally {
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
}
2.1.1.1 接收连接信息 创建NioSocketChannel
protected int doReadMessages(List<Object> buf) throws Exception {
//调用jdk方法 创建 SocketChannel
SocketChannel ch = javaChannel().accept();
...
//创建NioSocketChannel
buf.add(new NioSocketChannel(this, ch));
...
}
public NioSocketChannel(Channel parent, SocketChannel socket) {
//类图中有看过NioSocketChannel 和 NioServerSocketChannel的关系,他们都继承自AbstractNioChannel
//除了下面 传入的父类 channel(jdk) 和关心的操作不一样 后面的逻辑都是一样的
// super(parent, ch, SelectionKey.OP_READ);
//也就是获取唯一编号id,实例化unsafe,实例化pipeline ,保存socketChannel(jdk)引用,同时将socketChannel(jdk)设置为非阻塞
super(parent, socket);
config = new NioSocketChannelConfig(this, socket.socket());
}
2.1.2 ServerBootstrapAcceptor
在前文 服务端启动一章中 讲到 ServerBootstrap#init 有一段这样的代码 将我们自定义的handler 以及 ServerBootstrapAcceptor加入到 pipeline中
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
//config.handler() 其实就是我们传给ServerBootStrap的自定义handler
ChannelHandler handler = config.handler();
if (handler != null) {
//加入我们自定义的childHandler,异步任务,会在注册完成后 pipeline.invokeHandlerAddedIfNeeded();完成回调加入
pipeline.addLast(handler);
}
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
//添加一个接入器 用于接收新请求
pipeline.addLast(new ServerBootstrapAcceptor(
currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
//这些都是构造方法直接传入 我们声明的配置信息
private final EventLoopGroup childGroup;
private final ChannelHandler childHandler;
private final Entry<ChannelOption<?>, Object>[] childOptions;
private final Entry<AttributeKey<?>, Object>[] childAttrs;
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
//这里直接将msg 强转为 channel(netty) 这里是NioSocketChannel
final Channel child = (Channel) msg;
//NioSocketChannel的pipeline加入我们自定义的handler
child.pipeline().addLast(childHandler);
...设置 option attr 配置...
//下面的操作和服务端启动时注册时一样的逻辑 这里回顾下
//调用线程选择器获取获取EventExecutor[]数组中的一个也就是NioEventLoop 来执行register操作
//register是一个异步任务,NioEventLoop的首次任务会先激活线程,调用DefaultThreadFactory创建一个线程,
//并指向给NioEventLoop.thread,所以说NioEventLoop就是一个工作线程(但只有有任务进来才会激活线程),
//线程开始执行NioEventLoop#run方法
//register任务会调用jdk方法 将SocketChannel(jdk) 注册到selector上获取selectionKey并保留,
//invokeHandlerAddedIfNeeded回调 往pipeline中加入我们自定义的childHandler,当然最后还是会移除,因为它是ChannelInitializer,
//再之后就是回调各个handler的channelRegistered方法了。与服务端注册不同的点是,
//服务端注册需要在bind操作完成后才会回调channelActive事件,
//这里的channelActive判断依据是SocketChannel(jdk)保持连接就通过,所以会立即回调,
//其中会完成将刚兴趣的操作SelectionKey.OP_READ绑定。
//至此NioSocketChannel就绪,可以监听read事件了 和客户端进行交互了。
childGroup.register(child).addListener(....)
}
}
可以看到主reactor线程监听到accept事件后,调用 AbstractNioMessageChannel.NioMessageUnsafe#read方法,其中会调用jdk方法 创建 SocketChannel,并创建NioSocketChannel。之后pipeline传递channelRead事件,ServerBootstrapAcceptor为channel设置options和attr属性,同时从NioEventloopGroup(subreactor线程池)中分配一个NioEventloop与channel绑定。NioEventloop将channel注册到自己维护的selector上,同时监听读事件。后续此连接的读写都由NioEventloop负责。一个NioEventloop可以同时管理多个channel。这也就是io多路复用,充分利用cpu效能。
2.1.3处理请求 worker read
这里是AbstractNioByteChannel,其子类NioSocketChannel,负责处理请求,所以叫worker AbstractNioByteChannel.NioByteUnsafe#read
public final void read() {
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
try {
do {
//分配ByteBuf
byteBuf = allocHandle.allocate(allocator);
//基于SocketChannel(jdk)读取数据到ByteBuf
allocHandle.lastBytesRead(doReadBytes(byteBuf));
...
allocHandle.incMessagesRead(1);
readPending = false;
//传递channelRead事件 从head节点开始,挨个ChannelInboundHandler调用
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
allocHandle.readComplete();
//传递 channelReadComplete事件 从head节点开始,挨个ChannelInboundHandler调用
pipeline.fireChannelReadComplete();
if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
...
}
}
subReactor线程监听到读事件后,基于SocketChannel(jdk)读取数据到ByteBuf,调用pipeline传递各类事件。
3.处理任务队列
SingleThreadEventExecutor#runAllTasks(long)
//timeoutNanos 最多执行多长时间 默认和处理io事件时间相等。
protected boolean runAllTasks(long timeoutNanos) {
//从scheduledTaskQueue队列中取出时间到了的任务 添加到taskQueue中,如果加不进去,再回到scheduledTaskQueue中
fetchFromScheduledTaskQueue();
//从taskQueue取出任务
Runnable task = pollTask();
if (task == null) {
afterRunningAllTasks();
return false;
}
//截止时间
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
//try catch task.run() 防止影响其他任务执行
safeExecute(task);
runTasks ++;
//每64次任务检查一次 看看是不是 当前时间已经超过截止时间了
if ((runTasks & 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
//处理尾部队列 tailTasks中的任务,hasTasks()方法有提到这个队列。
// tailTasks 相比于普通任务队列优先级较低,可以理解为是收尾任务
// 尾部队列并不常用,如果想对 Netty 的运行状态做一些统计数据,例如任务循环的耗时、占用物理内存的大小等等,都可以向尾部队列添加一个收尾任务完成统计数据的实时更新
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}