1. 程式人生 > >Netty-Mina深入學習與對比(一)

Netty-Mina深入學習與對比(一)

感謝支付寶同事[易鴻偉]在本站釋出此文。

這博文的系列主要是為了更好的瞭解一個完整的nio框架的程式設計細節以及演進過程,我選了同父(Trustin Lee)的兩個框架netty與mina做對比。版本涉及了netty3.x、netty4.x、mina1.x、mina2.x、mina3.x。這裡並沒有寫netty5.x的細節,看了netty5的修改文件,似乎有一些比較有意思的改動,準備單獨寫一篇netty4.x與netty5.x的不同。

netty從twitter釋出的這篇《Netty 4 at Twitter: Reduced GC Overhead》文章讓國內Java界為之一振,也小火了一把,同時netty的社群發展也不錯,版本迭代非常快,半年不關注大、小版本就發了好幾輪了。但是mina就有點淡了,github上面它最後大多數程式碼最後的修改日期均在2013年,不過我從個人情感上還是挺喜歡mina3的程式碼,沒有太多的用不上的功能(支援各種協議啥的),跑自帶的benchmark效能也比netty4好一些。但是如果是生產用的話,就偏向netty多一些了,畢竟社群活躍,版本迭代也快。

1. mina、netty的執行緒模型

mina與netty都是Trustin Lee的作品,所以在很多方面都十分相似,他們執行緒模型也是基本一致,採用了Reactors in threads模型,即Main Reactor + Sub Reactors的模式。由main reactor處理連線相關的任務:accept、connect等,當連線處理完畢並建立一個socket連線(稱之為session)後,給每個session分配一個sub reactor,之後該session的所有IO、業務邏輯處理均交給了該sub reactor。每個reactor均是一個執行緒,sub reactor中只靠核心排程,沒有任何通訊且互不打擾。

在我自己的部落格裡面[Netty 4.x學習筆記 – 執行緒模型]也對netty的執行緒模型有一定的介紹。現在來講講我對執行緒模型演進的一些理解:

  • Thread per Connection: 在沒有nio之前,這是傳統的java網路程式設計方案所採用的執行緒模型。即有一個主迴圈,socket.accept阻塞等待,當建立連線後,建立新的執行緒/從執行緒池中取一個,把該socket連線交由新執行緒全權處理。這種方案優缺點都很明顯,優點即實現簡單,缺點則是方案的伸縮性受到執行緒數的限制。
  • Reactor in Single Thread: 有了nio後,可以採用IO多路複用機制了。我們抽取出一個單執行緒版的reactor模型,時序圖見下文,該方案只有一個執行緒,所有的socket連線均註冊在了該reactor上,由一個執行緒全權負責所有的任務。它實現簡單,且不受執行緒數的限制。這種方案受限於使用場景,僅適合於IO密集的應用,不太適合CPU密集的應用,且適合於CPU資源緊張的應用上。

reactor-single-thread

  • Reactor + Thread Pool: 方案2由於受限於使用場景,但為了可以更充分的使用CPU資源,抽取出一個邏輯處理執行緒池。reactor僅負責IO任務,執行緒池負責所有其它邏輯的處理。雖然該方案可以充分利用CPU資源,但是這個方案多了進出thread pool的兩次上下文切換。

reactor-thread-pool

  • Reactors in threads: 基於方案3缺點的考慮,將reactor分成兩個部分。main reactor負責連線任務(accept、connect等),sub reactor負責IO、邏輯任務,即mina與netty的執行緒模型。該方案適應性十分強,可以調整sub reactor的數量適應CPU資源緊張的應用;同時CPU密集型任務時,又可以在業務處理邏輯中將任務交由執行緒池處理,如方案5。該方案有一個不太明顯的缺點,即session沒有分優先順序,所有session平等對待均分到所有的執行緒中,這樣可能會導致優先順序低耗資源的session堵塞高優先順序的session,但似乎netty與mina並沒有針對這個做優化。

reactors in threads

  • Reactors in threads + Threads pool: 這也是我所在公司應用框架採用的模型,可以更為靈活的適應所有的應用場景:調整reactor數量、調整thread pool大小等。

reactors-in-threads-thread-pool

以上圖片及總結參考:《Linux多執行緒服務端程式設計》

2. mina、netty的任務排程粒度

mina、netty線上程模型上並沒有太大的差異性,主要的差異還是在任務排程的粒度的不同。任務從邏輯上我給它分為成三種類型:連線相關的任務(bind、connect等)、寫任務(write、flush)、排程任務(延遲、定時等),讀任務則由selector加迴圈時間控制了。mina、netty任務排程的趨勢是逐漸變小,從session級別的排程 -> 型別級別任務的排程 -> 任務的排程。

程式碼

  • mina-1.1.7: SocketIoProcessor$Worker.run
  • mina-2.0.4: AbstractPollingIoProcessor$Processor.run
  • mina-3.0.0.M3-SNAPSHOT: AbstractNioSession.processWrite
  • netty-3.5.8.Final: AbstractNioSelector.run
  • netty-4.0.6.Final: NioEventLoop.run

分析

mina1、2的任務排程粒度為session。mina會將有IO任務的的session寫入佇列中,當迴圈執行任務時,則會輪詢所有的session,並依次把session中的所有任務取出來執行。這樣粗粒度的排程是不公平排程,會導致某些請求的延遲很高。

mina3的模型改動比較大,程式碼相對就比較難看了,我僅是隨便掃了一下,它僅提煉出了writeQueue。

而netty3的排程粒度則是按照IO操作,分成了registerTaskQueue、writeTaskQueue、eventQueue三個佇列,當有IO任務時,依次processRegisterTaskQueue、processEventQueue、processWriteTaskQueue、processSelectedKeys(selector.selectedKeys)。

netty4可能覺得netty3的粒度還是比較粗,將佇列細分成了taskQueue和delayedTaskQueue,所有的任務均放在taskQueue中,delayedTaskQueue則是定時排程任務,且netty4可以靈活配置task與selectedKey處理的時間比例。

BTW: netty3.6.0之後,所有的佇列均合併成了一個taskQueue

有意思的是,netty4會優先處理selectedKeys,然後再處理任務,netty3則相反。mina1、2則是先處理新建的session,再處理selectedKeys,再處理任務。

難道selectedKeys處理順序有講究麼?


易鴻偉

支付寶中介軟體研發工程師,現專注於分散式系統、網路程式設計領域。 (Blog: 小e的分享,Weibo: 小e_鴻偉