<SOFA:Channel/>,有趣實用的分佈式架構頻道。html
本次是 SOFAChannel 第三期,SOFARPC 性能優化(下),進一步分享 SOFARPC 在性能上作的一些優化。java
本期你將收穫:git
- 如何控制序列化和反序列化的時機;github
- 如何經過線程池隔離,避免部分接口對總體性能的影響;算法
- 如何進行客戶端權重調節,優化啓動期和故障時的性能;json
- 服務端 Server Fail Fast 支持,減小無效操做;數組
- 在 Netty 內存操做中,如何優化內存使用。性能優化
歡迎加入直播互動釘釘羣:23127468,不錯過每場直播。bash
你們好,今天是 SOFAChannel 第三期,歡迎你們觀看。服務器
我是來自螞蟻金服中間件的雷志遠,花名碧遠,目前負責 SOFARPC 框架的相關工做。在上一期直播中,給你們介紹了 SOFARPC 性能優化方面的關於自定義協議、Netty 參數優化、動態代理等的優化。
往期的直播回顧,能夠在文末獲取。
本期互動中獎名單:
@司馬懿 @鄧從寶 @霧淵,請文章下方回覆進行禮品領取
今天咱們會從序列化控制、內存操做優化、線程池隔離等方面來介紹剩餘的部分。
上次介紹了序列化方式的選擇,此次主要介紹序列化和反序列化的時機、處理的位置以及這樣的好處,如避免佔用 IO 線程,影響 IO 性能等。
上一節,咱們介紹的 BOLT 協議的設計,回顧一下:
能夠看到有這三個地方不是經過原生類型直接寫的:ClassName,Header,Content 。其他的,例如 RequestId 是直接寫的,或者說跟具體請求對象無關的。因此在選擇序列化和反序列化時機的時候,咱們根據本身的需求,也精確的控制了協議以上三個部分的時機。
serializeClazz 是最簡單的:
byte[] clz = this.requestClass.getBytes(Configs.DEFAULT_CHARSET);複製代碼
直接將字符串轉換成 Byte 數組便可,跟具體的任何序列化方式,好比跟採用 Hessian 仍是 Pb 都是無關的。
serializeHeader 則是序列化 HeaderMap。這時候由於有了前面的 requestClass,就能夠根據這個名字拿到SOFARPC 層或者用戶本身註冊的序列化器。而後進行序列化 Header,這個對應 SOFARPC 框架中的 SofaRpcSerialization 類。在這個類裏,咱們能夠自由使用本次傳輸的對象,將一些必要信息提取到Header 中,並進行對應的編碼。這裏也不跟具體的序列化方式有關,是一個簡單 Map 的序列化,寫 key、寫 value、寫分隔符。有興趣的同窗能夠直接看源碼。
源碼連接:
serializeContent 序列化業務對象的信息,這裏 RPC 框架會根據本次用戶配置的信息決定如何操做序列化對象,是調用 Hessian 仍是調用 Pb 來序列化。
至此,完成了序列化過程。能夠看到,這些操做實際上都是在業務發起的線程裏面的,在請求發送階段,也就是在調用 Netty 的寫接口以前,跟 IO 線程池還沒什麼關係,因此都會在業務線程裏先作好序列化。
介紹完序列化,反序列化的時機就有一些差別,須要重點考慮。在服務端的請求接收階段,咱們有 IO 線程、業務線程兩種線程池。爲了最大程度的配合業務特性、保證總體吞吐,SOFABolt 設計了精細的開關來控制反序列化時機。
具體選擇邏輯以下:
用戶請求處理器圖
體如今代碼的這個類中。
com.alipay.remoting.rpc.protocol.RpcRequestProcessor#process複製代碼
從上圖能夠看到 反序列化 大體分紅如下三種狀況,適用於不一樣的場景。
IO 線程池動做 |
業務線程池 |
使用場景 |
反序列化 ClassName |
反序列化 Header 和 Content 處理業務 |
通常 RPC 默認場景。IO 線程池識別出來當前是哪一個類,調用用戶註冊的對應處理器 |
反序列化 ClassName 和 Header |
僅反序列化 Content 和業務處理 |
但願根據 Header 中的信息,選擇線程池,而不是直接註冊的線程池 |
一次性反序列化 ClassName、Header 和 Content,並直接處理 |
沒有邏輯 |
IO 密集型的業務 |
通過前面的介紹,能夠了解到,因爲業務邏輯一般狀況下在 SOFARPC 設置的一個默認線程池裏面處理,這個線程池是公用的。也就是說, 對於一個應用,當他做爲服務端時,全部的調用請求都會在這個線程池中處理。
舉個例子:若是應用 A 對外提供兩個接口,S1 和 S2,因爲 S2 接口的性能不足,多是下游系統的拖累,會致使這個默認線程池一直被佔用,沒法空閒出來被其餘請求使用。這會致使 S1 的處理能力受到影響,對外報錯,線程池已滿,致使整個業務鏈路不穩定,有時候 S1 的重要性可能比 S2 更高。
線程池隔離圖
所以,基於上面的設計,SOFARPC 框架容許在序列化的時候,根據用戶對當前接口的線程池配置將接口和服務信息放到 Header 中,反序列化的時候,根據這個 Header 信息選擇到用戶自定義的線程池。這樣,用戶能夠針對不一樣的服務接口配置不一樣的業務線程池,能夠避免部分接口對整個性能的影響。在系統接口較多的時候,能夠有效的提升總體的性能。
介紹完線程池隔離以後,咱們介紹一下 Netty 內存操做的一些注意事項。在 Netty 內存操做中,如何儘可能少的使用內存和避免垃圾回收,來優化性能。先看一些基礎概念。
在 JVM 中內存可分爲兩大塊,一個是堆內存,一個是直接內存。
堆內存是 JVM 所管理的內存。全部的對象實例都要在堆上分配,垃圾收集器能夠在堆上回收垃圾,有不一樣的運行條件和回收區域。
JVM 使用 Native 函數在堆外分配內存。爲何要在堆外分配內存?主要由於在堆上的話, IO 操做會涉及到頻繁的內存分配和銷燬,這會致使 GC 頻繁,對性能會有比較大的影響。
注意:直接分配自己也並不見得性能有多好,因此還要有池的概念,減小頻繁的分配。
所以 JVM 中的直接內存,存在堆內存中的其實就是 DirectByteBuffer 類,它自己其實很小,真的內存是在堆外,經過 JVM 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。直接內存不會受到 Java 堆的限制,只受本機內存影響。固然能夠設置最大大小。也並非 Direct 就徹底跟 Heap 沒什麼關係了,由於堆中的這個對象持有了堆外的地址,只有這個對象被回收了,直接內存才能釋放。
其中 DirectByteBuffer 通過幾回 young gc 以後,會進入老年代。當老年代滿了以後,會觸發 Full GC。
由於自己很小,很難佔滿老年代,所以基本不會觸發 Full GC,帶來的後果是大量堆外內存一直佔着不放,沒法進行內存回收,因此這裏要注意 -XX:+DisableExplicitGC
不要關閉。
Netty 從 4.1.x 開始,非 Android 平臺默認使用池化(PooledByteBufAllocator)實現,能最大程度的減小內存碎片。另一種方式是非池化(UnpooledByteBufAllocator),每次返回一個新實例。能夠查看 io.netty.buffer.ByteBufUtil
這個工具類。
在 4.1.x 以前,因爲 Netty 沒法確認 Pool 是否存在內存泄漏,因此並無打開。目前,SOFARPC 的 SOFABolt 中目前對於 Pool 和 Upool 是經過參數決定的,默認是 Unpool。使用 Pool 會有更好的性能數據。在 SOFABolt 1.5.0 中進行了打開,若是新開發 RPC 框架,能夠進行默認打開。SOFARPC 下個版本會進行打開。
可能你們對這個的感覺不是很直觀,所以咱們提供了一個測試 Demo。
注意:
DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
SystemPropertyUtil.getInt(
"io.netty.allocator.numDirectArenas",
(int) Math.min(
defaultMinNumArena,
PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));複製代碼
目前 Netty 在 write 的時候默認是 Direct ,而在 read 到字節流時會進行選擇。能夠查看以下代碼,`io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read`
。框架所採起的策略是:若是所運行的平臺提供了Unsafe 相關的操做,則調用 Unsafe 在 Direct 區域進行內存分配,不然在 Heap 上進行分配。
有興趣的同窗能夠經過 Demo 3 中的示例來 debug,斷點打在以下位置,就能夠看到 Netty 選擇的過程。
io.netty.buffer.AbstractByteBufAllocator#ioBuffer(int)複製代碼
正常 RPC 的開發中,基本上都會在 Direct 區域進行內存分配,在 Heap 中進行內存分配自己也不符合 RPC 的性能要求。由於 GC 有比較大的性能影響,而 GC 在運行中,業務的代碼影響比較大,可控性不強。
通常來講,咱們不會主動去分配 ByteBuf ,只要去操做讀寫 ByteBuf。因此:
下面,咱們說一下權重。在路由階段的權重調節,咱們一般可以拿到不少能夠調用的服務端。這時候一般狀況下,最好的負載均衡算法應該是隨機算法。固然若是有一些特殊的需求,好比但願一樣的參數落到固定的機器組,一致性 Hash 也是能夠選擇的。
不過,在系統規模到達很高的狀況下,須要對啓動期間和單機故障發生期間的調用有必定調整。
若是應用剛剛啓動完成,此時 JIT 的優化以及其餘相關組件還未充分預熱完成。此時,若是馬上收到正常的流量調用可能會致使當前機器處理很是緩慢,甚至直接當機沒法正常啓動。這時須要的操做:先關閉流量,而後重啓,以後開放流量。
爲此,SOFARPC 容許用戶在發佈服務時,設置當前服務在啓動後的一段時間內接受的權重數值,默認是100。
權重負載均衡圖
如上圖所示,假設用戶設置了某個服務 A 的啓動預熱時間爲 60s,期間權重是10,則 SOFARPC 在調用的時候會進行如圖所示的權重調節。
這裏咱們假設有三個服務端,兩個過了啓動期間,另外一個還在啓動期間。在負載均衡的時候,三個服務器會根據各自的權重佔總權重的比例來進行負載均衡。這樣,在啓動期間的服務方就會收到比較少的調用,防止打垮服務端。當過了啓動期間以後,會使用默認的 100 權重進行負載均衡。這個在 Demo 5 中有示例。
除了啓動期間保護服務端以外,還有個狀況,是服務端在運行期間假死,或者其餘故障。現象會是:服務發現中心認爲機器存活,仍然會給客戶端推送這個地址,可是調用一直超時,或者一直有其餘非業務異常。這種狀況下,若是仍是調用,一方面會影響鏈路的性能,由於線程佔用等;另外一方面會有持續的報錯。所以,這種狀況下還須要經過單機故障剔除的功能,對異常機器的權重進行調整,最終能夠在負載均衡的時候生效。
對於單機故障剔除,本次咱們不作爲重點講解,有興趣的同窗能夠看下相關文章介紹。
服務端根據客戶端的超時時間來決定是否丟棄已經超時的結果,而且不返回,以減小網絡數據以及減小沒必要要的處理,帶來性能提高。
這裏面分兩種。
對於 SOFABolt 層面, SOFABolt 會在 Decode 完字節流以後,記錄一個開始時間,而後在準備分發給 RPC 的業務線程池以前,比較一下當前時間,是否已經超過了用戶的超時時間。若是超過了,直接丟棄,不分發給 RPC,也不會給客戶端響應。
若是 SOFABolt 分發給 SOFARPC 的時候,尚未超時,可是 SOFARPC 走完了服務端業務邏輯以後,發現已經超時了。這時候,能夠不返回業務結果,直接構造異常超時結果,數據更少,但結果是同樣的。
注意:這裏會有個反作用,雖然服務端處理已經完成,可是日誌裏可能會打印一個錯誤碼,須要根據實際狀況開啓。
以後咱們也會開放參數,容許用戶設置。
對用戶的配置,你們均可以經過 com.alipay.sofa.rpc.boot.config.SofaBootRpcProperties 這個類來查看。
使用方式和標準的 SpringBoot 工程一致,開箱便可。
若是是特別特殊的需求,或者並不使用 Spring 做爲開發框架,咱們也容許用戶經過定製 rpc-config.json 文件來進行調整,包括動態代理生成方式、默認的 tracer、超時時間的控制、時機序列化黑名單是否開啓等等。這些參數在有特殊需求的狀況下能夠優化性能。
以業務線程數爲例,目前默認線程池,20核心線程數,200最大線程數,0隊列。能夠經過如下配置項來調整:
com.alipay.sofa.rpc.bolt.thread.pool.core.size # bolt 核心線程數
com.alipay.sofa.rpc.bolt.thread.pool.max.size # bolt 最大線程數
com.alipay.sofa.rpc.bolt.thread.pool.queue.size # bolt 線程池隊列複製代碼
這裏在線程池的設置上,主要關注隊列大小這個設置項。若是隊列數比較大,會致使若是上游系統處理能力不足的時候,請求積壓在隊列中,等真正處理的時候已通過了比較長的時間,並且若是請求量很是大,會致使以後的請求都至少等待整個隊列前面的數據。
因此若是業務是一個延遲敏感的系統, 建議不要設置隊列大小;若是業務能夠接受必定程度的線程池等待,能夠設置。這樣,能夠避太短暫的流量高峯。
SOFARPC 和 SOFABolt 在性能優化上作了一些工做,包括一些比較實際的業務需求產生的性能優化方式。兩篇文章不足以介紹更多的代碼實現細節和方式。錯過上期直播的能夠點擊文末連接進行回顧。
相信你們在 RPC 或者其餘中間件的開發中,也有本身獨到的性能優化方式,若是你們對 RPC 的性能和需求有本身的想法,歡迎你們在釘釘羣(搜索羣號便可加入:23127468)或者 Github 上與咱們討論交流。
到此,咱們 SOFAChannel 的 SOFARPC 系列主題關於性能優化相關的兩期分享就介紹完了,感謝你們。
關於 SOFAChannel 有想要交流的話題能夠在文末留言或者在公衆號留言告知咱們。
公衆號:金融級分佈式架構(Antfin_SOFA)