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

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


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

下期將在本月28號與你們見面, SOFARPC 性能優化(下),報名連接java

tech.antfin.com/activities/…linux


歡迎加入直播互動釘釘羣:23127468,不錯過咱們每場直播。git

你們好,今天是咱們 SOFAChannel 第二期。歡迎你們觀看。github

我是來自螞蟻金服中間件的雷志遠,花名碧遠,目前在負責 SOFARPC 框架相關工做。算法

去年的時候,咱們和外部的愛好者們一塊兒,作了一個基於 SOFARPC 的源碼解析系列,我同事已經發到羣裏了,你們能夠保存,直播以後查看。json

SOFARPC 源碼解析系列:(點擊【剖析 | SOFARPC 框架】便可查看)後端

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 數據流也是如此。

有了上面的共識以後,咱們要回答下面兩個問題:

  1. 爲何要自定義,不使用 Http2/Dubbo/Rest/Grpc?
  2. 自定義以後,帶來了什麼好處呢?

Http2 雖然更爲通用,可是一方面,出現較晚,遷移轉換成本高,而且通用則意味着傳輸的輔助數據會變多,會有一些額外的信息須要傳遞或者判斷。對於序列化反序列化的控制上,也不是很好擴展操做。

而 Dubbo,協議簡單強大。可是一些元信息須要解析,Header 中傳輸的數據太少,不少都須要依賴 body 中的數據反序列化完成後才能使用,頭部的信息太少。

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

BOLT 協議圖

通過咱們的實踐,大體來看,目前給咱們帶來的好處主要有如下的能力:

  1. Server Fast 的支持
  2. Header 和 Body 的分開序列化
  3. Crc 校驗的支持
  4. 版本的支持,預防將來可能出現的更好的設計方案
  5. 多種序列化方式的支持
  6. 安全認證,Mesh 路由

若是你要本身設計一個通信協議。能夠考慮使用 BOLT 協議,或者參考進行更好的設計和優化。

關於 SOFABolt 相關的源碼解析,也能夠經過這個系列來了解。

SOFABolt 源碼解析系列:(點擊【剖析 | SOFABOLT 框架】便可查看)

www.sofastack.tech/posts


Netty 性能參數優化

在介紹了自定義通信協議以後,也就是肯定好了怎麼封包解包以後,還須要肯定傳輸層的開發。一個 RPC 框架從如今的狀況來看,通常不太可能徹底基於 JAVA 的 NIO 或者其餘 IO 進行直接的開發,主要是一些 NIO 原生的問題和使用難度,而成熟的,目前可選的很少。基本上,你們都會基於 Netty 進行開發,HSF/Dubbo/Motan 等都是這樣。

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

一、SO_REUSEPORT/SO_REUSEADDR - 端口複用(容許多個 socket 監聽同一個IP+端口)

SO_REUSEPORT 支持多個進程或者線程綁定到同一端口,提升服務器的接收連接的併發能力,由內核層面實現對端口數據的分發的負載均衡,在服務器 socket 上沒有了鎖的競爭。

同時 SO_REUSEADDR也要打開,這樣針對 time-wait 連接 ,能夠確保 server 重啓成功。在一些服務端啓動很快的狀況下,能夠防止啓動失敗。

二、TCP_FASTOPEN - 3次握手時也用來交換數據

三次握手的過程當中,當用戶首次訪問服務端時,發送 syn 包,server 根據客戶端 IP 生成 cookie ,並與 syn+ack 一同發回客戶端;客戶端再次訪問服務端時,在 syn 包攜帶 TCP cookie;若是服務端校驗合法,則在用戶回覆 ack 前就能夠直接發送數據;不然按照正常三次握手進行。也就是說,若是客戶端中途斷開,再建聯的時候,會同時發送數據,會有必定的性能提高。

TFO 提升性能的關鍵是省去了熱請求的三次握手,這在小對象傳輸較多的移動應用場景中,可以極大提高性能。

Netty 中僅在 Epoll 的時候可用 Linux特性,不能在 Mac/Windows 上使用,SOFARPC 未開啓。

三、TCP_NODELAY-關閉 (納格) Nagle 算法,再小的包也發送,而不是等待

TCP/IP 協議中針對 TCP 默認開啓了 Nagle 算法。Nagle 算法經過減小須要傳輸的數據包個數,來優化網絡。可是如今的環境下,網絡帶寬足夠,須要進行關閉。這樣,對於傳輸數據量小的場景,能很好的提升性能,不至於出現數據包等待。

四、SO_KEEPALIVE –開啓 TCP 層面的 Keep Alive 能力

這個很少說,開啓一下 TCP 層面的 Keep Alive 的能力。

五、WRITE_BUFFER_WATER_MARK 設置

經過 WRITE_BUFFER_WATER_MARK 設置某個鏈接上能夠暫存的最大最小 Buffer 以後,若是該鏈接的等待發送的數據量大於設置的值時,則 isWritable 會返回不可寫。這樣,客戶端能夠再也不發送,防止這個量不斷的積壓,最終可能讓客戶端掛掉。若是發生這種狀況,通常是服務端處理緩慢致使。這個值能夠有效的保護客戶端。此時數據並無發送出去。

六、workerGroup

worker 線程數設置 處理器+1,Netty 默認是線程數*2,能夠根據本身的壓測狀況來判斷。Boss Group 用於服務端處理創建鏈接的請求,WorkGroup 用於處理I/O。爲了不線程上下文切換,只要能知足要求,這個值通常越少越好。

七、ioRadio 設置

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

八、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 的加持,仍是有一些損失。

說到這裏, 可能有些同窗有疑問:

  1. Keep Alive 不夠嗎?
  2. Bolt 的鏈接管理怎麼作的?
  3. 如何解決初次建聯的問題?
  4. 心跳是單向仍是雙向?

前面咱們說過了,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 的狀況就很重要。

基於咱們本身的狀況,在序列化方式的選擇上:

  1. 若是很長時間內,不存在跨語言的狀況,hessian 是兼容性和性能的綜合考慮
  2. 若是考慮跨語言,而且對性能要求很高,Pb 可做爲跨語言的狀況下的選擇。
  3. 在選型時也要考慮序列化框架的社區狀況。切勿選擇看上去性能高,可是已經再也不維護的庫,或者用戶量很是少的庫,一旦出現問題,比較難解決。


IO 線程池批量解包

批量解包圖

Netty 提供了一個方便的解碼工具類 ByteToMessageDecoder ,如圖上半部分所示,這個類具有 accumulate 批量解包能力,能夠儘量的從 socket 裏讀取字節,而後同步調用 decode 方法,解碼出業務對象,並組成一個 List 。最後再循環遍歷該 List ,依次提交到 ChannelPipeline 進行處理。改動後,如圖下半部分所示,即將提交的內容從單個 command ,改成整個 List 一塊兒提交,如此能減小 pipeline 的執行次數,同時提高吞吐量。這個模式在低併發場景下不明顯,可是在高併發場景下對吞吐量有不小的性能提高。

這一段是我改爲開關方式的,方便你們理解改動點。

if (batchSwitch) {
    ArrayList<Object> ret = new ArrayList<Object>(size);
    for (int i = 0; i < size; i++) {
        ret.add(out.get(i));
    }
    ctx.fireChannelRead(ret);
}else{
    for (int i = 0; i < size; i++) {
        ctx.fireChannelRead(out.get(i));
    }
}複製代碼

咱們的 DEMO 提供了一個驗證的方式,若是有相關的壓測環境,能夠參考進行多併發的驗證。

DEMO 連接:github.com/leizhiyuan/…


客戶端 Proxy 的性能優化

做爲一個 RPC 框架,最後,咱們還有給用戶的接口生成代理。目前通常你們都是要用動態代理來作。動態代理的性能有不一樣,使用上也有必定的差異。各個版本之間,也會有必定的差別。在選擇上,須要你們根據實際狀況,進行測試驗證。

咱們本身的測試數據顯示 Javassist Bytecode 的方式是除了 Asm 以外,性能最好的。Asm 因爲使用寫法很是反人類,因此咱們目前仍是使用的 Javassist Bytecode 的方式。

Benchmark

Mode

Cnt

Score

Error

Units

ProxyInvokeBenchmark.invokeByAsm

avgt

10

7.865

±0.028

ns/op

ProxyInvokeBenchmark.invokeByBytebuddy

avgt

10

14.318

± 0.41

ns/op

ProxyInvokeBenchmark.invokeByCglib

avgt

10

8.231

± 0.221

ns/op

ProxyInvokeBenchmark.invokeByJavassist

avgt

10

15.86

± 0.605

ns/op

ProxyInvokeBenchmark.invokeByJavassistByte

avgt

10

8.075

± 0.267

ns/op

ProxyInvokeBenchmark.invokeByJdk

avgt

10

12.774

± 0.806

ns/op

可優先選擇 javassist bytecode,有必定的性能優點,性能測試能夠根據本身的狀況,使用 JMH 進行測試。測試代碼和版本在 DEMO 中提供。


總結

得益於 Java 社區的發展以及前輩們的貢獻,目前寫一個 RPC 框架並非很難。可是做爲一個 RPC 框架,須要在可維護性的基礎上,儘量提升自身性能,將在實際過程當中遇到的一些場景和異常狀況進行修復和優化,並進行更好的代碼設計和實現。對於性能上的數據,能夠多使用 JMH 並結合實際業務場景,進行相應的測試。

最後感謝你們,今天的 SOFA Channel 直播到此結束。下期咱們將在本月28號與你們見面, SOFARPC 性能優化(下),咱們會帶來關於線程池隔離,Server Fail Fast,內存操做優化,用戶可調節參數等方面的介紹。你們能夠點擊連接進行報名:tech.antfin.com/activities/…


相關連接

視頻回放也給你準備好啦:

https://tech.antfin.com/activities/244


相關參考連接:


講師觀點

公衆號:金融級分佈式架構(Antfin_SOFA)

相關文章
相關標籤/搜索