引言
上篇文章我们分析了ServerBootstrap的启动这部分源码,从整体的角度初步接触了netty的编码设计的魅力,今天我们来分析一下Netty里面另一个核心角色–EventLoop。用过netty的同学应该都或多或少知道Netty的高性能很大一部分功劳都在EventLoop的设计,尤其是EventLoop对Reactor线程模型的实现,大大提升了Netty的并发能力,接下来我们就来揭开EventLoop的神秘面纱
UNIX网络I/O模型
Netty一直以其优异的性能表现收获粉丝无数,有研究称netty可以撑过单机10w的TPS。如此高的性能自然离不开精良的线程模型设计和UNIX网络I/O编程模型的选用,我们在看源码前先了解下UNIX网了I/O编程模型和Reactor线程模型。
UNIX网络I/O模型本来有五种,这里只列出三种与我们理解Netty源码有关的,即阻塞I/O模型、非阻塞I/O模型、I/O多路复用模型。
阻塞I/O模型 在JDK1.4之前默认使用的就是阻塞I/O模型,阻塞I/O模型所有的文件操作都是阻塞的。我们以套接字接口为例:在进程空间中调用recvfrom,系统调用知道数据包到达并且被复制到应用进程(用户态)的缓冲区或者发生错误时才返回,在返回之前一直等待,进程从调用recvfrom到返回一直被阻塞。了解这一点,我们就知道为什么JDK1.4之前java的网络通信为什么如此为人所诟病了,只要并发量上来,假设因为某些客观原因网络拥堵,很有可能会有大量accept进来的客户端连接会被阻塞,导致大量连接超时。
非阻塞I/O模型 非阻塞I/O模型与阻塞I/O模型的唯一区别就是recvfrom系统调用发出后,如果应用进程的缓存区没有数据,就直接返回一个EWOULDBLOCK的错误,一般都是使用一个线程进行轮询这个状态,看内核是否有数据到来。这个非阻塞I/O模型对于阻塞I/O模型是个进步,但是为每个连接都是用一个线程进行轮询,这会对cpu时间大量的耗费,性能也不高。
I/O多路复用模型 进程通过将一个或多个文件描述符(fd)传递给select/poll系统调用,阻塞在select操作上,虽然都会阻塞,但是可以实现多个fd阻塞在这个select上,这样我们通过一个线程就可以使用select/poll监听多个文件描述符是否处于就绪状态,这个进步是巨大的,一个线程可以监听大量的连接情况,Java的NIO就是使用的I/O多路复用模型,而Netty就是使用了Java的NIO。
Reactor线程模型
Reactor是一个形象的名称,具体下来可以理解为这么一段伪代码 while(true) {selector.select();后续处理…} ,就是不断的select一把,然后对select出来的连接进行对应的业务操作。Reactor模型实质上是一个观察者模式,所有的事件处理器注册在Dispatcher上,只要select出多个连接,并监听感兴趣的事件,Dispatcher就调用这些对应的事件处理器,并且这些操作是异步的。Reactor线程模型有三种:Reactor单线程模型、Reactor多线程模型、主从多线程模型。
Reactor单线程模型
Reactor单线程模型比较简单,就是一个线程把select操作、监听读写事件,处理业务操作全在一个线程内,虽然简单但也强大,因为使用的I/O多路复用模型,除非你的业务逻辑比较耗时,一般体量的业务Reactor单线程模型就足够了。但是一旦并发连接数上来恐怕是hold不住,单线程无法满足大量的消息编解码、消息读取和发送,可能会导致大量的连接超时。基于这些问题演进了Reactor多线程模型。
Reactor多线程模型
Reactor多线程模型为了解决单线程无法满足大量消息编解码,I/O事件的处理,将Accept事件和读写I/O事件区分开,Accept事件单独使用单线程处理,因Accept操作简单,资源占用少,所以单线程足以支撑海量连接的Accept的操作。而I/O读写事件通过一个线程池处理.Reactor多线程模型充分利用了现代计算机多核cpu的特性,大大提升了TCPServer的并发性能。而Netty正是使用了Reactor多线程模型,只不过在Netty中对多线程模型的线程池做了适应性改造,一个连接绑定一个线程,而不是像JDK原生线程池一样通过一个队列接收所有的task,多个线程竞争获取task执行。这样减少了因为同步导致的资源消耗,一定程度上提高了性能。除了Reactor多线程模型,还有一种Reactor主从多线程模型。
Reactor主从多线程模型
其实一般项目使用Reactor多线程模型足够的,目前本人并不太理解这个主从多线程模型的需求,因为Accept操作并不太消耗资源,加上使用的是I/O多路复用模型,一个线程处理足以大量的Accept请求。尽管如此我们还是简单了解一下主从多线程模型吧,有对主从多线程模型理解更深的欢迎交流。
主从多线程模型除了对Accept操作把单线程换成一个线程池之外,其他的和Reactor多线程模型一模一样。应该是觉得可能在Accept阶段如果使用单线程处理会成为一个瓶颈,所以Accept阶段也使用一个线程池处理,查看李林峰老师的Netty权威指南也指明了很多项目有安全需求,比如服务端需要对客户端握手进行安全认证,安全认证的过程是比较消耗性能的,这种场景下单线程处理Accept过程可能存在性能问题。
NioEventLoopGroup初始化
了解了UNIX网络I/O模型和Reactor线程模型之后我们正式开始了解Netty的EventLoop。我们可以回忆一下我们是怎么在启动ServerBootstrap时设置EventLoop的.
this.bossGroup = new NioEventLoopGroup(bossThreadNumber, new DefaultThreadFactory(bossThreadName, true));
this.workerGroup = new NioEventLoopGroup(workerThreadNumber, new DefaultThreadFactory(workerThreadName, true));
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(this.bossGroup, this.workerGroup).channel(NioServerSocketChannel.class).childHandler(channelInitializer);
我们创建两个NioEventLoopGroup实例,从名称可以大概猜到这两个是线程池,一个处理三次握手的Accept过程,一般我们称为Boss线程池,一个是处理读写I/O的网络事件,一般我们称为worker线程池。我们先看看这个NioEventLoopGroup的类层次结构
可以看到NioEventLoopGroup类的继承体系还是比较复杂的,首先继承了MultithreadEventLoopGroup,然后继承MultithreadEventExecutorGroup,AbstractEventExecutorGroup,并且还实现了ScheduledExecutorService接口,我们可以初步判断这个NioEventLoopGroup不仅仅是个线程池可以提交任务执行,还能提交定时任务执行。接下来我们看看NioEventLoopGroup的初始化。
可以看到NioEventLoop经过自己的构造函数多层调用后拿到了线程数、threadFactory、selectorProvider,selectStrategyFactory和rejectHandler调用了父类MultithreadEventLoopGroup的构造函数。其中线程数和threadFactory我们不用多说大家都清楚,而selectorProvider就是提供selector对象的,而selectStrategyFactory这里用不到先不说。
接下来MultithreadEventLoopGroup又经过层层调用拿到了ThreadPreTaskExecutor实例和DefaultEventExecutorChooserFactory实例,这里ThreadPreTaskExecutor实现了Executor接口,所以ThreadPreTaskExecutor基本实现了线程池的功能–创建线程和启动线程。而这个DefaultEventExecutorChooserFactory则是多线程中选择的负载均衡器,说白了就是线程选择器。最终调用到了MultithreadEventLoopGroup关键的构造函数,我们看看代码
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
//去除无关代码
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
throw new IllegalStateException("failed to create a child event loop", e);
}
chooser = chooserFactory.newChooser(children);
}
在这个关键的构造函数里面初始化了nthreads个EventExecutor数组,for循环创建子线程,这个newChild方法是个抽象方法,实现在NioEventLoopGroup中。
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}
进入NioEventLoop类中我们可以看到该类也很复杂,查看类层次结构,可以看到它的父类维护了thread的属性,所以我们可以认为NioEventLoop其实是线程包装类,所以我们可以认为这个EventExecutor数组其实是NioEventLoop–线程包装类数组。所以我们已经可以断定这个MultithreadEventLoopGroup其实就是线程池。
NioEventLoop
NioEventLoop类层次结构也很复杂,首先它继承SingleThreadEventLoop,看到类名大概也知道了这个NioEventLoop首先是个单线程包装类,然后SingleThreadEventLoop继承SingleThreadEventExecuto、AbstractScheduledEventExecutor,最上层还实现了ScheduledExecutorService,所以NioEventLoop还实现了scheduled的逻辑可以执行定时调度任务。我们看看类图
首先我们看看NioEventLoop的构造函数
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
if (selectorProvider == null) {
throw new NullPointerException("selectorProvider");
}
if (strategy == null) {
throw new NullPointerException("selectStrategy");
}
provider = selectorProvider;
final SelectorTuple selectorTuple = openSelector();
selector = selectorTuple.selector;
unwrappedSelector = selectorTuple.unwrappedSelector;
selectStrategy = strategy;
}
NioEventLoop的构造函数拿到了selectorProvider和selectStrategy,并赋值给全局变量,最后通过openSelector()方法拿到一个selector对象,并通过selectorTuple包装好。到这我们大概可以知道每个线程持有一个selector对象,联系到上篇文章分析的channel的注册过程,我们应该知道每个线程应该会通过一个忙循环来select一把来监听所有感兴趣的I/O事件,最终通过pipeline把事件传到业务handler中对这些I/O事件进行处理。最后我们看看刚刚说的父类SingleThreadEventExecutor类构造函数。
private final Queue<Runnable> taskQueue;
private volatile Thread thread;
@SuppressWarnings("unused")
private volatile ThreadProperties threadProperties;
private final Executor executor;
private volatile boolean interrupted;
private final Semaphore threadLock = new Semaphore(0);
private final Set<Runnable> shutdownHooks = new LinkedHashSet<Runnable>();
private final boolean addTaskWakesUp;
private final int maxPendingTasks;
private final RejectedExecutionHandler rejectedExecutionHandler;
private long lastExecutionTime;
@SuppressWarnings({ "FieldMayBeFinal", "unused" })
private volatile int state = ST_NOT_STARTED;
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
boolean addTaskWakesUp, int maxPendingTasks,
RejectedExecutionHandler rejectedHandler) {
super(parent);
this.addTaskWakesUp = addTaskWakesUp;
this.maxPendingTasks = Math.max(16, maxPendingTasks);
this.executor = ObjectUtil.checkNotNull(executor, "executor");
taskQueue = newTaskQueue(this.maxPendingTasks);
rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}
可以看到它持有了一个thread,并且还在构造函数中new了一个TaskQueue可以接收任务执行,这个TaskQueue也证实了Netty的EventLoop不仅可以执行I/O任务,也可以把业务任务提交到TaskQueue里执行用户非I/O任务的业务任务。
NioEventLoop启动
经过NioEventLoopGroup的初始化分析,我们知道了NioEventLoopGroup是个线程池,具体子线程维护在NioEventLoop中,并且在上篇文章服务端启动分析中我们也知道了eventLoop是在channel注册的时候和channel绑定了,接下来我们来看看NioEventLoop的线程到底是什么时候启动的。看SingleThreadEventEexecutor代码时,很容易就能注意到startThread()这个方法,从方法名就知道,该方法就是启动线程的方法。
private void startThread() {
if (state == ST_NOT_STARTED) {
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
doStartThread();
}
}
}
private void doStartThread() {
assert thread == null;
executor.execute(new Runnable() {
@Override
public void run() {
thread = Thread.currentThread();
if (interrupted) {
thread.interrupt();
}
try {
SingleThreadEventExecutor.this.run();
success = true;
} catch (Throwable t) {
logger.warn("Unexpected exception from an event executor: ", t);
}
});
}
//删除无关代码
可以看到SingleThreadEventExecutor内部维护了线程状态state,如果线程还没启动,那么先CAS把状态修改为启动状态,接着调用doStartThread方法,在doStartThread方法中通过子类传上来的executor对象调用execute方法,而这个executor对象其实是ThreadPreTaskExecutor类,execute方法则是通过threadFactory创建线程并启动线程。
public final class ThreadPerTaskExecutor implements Executor {
private final ThreadFactory threadFactory;
public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
if (threadFactory == null) {
throw new NullPointerException("threadFactory");
}
this.threadFactory = threadFactory;
}
@Override
public void execute(Runnable command) {
threadFactory.newThread(command).start();
}
}
接着回到上面SingleThreadEventExecutor的doStartThread方法,线程启动后把当前的线程赋值给thread这个全局变量,接着调用SingleThreadEventExecutor的run方法,而这个方法不出意外是个抽象方法,其具体实现在子类NioEventLoop中
@Override
protected void run() {
for (;;) {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
// fallthrough
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
看到这个run方法,更加证实了之前我们说的eventLoop与selector绑定,通过一个忙循环监听I/O事件并处理的逻辑。那么到现在我们就只剩一个问题了,什么时候调用了这个startThread方法?要解答这个问题,我们还是回顾下Channel注册到Selector上的过程吧。
AbstractBootstrap.initAndRegister()
->MultithreadEventLoopGroup.register(Channel channel)
->SingleThreadEventLoop.register(Channel channel)
->SingleThreadEventLoop.register(ChannelPromise promise)
->AbstractChannel.AbstractUnsafe.register(EventLoop eventLoop,ChannelPromise promise)
在ServerBootstrap.bind的时候,会先初始化Channel和把Channel注册到Selector上,整个调用链如上所示,最终调用的是Unsafe的注册方法,unsafe的注册方法如下
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
//忽略无关代码
AbstractChannel.this.eventLoop = eventLoop;
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
logger.warn(
"Force-closing a channel whose registration task was not accepted by an event loop: {}",
AbstractChannel.this, t);
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
}
在这里会先把eventLoop绑定到channel上,然后调用eventLoop.inEventLoop()
方法判断当前的线程是否就是该eventLoop持有的线程,因为是在初始化阶段,eventLoop的线程都还没启动,这里eventLoop.inEventLoop()返回当然是false,所以执行else阶段的代码,eventLoop.execute(new Runnable(){})这个execute是个接口方法,真正实现在SingleThreadEventExecutor类里面。
@Override
public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}
boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
startThread();
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}
同样,这个execute方法先判断是否为eventLoop自己持有的线程在执行,在这当然是false,然后就来到了else部分,在这会执行两个方法startThread()和addTask(task),还记得上面分析的startThread()么,这就串起来了,通过提交注册channel到selecot上的任务,触发线程的启动。启动之后把刚刚提交的注册任务添加到TaskQueue中,这个注册任务非I/O处理任务,所以提交到TaskQueue中。