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