最近一個圈內朋友經過私信告訴我,經過使用Netty4 + Thrift壓縮二進制編解碼技術,他們實現了10W TPS(1K的複雜POJO對象)的跨節點遠程服務調用。相比於傳統基於Java序列化+BIO(同步阻塞IO)的通訊框架,性能提高了8倍多。html
事實上,我對這個數據並不感到驚訝,根據我5年多的NIO編程經驗,經過選擇合適的NIO框架,加上高性能的壓縮二進制編解碼技術,精心的設計Reactor線程模型,達到上述性能指標是徹底有可能的。react
下面咱們就一塊兒來看下Netty是如何支持10W TPS的跨節點遠程服務調用的,在正式開始講解以前,咱們先簡單介紹下Netty。算法
Netty是一個高性能、異步事件驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持,做爲一個異步NIO框架,Netty的全部IO操做都是異步非阻塞的,經過Future-Listener機制,用戶能夠方便的主動獲取或者經過通知機制得到IO操做結果。安全
做爲當前最流行的NIO框架,Netty在互聯網領域、大數據分佈式計算領域、遊戲行業、通訊行業等得到了普遍的應用,一些業界著名的開源組件也基於Netty的NIO框架構建。服務器
網絡傳輸方式問題:傳統的RPC框架或者基於RMI等方式的遠程服務(過程)調用採用了同步阻塞IO,當客戶端的併發壓力或者網絡時延增大以後,同步阻塞IO會因爲頻繁的wait致使IO線程常常性的阻塞,因爲線程沒法高效的工做,IO處理能力天然降低。微信
下面,咱們經過BIO通訊模型圖看下BIO通訊的弊端:網絡
圖2-1 BIO通訊模型圖
採用BIO通訊模型的服務端,一般由一個獨立的Acceptor線程負責監聽客戶端的鏈接,接收到客戶端鏈接以後爲客戶端鏈接建立一個新的線程處理請求消息,處理完成以後,返回應答消息給客戶端,線程銷燬,這就是典型的一請求一應答模型。該架構最大的問題就是不具有彈性伸縮能力,當併發訪問量增長後,服務端的線程個數和併發訪問數成線性正比,因爲線程是JAVA虛擬機很是寶貴的系統資源,當線程數膨脹以後,系統的性能急劇降低,隨着併發量的繼續增長,可能會發生句柄溢出、線程堆棧溢出等問題,並致使服務器最終宕機。
序列化方式問題:Java序列化存在以下幾個典型問題:
1) Java序列化機制是Java內部的一種對象編解碼技術,沒法跨語言使用;例如對於異構系統之間的對接,Java序列化後的碼流須要可以經過其它語言反序列化成原始對象(副本),目前很難支持;
2) 相比於其它開源的序列化框架,Java序列化後的碼流太大,不管是網絡傳輸仍是持久化到磁盤,都會致使額外的資源佔用;
3) 序列化性能差(CPU資源佔用高)。
線程模型問題:因爲採用同步阻塞IO,這會致使每一個TCP鏈接都佔用1個線程,因爲線程資源是JVM虛擬機很是寶貴的資源,當IO讀寫阻塞致使線程沒法及時釋放時,會致使系統性能急劇降低,嚴重的甚至會致使虛擬機沒法建立新的線程。
1) 傳輸:用什麼樣的通道將數據發送給對方,BIO、NIO或者AIO,IO模型在很大程度上決定了框架的性能。
2) 協議:採用什麼樣的通訊協議,HTTP或者內部私有協議。協議的選擇不一樣,性能模型也不一樣。相比於公有協議,內部私有協議的性能一般能夠被設計的更優。
3) 線程:數據報如何讀取?讀取以後的編解碼在哪一個線程進行,編解碼後的消息如何派發,Reactor線程模型的不一樣,對性能的影響也很是大。
圖2-2 RPC調用性能三要素
在IO編程過程當中,當須要同時處理多個客戶端接入請求時,能夠利用多線程或者IO多路複用技術進行處理。IO多路複用技術經過把多個IO的阻塞複用到同一個select的阻塞上,從而使得系統在單線程的狀況下能夠同時處理多個客戶端請求。與傳統的多線程/多進程模型比,I/O多路複用的最大優點是系統開銷小,系統不須要建立新的額外進程或者線程,也不須要維護這些進程和線程的運行,下降了系統的維護工做量,節省了系統資源。
JDK1.4提供了對非阻塞IO(NIO)的支持,JDK1.5_update10版本使用epoll替代了傳統的select/poll,極大的提高了NIO通訊的性能。
JDK NIO通訊模型以下所示:
圖2-3 NIO的多路複用模型圖
與Socket類和ServerSocket類相對應,NIO也提供了SocketChannel和ServerSocketChannel兩種不一樣的套接字通道實現。這兩種新增的通道都支持阻塞和非阻塞兩種模式。阻塞模式使用很是簡單,可是性能和可靠性都很差,非阻塞模式正好相反。開發人員通常能夠根據本身的須要來選擇合適的模式,通常來講,低負載、低併發的應用程序能夠選擇同步阻塞IO以下降編程複雜度。可是對於高負載、高併發的網絡應用,須要使用NIO的非阻塞模式進行開發。
Netty架構按照Reactor模式設計和實現,它的服務端通訊序列圖以下:
圖2-3 NIO服務端通訊序列圖
客戶端通訊序列圖以下:
圖2-4 NIO客戶端通訊序列圖
Netty的IO線程NioEventLoop因爲聚合了多路複用器Selector,能夠同時併發處理成百上千個客戶端Channel,因爲讀寫操做都是非阻塞的,這就能夠充分提高IO線程的運行效率,避免因爲頻繁IO阻塞致使的線程掛起。另外,因爲Netty採用了異步通訊模式,一個IO線程能夠併發處理N個客戶端鏈接和讀寫操做,這從根本上解決了傳統同步阻塞IO一鏈接一線程模型,架構的性能、彈性伸縮能力和可靠性都獲得了極大的提高。
不少用戶都據說過Netty具備「零拷貝」功能,可是具體體如今哪裏又說不清楚,本小節就詳細對Netty的「零拷貝」功能進行講解。
Netty的「零拷貝」主要體如今以下三個方面:
1) Netty的接收和發送ByteBuffer採用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不須要進行字節緩衝區的二次拷貝。若是使用傳統的堆內存(HEAP BUFFERS)進行Socket讀寫,JVM會將堆內存Buffer拷貝一份到直接內存中,而後才寫入Socket中。相比於堆外直接內存,消息在發送過程當中多了一次緩衝區的內存拷貝。
2) Netty提供了組合Buffer對象,能夠聚合多個ByteBuffer對象,用戶能夠像操做一個Buffer那樣方便的對組合Buffer進行操做,避免了傳統經過內存拷貝的方式將幾個小Buffer合併成一個大的Buffer。
3) Netty的文件傳輸採用了transferTo方法,它能夠直接將文件緩衝區的數據發送到目標Channel,避免了傳統經過循環write方式致使的內存拷貝問題。
下面,咱們對上述三種「零拷貝」進行說明,先看Netty 接收Buffer的建立:
圖2-5 異步消息讀取「零拷貝」
每循環讀取一次消息,就經過ByteBufAllocator的ioBuffer方法獲取ByteBuf對象,下面繼續看它的接口定義:
圖2-6 ByteBufAllocator 經過ioBuffer分配堆外內存
當進行Socket IO讀寫的時候,爲了不從堆內存拷貝一份副本到直接內存,Netty的ByteBuf分配器直接建立非堆內存避免緩衝區的二次拷貝,經過「零拷貝」來提高讀寫性能。
下面咱們繼續看第二種「零拷貝」的實現CompositeByteBuf,它對外將多個ByteBuf封裝成一個ByteBuf,對外提供統一封裝後的ByteBuf接口,它的類定義以下:
圖2-7 CompositeByteBuf類繼承關係
經過繼承關係咱們能夠看出CompositeByteBuf實際就是個ByteBuf的包裝器,它將多個ByteBuf組合成一個集合,而後對外提供統一的ByteBuf接口,相關定義以下:
圖2-8 CompositeByteBuf類定義
添加ByteBuf,不須要作內存拷貝,相關代碼以下:
圖2-9 新增ByteBuf的「零拷貝」
最後,咱們看下文件傳輸的「零拷貝」:
圖2-10 文件傳輸「零拷貝」
Netty文件傳輸DefaultFileRegion經過transferTo方法將文件發送到目標Channel中,下面重點看FileChannel的transferTo方法,它的API DOC說明以下:
圖2-11 文件傳輸 「零拷貝」
對於不少操做系統它直接將文件緩衝區的內容發送到目標Channel中,而不須要經過拷貝的方式,這是一種更加高效的傳輸方式,它實現了文件傳輸的「零拷貝」。
隨着JVM虛擬機和JIT即時編譯技術的發展,對象的分配和回收是個很是輕量級的工做。可是對於緩衝區Buffer,狀況卻稍有不一樣,特別是對於堆外直接內存的分配和回收,是一件耗時的操做。爲了儘可能重用緩衝區,Netty提供了基於內存池的緩衝區重用機制。下面咱們一塊兒看下Netty ByteBuf的實現:
圖2-12 內存池ByteBuf
Netty提供了多種內存管理策略,經過在啓動輔助類中配置相關參數,能夠實現差別化的定製。
下面經過性能測試,咱們看下基於內存池循環利用的ByteBuf和普通ByteBuf的性能差別。
用例一,使用內存池分配器建立直接內存緩衝區:
圖2-13 基於內存池的非堆內存緩衝區測試用例
用例二,使用非堆內存分配器建立的直接內存緩衝區:
圖2-14 基於非內存池建立的非堆內存緩衝區測試用例
各執行300萬次,性能對比結果以下所示:
圖2-15 內存池和非內存池緩衝區寫入性能對比
性能測試代表,採用內存池的ByteBuf相比於朝生夕滅的ByteBuf,性能高23倍左右(性能數據與使用場景強相關)。
下面咱們一塊兒簡單分析下Netty內存池的內存分配:
圖2-16 AbstractByteBufAllocator的緩衝區分配
繼續看newDirectBuffer方法,咱們發現它是一個抽象方法,由AbstractByteBufAllocator的子類負責具體實現,代碼以下:
圖2-17 newDirectBuffer的不一樣實現
代碼跳轉到PooledByteBufAllocator的newDirectBuffer方法,從Cache中獲取內存區域PoolArena,調用它的allocate方法進行內存分配:
圖2-18 PooledByteBufAllocator的內存分配
PoolArena的allocate方法以下:
圖2-18 PoolArena的緩衝區分配
咱們重點分析newByteBuf的實現,它一樣是個抽象方法,由子類DirectArena和HeapArena來實現不一樣類型的緩衝區分配,因爲測試用例使用的是堆外內存,
圖2-19 PoolArena的newByteBuf抽象方法
所以重點分析DirectArena的實現:若是沒有開啓使用sun的unsafe,則
圖2-20 DirectArena的newByteBuf方法實現
執行PooledDirectByteBuf的newInstance方法,代碼以下:
圖2-21 PooledDirectByteBuf的newInstance方法實現
經過RECYCLER的get方法循環使用ByteBuf對象,若是是非內存池實現,則直接建立一個新的ByteBuf對象。從緩衝池中獲取ByteBuf以後,調用AbstractReferenceCountedByteBuf的setRefCnt方法設置引用計數器,用於對象的引用計數和內存回收(相似JVM垃圾回收機制)。
經常使用的Reactor線程模型有三種,分別以下:
1) Reactor單線程模型;
2) Reactor多線程模型;
3) 主從Reactor多線程模型
Reactor單線程模型,指的是全部的IO操做都在同一個NIO線程上面完成,NIO線程的職責以下:
1) 做爲NIO服務端,接收客戶端的TCP鏈接;
2) 做爲NIO客戶端,向服務端發起TCP鏈接;
3) 讀取通訊對端的請求或者應答消息;
4) 向通訊對端發送消息請求或者應答消息。
Reactor單線程模型示意圖以下所示:
圖2-22 Reactor單線程模型
因爲Reactor模式使用的是異步非阻塞IO,全部的IO操做都不會致使阻塞,理論上一個線程能夠獨立處理全部IO相關的操做。從架構層面看,一個NIO線程確實能夠完成其承擔的職責。例如,經過Acceptor接收客戶端的TCP鏈接請求消息,鏈路創建成功以後,經過Dispatch將對應的ByteBuffer派發到指定的Handler上進行消息解碼。用戶Handler能夠經過NIO線程將消息發送給客戶端。
對於一些小容量應用場景,可使用單線程模型。可是對於高負載、大併發的應用卻不合適,主要緣由以下:
1) 一個NIO線程同時處理成百上千的鏈路,性能上沒法支撐,即使NIO線程的CPU負荷達到100%,也沒法知足海量消息的編碼、解碼、讀取和發送;
2) 當NIO線程負載太重以後,處理速度將變慢,這會致使大量客戶端鏈接超時,超時以後每每會進行重發,這更加劇了NIO線程的負載,最終會致使大量消息積壓和處理超時,NIO線程會成爲系統的性能瓶頸;
3) 可靠性問題:一旦NIO線程意外跑飛,或者進入死循環,會致使整個系統通訊模塊不可用,不能接收和處理外部消息,形成節點故障。
爲了解決這些問題,演進出了Reactor多線程模型,下面咱們一塊兒學習下Reactor多線程模型。
Rector多線程模型與單線程模型最大的區別就是有一組NIO線程處理IO操做,它的原理圖以下:
圖2-23 Reactor多線程模型
Reactor多線程模型的特色:
1) 有專門一個NIO線程-Acceptor線程用於監聽服務端,接收客戶端的TCP鏈接請求;
2) 網絡IO操做-讀、寫等由一個NIO線程池負責,線程池能夠採用標準的JDK線程池實現,它包含一個任務隊列和N個可用的線程,由這些NIO線程負責消息的讀取、解碼、編碼和發送;
3) 1個NIO線程能夠同時處理N條鏈路,可是1個鏈路只對應1個NIO線程,防止發生併發操做問題。
在絕大多數場景下,Reactor多線程模型均可以知足性能需求;可是,在極特殊應用場景中,一個NIO線程負責監聽和處理全部的客戶端鏈接可能會存在性能問題。例如百萬客戶端併發鏈接,或者服務端須要對客戶端的握手消息進行安全認證,認證自己很是損耗性能。在這類場景下,單獨一個Acceptor線程可能會存在性能不足問題,爲了解決性能問題,產生了第三種Reactor線程模型-主從Reactor多線程模型。
主從Reactor線程模型的特色是:服務端用於接收客戶端鏈接的再也不是個1個單獨的NIO線程,而是一個獨立的NIO線程池。Acceptor接收到客戶端TCP鏈接請求處理完成後(可能包含接入認證等),將新建立的SocketChannel註冊到IO線程池(sub reactor線程池)的某個IO線程上,由它負責SocketChannel的讀寫和編解碼工做。Acceptor線程池僅僅只用於客戶端的登錄、握手和安全認證,一旦鏈路創建成功,就將鏈路註冊到後端subReactor線程池的IO線程上,由IO線程負責後續的IO操做。
它的線程模型以下圖所示:
圖2-24 Reactor主從多線程模型
利用主從NIO線程模型,能夠解決1個服務端監聽線程沒法有效處理全部客戶端鏈接的性能不足問題。所以,在Netty的官方demo中,推薦使用該線程模型。
事實上,Netty的線程模型並不是固定不變,經過在啓動輔助類中建立不一樣的EventLoopGroup實例並經過適當的參數配置,就能夠支持上述三種Reactor線程模型。正是由於Netty 對Reactor線程模型的支持提供了靈活的定製能力,因此能夠知足不一樣業務場景的性能訴求。
在大多數場景下,並行多線程處理能夠提高系統的併發性能。可是,若是對於共享資源的併發訪問處理不當,會帶來嚴重的鎖競爭,這最終會致使性能的降低。爲了儘量的避免鎖競爭帶來的性能損耗,能夠經過串行化設計,即消息的處理儘量在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖。
爲了儘量提高性能,Netty採用了串行無鎖化設計,在IO線程內部進行串行操做,避免多線程競爭致使的性能降低。表面上看,串行化設計彷佛CPU利用率不高,併發程度不夠。可是,經過調整NIO線程池的線程參數,能夠同時啓動多個串行化的線程並行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工做線程模型性能更優。
Netty的串行化設計工做原理圖以下:
圖2-25 Netty串行化工做原理圖
Netty的NioEventLoop讀取到消息以後,直接調用ChannelPipeline的fireChannelRead(Object msg),只要用戶不主動切換線程,一直會由NioEventLoop調用到用戶的Handler,期間不進行線程切換,這種串行化處理方式避免了多線程操做致使的鎖的競爭,從性能角度看是最優的。
Netty的高效併發編程主要體如今以下幾點:
1) volatile的大量、正確使用;
2) CAS和原子類的普遍使用;
3) 線程安全容器的使用;
4) 經過讀寫鎖提高併發性能。
若是你們想了解Netty高效併發編程的細節,能夠閱讀以前我在微博分享的《多線程併發編程在 Netty 中的應用分析》,在這篇文章中對Netty的多線程技巧和應用進行了詳細的介紹和分析。
影響序列化性能的關鍵因素總結以下:
1) 序列化後的碼流大小(網絡帶寬的佔用);
2) 序列化&反序列化的性能(CPU資源佔用);
3) 是否支持跨語言(異構系統的對接和開發語言切換)。
Netty默認提供了對Google Protobuf的支持,經過擴展Netty的編解碼接口,用戶能夠實現其它的高性能序列化框架,例如Thrift的壓縮二進制編解碼框架。
下面咱們一塊兒看下不一樣序列化&反序列化框架序列化後的字節數組對比:
圖2-26 各序列化框架序列化碼流大小對比
從上圖能夠看出,Protobuf序列化後的碼流只有Java序列化的1/4左右。正是因爲Java原生序列化性能表現太差,才催生出了各類高性能的開源序列化技術和框架(性能差只是其中的一個緣由,還有跨語言、IDL定義等其它因素)。
合理設置TCP參數在某些場景下對於性能的提高能夠起到顯著的效果,例如SO_RCVBUF和SO_SNDBUF。若是設置不當,對性能的影響是很是大的。下面咱們總結下對性能影響比較大的幾個配置項:
1) SO_RCVBUF和SO_SNDBUF:一般建議值爲128K或者256K;
2) SO_TCPNODELAY:NAGLE算法經過將緩衝區內的小封包自動相連,組成較大的封包,阻止大量小封包的發送阻塞網絡,從而提升網絡應用效率。可是對於時延敏感的應用場景須要關閉該優化算法;
3) 軟中斷:若是Linux內核版本支持RPS(2.6.35以上版本),開啓RPS後能夠實現軟中斷,提高網絡吞吐量。RPS根據數據包的源地址,目的地址以及目的和源端口,計算出一個hash值,而後根據這個hash值來選擇軟中斷運行的cpu,從上層來看,也就是說將每一個鏈接和cpu綁定,並經過這個hash值,來均衡軟中斷在多個cpu上,提高網絡並行處理性能。
Netty在啓動輔助類中能夠靈活的配置TCP參數,知足不一樣的用戶場景。相關配置接口定義以下:
圖2-27 Netty的TCP參數配置定義
經過對Netty的架構和性能模型進行分析,咱們發現Netty架構的高性能是被精心設計和實現的,得益於高質量的架構和代碼,Netty支持10W TPS的跨節點服務調用並非件十分困難的事情。
李林鋒,2007年畢業於東北大學,2008年進入華爲公司從事高性能通訊軟件的設計和開發工做,有6年NIO設計和開發經驗,精通Netty、Mina等NIO框架。Netty中國社區創始人,《Netty權威指南》做者。
聯繫方式:新浪微博 Nettying 微信:Nettying