SOFARPC 效能優化實踐(下)| SOFAChannel#3 直播整理

螞蟻金服分散式架構發表於2019-03-01

<SOFA:Channel/>,有趣實用的分散式架構頻道。


本次是 SOFAChannel 第三期,SOFARPC 效能優化(下),進一步分享 SOFARPC 在效能上做的一些優化。


本期你將收穫:

- 如何控制序列化和反序列化的時機;

- 如何通過執行緒池隔離,避免部分介面對整體效能的影響;

- 如何進行客戶端權重調節,優化啟動期和故障時的效能;

- 服務端 Server Fail Fast 支援,減少無效操作;

- 在 Netty 記憶體操作中,如何優化記憶體使用。


歡迎加入直播互動釘釘群:23127468,不錯過每場直播。

SOFARPC 效能優化實踐(下)| SOFAChannel#3 直播整理

大家好,今天是 SOFAChannel 第三期,歡迎大家觀看。

我是來自螞蟻金服中介軟體的雷志遠,花名碧遠,目前負責 SOFARPC 框架的相關工作。在上一期直播中,給大家介紹了 SOFARPC 效能優化方面的關於自定義協議、Netty 引數優化、動態代理等的優化。

往期的直播回顧,可以在文末獲取。
本期互動中獎名單:
@司馬懿 @鄧從寶 @霧淵,請文章下方回覆進行禮品領取

今天我們會從序列化控制、記憶體操作優化、執行緒池隔離等方面來介紹剩餘的部分。


序列化優化

上次介紹了序列化方式的選擇,這次主要介紹序列化和反序列化的時機、處理的位置以及這樣的好處,如避免佔用 IO 執行緒,影響 IO 效能等。

上一節,我們介紹的 BOLT 協議的設計,回顧一下:

SOFARPC 效能優化實踐(下)| SOFAChannel#3 直播整理

可以看到有這三個地方不是通過原生型別直接寫的: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、寫分隔符。有興趣的同學可以直接看原始碼。

原始碼連結:

github.com/alipay/sofa…

serializeContent 序列化業務物件的資訊,這裡 RPC 框架會根據本次使用者配置的資訊決定如何操作序列化物件,是呼叫 Hessian 還是呼叫 Pb 來序列化。

至此,完成了序列化過程。可以看到,這些操作實際上都是在業務發起的執行緒裡面的,在請求傳送階段,也就是在呼叫 Netty 的寫介面之前,跟 IO 執行緒池還沒什麼關係,所以都會在業務執行緒裡先做好序列化。

對於反序列化

介紹完序列化,反序列化的時機就有一些差異,需要重點考慮。在服務端的請求接收階段,我們有 IO 執行緒、業務執行緒兩種執行緒池。為了最大程度的配合業務特性、保證整體吞吐,SOFABolt 設計了精細的開關來控制反序列化時機。

具體選擇邏輯如下:

SOFARPC 效能優化實踐(下)| SOFAChannel#3 直播整理

使用者請求處理器圖

體現在程式碼的這個類中。

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 效能優化實踐(下)| SOFAChannel#3 直播整理

執行緒池隔離圖

因此,基於上面的設計,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 不要關閉。

Pool 還是 UnPool

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。

注意:

  • 如果 DirectMemory 設定過小,是不會啟用 Pooled 的。
  • 另外需要注意 PooledByteBufAllocator 的 MaxDirectMemorySize 設定。本機驗證的話,大概需要 96M 以上,在 Demo中有說明。
  • Demo地址: github.com/leizhiyuan/…
 DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
                SystemPropertyUtil.getInt(
                        "io.netty.allocator.numDirectArenas",
                        (int) Math.min(
                                defaultMinNumArena,
                                PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));複製程式碼

Direct 還是 Heap

目前 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。所以:

  1. 使用 Bytebuf.forEachByte() ,傳入 Processor 來代替迴圈 ByteBuf.readByte() 的遍歷操作,避免rangeCheck() 。因為每次 readByte() 都不是讀一個位元組這麼簡單,首先要判斷 refCnt() 是否大於0,然後再做範圍檢查防止越界。getByte(i=int) 又有一些檢查函式,JVM 沒有內連的時候,效能就有一定的損耗。
  2. 使用 CompositeByteBuf 來避免不必要的記憶體拷貝。在操作一些協議包資料拼接時會比較有用,比如在 Service Mesh 的場景,如果我們需要改變 Header 中的 RequestId,然後和原始的 Body 資料拼接。
  3. 如果要讀1個 int , 用 Bytebuf.readInt() , 不要使用 Bytebuf.readBytes(buf, 0, 4) 。這樣能避免一次記憶體拷貝,其他 long 等同理,畢竟還要轉換回來,效能也更好。在 Demo 4 中有體現。
  4. RecyclableArrayList ,在出現頻繁 new ArrayList 的場景可考慮 。例如:SOFABolt 在批量解包的時候使用了 RecyClableList ,可以讓 Netty 來回收。上期分享中有介紹到這個功能,詳情可以見文末上期回顧連結。
  5. 避免拷貝,為了失敗時重試,假設要保留內容稍後使用。不想 Netty 在傳送完畢後把 buffer 就直接釋放了,可以用 copy() 複製一個新的 ByteBuf。但是下面這樣更高效,Bytebuf newBuf=oldBuf.duplicate().retain(); 只是複製出獨立的讀寫索引, 底下的 ByteBuffer 是共享的,同時將 ByteBuffer 的計數器+1,這樣可以避免釋放,而不是通過拷貝來阻止釋放。
  6. 最後可能出現問題,使用 PooledBytebuf 時要善於利用 -Dio.netty.leakDetection.level 引數,可以定位記憶體洩漏出現的資訊。


客戶端權重調節

下面,我們說一下權重。在路由階段的權重調節,我們通常能夠拿到很多可以呼叫的服務端。這時候通常情況下,最好的負載均衡演算法應該是隨機演算法。當然如果有一些特殊的需求,比如希望同樣的引數落到固定的機器組,一致性 Hash 也是可以選擇的。

不過,在系統規模到達很高的情況下,需要對啟動期間和單機故障發生期間的呼叫有一定調整。

啟動期權重調節

如果應用剛剛啟動完成,此時 JIT 的優化以及其他相關元件還未充分預熱完成。此時,如果立刻收到正常的流量呼叫可能會導致當前機器處理非常緩慢,甚至直接當機無法正常啟動。這時需要的操作:先關閉流量,然後重啟,之後開放流量。

為此,SOFARPC 允許使用者在釋出服務時,設定當前服務在啟動後的一段時間內接受的權重數值,預設是100。

SOFARPC 效能優化實踐(下)| SOFAChannel#3 直播整理

權重負載均衡圖

如上圖所示,假設使用者設定了某個服務 A 的啟動預熱時間為 60s,期間權重是10,則 SOFARPC 在呼叫的時候會進行如圖所示的權重調節。

這裡我們假設有三個服務端,兩個過了啟動期間,另一個還在啟動期間。在負載均衡的時候,三個伺服器會根據各自的權重佔總權重的比例來進行負載均衡。這樣,在啟動期間的服務方就會收到比較少的呼叫,防止打垮服務端。當過了啟動期間之後,會使用預設的 100 權重進行負載均衡。這個在 Demo 5 中有示例。

執行時單機故障權重調節

除了啟動期間保護服務端之外,還有個情況,是服務端在執行期間假死,或者其他故障。現象會是:服務發現中心認為機器存活,仍然會給客戶端推送這個地址,但是呼叫一直超時,或者一直有其他非業務異常。這種情況下,如果還是呼叫,一方面會影響鏈路的效能,因為執行緒佔用等;另一方面會有持續的報錯。因此,這種情況下還需要通過單機故障剔除的功能,對異常機器的權重進行調整,最終可以在負載均衡的時候生效。

對於單機故障剔除,本次我們不做為重點講解,有興趣的同學可以看下相關文章介紹。

附:【剖析 | SOFARPC 框架】系列之 SOFARPC 單機故障剔除剖析


Server Fail Fast 支援

服務端根據客戶端的超時時間來決定是否丟棄已經超時的結果,並且不返回,以減少網路資料以及減少不必要的處理,帶來效能提升。

SOFARPC 效能優化實踐(下)| SOFAChannel#3 直播整理

這裡面分兩種。

第一種是 SOFABolt 在網路層的 Server Fail Fast

對於 SOFABolt 層面, SOFABolt 會在 Decode 完位元組流之後,記錄一個開始時間,然後在準備分發給 RPC 的業務執行緒池之前,比較一下當前時間,是否已經超過了使用者的超時時間。如果超過了,直接丟棄,不分發給 RPC,也不會給客戶端響應。

第二種是 SOFARPC 在業務層的 Server Fail Fast

如果 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 有想要交流的話題可以在文末留言或者在公眾號留言告知我們。


本期視訊回顧

tech.antfin.com/activities/…

往期直播精彩回顧

相關參考連結

講師觀點

SOFARPC 效能優化實踐(下)| SOFAChannel#3 直播整理


公眾號:金融級分散式架構(Antfin_SOFA)


相關文章