在本人寫的前一篇文章中,談及有關如何利用Netty開發實作,高性能RPC伺服器的一些設計思路、設計原理,以及具體的實作方案(具體參見:談談如何使用Netty開發實作高性能的RPC伺服器)。在文章的最後提及到,其實基于該方案設計的RPC伺服器的處理性能,還有優化的餘地。于是利用周末的時間,在原來NettyRPC架構的基礎上,加以優化重構,本次主要優化改造點如下:
1、NettyRPC中對RPC消息進行編碼、解碼采用的是Netty自帶的ObjectEncoder、ObjectDecoder(對象編碼、解碼器),該編碼、解碼器基于的是Java的原生序列化機制,從已有的文章以及測試資料來看,Java的原生序列化性能效率不高,而且産生的序列化二進制碼流太大,故本次在優化中,引入RPC消息序列化協定的概念。所謂消息序列化協定,就是針對RPC消息的序列化、反序列化過程進行特殊的定制,引入第三方編解碼架構。本次引入的第三方編解碼架構有Kryo、Hessian。這裡,不得不再次提及一下,對象序列化、反序列化的概念,在RPC的遠端服務調用過程中,需要把消息對象通過網絡傳輸,這個就要用到序列化将對象轉變成位元組流,到達另外一端之後,再反序列化回來變成消息對象。
2、引入Google Guava并發程式設計架構對NettyRPC的NIO線程池、業務線程池進行重新梳理封裝。
3、利用第三方編解碼架構(Kryo、Hessian)的時候,考慮到高并發的場景下,頻繁的建立、銷毀序列化對象,會非常消耗JVM的記憶體資源,影響整個RPC伺服器的處理性能,是以引入對象池化(Object Pooling)技術。衆所周知,建立新對象并初始化,可能會消耗很多的時間。當需要産生大量對象的時候,可能會對性能造成一定的影響。為了解決這個問題,除了提升硬體條件之外,對象池化技術就是這方面的銀彈,而Apache Commons Pool架構就是對象池化技術的一個很好的實作(開源項目路徑:http://commons.apache.org/proper/commons-pool/download_pool.cgi)。本文中的Hessian池化工作,主要是基于Apache Commons Pool架構,進行封裝處理。
本文将着重,從上面的三個方面,對重構優化之後的NettyRPC伺服器的實作思路、實作方式進行重點講解。首先請大家簡單看下,本次優化之後的NettyRPC伺服器支援的序列化協定,如下圖所示:

可以很清楚的看到,優化之後的NettyRPC可以支援Kryo、Hessian、Java本地序列化三種消息序列化方式。其中Java本地序列化方式,相信大家應該很熟悉了,再次不在重複講述。現在我們重點講述一下,另外兩種序列化方式:
1、Kryo序列化。它是針對Java,而定制實作的高效對象序列化架構,相比Java本地原生序列化方式,Kryo在處理性能上、碼流大小上等等方面有很大的優化改進。目前已知的很多著名開源項目,都引入采用了該序列化方式。比如alibaba開源的dubbo RPC等等。本文中采用的Kryo的預設版本是基于:kryo-3.0.3。它的下載下傳連結是:https://github.com/EsotericSoftware/kryo/releases/tag/kryo-parent-3.0.3。為什麼采用這個版本?主要原因我上面也說明了,出于應對高并發場景下,頻繁地建立、銷毀序列化對象,會非常消耗JVM的記憶體資源、以及時間。Kryo的這個發行版本中,內建引入了序列化對象池功能子產品(KryoFactory、KryoPool),這樣我們就不必再利用Apache Commons Pool對其進行二次封裝。
2、Hessian序列化。Hessian本身是一種序列化協定,它比Java原生的序列化、反序列化速度更快、序列化出來的資料也更小。它是采用二進制格式進行資料傳輸,而且,目前支援多種語言格式。本文中采用的是:hessian-4.0.37 版本,它的下載下傳連結是:http://hessian.caucho.com/#Java。
接下來,先來看下優化之後的NettyRPC的消息協定編解碼包(newlandframework.netty.rpc.serialize.support、newlandframework.netty.rpc.serialize.support.kryo、newlandframework.netty.rpc.serialize.support.hessian)的結構,如下圖所示:
其中RPC請求消息結構代碼如下:
RPC應答消息結構,如下所示:
現在,我們就來對上述的RPC請求消息、應答消息進行編解碼架構的設計。由于NettyRPC中的協定類型,目前已經支援Kryo序列化、Hessian序列化、Java原生本地序列化方式。考慮到可擴充性,故要抽象出RPC消息序列化,協定類型對象(RpcSerializeProtocol),它的代碼實作如下所示:
針對不同編解碼序列化的架構(這裡主要是指Kryo、Hessian),再抽象、萃取出一個RPC消息序列化/反序列化接口(RpcSerialize)、RPC消息編解碼接口(MessageCodecUtil)。
最後我們的NettyRPC架構要能自由地支配、定制Netty的RPC服務端、用戶端,采用何種序列化來進行RPC消息對象的網絡傳輸。是以,要再抽象一個RPC消息序列化協定選擇器接口(RpcSerializeFrame),對應的實作如下:
現在有了上面定義的一系列的接口,現在就可以定制實作,基于Kryo、Hessian方式的RPC消息序列化、反序列化子產品了。先來看下整體的類圖結構:
首先是RPC消息的編碼器MessageEncoder,它繼承自Netty的MessageToByteEncoder編碼器。主要是把RPC消息對象編碼成二進制流的格式,對應實作如下:
接下來是RPC消息的解碼器MessageDecoder,它繼承自Netty的ByteToMessageDecoder。主要針對二進制流反序列化成消息對象。當然了,在之前的一篇文章中我曾經提到,NettyRPC是基于TCP協定的,TCP在傳輸資料的過程中會出現所謂的“粘包”現象,是以我們的MessageDecoder要對RPC消息體的長度進行校驗,如果不滿足RPC消息封包頭中指定的消息體長度,我們直接重置一下ByteBuf讀索引的位置,具體可以參考如下的代碼方式,進行RPC消息協定的解析:
現在,我們進一步實作,利用Kryo序列化方式,對RPC消息進行編解碼的子產品。首先是要實作NettyRPC消息序列化接口(RpcSerialize)的方法。
接着利用Kryo庫裡面的對象池,對RPC消息對象進行編解碼。首先是Kryo對象池工廠(KryoPoolFactory),這個也是我為什麼選擇kryo-3.0.3版本的原因了。代碼如下:
Kryo對RPC消息進行編碼、解碼的工具類KryoCodecUtil,實作了RPC消息編解碼接口(MessageCodecUtil),具體實作方式如下:
最後是,Kryo自己的編碼器、解碼器,其實隻要調用Kryo編解碼工具類(KryoCodecUtil)裡面的encode、decode方法就可以了。現在貼出具體的代碼:
最後,我們再來實作一下,利用Hessian實作RPC消息的編碼、解碼器代碼子產品。首先還是Hessian序列化/反序列化實作(HessianSerialize),它同樣實作了RPC消息序列化/反序列化接口(RpcSerialize),對應的代碼如下:
現在利用對象池(Object Pooling)技術,對Hessian序列化/反序列化類(HessianSerialize)進行池化處理,對應的代碼如下:
Hessian序列化對象經過池化處理之後,我們通過Hessian編解碼工具類,來“借用”Hessian序列化對象(HessianSerialize),當然了,你借出來之後,一定要還回去嘛。Hessian編解碼工具類的實作方式如下:
最後Hessian對RPC消息的編碼器、解碼器參考實作代碼如下所示:
到目前為止,NettyRPC所針對的Kryo、Hessian序列化協定子產品,已經設計實作完畢,現在我們就要把這個協定,嵌入NettyRPC的核心子產品包(newlandframework.netty.rpc.core),下面隻給出優化調整之後的代碼,其它代碼子產品的内容,可以參考我上一篇的文章:談談如何使用Netty開發實作高性能的RPC伺服器。好了,我們先來看下,NettyRPC核心子產品包(newlandframework.netty.rpc.core)的層次結構:
先來看下,NettyRPC服務端的實作部分。首先是,Rpc服務端管道初始化(MessageRecvChannelInitializer),跟上一版本對比,主要引入了序列化消息對象(RpcSerializeProtocol),具體實作代碼如下:
Rpc伺服器執行子產品(MessageRecvExecutor)中,預設的序列化采用Java原生本地序列化機制,并且優化了線程池異步調用的層次結構。具體代碼如下:
Rpc伺服器消息處理(MessageRecvHandler)也跟随着調整:
Rpc伺服器消息線程任務處理(MessageRecvInitializeTask)完成的任務也更加單純,即根據RPC消息的請求封包,利用反射得到最終的計算結果,并把結果寫入RPC應答封包結構。代碼如下:
剛才說到了,NettyRPC的服務端,可以選擇具體的序列化協定,目前是通過寫死方式實作。後續可以考慮,通過Spring IOC方式,依賴注入。其對應代碼如下:
到目前為止,NettyRPC的服務端的設計實作,已經告一段落。
現在繼續實作一下NettyRPC的用戶端子產品。其中,Rpc用戶端管道初始化(MessageSendChannelInitializer)子產品調整之後,同樣也支援選擇具體的消息序列化協定(RpcSerializeProtocol)。代碼如下:
Rpc用戶端執行子產品(MessageSendExecutor)代碼實作如下:
Rpc用戶端線程任務處理(MessageSendInitializeTask),其中參數增加了協定類型(RpcSerializeProtocol),具體代碼如下:
Rpc用戶端消息處理(MessageSendProxy)的實作方式調整重構之後,如下所示:
同樣,NettyRPC的用戶端也是可以選擇協定類型的,必須注意的是,NettyRPC的用戶端和服務端的協定類型必須一緻,才能互相通信。NettyRPC的用戶端消息序列化協定架構代碼實作方式如下:
最後,NettyRPC用戶端,要加載NettyRPC服務端的一些上下文(Context)資訊。是以,RPC伺服器配置加載(RpcServerLoader)的代碼重構調整如下:
到目前為止,NettyRPC的主要核心子產品的代碼,全部呈現出來了。到底經過改良重構之後,NettyRPC伺服器的性能如何?還是那句話,實踐是檢驗真理的唯一标準。現在,我們就來啟動三台NettyRPC伺服器進行驗證。具體服務端的配置參數,參考如下:
1、Java原生本地序列化NettyRPC伺服器,對應IP為:127.0.0.1:18887。
2、Kryo序列化NettyRPC伺服器,對應IP為:127.0.0.1:18888。
3、Hessian序列化NettyRPC伺服器,對應IP為:127.0.0.1:18889。
具體的Spring配置檔案結構如下所示:
參數配置的内容如下:
rpc-server-jdknative.properties
rpc-server-kryo.properties
rpc-server-hessian.properties
rpc-invoke-config-jdknative.xml
rpc-invoke-config-kryo.xml
rpc-invoke-config-hessian.xml
然後,對應的NettRPC伺服器啟動方式參考如下:
如果一切順利的話,在控制台上,會列印出支援Java原生序列化、Kryo序列化、Hessian序列化的NettyRPC伺服器的啟動資訊,具體截圖如下:
首先是Java原生序列化NettyRPC啟動成功截圖:
然後是Kryo序列化NettyRPC啟動成功截圖:
最後是Hessian序列化NettyRPC啟動成功截圖:
現在,還是跟我上一篇文章用到的并發測試用例一樣,設計構造一個,瞬時值并行度1W的求和計算RPC請求,總共請求10筆,然後觀察每一筆具體協定(Java原生序列化、Kryo、Hessian)的RPC消息編碼、解碼消耗時長(毫秒)。
測試代碼如下所示:
運作截圖如下:
現在,我就收集彙總一下測試資料,分析對比一下,每一種協定對RPC消息序列化/反序列化的性能(注意:由于每台計算機的配置差異,下面的測試結論可能存在出入,本次測試結果僅僅是學習交流之用!)。
經過10輪的壓力測試,具體的資料如下所示:
可以很明顯的發現,經過上述代碼架構優化調整之後,Java原生本地序列化的處理性能,跟之前部落格文章中設計實作處理性能上對比,運作效率有較大的提升(RPC消息序列化/反序列耗時更少)。Java本地序列化、Kryo序列化、Hessian序列化在10次的壓力測試中,分别有1次耗時大于10S(秒)的操作。經過統計分析之後,結果如下圖:
Kryo序列化、Hessian序列化的性能不分伯仲,并且總體優于Java本地序列化的性能水準。
再來看下,10輪壓力測試中,Java本地序列化、Kryo序列化、Hessian序列化的耗時波動情況,如下圖所示:
可以很清楚的發現,三種序列化方式分别有個“拐點”,除開這個“拐點”,三種序列化方式耗時相對來說比較平穩。但是總體而言,Kryo、Hessian序列化耗時有适當的波動,震蕩相對比較明顯;而Java原生序列化耗時相對來說比較平穩,沒有出現頻繁的震蕩,但是耗時較長。
寫在最後:本文是前一篇文章“談談如何使用Netty開發實作高性能的RPC伺服器”的性能優化篇,主要從RPC消息序列化機制、對象池(Object Pooling)、多線程優化等角度,對之前設計實作的基于Netty的RPC伺服器架構進行優化重構。當然目前的RPC伺服器,還僅僅處于“各自為政”的狀态,能不能把叢集中的若幹台RPC伺服器,通過某種機制進行統一的分布式協調管理、以及服務排程呢?答案是肯定的,一種可行的方案就是引入Zookeeper,進行服務治理。後續有時間,我會繼續加以優化改良,到時再以部落格的形式,呈現給大家!由于本人的認知水準、技術能力的限制,本文中涉及的技術觀點、測試資料、測試結論等等,僅限于部落格園中園友們的學習交流之用。如果本人有說得不對的地方,歡迎各位園友批評指正!
洋洋灑灑地寫了這麼多,感謝您的耐心閱讀。相信讀完本篇文章,面前的您,對于利用Java開發高性能的服務端應用,又多了一份了解和自信。路漫漫其修遠兮,吾将上下而求索。對于軟體知識的求學探索之路沒有止境,謹以此話和大家共勉之!
PS:自從在部落格園發表了兩篇:基于Netty開發高性能RPC伺服器的文章之後,本人收到很多園友們索要源代碼進行學習交流的請求。為了友善大家,本人把NettyRPC的代碼開源托管到github上面,歡迎有興趣的朋友一起學習、研究!
附上NettyRPC項目的下載下傳路徑:https://github.com/tang-jie/NettyRPC