歡迎光臨
每天分享高質量文章

SOFARPC 性能優化實踐(上)| SOFAChannel#2 直播整理

,有趣實用的分佈式架構頻道。

本次是 SOFAChannel 第二期,主要分享 SOFARPC 在性能上做的一些優化,這個系列會分成上下兩部分進行分享,今天是 SOFARPC 性能優化(上),也會對本次分享中的一些結論,提供部分代碼 Demo,供大家瞭解驗證。

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

大家好,今天是我們 SOFAChannel 第二期。歡迎大家觀看。

我是來自螞蟻金服中間件的雷志遠,花名碧遠,目前在負責 SOFARPC 框架相關工作。
去年的時候,我們和外部的愛好者們一起,做了一個基於 SOFARPC 的原始碼解析系列,我同事已經發到群里了,大家可以儲存,直播之後查看。
SOFARPC 原始碼解析系列:點擊【剖析 | SOFARPC 框架】即可查看
https://www.sofastack.tech/posts  

今年,基於原始碼解析的基礎,我們來多講講實踐,如何應用到大家的業務,來幫助大家解決實際問題。在直播過程中有相關的問題想提問,可以在釘釘群互動。

前言

在上一期中,餘淮分享了《從螞蟻金服微服務實踐談起》。介紹了螞蟻微服務的起源,以及之後服務化,單元化的情況。同時介紹了 SOFAStack 目前開源的情況。最後也分享了一下整個微服務中 SOFARPC 的設計與實現。

本期,我們主要分享 SOFARPC 在性能上做的一些優化。這個系列會分成上下兩部分進行分享,今天是 SOFARPC 性能優化(上),也會對本次分享中的一些結論,提供部分代碼 Demo,供大家瞭解驗證。

我們先簡要介紹一下 SOFARPC 的框架分層。這個在上次的分享中已經進行了介紹。

下層是網絡傳輸層,依次是協議,序列化,服務發現和 Filter 等。
Transport 主要負責資料傳輸,可以是 Http2Transport,也可以是 BoltTransport,還有可能是其他。
Protocol 層是協議,是 Rest 還是 Bolt ,或是 Dubbo 。
Serialization 是序列化,對於每種協議,可以是用不同的序列化方式,比如 hessian,pb,json 等。
Filter 是通用的過濾器層,主要是為了留出一些擴展,完成一些其他擴展功能,比如 Tracer 的埋點等。
Router 是路由層,主要是做尋址,這裡可能是 Zk,也可能是 LVS,也可能是直連。
Cluster 是客戶端集群方式的表示。

自定義通訊協議使用

首先我想介紹一下自定義通訊協議。

在說明自定義通訊協議之前,我先簡單介紹一下通訊協議。在TCP之上,RPC框架通常還需要將請求和響應資料進行一定的封裝,組裝成 Packet,然後發送出去。這樣,服務端收到之後,才能正確識別整個 TCP 發過來的位元組流中,哪一部分是我們可以進行處理的一個完整單位。反之,客戶端收到服務端的TCP 資料流也是如此。

有了上面的共識之後,我們要回答下麵兩個問題:

  • 為什麼要自定義,不使用 

    Http2/Dubbo/Rest/Grpc?

  • 自定義之後,帶來了什麼好處呢?

Http2 雖然更為通用,但是一方面,出現較晚,遷移轉換成本高,並且通用則意味著傳輸的輔助資料會變多,會有一些額外的信息需要傳遞或者判斷。對於序列化反序列化的控制上,也不是很好擴展操作。

而 Dubbo,協議簡單強大。但是一些元信息需要解析,Header 中傳輸的資料太少,很多都需要依賴 body 中的資料反序列化完成後才能使用,頭部的信息太少。

而使用了自研的協議之後,Header 中可自定義傳輸更多的元信息,序列化方式,Server Fail Fast,服務端執行緒隔離等也都成為可能。甚至螞蟻在 ServiceMesh 的場景下,Mesh 本身也能利用 Bolt 的協議,進行部分資料的讀取,而不依賴具體的序列化實現。


BOLT 協議圖

經過我們的實踐,大致來看,目前給我們帶來的好處主要有以下的能力:

  • Server Fast 的支持

  • Header 和 Body 的分開序列化

  • Crc 校驗的支持

  • 版本的支持,預防未來可能出現更好的設計方案

  • 多種序列化方式的支持

  • 安全認證,Mesh 路由

如果你要自己設計一個通訊協議。可以考慮使用 BOLT 協議,或者參考進行更好的設計和優化。
關於 SOFABolt 相關的原始碼解析,也可以通過這個系列來瞭解。
SOFABolt  原始碼解析系列:點擊【剖析 | SOFABOLT 框架】即可查看
https://www.sofastack.tech/posts 

Netty 性能引數優化

在介紹了自定義通訊協議之後,也就是確定好了怎麼封包解包之後,還需要確定傳輸層的開發。一個 RPC 框架從現在的情況來看,一般不太可能完全基於 JAVA 的 NIO 或者其他 IO 進行直接的開發,主要是一些 NIO 原生的問題和使用難度,而成熟的,目前可選的不多。基本上,大家都會基於 Netty 進行開發,HSF/Dubbo/Motan 等都是這樣。

直接使用是比較簡單的。在 Netty 的 Bootstrap 的設置中,有一些可選的優化項,有必要跟大家分享一下。

1、SO_REUSEPORT/SO_REUSEADDR – 端口復用(允許多個 socket 監聽同一個IP+端口)
SOREUSEPORT 支持多個行程或者執行緒系結到同一端口,提高服務器的接收鏈接的併發能力,由內核層面實現對端口資料的分發的負載均衡,在服務器 socket 上沒有了鎖的競爭。
同時 SO
REUSEADDR也要打開,這樣針對 time-wait 鏈接 ,可以確保 server 重啟成功。在一些服務端啟動很快的情況下,可以防止啟動失敗。

2、TCP_FASTOPEN – 3次握手時也用來交換資料
三次握手的過程中,當用戶首次訪問服務端時,發送 syn 包,server 根據客戶端 IP 生成 cookie ,並與 syn+ack 一同發回客戶端;客戶端再次訪問服務端時,在 syn 包攜帶 TCP cookie;如果服務端校驗合法,則在用戶回覆 ack 前就可以直接發送資料;否則按照正常三次握手進行。也就是說,如果客戶端中途斷開,再建聯的時候,會同時發送資料,會有一定的性能提升。
TFO 提高性能的關鍵是省去了熱請求的三次握手,這在小物件傳輸較多的移動應用場景中,能夠極大提升性能。
Netty 中僅在 Epoll 的時候可用 Linux特性,不能在 Mac/Windows 上使用,SOFARPC 未開啟。

3、TCP_NODELAY-關閉 (納格) Nagle 演算法,再小的包也發送,而不是等待
TCP/IP 協議中針對 TCP 預設開啟了 Nagle 演算法。Nagle 演算法通過減少需要傳輸的資料包個數,來優化網絡。但是現在的環境下,網絡帶寬足夠,需要進行關閉。這樣,對於傳輸資料量小的場景,能很好的提高性能,不至於出現資料包等待。

4、SO_KEEPALIVE –開啟 TCP 層面的 Keep Alive 能力
這個不多說,開啟一下 TCP 層面的 Keep Alive 的能力。

5、WRITE_BUFFER_WATER_MARK 設置
通過 WRITE
BUFFERWATERMARK 設置某個連接上可以暫存的最大最小 Buffer 之後,如果該連接的等待發送的資料量大於設置的值時,則 isWritable 會傳回不可寫。這樣,客戶端可以不再發送,防止這個量不斷的積壓,最終可能讓客戶端掛掉。如果發生這種情況,一般是服務端處理緩慢導致。這個值可以有效的保護客戶端。此時資料並沒有發送出去。

6、workerGroup
worker 執行緒數設置處理器+1,Netty 預設是執行緒數*2,可以根據自己的壓測情況來判斷。Boss Group 用於服務端處理建立連接的請求,WorkGroup 用於處理 I/O。為了避免執行緒背景關係切換,只要能滿足要求,這個值一般越少越好。

7、ioRadio 設置
EventLoop#ioRatio 的設置(預設50), 這是 EventLoop 執行 IO 任務和非 IO 任務的一個時間比例上的控制,BOLT 最佳實踐是70,表示70%的時間在執行 IO 任務。

8、SO_BACKLOG 設置
在 Linux 系統內核中維護了兩個佇列:syns queue 和 accept queue。第一個是半連接佇列,儲存收到客戶端 syn 之後,進入 syn
recv 狀態的這些連接,預設 netty 中是128,
io.netty.util.NetUtil#SOMAXCONN ,然後讀取 /proc/sys/net/core/somaxconn  來繼續確定,之後還有一些系統級別的改寫邏輯。
在一些場景下,如果客戶端遠遠多餘服務端,併發建聯,可能不夠。這個值也不能太大,否則會無法防止 SYN-Flood 攻擊。Bolt 中目前這個值修改成了1024。通過設置之後,由於自己設置的和系統的取小,所以自己設置的值相當於設置了上限。如果 Linux 系統運維某些設置錯誤,也能通過代碼層面進行避免。
目前我們的 Linux 層面,通常設置的是 128,最終經過計算會設置為 128。

SOFARPC 連接保持

Netty 設置基本 ok,協議也確定之後,連接的保持就比較重要,否則,第一次發送或者每次發送都要走一次建聯的過程。雖然有 FAST OPEN 的加持,還是有一些損失。

說到這裡, 可能有些同學有疑問:

  • Keep Alive 不夠嗎?

  • Bolt 的連接管理怎麼做的?

  • 如何解決初次建聯的問題?

  • 心跳是單向還是雙向?

前面我們說過了,Keep Alive 已經打開了。不過,Keep Alive 還不夠,主要是經過很多網絡設備之後,Keep Alive可能失效,另外 Keep Alive 是一個 Linux 層面的設置,有時候整個系統並未打開。這些不可控的因素都會導致我們的連接管理失效。


Keep Alive 圖

上面是 Keep Alive 的處理,主要是在沒有讀寫事件一段時間後,進行資料包的發送來保活。

因為我們需要更通用的連接保持方案。連接管理核心的基於 Netty 的 Idle 事件來做。BOLT 的設置為單向心跳,客戶端發,服務端收,減少心跳資料在網絡上的傳輸量。有些 RPC 框架會使用雙向心跳,同時,BOLT 在連接管理上,也允許一個地址,建立多個連接,這樣可以在發送時,最大限度的利用網卡。預設為1,連接數在滿足傳輸吞吐量的情況下越少越好。

但是這裡要註意,如果你的場景是有大量的服務端,那麼這個資料不建議進行擴大。因為 tcp 連接會成倍增長,反而帶來性能下降。目前螞蟻這邊大部分也多為1。


RPC 連接管理

在 BOLT 連接管理的基礎上,RPC 為了避免第一次用戶請求,進行建聯併發送的延遲,RPC 還有一個連接管理的執行緒,會異步的進行連接初始化。這樣,當真正的請求發起的時候,連接已經準備好了,可以減少一次建聯的耗時對業務的影響。

對於 LVS 和 VIP 的場景下,由於長連接的特性,即使後端有 100個 IP,對客戶端來說,也只能和一個 IP 進行通信,因為這些設備是建聯層面的,並非通信層面的。所以對這種情況,一個 RPC 框架也要考慮支持定時斷鏈和重連。

序列化選擇

以上都準備好了之後,序列化方式的選擇決定了業務傳輸物件能夠有多小,也決定了在傳輸之前,序列化和反序列化的時候能有多快或者有多占用 CPU 。


序列化圖

螞蟻這邊長期使用 hessian 作為序列化方式,在出現跨語言需求後,同時支持 pb 。如果你還有考慮其他的序列化方式,可以參考附錄中的序列化框架性能測試套件來進行選擇。
需要註意的是,在 RPC 場景的序列化中,一定要考慮接口變更,欄位新增的兼容性。因為一旦一個接口被客戶 A 和 B 取用,此時 C 要升級 facade 接口,能否兼容 A 和 B 的情況就很重要。
基於我們自己的情況,在序列化方式的選擇上:

  • 如果很長時間內,不存在跨語言的情況,hessian 是兼容性和性能的綜合考慮

  • 如果考慮跨語言,並且對性能要求很高,Pb 可作為跨語言的情況下的選擇。

  • 在選型時也要考慮序列化框架的社區情況。切勿選擇看上去性能高,但是已經不再維護的庫,或者用戶量非常少的庫,一旦出現問題,比較難解決。

IO 執行緒池批量解包


批量解包圖

Netty 提供了一個方便的解碼工具類 ByteToMessageDecoder ,如圖上半部分所示,這個類具備 accumulate 批量解包能力,可以盡可能的從 socket 里讀取位元組,然後同步呼叫 decode 方法,解碼出業務物件,並組成一個 List 。最後再迴圈遍歷該 List ,依次提交到 ChannelPipeline 進行處理。改動後,如圖下半部分所示,即將提交的內容從單個 command ,改為整個 List 一起提交,如此能減少 pipeline 的執行次數,同時提升吞吐量。這個樣式在低併發場景下不明顯,但是在高併發場景下對吞吐量有不小的性能提升。

這一段是我改成開關方式的,方便大家理解改動點。

  1. if (batchSwitch) {

    
    
  2.    ArrayList<Object> ret = new ArrayList<Object>(size);

    
    
  3.    for (int i = 0; i < size; i++) {

    
    
  4.        ret.add(out.get(i));    }

    
    
  5.    ctx.fireChannelRead(ret); }else{

    
    
  6.    for (int i = 0; i < size; i++) {

    
    
  7.        ctx.fireChannelRead(out.get(i));    } }

    
    

我們的 DEMO 提供了一個驗證的方式,如果有相關的壓測環境,可以參考進行多併發的驗證。
DEMO 鏈接:

https://github.com/leizhiyuan/rpcchannel

客戶端 Proxy 的性能優化

作為一個 RPC 框架,最後,我們還有給用戶的接口生成代理。目前一般大家都是要用動態代理來做。動態代理的性能有不同,使用上也有一定的差別。各個版本之間,也會有一定的差異。在選擇上,需要大家根據實際情況,進行測試驗證。
我們自己的測試資料顯示 Javassist Bytecode 的方式是除了 Asm 之外,性能最好的。Asm 由於使用寫法非常反人類,所以我們目前還是使用的 Javassist Bytecode 的方式。

可優先選擇 javassist bytecode,有一定的性能優勢,性能測試可以根據自己的情況,使用 JMH 進行測試。測試代碼和版本在 DEMO 中提供。

總結

得益於 Java 社區的發展以及前輩們的貢獻,目前寫一個 RPC 框架並不是很難。但是作為一個 RPC 框架,需要在可維護性的基礎上,盡可能提高自身性能,將在實際過程中遇到的一些場景和異常情況進行修複和優化,併進行更好的代碼設計和實現。對於性能上的資料,可以多使用 JMH 並結合實際業務場景,進行相應的測試。

最後感謝大家,今天的 SOFA Channel 直播到此結束。下期我們將在本月28號與大家見面, SOFARPC 性能優化(下),我們會帶來關於執行緒池隔離,Server Fail Fast,記憶體操作優化,用戶可調節引數等方面的介紹。

大家可以點擊閱讀原文或者鏈接進行報名。

https://tech.antfin.com/activities/245?chInfo=wx 

相關鏈接

視頻回放也給你準備好啦:
https://tech.antfin.com/activities/244

相關參考鏈接:

  • DEMO 鏈接:

    https://github.com/leizhiyuan/rpcchannel

  • SOFARPC:

    https://github.com/alipay/sofa-rpc

  • SOFARPC 原始碼解析系列 (點擊【剖析 | SOFARPC 框架】即可查看

    https://www.sofastack.tech/posts  

  • SOFABolt 原始碼解析系列 (點擊【剖析 | SOFABOLT 框架】即可查看

    https://www.sofastack.tech/posts  

  • TCP man:

    https://linux.die.net/man/7/tcp

  • FAST OPEN :

    https://tools.ietf.org/html/rfc7413

  • netty :

    https://netty.io/news/2015/09/30/4-0-32-Final.html

  • jvm-serializers:

    https://github.com/eishay/jvm-serializers/wiki

  • 半連接 :

    https://www.cnxct.com/something-about-phpfpm-s-backlog/

  • SYN Flood :

    https://zh.wikipedia.org/wiki/SYN_flood

講師觀點

長按關註,不錯過每一場技術直播

歡迎大家共同打造 SOFAStack https://github.com/alipay

    閱讀原文

    赞(0)

    分享創造快樂