引言
上篇文章我們分析了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中。