本文是Netty系列第6篇
上一篇文章我們從一個Netty的使用Demo,瞭解了用Netty構建一個Server服務端應用的基本方式。
並且從這個Demo出發,簡述了Netty的邏輯架構,並對Channel、ChannelHandler、ChannelPipeline、EventLoop、EventLoopGroup等概念有了初步的認識。
回顧一下邏輯架構圖。
今天主要是深入學習下邏輯架構中的EventLoop 和 EventLoopGroup,掌握Netty的執行緒模型,這是Netty最精髓的知識點之一。
本文預計閱讀時間約 15分鐘,將重點圍繞以下幾個問題展開:
- 什麼是Reactor執行緒模型?
- EventLoopGroup、EventLoop 怎麼實現Reactor執行緒模型?
- 深入Netty的執行緒模型優化
- Netty3和Netty4的執行緒模型變化
- 什麼是Netty4執行緒模型的無鎖序列化
- 從執行緒模型看最佳實踐
1.什麼是Reactor執行緒模型?
先來回顧下我們在Netty系列的第2篇介紹的I/O執行緒模型,包括BIO、NIO、I/O多路複用、訊號驅動IO、AIO。IO多路複用在Java中有專門的NIO包封裝了相關的方法。
前面的文章也說過,使用Netty而不是直接使用Java NIO包,就是因為Netty幫我們封裝了許多對NIO包的使用細節,做了許多優化。
其中非常著名的,就是Netty的「Reactor執行緒模型」。
前置知識如果還不太清楚,可以回頭看看前面幾篇文章:
《沒搞清楚網路I/O模型?那怎麼入門Netty》
《從網路I/O模型到Netty,先深入瞭解下I/O多路複用》
《從I/O多路複用到Netty,還要跨過Java NIO包》
Reactor模式 是一種「事件驅動」模式。
「Reactor執行緒模型」就是通過 單個執行緒 使用Java NIO包中的Selector的select()方法,進行監聽。當獲取到事件(如accept、read等)後,就會分配(dispatch)事件進行相應的事件處理(handle)。
如果要給 Reactor執行緒模型 下一個更明確的定義,應該是:
Reactor執行緒模式 = Reactor(I/O多路複用)+ 執行緒池
其中Reactor負責監聽和分配事件,執行緒池負責處理事件。
然後,根據Reactor的數量和執行緒池的數量,又可以將Reactor分為三種模型
- 單Reactor單執行緒模型 (固定大小為1的執行緒池)
- 單Reactor多執行緒模型
- 多Reactor多執行緒模型 (一般是主從2個Reactor)
1.1 單Reactor單執行緒模型
Reactor內部通過 selector 監聽連線事件,收到事件後通過dispatch進行分發。
- 如果是連線建立的事件,通過accept接受連線,並建立一個Handler來處理連線後續的各種事件。
- 如果是讀寫事件,直接呼叫連線對應的Handler來處理,Handler完成 read => (decode => compute => encode) => send 的全部流程
這個過程中,無論是事件監聽、事件分發、還是事件處理,都始終只有 一個執行緒 執行所有的事情。
缺點:
在請求過多時,會無法支撐。因為只有一個執行緒,無法發揮多核CPU效能。
而且一旦某個Handler發生阻塞,服務端就完全無法處理其他連線事件。
1.2 單Reactor多執行緒模型
為了提高效能,我們可以把複雜的事件處理handler交給執行緒池,那就可以演進為 「單Reactor多執行緒模型」 。
這種模型和第一種模型的主要區別是把業務處理從之前的單一執行緒脫離出來,換成執行緒池處理。
1)Reactor執行緒
通過select監聽客戶請求,如果是連線建立的事件,通過accept接受連線,並建立一個Handler來處理連線後續的讀寫事件。這裡的Handler只負責響應事件、read和write事件,會將具體的業務處理交由Worker執行緒池處理。
只處理連線事件、讀寫事件。
2)Worker執行緒池
處理所有業務事件,包括(decode => compute => encode) 過程。
充分利用多核機器的資源,提高效能。
缺點:
在極個別特殊場景中,一個Reactor執行緒負責監聽和處理所有的客戶端連線可能會存在效能問題。例如併發百萬客戶端連線(雙十一、春運搶票)
1.3 多Reactor多執行緒模型
為了充分利用多核能力,可以構建兩個 Reactor,這就演進為 「主從Reactor執行緒模型」 。
1)主Reactor
主 Reactor 單獨監聽server socket,accept新連線,然後將建立的 SocketChannel 註冊給指定的 從Reactor,
2)從Reactor
從Reactor 將連線加入到連線佇列進行監聽,並建立handler進行事件處理。執行事件的讀寫、分發,把業務處理就扔給worker執行緒池完成。
3)Worker執行緒池
處理所有業務事件,充分利用多核機器的資源,提高效能。
輕鬆處理百萬併發。
缺點:
實現比較複雜。
不過有了Netty,一切都變得簡單了。
Netty幫我們封裝好了一切,可以快速使用主從Reactor執行緒模型(Netty4的實現上增加了無鎖序列化設計),具體程式碼這裡就不貼了,可以看看上一篇的Demo。
2.EventLoop、EventLoopGroup 怎麼實現Reactor執行緒模型?
上面我們已經瞭解了Reactor執行緒模型,瞭解了它的核心就是:
Reactor執行緒模式 = Reactor(I/O多路複用)+ 執行緒池
它的執行模式包括四個步驟:
- 連線註冊:建立連線後,將channel註冊到selector上
- 事件輪詢:selcetor上輪詢(select()函式)獲取已經註冊的channel的所有I/O事件(多路複用)
- 事件分發:把準備就緒的I/O事件分配到對應執行緒進行處理
- 事件處理:每個worker執行緒執行事件任務
那這樣的模型在Netty中具體怎麼實現呢?
這就需要我們瞭解下EventLoop和EventLoopGroup了。
2.1 EventLoop是什麼
EventLoop 不是Netty獨有的,它本身是一個通用的 事件等待和處理的程式模型。主要用來解決多執行緒資源消耗高的問題。例如 Node.js 就採用了 EventLoop 的執行機制。
那麼,在Netty中,EventLoop是什麼呢?
- 一個Reactor模型的事件處理器。
- 單獨一個執行緒。
- 一個EventLoop內部會維護一個selector和一個「taskQueue任務佇列」,分別負責處理 「I/O事件」 和 「任務」。
「taskQueue任務佇列」是多生產者單消費者佇列,在多執行緒併發新增任務時,可以保證執行緒安全。
「I/O事件」即selectionKey中的事件,如accept、connect、read、write等;
「任務」包括 普通任務、定時任務等。
- 普通任務:通過 NioEventLoop 的 execute() 方法向任務佇列 taskQueue 中新增任務。例如 Netty 在寫資料時會封裝 WriteAndFlushTask 提交給 taskQueue。
- 定時任務:通過呼叫 NioEventLoop 的 schedule() 方法向 定時任務佇列 scheduledTaskQueue 新增一個定時任務,用於週期性執行該任務(如心跳訊息傳送等)。定時任務佇列的任務 到了執行時間後,會合併到 普通任務 佇列中進行真正執行。
一圖勝千言:
EventLoop單執行緒執行,迴圈往復執行三個動作:
- selector事件輪詢
- I/O事件處理
- 任務處理
2.2 EventLoopGroup是什麼
EventLoopGroup比較簡單,可以簡單理解為一個“EventLoop執行緒池”。
Tips:
監聽一個埠,只會繫結到 BossEventLoopGroup 中的一個 Eventloop,所以, BossEventLoopGroup 配置多個執行緒也無用,除非你同時監聽多個埠。
2.3 具體實現
Netty可以通過簡單配置,支援單Reactor單執行緒模型 、單Reactor多執行緒模型 、多Reactor多執行緒模型。
我們以 「多Reactor多執行緒模型」 為例,來看看Netty是如何通過EventLoop來實現的。
還是一圖勝千言:
我們結合Reactor執行緒模型的四個步驟來梳理一下:
1)連線註冊
master EventLoopGroup中有一個EventLoop,繫結某個特定埠進行監聽。
一旦有新的連線進來觸發accept型別事件,就會在當前EventLoop的I/O事件處理階段,將這個連線分配給slave EventLoopGroup中的某一個EventLoop,進行後續 事件的監聽。
2)事件輪詢
slave EventLoopGroup中的EventLoop,會通過selcetor對繫結到自身的channel進行輪詢,獲取已經註冊的channel的所有I/O事件(多路複用)。
當然,EventLoopGroup中會有 多個EventLoop 執行,各自迴圈處理。具體EventLoop數量是由 使用者指定的執行緒數 或者 預設為核數的2倍。
3)事件分發
當slave EventLoopGroup中的EventLoop獲取到I/O事件後,會在EventLoop的 I/O事件處理(processSelectedKeys) 階段分發給對應ChannelPipeline進行處理。
注意,仍然在當前執行緒進行序列處理
4)事件處理
在ChannelPipeline中對I/O事件進行處理。
I/O事件處理完後,EventLoop在 任務處理(runAllTasks) 階段,對佇列中的任務進行消費處理。
至此,我們就能完全梳理清楚EventLoopGroup/EventLoop 和 Reactor執行緒模型的關係了。
咦,好像有什麼地方不對勁?
沒錯,細心的朋友可能會發現,slave EventLoopGroup中並不是
一個selector + 執行緒池
而是有多個EventLoop組成的
多selector + 多個單執行緒
這是為什麼呢?
那就得繼續深入瞭解下Netty4的執行緒模型優化了。
3.深入Netty的執行緒模型優化
上文說過,對每個EventLoop來說,都是單執行緒執行,並迴圈往復執行三個動作:
- selector事件輪詢
- I/O事件處理
- 任務處理
在slave EventLoopGroup中,並不是 “一個selector + 執行緒池”模式,而是有多個EventLoop組成的 “多selector + 多個單執行緒“ 模型,這是為什麼呢?
這主要是因為我們分析的是Netty4的執行緒模型,跟Netty3的傳統Reactor模型相比有了不同之處。
3.1 Netty3和Netty4的執行緒模型變化
在Netty3的執行緒模型中,分為 讀事件處理模型 和 寫事件處理模型。
- read事件的ChannelHandler都是由Netty的 I/O 執行緒(對應Netty 4 中的 EventLoop)中負責執行。
- I/O執行緒排程執行ChannelPipeline中Handler鏈的對應方法,直到業務實現的End Handler。
- End Handler將訊息封裝成Runnable,放入到業務執行緒池中執行,I/O執行緒返回,繼續讀/寫等I/O操作。
- write事件是由呼叫執行緒處理,可能是 I/O 執行緒,也可能是業務執行緒。
- 如果是業務執行緒,那麼業務執行緒會執行ChannelPipeline中的Channel Handler。
- 執行到系統最後一個ChannelHandler,將編碼後的訊息Push到傳送佇列中,業務執行緒返回。
- Netty的I/O執行緒從傳送訊息佇列中取出訊息,呼叫SocketChannel的write方法進行訊息傳送。
由上文可以看到,在Netty3的執行緒模型中,是採用“selector + 業務執行緒池”的模型。
注意,在這種模型下,讀寫模型不一致。尤其是讀事件、寫事件的「執行執行緒」是不一樣的。
但是在Netty4的執行緒模型中,採用了“多selector + 多個單執行緒”模型。
讀事件:
- I/O執行緒NioEventLoop從SocketChannel中讀取資料包,將ByteBuf投遞到ChannelPipeline,觸發ChannelRead事件;
- I/O執行緒NioEventLoop呼叫ChannelHandler鏈,直到將訊息投遞到業務執行緒,然後I/O執行緒返回,繼續後續的操作。
寫事件:
- 業務執行緒呼叫ChannelHandlerContext.write(Object msg)方法進行訊息傳送。
- ChannelHandlerInvoker將傳送訊息封裝成 任務,放入到EventLoop的Mpsc任務佇列中,業務執行緒返回。後續由EventLoop在迴圈中統一排程和執行。
- I/O執行緒EventLoop在進行 任務處理 時,從Mpsc任務佇列中獲取任務,呼叫ChannelPipeline進行處理,處理Outbound事件,直到將訊息放入傳送佇列,然後喚醒Selector,執行寫操作。
Netty4中,無論讀寫,都是通過I/O執行緒(也就是EventLoop)來統一處理。
為什麼Netty4的執行緒模型做了這樣的變化?答案就是 無鎖序列化設計。
3.2 什麼是Netty4執行緒模型的無鎖序列化
我們先看看Netty3的執行緒模型存在什麼問題:
- 讀/寫執行緒模型 不一致,帶來額外的開發心智負擔。
- 寫操作由業務執行緒發起時,通常業務會使用 執行緒池多執行緒併發執行 某個業務流程,所以某一個時刻會有多個業務執行緒同時操作ChannelHandler,我們需要對ChannelHandler進行併發保護,大大降低了開發效率。
- 頻繁的執行緒上下文切換,會帶來額外的效能損耗。
而Netty4執行緒模型的 「無鎖序列化」設計,就很好地解決了這些問題。
一圖勝千言:
從事件輪詢、訊息的讀取、編碼以及後續Handler的執行,始終都由I/O執行緒NioEventLoop內部進行序列操作,這就意味著整個流程不會進行執行緒上下文的切換,避免多執行緒競爭導致的效能下降,資料也不會面臨被併發修改的風險。
表面上看,序列化設計似乎CPU利用率不高,併發程度不夠。但是,通過調整slave EventLoopGroup的執行緒引數,可以同時啟動多個NioEventLoop,序列化的執行緒並行執行,這種區域性無鎖化的序列執行緒設計相比「一個佇列-多個工作執行緒模型」效能更優。
總結下Netty4無鎖序列化設計的優點:
- 一個EventLoop會處理一個channel全生命週期的所有事件。從訊息的讀取、編碼以及後續Handler的執行,始終都由I/O執行緒NioEventLoop負責。
- 每個EventLoop會有自己獨立的任務佇列。
- 整個流程不會進行執行緒上下文的切換,資料也不會面臨被併發修改的風險。
- 對於使用者而言,統一的讀寫執行緒模型,也降低了使用的心智負擔。
4.從執行緒模型看最佳實踐
NioEventLoop 無鎖序列化的設計這麼好,它就完美無缺了嗎?
不是的!
在特定的場景下,Netty3的執行緒模型可能效能更高。比如編碼和其它寫操作非常耗時,由多個業務執行緒併發執行,效能肯定高於單個EventLoop執行緒序列執行。
因此,雖然單執行緒執行避免了執行緒切換,但是它的缺陷就是不能執行時間過長的 I/O 操作,一旦某個 I/O 事件發生阻塞,那麼後續的所有 I/O 事件都無法執行,甚至造成事件積壓。
所以,Netty4的執行緒模型的最佳實踐需要注意以下兩點:
- 無論讀/寫,不在自定義ChannelHandler中做耗時操作。
- 不把耗時操作放進 任務佇列。
本文深入學習了Netty邏輯架構中的EventLoop,掌握Netty最精髓的知識點 執行緒模型。
從Reactor執行緒模型開始說起,到Netty如何用EventLoop實現Reactor執行緒模型。
然後對Netty4的執行緒模型優化做了詳細介紹,尤其是「無鎖序列化設計」。
最後從EventLoop執行緒模型出發,說明了日常開發中使用Netty4開發的最佳實踐。
希望大家能對EventLoop有全面的認識。
另外,限於篇幅,EventLoop中有兩個非常重要的資料結構沒有展開介紹,你們知道是什麼嗎?
後面會單獨寫兩篇進行分析,敬請期待。
參考書目:
《Netty in Action》
都看到最後了,原創不易,點個關注,點個贊吧~
文章持續更新,可以微信搜尋「阿丸筆記 」第一時間閱讀,回覆【筆記】獲取Canal、MySQL、HBase、JAVA實戰筆記,回覆【資料】獲取一線大廠面試資料。
知識碎片重新梳理,構建Java知識圖譜:github.com/saigu/JavaK…(歷史文章查閱非常方便)