老面試官竟問我 Reactor 在 Netty 中是如何實現的
本文轉載自微信公眾號「yes的練級攻略」,作者是Yes呀。轉載本文請聯系yes的練級攻略公眾號。
你好,我是yes。
開年第一篇技術文哈,這是之前的存貨,最近一段時間的更新還是會以面試題為主,畢竟金三銀四哈。
這篇其實也算是一個面試點,畢竟 Reactor 也是可能被問到的,讓我們來看看 Reactor 具體是如何在 Netty 中落地實現的。
可以加深下對 Reactor 的印象,還有 Reactor 模型的演進過程。
對了,之前已經寫了一篇對 Reactor 的理解,沒看過的建議先看那篇,然后再來看這篇。
話不多說,發車!
Netty 的 Reactor
我們都知道 Netty 可以有兩個線程組,一個是 bossGroup,一個是 workerGroup。
之前也提到了 bossGroup 主要是接待新連接(老板接活),workerGroup 是負責新連接后續的一切 I/O (員工干活)
對應到 Reactor 模型中,bossGroup 中的 eventLoop 就是主 Reactor。它的任務就是監聽等待連接事件的到來,即 OP_ACCEPT。
然后創建子 channel ,從 workerGroup 中選擇一個 eventLoop ,將子 channel 與這個 eventLoop 綁定,之后這個子 channel 對應的 I/O事件,都由這個 eventLoop 負責。
而這個 workerGroup 中的 eventLoop 就是所謂的子 Reactor,它的任務就是負責已經建連完畢的連接之后的所有 I/O 請求。
其實從 eventLoop 這個名字就能看出,它的作用就是 loop event,說白了就是一個線程,死循環的等待事件的發生,然后根據不同的事件類型進行不一樣的后續處理,僅此而已。
正常情況下 bossGroup 只會配置一個 eventLoop,即一個線程,因為一般服務只會暴露一個端口,所以只要一個 eventLoop 監聽這個端口,然后 accept 連接。
而 workerGroup 在 Netty 中,默認是 cpu 核心數*2,例如 4 核 CPU ,默認會在 workerGroup 建 8 個 eventLoop,所以就有 8 個子 Reactor。
所以正常 Netty 服務端的配置是,1個主 Reactor,多個從 Reactor,這就是所謂的主從 Reactor。
基本上現在的主流配置都是主從 Reactor。
關于 Reactor 模型的演進
在深入 Netty 的 Reactor 實現之前,我們先來看看,為什么會演變成主從 Reactor?
最開始的模型是單 Reactor 單線程 ,你可以理解成一個線程來即監聽新的連接,又要響應老的連接的請求,如果邏輯處理的很快,那沒有問題,看看人家 redis 就夠用,但是如果邏輯處理的慢,那就會阻塞其他請求。
所以就有了單 Reactor 多線程,還是由一個線程來監聽所有的底層 Socket,但是一些耗時的操作可以分配給線程池進行業務處理,這樣就不會因為邏輯處理慢導致 Reactor 的阻塞。
但是這個模型還會有瓶頸,即監聽新的連接和響應老的連接的請求都由一個線程處理,積累的老連接多了,有很多事件需要響應,就會影響新連接的接入,這就不太舒服了,況且我們現在都是多核 CPU,還差這么一個線程嗎?
所以就又演進成主從 Reactor,由一個線程,即主 Reactor 專門等待新連接的建連,然后創建多個線程作為子 Reactor,均勻的負責已經接入的老連接,這樣一來既不會影響接待新連接的速度,也能更好的利用多核 CPU 的能力響應老連接的請求。
這就是關于 Reactor 模型的演進了。
好了,接下來我們再看看 Netty 實現 Reactor 的核心類,我們現在一般都是用 NIO ,所以我們看 NioEventLoop 這個類。
友情提示,有條件建議在PC端看下面的內容,源碼類的手機上看不太舒服
NioEventLoop
前面我們已經提到一個 NioEventLoop 就是一個線程,那線程的核心肯定就是它的 run 方法。
基于我們的理解,我們知道這個 run 方法的主基調肯定是死循環等待 I/O 事件產生,然后處理事件。
事實也是如此, NioEventLoop 主要做了三件事:
- select 等待 I/O 事件的發生
- 處理發生的 I/O 事件
- 處理提交至線程中的任務,包括提交的異步任務、定時任務、尾部任務。
首先折疊下代碼,可以看到妥妥的死循環,這也是 Reactor 線程的標配,這輩子無限只為了等待事件發生且處理事件。
在 Netty 的實現里,NioEventLoop 線程不僅要處理 I/O 事件,還需要處理提交的異步任務、定時任務和尾部任務,所以這個線程需要平衡 I/O 事件處理和任務處理的時間。
因此有個 selectStrategy 這樣的策略,根據判斷當前是否有任務在等待被執行,如果有則立即進行一次不會阻塞的 select 來嘗試獲取 I/O 事件,如果沒任務則會選擇 SelectStrategy.SELECT 這個策略。
從圖中也可以看到,這個策略會根據最近將要發生的定時任務的執行時間來控制 select 最長阻塞的時間。
從下面的代碼可以看到,根據定時任務即將執行的時間還預留了 5 微秒的時間窗口,如果 5 微秒內就要到了,那就不阻塞了,直接進行一個非阻塞的 select 立刻嘗試獲取 I/O 事件。
經過上面的這個操作,select 算是完畢了,最終會把就緒的 I/O 事件個數賦值給 strategy,如果沒有的話那 strategy 就是 0 ,接著就該處理 I/O 事件和任務了。
上面代碼我把重點幾個部分都框出來了,這里有個 selectCnt 來統計 select 的次數,這個用于處理 JDK Selector 空輪詢的 bug ,下面會提。
ioRatio 這個參數用來控制 I/O 事件執行的時間和任務執行時間的占比,畢竟一個線程要做多個事情,要做到雨露均沾對吧,不能冷落了誰。
可以看到,具體的實現是記錄 I/O 事件的執行時間,然后再根據比例算出任務能執行的最長的時間來控制任務的執行。
I/O 事件的處理
我們來看看 I/O 事件具體是如何處理的,也就是 processSelectedKeys 方法。
點進去可以看到,實際上會有兩種處理的方法,一種是優化版,一種是普通版。
這兩個版本的邏輯都是一樣的,區別就在于優化版會替換 selectedKeys 的類型,JDK 實現的 selectedKeys 是 set 類型,而 Netty 認為這個類型的選擇還是有優化的余地的。
Netty 用 SelectedSelectionKeySet 類型來替換了 set 類型,其實就是用數組來替換了 set
相比 set 類型而言,數組的遍歷更加高效,其次數組尾部添加的效率也高于 set,畢竟 set 還可能會有 hash沖突。當然這是 Netty 為追求底層極致優化所做的,我們平日的代碼沒必要這般“斤斤計較”,意義不大。
那 Netty 是通過什么辦法替換了這個類型呢?
反射。
看下代碼哈,不是很復雜:
這也能給我們提供一些思路,比方你調用三方提供的 jar 包,你無法修改它的源碼,但是你又想對它做一些增強,那么就可以仿照 Netty 的做法,通過反射來替換之~
我們打個斷點看下替換前后 selectedKey 的類型,之前是 HashSet:
替換了后就變成了 SelectedSelectionKeySet 了。
ok,現在我們再看下優化版的處理 I/O 事件的遍歷方法,和普通版邏輯一樣的,只是遍歷是利用數組罷了。
沒啥好說的,就那個幫助 GC 可以提一下,如果你看過很多開源軟件你就會發現有很多這樣的實現,直接置為 null 的語句,這是為了幫助 GC。
緊接著看下真正處理 I/O 事件的方法 processSelectedKey
可以看到,這個方法本質就是根據不同的事件進行不同的處理,實際上會將事件在對應的 channel 的 pipeline 上面傳播,并且觸發各種相應的自定義事件,我拿 OP_ACCEPT 事件作為例子分析。
針對 OP_ACCEPT 事件,unsafe.read 實際會調用 NioMessageUnsafe#read 方法。
從上面代碼來看,邏輯并不復雜,主要就是循環讀取新建立的子 channel,并觸發 ChannelRead 和 ChannelReadComplete 事件,使之在 pipeline 中傳播,期間就會觸發之前添加的 ServerBootstrapAcceptor#channelRead,將其分配給 workerGroup 中的 eventLoop ,即子 Reactor 線程。
當然,我們自定義的 handler 也可以實現這兩個事件方法,這樣對應的事件到來后,我們能進行相應的邏輯處理。
好了,Netty 的 OP_ACCEPT 事件處理分析到此結束,其他事件也是類似的,都會觸發相應的事件,然后在 pipeline 中傳遞,觸發不同 Channelhandler 的方法,進行邏輯處理。
以上,就是 Netty 實現的主從 Reactor 模型。
當然,Netty 也支持單 Reactor,無非就是不要 workerGroup,至于線程數也可以自行配置,十分靈活,不過現在一般用的都是主從 Reactor 模型。
最后
這篇不僅講了 Netty 的 Reactor 實現,也把 Netty 是如何處理 I/O 操作的部分也囊括了。
下篇關于 Netty 的再盤盤 pipeline 機制,這個責任鏈模式也是很重要的,很有啟發性。
等 pipeline 寫完之后,你對 Netty 整體應該有一個比較清晰的認識了,然后會開始寫一些粘包半包、內存管理等內容,包括一些 Netty 的“高級”用法啥的,總之大概還有一半的內容沒寫,等寫完之后,完整的回顧一遍,出去可以拿 Netty “吹”了。
好嘞,等我更新哈,不多BB了。