前奏
https://tech.meituan.com/2016/11/04/nio.html
綜述
netty通過Reactor模型基于多路復用器接收并處理用戶請求(能講就多講一點),內部實現了兩個線程池,boss線程池和work線程池,其中boss線程池的線程負責處理請求的accept事件,當接收到accept事件的請求時,把對應的socket封裝到一個NioSocketChannel中,并交給work線程池,其中work線程池負責請求的read和write事件(通過口述加畫圖的方式,把請求的執行過程大概描述了一遍,時間有限,也不可能把所有的細節都說完,挑重點講,挑記憶深刻的講)
框架
-
一種理解方式
另一種理解方式
netty線程模型采用“服務端監聽線程”和“IO線程”分離的方式,與多線程Reactor模型類似。
抽象出NioEventLoop來表示一個不斷循環執行處理任務的線程,每個NioEventLoop有一個selector,用于監聽綁定在其上的socket鏈路
- 功能架構
邏輯架構
Reactor 通信調度層:它由一系列輔助類完成,包括 Reactor 線程 NioEventLoop
及其父類,NioSocketChannel/ NioServerSocketChannel 及其父類,
ByteBuffer 以及由其衍生出來的各種 Buffer,Unsafe 以及其衍生出的各種內部
類等。該層的主要職責就是監聽網絡的讀寫和連接操作,負責將網絡層的數據讀
取到內存緩沖區中,然后觸發各種網絡事件,例如連接創建、連接激活、讀事件、
寫事件等,將這些事件觸發到 PipeLine 中,由 PipeLine 管理的職責鏈來進行后
續的處理。職責鏈 ChannelPipeline:它負責事件在職責鏈中的有序傳播,同時負責動
態地編排職責鏈。職責鏈可以選擇監聽和處理自己關心的事件,它可以攔截處理
和向后 / 向前傳播事件。不同應用的 Handler 節點的功能也不同,通常情況下,
往往會開發編解碼 Hanlder 用于消息的編解碼,它可以將外部的協議消息轉換成
內部的 POJO 對象,這樣上層業務則只需要關心處理業務邏輯即可,不需要感知
底層的協議差異和線程模型差異,實現了架構層面的分層隔離。業務邏輯編排層(Service ChannelHandler):業務邏輯編排層通常有兩類:
一類是純粹的業務邏輯編排,還有一類是其他的應用層協議插件,用于特定協議
相關的會話和鏈路管理。例如 CMPP 協議,用于管理和中國移動短信系統的對接。架構的不同層面,需要關心和處理的對象都不同,通常情況下,對于業務開
發者,只需要關心職責鏈的攔截和業務 Handler 的編排。因為應用層協議棧往往
是開發一次,到處運行,所以實際上對于業務開發者來說,只需要關心服務層的
業務邏輯開發即可。各種應用協議以插件的形式提供,只有協議開發人員需要關
注協議插件,對于其他業務開發人員來說,只需關心業務邏輯定制。這種分層的
架構設計理念實現了 NIO 框架各層之間的解耦,便于上層業務協議棧的開發和業
務邏輯的定制
Netty的特點
- 一個高性能、異步事件驅動的NIO框架
- 支持TCP、UDP和文件傳輸
- 使用更高效的socket底層,對epoll空輪詢引起的cpu占用飆升在內部進行了處理
- 避免了直接使用NIO的陷阱,簡化了NIO的處理方式。
- 采用多種decoder/encoder 支持,對TCP粘包/分包進行自動化處理
- 可使用接受/處理線程池,提高連接效率,對重連、心跳檢測的簡單支持
- 可配置IO線程數、TCP參數, TCP接收和發送緩沖區使用直接內存代替堆內存,通過內存池的方式循環利用ByteBuf
- 通過引用計數器及時申請釋放不再引用的對象,降低了GC頻率
- 使用單線程串行化的方式,高效的Reactor線程模型
- 大量使用了volitale、使用了CAS和原子類、線程安全類的使用、讀寫鎖的使用
Netty的高性能表現在哪些方面?
(1)采用異步非阻塞的 I/O 類庫,基于 Reactor 模式實現,解決了傳統同
步阻塞 I/O 模式下一個服務端無法平滑地處理線性增長的客戶端的問題。
(2)TCP 接收和發送緩沖區使用直接內存代替堆內存,避免了內存復制,
提升了 I/O 讀取和寫入的性能。
(3)支持通過內存池的方式循環利用 ByteBuf,避免了頻繁創建和銷毀
ByteBuf 帶來的性能損耗。
(4)可配置的 I/O 線程數、TCP 參數等,為不同的用戶場景提供定制化的
調優參數,滿足不同的性能場景。
(5)采用環形數組緩沖區實現無鎖化并發編程,代替傳統的線程安全容器
或者鎖。
(6)合理地使用線程安全容器、原子類等,提升系統的并發處理能力。
(7)關鍵資源的處理使用單線程串行化的方式,避免多線程并發訪問帶來
的鎖競爭和額外的 CPU 資源消耗問題。
(8)通過引用計數器及時地申請釋放不再被引用的對象,細粒度的內存管
理降低了 GC 的頻率,減少了頻繁 GC 帶來的時延增大和 CPU 損耗。
- 心跳檢測空閑連接
為了支持心跳,Netty 提供了如下兩種鏈路空閑檢測機制: - 讀空閑超時機制:當連續周期T沒有消息可讀時,觸發超時Handler,用戶
可以基于讀空閑超時發送心跳消息,進行鏈路檢測;如果連續N個周期仍
然沒有讀取到心跳消息,可以主動關閉鏈路 - 寫空閑超時機制:當連續周期T沒有消息要發送時,觸發超時Handler,用
戶可以基于寫空閑超時發送心跳消息,進行鏈路檢測;如果連續N個周期
仍然沒有接收到對方的心跳消息,可以主動關閉鏈路。
對服務端:會定時清除閑置會話inactive(netty5)
對客戶端:用來檢測會話是否斷開,是否重來,檢測網絡延遲,其中idleStateHandler類 用來檢測會話狀態
分析:
? IdleStateHandler 將通過 IdleStateEvent 調用 userEventTriggered
? 如果連接沒有接收或發送數據超過60秒鐘,則心跳發送到遠端
? 發送的心跳并添加一個偵聽器,如果發送操作失敗將關閉連接
? 若事件不是 IdleStateEvent ,就將它傳遞給下一個處理程序內存保護機制
Netty 提供多種機制對內存進行保護,包括以下幾個方面:
?通過對象引用計數器對Netty的ByteBuf等內置對象進行細粒度的內存申請
和釋放,對非法的對象引用進行檢測和保護。
? 通過內存池來重用ByteBuf,節省內存。
? 可設置的內存容量上限,包括ByteBuf、線程池線程數等。
如果長度解碼器沒有單個消息最大報文長度限制,當解碼錯位或者讀取到畸
形碼流時,長度值可能是個超大整數值,例如 4294967296,這很容易導致內存
溢出。如果有上限保護,例如單條消息最大不允許超過 10MB,當讀取到非法消
息長度 4294967296 后,直接拋出解碼異常,這樣就避免了大內存的分配。優雅停機
優雅停機功能指的是當系統退出時,JVM 通過注冊的 Shutdown Hook 攔截到退出
信號量,然后執行退出操作
釋放相關模塊的資源占用,將緩沖區的消息處理完成或者清空,將待刷新的數據持久化到磁盤或者數據庫中,
等到資源回收和緩沖區消息處理完成之后,再退出。
優雅停機往往需要設置個最大超時時間 T,如果達到 T 后系統仍然沒有退出,
則通過 Kill - 9 pid 強殺當前的進程。串行無鎖化設計
即消息的處理盡可能在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖。
表面上看,串行化設計似乎CPU利用率不高,并發程度不夠。但是,通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程并行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工作線程模型性能更優。
串行執行Handler鏈
分析:NioEventLoop讀取到消息之后,直接調用ChannelPipeline的fireChannelRead(Object msg)方法,只要用戶不主動切換線程,一直會由NioEventLoop調用到用戶的Handler,期間不進行線程切換。
一個NioEventLoop聚合了一個多路復用器Selector,因此可以處理成百上千的客戶端連接,Netty的處理策略是每當有一個新的客戶端接入,則從NioEventLoop線程組中順序獲取一個可用的NioEventLoop,當到達數組上限之后,重新返回到0,通過這種方式,可以基本保證各個NioEventLoop的負載均衡。一個客戶端連接只注冊到一個NioEventLoop上,這樣就避免了多個IO線程去并發操作它。
- 可靠性
鏈路有效性檢測:鏈路空閑檢測機制,讀/寫空閑超時機制;
內存保護機制:通過內存池重用ByteBuf;ByteBuf的解碼保護;
優雅停機:不再接收新消息、退出前的預處理操作、資源的釋放操作。
-
Netty安全性
支持的安全協議:SSL V2和V3,TLS,SSL單向認證、雙向認證和第三方CA認證。- SSL的三種認證方式
單向認證:客戶端只驗證服務端的合法性,服務端不驗證客戶端。
雙向認證:與單向認證不同的是服務端也需要對客戶端進行安全認證。這就意味著客戶端的自簽名證書也需要導入到服務端的數字證書倉庫中。
CA認證:基于自簽名的SSL雙向認證,只要客戶端或者服務端修改了密鑰和證書,就需要重新進行簽名和證書交換,這種調試和維護工作量是非常大的。因此,在實際的商用系統中往往會使用第三方CA證書頒發機構進行簽名和驗證。我們的瀏覽器就保存了幾個常用的CA_ROOT。每次連接到網站時只要這個網站的證書是經過這些CA_ROOT簽名過的。就可以通過驗證了。
- SSL的三種認證方式
高效并發編程的體現:
volatile的大量、正確使用;
CAS和原子類的廣泛使用;
線程安全容器的使用;
通過讀寫鎖提升并發性能。
IO通信性能三原則:傳輸(AIO)、協議(Http)、線程(主從多線程)
- 流量整型的作用(變壓器)
防止由于上下游網元性能不均衡導致下游網元被壓垮,業務流中斷;
防止由于通信模塊接受消息過快,后端業務線程處理不及時導致撐死問題。
- TCP參數配置
SO_RCVBUF和SO_SNDBUF:通常建議值為128K或者256K;SO_TCPNODELAY:NAGLE算法通過將緩沖區內的小封包自動相連,組成較大的封包,阻止大量小封包的發送阻塞網絡,從而提高網絡應用效率。但是對于時延敏感的應用場景需要關閉該優化算法;
軟中斷:如果Linux內核版本支持RPS(2.6.35以上版本),開啟RPS后可以實現軟中斷,提升網絡吞吐量。RPS根據數據包的源地址,目的地址以及目的和源端口,計算出一個hash值,然后根據這個hash值來選擇軟中斷運行的cpu,從上層來看,也就是說將每個連接和cpu綁定,并通過這個hash值,來均衡軟中斷在多個cpu上,提升網絡并行處理性能。
Netty的高效并發編程主要體現在如下幾點:
- volatile的大量、正確使用;
- CAS和原子類的廣泛使用;
- 線程安全容器的使用;
- 通過讀寫鎖提升并發性能。
Netty除了使用reactor來提升性能,當然還有
1、零拷貝,IO性能優化
2、通信上的粘包拆包
3、同步的設計
4、高性能的序列
Netty的線程模型
Netty 的線程模型并不是一成不變的,它實際取決于用戶的啟動參數配置。
通過設置不同的啟動參數,Netty 可以同時支持 Reactor 單線程模型、多線程模
型和主從 Reactor 多線層模型。
- Netty通過Reactor模型基于多路復用器接收并處理用戶請求,內部實現了兩個線程池,boss線程池和work線程池,其中boss線程池的線程負責處理請求的accept事件,當接收到accept事件的請求時,把對應的socket封裝到一個NioSocketChannel中,并交給work線程池,其中work線程池負責請求的read和write事件,由對應的Handler處理。
服務端啟動的時候,創建了兩個 NioEventLoopGroup,它們實際是兩個獨立
的 Reactor 線程池。一個用于接收客戶端的 TCP 連接,另一個用于處理 I/O 相關
的讀寫操作,或者執行系統 Task、定時任務 Task 等。
- Netty 用于接收客戶端請求的線程池職責如下。
(1)接收客戶端 TCP 連接,初始化 Channel 參數;
(2)將鏈路狀態變更事件通知給 ChannelPipeline。 - Netty 處理 I/O 操作的 Reactor 線程池職責如下。
(1)異步讀取通信對端的數據報,發送讀事件到 ChannelPipeline;
(2)異步發送消息到通信對端,調用 ChannelPipeline 的消息發送接口;
(3)執行系統調用 Task;
(4)執行定時任務 Task,例如鏈路空閑狀態監測定時任務。
NioEventLoop設計原理
TCP 粘包/拆包的原因及解決方法
- TCP是以流的方式來處理數據,一個完整的包可能會被TCP拆分成多個包進行發送,也可能把小的封裝成一個大的數據包發送。
- TCP粘包/分包的原因:
- 應用程序寫入的字節大小大于套接字發送緩沖區的大小,會發生拆包現象,而應用程序寫入數據小于套接字緩沖區大小,網卡將應用多次寫入的數據發送到網絡上,這將會發生粘包現象;
- 進行MSS大小的TCP分段,當TCP報文長度-TCP頭部長度>MSS的時候將發生拆包
- 以太網幀的payload(凈荷)大于MTU(1500字節)進行ip分片。
- 解決方法
- 消息定長:FixedLengthFrameDecoder類
- 包尾增加特殊字符分割:行分隔符類:LineBasedFrameDecoder或自定義分隔符類 :DelimiterBasedFrameDecoder
- 將消息分為消息頭和消息體:LengthFieldBasedFrameDecoder類。分為有頭部的拆包與粘包、長度字段在前且有頭部的拆包與粘包、多擴展頭部的拆包與粘包。
了解哪幾種序列化協議
序列化(編碼)是將對象序列化為二進制形式(字節數組),主要用于網絡傳輸、數據持久化等;而反序列化(解碼)則是將從網絡、磁盤等讀取的字節數組還原成原始對象,主要用于網絡傳輸對象的解碼,以便完成遠程調用。
影響序列化性能的關鍵因素:
序列化后的碼流大小(網絡帶寬的占用);
序列化的性能(CPU資源占用);
是否支持跨語言(異構系統的對接和開發語言切換)。Java默認提供的序列化:無法跨語言、序列化后的碼流太大、序列化的性能差
XML
優點:人機可讀性好,可指定元素或特性的名稱。
缺點:序列化數據只包含數據本身以及類的結構,不包括類型標識和程序集信息;只能序列化公共屬性和字段;不能序列化方法;文件龐大,文件格式復雜,傳輸占帶寬。
適用場景:當做配置文件存儲數據,實時數據轉換。JSON,是一種輕量級的數據交換格式,
優點:兼容性高、數據格式比較簡單,易于讀寫、序列化后數據較小,可擴展性好,兼容性好、與XML相比,其協議比較簡單,解析速度比較快。
缺點:數據的描述性比XML差、不適合性能要求為ms級別的情況、額外空間開銷比較大。
適用場景(可替代XML):跨防火墻訪問、可調式性要求高、基于Web browser的Ajax請求、傳輸數據量相對小,實時性要求相對低(例如秒級別)的服務。Fastjson,采用一種“假定有序快速匹配”的算法。
優點:接口簡單易用、目前java語言中最快的json庫。
缺點:過于注重快,而偏離了“標準”及功能性、代碼質量不高,文檔不全。
適用場景:協議交互、Web輸出、Android客戶端Thrift,不僅是序列化協議,還是一個RPC框架。
優點:序列化后的體積小, 速度快、支持多種語言和豐富的數據類型、對于數據字段的增刪具有較強的兼容性、支持二進制壓縮編碼。
缺點:使用者較少、跨防火墻訪問時,不安全、不具有可讀性,調試代碼時相對困難、不能與其他傳輸層協議共同使用(例如HTTP)、無法支持向持久層直接讀寫數據,即不適合做數據持久化序列化協議。
適用場景:分布式系統的RPC解決方案Avro,Hadoop的一個子項目,解決了JSON的冗長和沒有IDL的問題。
優點:支持豐富的數據類型、簡單的動態語言結合功能、具有自我描述屬性、提高了數據解析速度、快速可壓縮的二進制數據形式、可以實現遠程過程調用RPC、支持跨編程語言實現。
缺點:對于習慣于靜態類型語言的用戶不直觀。
適用場景:在Hadoop中做Hive、Pig和MapReduce的持久化數據格式。Protobuf,將數據結構以.proto文件進行描述,通過代碼生成工具可以生成對應數據結構的POJO對象和Protobuf相關的方法和屬性。
優點:序列化后碼流小,性能高、結構化數據存儲格式(XML JSON等)、通過標識字段的順序,可以實現協議的前向兼容、結構化的文檔更容易管理和維護。
缺點:需要依賴于工具生成代碼、支持的語言相對較少,官方只支持Java 、C++ 、python。
適用場景:對性能要求高的RPC調用、具有良好的跨防火墻的訪問屬性、適合應用層對象的持久化
如何選擇序列化協議
-
具體場景
- 對于公司間的系統調用,如果性能要求在100ms以上的服務,基于XML的SOAP協議是一個值得考慮的方案。
- 基于Web browser的Ajax,以及Mobile app與服務端之間的通訊,JSON協議是首選。對于性能要求不太高,或者以動態類型語言為主,或者傳輸數據載荷很小的的運用場景,JSON也是非常不錯的選擇。
- 對于調試環境比較惡劣的場景,采用JSON或XML能夠極大的提高調試效率,降低系統開發成本。
- 當對性能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro之間具有一定的競爭關系。
- 對于T級別的數據的持久化應用場景,Protobuf和Avro是首要選擇。如果持久化后的數據存儲在hadoop子項目里,Avro會是更好的選擇。
- 對于持久層非Hadoop項目,以靜態類型語言為主的應用場景,Protobuf會更符合靜態類型語言工程師的開發習慣。由于Avro的設計理念偏向于動態類型語言,對于動態語言為主的應用場景,Avro是更好的選擇。
- 如果需要提供一個完整的RPC解決方案,Thrift是一個好的選擇。
- 如果序列化之后需要支持不同的傳輸層協議,或者需要跨防火墻訪問的高性能場景,Protobuf可以優先考慮。
protobuf的數據類型有多種:bool、double、float、int32、int64、string、bytes、enum、message。protobuf的限定符:required: 必須賦值,不能為空、optional:字段可以賦值,也可以不賦值、repeated: 該字段可以重復任意次數(包括0次)、枚舉;只能用指定的常量集中的一個值作為其值;
protobuf的基本規則:每個消息中必須至少留有一個required類型的字段、包含0個或多個optional類型的字段;repeated表示的字段可以包含0個或多個數據;[1,15]之內的標識號在編碼的時候會占用一個字節(常用),[16,2047]之內的標識號則占用2個字節,標識號一定不能重復、使用消息類型,也可以將消息嵌套任意多層,可用嵌套消息類型來代替組。
protobuf的消息升級原則:不要更改任何已有的字段的數值標識;不能移除已經存在的required字段,optional和repeated類型的字段可以被移除,但要保留標號不能被重用。新添加的字段必須是optional或repeated。因為舊版本程序無法讀取或寫入新增的required限定符的字段。
編譯器為每一個消息類型生成了一個.java文件,以及一個特殊的Builder類(該類是用來創建消息類接口的)。如:UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();
Netty中的使用:ProtobufVarint32FrameDecoder 是用于處理半包消息的解碼類;ProtobufDecoder(UserProto.User.getDefaultInstance())這是創建的UserProto.java文件中的解碼類;ProtobufVarint32LengthFieldPrepender 對protobuf協議的消息頭上加上一個長度為32的整形字段,用于標志這個消息的長度的類;ProtobufEncoder 是編碼類
將StringBuilder轉換為ByteBuf類型:copiedBuffer()方法
Netty的零拷貝實現
Netty的接收和發送ByteBuffer采用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不需要進行字節緩沖區的二次拷貝。堆內存多了一次內存拷貝,JVM會將堆內存Buffer拷貝一份到直接內存中,然后才寫入Socket中。ByteBuffer由ChannelConfig分配,而ChannelConfig創建ByteBufAllocator默認使用Direct Buffer
CompositeByteBuf 類可以將多個 ByteBuf 合并為一個邏輯上的 ByteBuf, 避免了傳統通過內存拷貝的方式將幾個小Buffer合并成一個大的Buffer。addComponents方法將 header 與 body 合并為一個邏輯上的 ByteBuf, 這兩個 ByteBuf 在CompositeByteBuf 內部都是單獨存在的, CompositeByteBuf 只是邏輯上是一個整體
通過 FileRegion 包裝的FileChannel.tranferTo方法 實現文件傳輸, 可以直接將文件緩沖區的數據發送到目標 Channel,避免了傳統通過循環write方式導致的內存拷貝問題。
通過 wrap方法, 我們可以將 byte[] 數組、ByteBuf、ByteBuffer等包裝成一個 Netty ByteBuf 對象, 進而避免了拷貝操作。
Selector BUG:若Selector的輪詢結果為空,也沒有wakeup或新消息處理,則發生空輪詢,CPU使用率100%;
Netty的解決辦法:對Selector的select操作周期進行統計,每完成一次空的select操作進行一次計數,若在某個周期內連續發生N次空輪詢,則觸發了epoll死循環bug。重建Selector,判斷是否是其他線程發起的重建請求,若不是則將原SocketChannel從舊的Selector上去除注冊,重新注冊到新的Selector上,并將原來的Selector關閉
串行化設計避免線程競爭
- netty采用串行化設計理念,從消息的讀取->解碼->處理->編碼->發送,始終由IO線程NioEventLoop負責。整個流程不會進行線程上下文切換,數據無并發修改風險。
- 一個NioEventLoop聚合一個多路復用器selector,因此可以處理多個客戶端連接。
- netty只負責提供和管理“IO線程”,其他的業務線程模型由用戶自己集成。
- 時間可控的簡單業務建議直接在“IO線程”上處理,復雜和時間不可控的業務建議投遞到后端業務線程池中處理。
定時任務與時間輪算法
在Netty中,有很多功能依賴定時任務,比較典型的有兩種:
- 客戶端連接超時控制;
- 鏈路空閑檢測。
NioEventLoop中的Thread線程按照時間輪中的步驟不斷循環執行:
a)在時間片Tirck內執行selector.select()輪詢監聽IO事件;
b)處理監聽到的就緒IO事件;
c)執行任務隊列taskQueue/delayTaskQueue中的非IO任務。
一種比較常用的設計理念是在NioEventLoop中聚合JDK的定時任務線程池ScheduledExecutorService,通過它來執行定時任務。這樣做單純從性能角度看不是最優,原因有如下三點:
- 在IO線程中聚合了一個獨立的定時任務線程池,這樣在處理過程中會存在線程上下文切換問題,這就打破了Netty的串行化設計理念;
- 存在多線程并發操作問題,因為定時任務Task和IO線程NioEventLoop可能同時訪問并修改同一份數據;
- JDK的ScheduledExecutorService從性能角度看,存在性能優化空間。
最早面臨上述問題的是操作系統和協議棧,例如TCP協議棧,其可靠傳輸依賴超時重傳機制,因此每個通過TCP傳輸的 packet 都需要一個 timer來調度 timeout 事件。這類超時可能是海量的,如果為每個超時都創建一個定時器,從性能和資源消耗角度看都是不合理的。
根據George Varghese和Tony Lauck 1996年的論文《Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility》提出了一種定時輪的方式來管理和維護大量的timer調度。
Netty的定時任務調度就是基于時間輪算法調度,下面我們一起來看下Netty的實現。
定時輪是一種數據結構,其主體是一個循環列表,每個列表中包含一個稱之為slot的結構,它的原理圖如下:
定時輪的工作原理可以類比于時鐘,如上圖箭頭(指針)按某一個方向按固定頻率輪動,每一次跳動稱為一個tick。這樣可以看出定時輪由個3個重要的屬性參數:ticksPerWheel(一輪的tick數),tickDuration(一個tick的持續時間)以及 timeUnit(時間單位),例如當ticksPerWheel=60,tickDuration=1,timeUnit=秒,這就和時鐘的秒針走動完全類似了。
下面我們具體分析下Netty的實現:時間輪的執行由NioEventLoop來復雜檢測,首先看任務隊列中是否有超時的定時任務和普通任務,如果有則按照比例循環執行這些任務,代碼如下:
如果沒有需要立即執行的任務,則調用Selector的select方法進行等待,等待的時間為定時任務隊列中第一個超時的定時任務時延,代碼如下:
從定時任務Task隊列中彈出delay最小的Task,計算超時時間;
定時任務的執行:經過周期tick之后,掃描定時任務列表,將超時的定時任務移除到普通任務隊列中,等待執行,相關代碼如下:
檢測和拷貝任務完成之后,就執行超時的定時任務,代碼如下:
為了保證定時任務的執行不會因為過度擠占IO事件的處理,Netty提供了IO執行比例供用戶設置,用戶可以設置分配給IO的執行比例,防止因為海量定時任務的執行導致IO處理超時或者積壓。
因為獲取系統的納秒時間是件耗時的操作,所以Netty每執行64個定時任務檢測一次是否達到執行的上限時間,達到則退出。如果沒有執行完,放到下次Selector輪詢時再處理,給IO事件的處理提供機會,代碼如下:
聚焦而不是膨脹
Netty是個異步高性能的NIO框架,它并不是個業務運行容器,因此它不需要也不應該提供業務容器和業務線程。合理的設計模式是Netty只負責提供和管理NIO線程,其它的業務層線程模型由用戶自己集成,Netty不應該提供此類功能,只要將分層劃分清楚,就會更有利于用戶集成和擴展。
令人遺憾的是在Netty 3系列版本中,Netty提供了類似Mina異步Filter的ExecutionHandler,它聚合了JDK的線程池java.util.concurrent.Executor,用戶異步執行后續的Handler。
ExecutionHandler是為了解決部分用戶Handler可能存在執行時間不確定而導致IO線程被意外阻塞或者掛住,從需求合理性角度分析這類需求本身是合理的,但是Netty提供該功能卻并不合適。原因總結如下:
- 它打破了Netty堅持的串行化設計理念,在消息的接收和處理過程中發生了線程切換并引入新的線程池,打破了自身架構堅守的設計原則,實際是一種架構妥協;
- 潛在的線程并發安全問題,如果異步Handler也操作它前面的用戶Handler,而用戶Handler又沒有進行線程安全保護,這就會導致隱蔽和致命的線程安全問題;
- 用戶開發的復雜性,引入ExecutionHandler,打破了原來的ChannelPipeline串行執行模式,用戶需要理解Netty底層的實現細節,關心線程安全等問題,這會導致得不償失。
鑒于上述原因,Netty的后續版本徹底刪除了ExecutionHandler,而且也沒有提供類似的相關功能類,把精力聚焦在Netty的IO線程NioEventLoop上,這無疑是一種巨大的進步,Netty重新開始聚焦在IO線程本身,而不是提供用戶相關的業務線程模型。
Netty線程開發最佳實踐
時間可控的簡單業務直接在IO線程上處理
如果業務非常簡單,執行時間非常短,不需要與外部網元交互、訪問數據庫和磁盤,不需要等待其它資源,則建議直接在業務ChannelHandler中執行,不需要再啟業務的線程或者線程池。避免線程上下文切換,也不存在線程并發問題。復雜和時間不可控業務建議投遞到后端業務線程池統一處理
對于此類業務,不建議直接在業務ChannelHandler中啟動線程或者線程池處理,建議將不同的業務統一封裝成Task,統一投遞到后端的業務線程池中進行處理。
過多的業務ChannelHandler會帶來開發效率和可維護性問題,不要把Netty當作業務容器,對于大多數復雜的業務產品,仍然需要集成或者開發自己的業務容器,做好和Netty的架構分層。業務線程避免直接操作ChannelHandler
對于ChannelHandler,IO線程和業務線程都可能會操作,因為業務通常是多線程模型,這樣就會存在多線程操作ChannelHandler。為了盡量避免多線程并發問題,建議按照Netty自身的做法,通過將操作封裝成獨立的Task由NioEventLoop統一執行,而不是業務線程直接操作,相關代碼如下所示:
Ref:
http://www.lxweimin.com/p/03bb8a945b37
http://www.voidcn.com/article/p-flamborw-bhh.html
http://www.voidcn.com/article/p-onnrusud-mz.html
http://blog.csdn.net/qq924862077/article/details/53316490