Netty是一個高性能、異步事件驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持,作為一個異步NIO框架,Netty的所有IO操作都是異步非阻塞的,通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結果。?
作為當前最流行的NIO框架,Netty在互聯網領域、大數據分布式計算領域、游戲行業、通信行業等獲得了廣泛的應用,一些業界著名的開源組件也基于Netty的NIO框架構建。
為什么選擇Netty
Netty是業界最流行的NIO框架之一,它的健壯性、功能、性能、可定制性和可擴展性在同類框架中都是首屈一指的,它已經得到成百上千的商用項目驗證,例如Hadoop的RPC框架avro使用Netty作為底層通信框架;很多其他業界主流的RPC框架,也使用Netty來構建高性能的異步通信能力。
Netty架構分析
Netty 采用了比較典型的三層網絡架構進行設計,邏輯架構圖如下所示:
#第一層,Reactor 通信調度層,它由一系列輔助類完成,包括 Reactor 線程 NioEventLoop 以及其父類、NioSocketChannel/NioServerSocketChannel 以及其父 類、ByteBuffer 以及由其衍生出來的各種 Buffer、Unsafe 以及其衍生出的各種內部類等。該層的主要職責就是監聽網絡的讀寫和連接操作,負責將網絡層的數據讀取到內存緩沖區中,然后觸發各種網絡事件,例如連接創建、連接激活、讀事 件、寫事件等等,將這些事件觸發到 PipeLine 中,由 PipeLine 充當的職責鏈來進行后續的處理。
#第二層,職責鏈 PipeLine,它負責事件在職責鏈中的有序傳播,同時負責動態的編排職責鏈,職責鏈可以選擇監聽和處理自己關心的事件,它可以攔截處理和向后/向前傳播事件,不同的應用的 Handler 節點的功能也不同,通常情況下,往往會開發編解碼 Hanlder 用于消息的編解碼,它可以將外部的協議消息轉換成內部的 POJO 對象,這樣上層業務側只需要關心處理業務邏輯即可,不需要感知底層的協議差異和線程模型差異,實現了架構層面的分層隔離。
#第三層,業務邏輯處理層。可以分為兩類:純粹的業務邏輯 處理,例如訂單處理;應用層協議管理,例如 HTTP 協議、FTP 協議等。
I/O模型
傳統同步阻塞I/O模式如下圖所示:
幾種I/O模型的功能和特性對比:
Netty的I/O模型基于非阻塞I/O實現,底層依賴的是JDK NIO框架的Selector。Selector提供選擇已經就緒的任務的能力。簡單來講,Selector會不斷地輪詢注冊在其上的Channel,如果某個Channel上面有新的TCP連接接入、讀和寫事件,這個Channel就處于就緒狀態,會被Selector輪詢出來,然后通過SelectionKey可以獲取就緒Channel的集合,進行后續的I/O操作。
一個多路復用器Selector可以同時輪詢多個Channel,由于JDK1.5_update10版本(+)使用了epoll()代替傳統的select實現,所以它并沒有最大連接句柄1024/2048的限制。這也就意味著只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端,這確實是個非常巨大的技術進步。使用非阻塞I/O模型之后,Netty解決了傳統同步阻塞I/O帶來的性能、吞吐量和可靠性問題。
線程調度模型
常用的Reactor線程模型有三種,分別如下:
#Reactor單線程模型:Reactor單線程模型,指的是所有的I/O操作都在同一個NIO線程上面完成。對于一些小容量應用場景,可以使用單線程模型。
#Reactor多線程模型:Rector多線程模型與單線程模型最大的區別就是有一組NIO線程處理I/O操作。主要用于高并發、大業務量場景。
#主從Reactor多線程模型:主從Reactor線程模型的特點是服務端用于接收客戶端連接的不再是個1個單獨的NIO線程,而是一個獨立的NIO線程池。利用主從NIO線程模型,可以解決1個服務端監聽線程無法有效處理所有客戶端連接的性能不足問題。
事實上,Netty的線程模型并非固定不變,通過在啟動輔助類中創建不同的EventLoopGroup實例并通過適當的參數配置,就可以支持上述三種Reactor線程模型.
在大多數場景下,并行多線程處理可以提升系統的并發性能。但是,如果對于共享資源的并發訪問處理不當,會帶來嚴重的鎖競爭,這最終會導致性能的下降。為了盡可能的避免鎖競爭帶來的性能損耗,可以通過串行化設計,即消息的處理盡可能在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖。
為了盡可能提升性能,Netty采用了串行無鎖化設計,在I/O線程內部進行串行操作,避免多線程競爭導致的性能下降。表面上看,串行化設計似乎CPU利用率不高,并發程度不夠。但是,通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程并行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工作線程模型性能更優。
Reactor模型
Java NIO非堵塞技術實際是采取反應器模式,或者說是觀察者(observer)模式為我們監察I/O端口,如果有內容進來,會自動通知我們,這樣,我們就不必開啟多個線程死等,從外界看,實現了流暢的I/O讀寫,不堵塞了。
NIO 有一個主要的類Selector,這個類似一個觀察者,只要我們把需要探知的socketchannel告訴Selector,我們接著做別的事情,當有事件發生時,他會通知我們,傳回一組SelectionKey,我們讀取這些Key,就會獲得我們剛剛注冊過的socketchannel,然后,我們從這個Channel中讀取數據,接著我們可以處理這些數據。
反應器模式與觀察者模式在某些方面極為相似:當一個主體發生改變時,所有依屬體都得到通知。不過,觀察者模式與單個事件源關聯,而反應器模式則與多個事件源關聯 。
一般模型
EventLoopGroup:對應于Reactor模式中的定時器的角色,不斷地檢索是否有事件可用(I/O線程-BOSS),然后交給分離者將事件分發給對應的事件綁定的handler(WORK線程)。
經驗分享:在客戶端編程中經常容易出現在EVENTLOOP上做定時任務的,如果定時任務耗時很長或者存在阻塞,那么可能會將I/O操作掛起(因為要等到定時任務做完才能做別的操作)。解決方法:用獨立的EventLoopGroup
序列化方式
影響序列化性能的關鍵因素總結如下:
- 序列化后的碼流大小(網絡帶寬占用)
- 序列化&反序列化的性能(CPU資源占用)
- 并發調用的性能表現:穩定性、線性增長、偶現的時延毛刺等
對Java序列化和二進制編碼分別進行性能測試,編碼100萬次,測試結果表明:Java序列化的性能只有二進制編碼的6.17%左右。
Netty默認提供了對Google Protobuf的支持,通過擴展Netty的編解碼接口,用戶可以實現其它的高性能序列化框架,例如Thrift的壓縮二進制編解碼框架。
不同的應用場景對序列化框架的需求也不同,對于高性能應用場景Netty默認提供了Google的Protobuf二進制序列化框架,如果用戶對其它二進制序列化框架有需求,也可以基于Netty提供的編解碼框架擴展實現。
Netty架構剖析之可靠性
Netty面臨的可靠性挑戰:
1. 作為RPC框架的基礎網絡通信框架,一旦故障將導致無法進行遠程服務(接口)調用。
2. 作為應用層協議的基礎通信框架,一旦故障將導致應用協議棧無法正常工作。
3. 網絡環境復雜(例如推送服務的GSM/3G/WIFI網絡),故障不可避免,業務卻不能中斷。
從應用場景看,Netty是基礎的通信框架,一旦出現Bug,輕則需要重啟應用,重則可能導致整個業務中斷。它的可靠性會影響整個業務集群的數據通信和交換,在當今以分布式為主的軟件架構體系中,通信中斷就意味著整個業務中斷,分布式架構下對通信的可靠性要求非常高。
從運行環境看,Netty會面臨惡劣的網絡環境,這就要求它自身的可靠性要足夠好,平臺能夠解決的可靠性問題需要由Netty自身來解決,否則會導致上層用戶關注過多的底層故障,這將降低Netty的易用性,同時增加用戶的開發和運維成本。
Netty的可靠性是如此重要,它的任何故障都可能會導致業務中斷,蒙受巨大的經濟損失。因此,Netty在版本的迭代中不斷加入新的可靠性特性來滿足用戶日益增長的高可靠和健壯性需求。
鏈路有效性檢測
Netty提供的心跳檢測機制分為三種:
- 讀空閑,鏈路持續時間t沒有讀取到任何消息
- 寫空閑,鏈路持續時間t沒有發送任何消息
- 讀寫空閑,鏈路持續時間t沒有接收或者發送任何消息
當網絡發生單通、連接被防火墻Hang住、長時間GC或者通信線程發生非預期異常時,會導致鏈路不可用且不易被及時發現。特別是異常發生在凌晨業務低谷期間,當早晨業務高峰期到來時,由于鏈路不可用會導致瞬間的大批量業務失敗或者超時,這將對系統的可靠性產生重大的威脅。
從技術層面看,要解決鏈路的可靠性問題,必須周期性的對鏈路進行有效性檢測。目前最流行和通用的做法就是心跳檢測。
心跳檢測機制分為三個層面:
1. TCP層面的心跳檢測,即TCP的Keep-Alive機制,它的作用域是整個TCP協議棧;
2. 協議層的心跳檢測,主要存在于長連接協議中。例如SMPP協議;
3. 應用層的心跳檢測,它主要由各業務產品通過約定方式定時給對方發送心跳消息實現。
Keep-Alive僅僅是TCP協議層會發送連通性檢測包,但并不代表設置了Keep-Alive就是長連接了。
心跳檢測的目的就是確認當前鏈路可用,對方活著并且能夠正常接收和發送消息。
做為高可靠的NIO框架,Netty也提供了基于鏈路空閑的心跳檢測機制:
- 讀空閑,鏈路持續時間t沒有讀取到任何消息
- 寫空閑,鏈路持續時間t沒有發送任何消息
- 讀寫空閑,鏈路持續時間t沒有接收或者發送任何消息(netty自帶心跳處理Handler IdleStateHandler)
在此我向大家推薦一個架構學習交流群。交流學習群號: 744642380, 里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化、分布式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良
客戶端和服務端之間連接斷開機制
TCP連接的建立需要三個分節(三次握手),終止則需要四個分節。?
對于大量短連接的情況下,經常出現卡在FIN_WAIT2和TIMEWAIT狀態的連接,等待系統回收,但是操作系統底層回收的時間頻率很長,導致SOCKET被耗盡。
TCP狀態圖
TCP/IP半關閉
從上述講的TCP關閉的四個分節可以看出,被動關閉執行方,發送FIN分節的前提是TCP套接字對應應用程序調用close產生的。如果服務端有數據發送給客戶端那么可能存在服務端在接受到FIN之后,需要將數據發送到客戶端才能發送FIN字節。這種處于業務考慮的情形通常稱為半關閉。
半關閉可能導致大量socket處于CLOSE_WAIT狀態
誰負責關閉連接合理
連接關閉觸發的條件通常分為如下幾種:
1. 數據發送完成(發送到對端并且收到響應),關閉連接
2. 通信過程中產生異常
3. 特殊指令強制要求關閉連接
對于第一種,通常關閉時機是,數據發送完成方發起(客戶端觸發居多); 對于第二種,異常產生方觸發(例如殘包、錯誤數據等)發起。但是此種情況可能也導致壓根無法發送FIN。對于第三種,通常是用于運維等。由命令發起方產生。
流量整形
流量整形(Traffic Shaping)是一種主動調整流量輸出速率的措施。
Netty的流量整形有兩個作用:
1. 防止由于上下游網元性能不均衡導致下游網元被壓垮,業務流程中斷
2. 防止由于通信模塊接收消息過快,后端業務線程處理不及時導致的"撐死"問題
流量整形的原理示意圖如下:
流量整形(Traffic Shaping)是一種主動調整流量輸出速率的措施。一個典型應用是基于下游網絡結點的TP指標來控制本地流量的輸出。流量整形與流量監管的主要區別在于,流量整形對流量監管中需要丟棄的報文進行緩存——通常是將它們放入緩沖區或隊列內,也稱流量整形(Traffic Shaping,簡稱TS)。當令牌桶有足夠的令牌時,再均勻的向外發送這些被緩存的報文。流量整形與流量監管的另一區別是,整形可能會增加延遲,而監管幾乎不引入額外的延遲。
#全局流量整形:全局流量整形的作用范圍是進程級的,無論你創建了多少個Channel,它的作用域針對所有的Channel。用戶可以通過參數設置:報文的接收速率、報文的發送速率、整形周期。[GlobalChannelTrafficShapingHandler]
#鏈路級流量整形:單鏈路流量整形與全局流量整形的最大區別就是它以單個鏈路為作用域,可以對不同的鏈路設置不同的整形策略。[ChannelTrafficShapingHandler針對于每個channel]
優雅停機
Netty的優雅停機三部曲: 1. 不再接收新消息 2. 退出前的預處理操作 3. 資源的釋放操作
Java的優雅停機通常通過注冊JDK的ShutdownHook來實現,當系統接收到退出指令后,首先標記系統處于退出狀態,不再接收新的消息,然后將積壓的消息處理完,最后調用資源回收接口將資源銷毀,最后各線程退出執行。
通常優雅退出需要有超時控制機制,例如30S,如果到達超時時間仍然沒有完成退出前的資源回收等操作,則由停機腳本直接調用kill -9 pid,強制退出。
在實際項目中,Netty作為高性能的異步NIO通信框架,往往用作基礎通信框架負責各種協議的接入、解析和調度等,例如在RPC和分布式服務框架中,往往會使用Netty作為內部私有協議的基礎通信框架。 當應用進程優雅退出時,作為通信框架的Netty也需要優雅退出,主要原因如下:
1. 盡快的釋放NIO線程、句柄等資源
2. 如果使用flush做批量消息發送,需要將積攢在發送隊列中的待發送消息發送完成
3. 正在write或者read的消息,需要繼續處理
4. 設置在NioEventLoop線程調度器中的定時任務,需要執行或者清理
Netty架構剖析之安全性
Netty面臨的安全挑戰:
- 對第三方開放
- 作為應用層協議的基礎通信框架
安全威脅場景分析:
#對第三方開放的通信框架:如果使用Netty做RPC框架或者私有協議棧,RPC框架面向非授信的第三方開放,例如將內部的一些能力通過服務對外開放出去,此時就需要進行安全認證,如果開放的是公網IP,對于安全性要求非常高的一些服務,例如在線支付、訂購等,需要通過SSL/TLS進行通信。
#應用層協議的安全性:作為高性能、異步事件驅動的NIO框架,Netty非常適合構建上層的應用層協議。由于絕大多數應用層協議都是公有的,這意味著底層的Netty需要向上層提供通信層的安全傳輸功能。
SSL/TLS
Netty安全傳輸特性:
- 支持SSL V2和V3
- 支持TLS
- 支持SSL單向認證、雙向認證和第三方CA認證。
SSL單向認證流程圖如下:
Netty通過SslHandler提供了對SSL的支持,它支持的SSL協議類型包括:SSL V2、SSL V3和TLS。
#單向認證:單向認證,即客戶端只驗證服務端的合法性,服務端不驗證客戶端。
#雙向認證:與單向認證不同的是服務端也需要對客戶端進行安全認證。這就意味著客戶端的自簽名證書也需要導入到服務端的數字證書倉庫中。
#CA認證:基于自簽名的SSL雙向認證,只要客戶端或者服務端修改了密鑰和證書,就需要重新進行簽名和證書交換,這種調試和維護工作量是非常大的。因此,在實際的商用系統中往往會使用第三方CA證書頒發機構進行簽名和驗證。我們的瀏覽器就保存了幾個常用的CA_ROOT。每次連接到網站時只要這個網站的證書是經過這些CA_ROOT簽名過的。就可以通過驗證了。
可擴展的安全特性
通過Netty的擴展特性,可以自定義安全策略:
- IP地址黑名單機制
- 接入認證
- 敏感信息加密或者過濾機制
IP地址黑名單是比較常用的弱安全保護策略,它的特點就是服務端在與客戶端通信的過程中,對客戶端的IP地址進行校驗,如果發現對方IP在黑名單列表中,則拒絕與其通信,關閉鏈路。
接入認證策略非常多,通常是較強的安全認證策略,例如基于用戶名+密碼的認證,認證內容往往采用加密的方式,例如Base64+AES等。
Netty架構剖析之擴展性
通過Netty的擴展特性,可以自定義安全策略:
- 線程模型可擴展
- 序列化方式可擴展
- 上層協議棧可擴展
- 提供大量的網絡事件切面,方便用戶功能擴展
Netty的架構可擴展性設計理念如下:
1. 判斷擴展點,事先預留相關擴展接口,給用戶二次定制和擴展使用
2. 主要功能點都基于接口編程,方便用戶定制和擴展。
粘連包解決方案
TCP粘包是指發送方發送的若干包數據到接收方接收時粘成一包,從接收緩沖區看,后一包數據的頭緊接著前一包數據的尾。
出現粘包現象的原因是多方面的,它既可能由發送方造成,也可能由接收方造成。發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一包數據。若連續幾次發送的數據都很少,通常TCP會根據優化算法把這些數據合成一包后一次發送出去,這樣接收方就收到了粘包數據。接收方引起的粘包是由于接收方用戶進程不及時接收數據,從而導致粘包現象。這是因為接收方先把收到的數據放在系統接收緩沖區,用戶進程從該緩沖區取數據,若下一包數據到達時前一包數據尚未被用戶進程取走,則下一包數據放到系統接收緩沖區時就接到前一包數據之后,而用戶進程根據預先設定的緩沖區大小從系統接收緩沖區取數據,這樣就一次取到了多包數據。
粘包情況有兩種:
1. 粘在一起的包都是完整的數據包
2. 粘在一起的包有不完整的包
解決粘連包的方法大致分為如下三種:
1. 發送方開啟TCP_NODELAY?
2. 接收方簡化或者優化流程盡可能快的接收數據
3. 認為強制分包每次只讀一個完整的包
對于以上三種方式,第一種會加重網絡負擔,第二種治標不治本,第三種算比較合理的。?
第三種又可以分兩種方式:
1. 每次都只讀取一個完整的包,不如不足一個完整的包,就等下次再接收,如果緩沖區有N個包要接受,那么需要分N次才能接收完成
2. 有多少接收多少,將接収的數據緩存在一個臨時的緩存中,交由后續的專門解碼的線程/進程處理
以上兩種分包方式,如果強制關閉程序,數據會存在丟失,第一種數據丟失在接收緩沖區;第二種丟失在程序自身緩存。
Netty自帶的幾種粘連包解決方案:
1. DelimiterBasedFrameDecoder
2. FixedLengthFrameDecoder?
3. LengthFieldBasedFrameDecoder
Netty解包組包
對于TCP編程最常遇到的就是根據具體的協議進行組包或者解包。
根據協議的不同大致可以分為如下幾種類型:?
1. JAVA平臺之間通過JAVA序列化進行解包組包(object->byte->object)
2. 固定長度的包結構(定長每個包都是M個字節的長度)
3. 帶有明確分隔符協議的解包組包(例如HTTP協議\r\n\r\n)
4. 可動態擴展的協議,此種協議通常遵循消息頭+消息體的機制,其中消息頭的長度是固定的,消息體的長度根據具體業務的不同長度可能不同。例如(SMPP協議、CMPP協議)
#序列化協議組包解包
可以使用的有:MessagePack、Google Protobuf、Hessian2
#固定長度解包組包
FixedLengthFrameDecoder 解包,MessageToByteEncoder 組包
#帶有分隔符協議的解包組包
DelimiterBasedFrameDecoder 解包,MessageToByteEncoder 組包
#HTTP
io.netty.codec.http
#消息頭固定長度,消息體不固定長度協議解包組包
LengthFieldBasedFrameDecoder
需要注意的是:對于解碼的Handler必須做到在將ByteBuf解析成Object之后,需要將ByteBuf release()。
Netty Client斷網重連機制
對于長連接的程序斷網重連幾乎是程序的標配。
斷網重連具體可以分為兩類:
1. CONNECT失敗,需要重連
2. 程序運行過程中斷網、遠程強制關閉連接、收到錯誤包必須重連
對于第一種解決方案是:實現ChannelFutureListener 用來啟動時監測是否連接成功,不成功的話重試。
Future-Listener機制
在并發編程中,我們通常會用到一組非阻塞的模型:Promise,Future,Callback。
其中的Future表示一個可能還沒有實際完成的異步任務的結果,針對這個結果添加Callback以便在執行任務成功或者失敗后做出響應的操作。而經由Promise交給執行者,任務執行者通過Promise可以標記任務完成或者失敗。以上這套模型是很多異步非阻塞框架的基礎。具體的理解可參見JDK的FutureTask和Callable。JDK的實現版本,在獲取最終結果的時候,不得不做一些阻塞的方法等待最終結果的到來。Netty的Future機制是JDK機制的一個子版本,它支持給Future添加Listener,以方便EventLoop在任務調度完成之后調用。
數據安全性之滑動窗口協議
我們假設一個場景,客戶端每次請求服務端必須得到服務端的一個響應,由于TCP的數據發送和數據接收是異步的,就存在必須存在一個等待響應的過程。該過程根據實現方式不同可以分為一下幾類(部分是錯誤案例):
1. 每次發送一個數據包,然后進入休眠(sleep)或者阻塞(await)狀態,直到響應回來或者超時,整個調用鏈結束。此場景是典型的一問一答的場景,效率極其低下
2. 讀寫分離,寫模塊只負責寫,讀模塊則負責接收響應,然后做后續的處理。此種場景能盡可能的利用帶寬進行讀寫。但是此場景不坐控速操作可能導致大量報文丟失或者重復發送。
3. 實現類似于Windowed Protocol。此窗口是以上兩種方案的折中版,即允許一定數量的批量發送,又能保證數據的完整性。
在此我向大家推薦一個架構學習交流群。交流學習群號: 744642380, 里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化、分布式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良