<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 框架】便可查看)後端
今年,基於源碼解析的基礎,咱們來多講講實踐,如何應用到你們的業務,來幫助你們解決實際問題。在直播過程當中有相關的問題想提問,能夠在釘釘羣互動。
在上一期中,餘淮分享了《從螞蟻金服微服務實踐談起》。介紹了螞蟻微服務的起源,以及以後服務化,單元化的狀況。同時介紹了 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,協議簡單強大。可是一些元信息須要解析,Header 中傳輸的數據太少,不少都須要依賴 body 中的數據反序列化完成後才能使用,頭部的信息太少。
而使用了自研的協議以後,Header 中可自定義傳輸更多的元信息,序列化方式,Server Fail Fast,服務端線程隔離等也都成爲可能。甚至螞蟻在 ServiceMesh 的場景下,Mesh 自己也能利用 Bolt 的協議,進行部分數據的讀取,而不依賴具體的序列化實現。
BOLT 協議圖
通過咱們的實踐,大體來看,目前給咱們帶來的好處主要有如下的能力:
若是你要本身設計一個通信協議。能夠考慮使用 BOLT 協議,或者參考進行更好的設計和優化。
關於 SOFABolt 相關的源碼解析,也能夠經過這個系列來了解。
SOFABolt 源碼解析系列:(點擊【剖析 | SOFABOLT 框架】便可查看)
在介紹了自定義通信協議以後,也就是肯定好了怎麼封包解包以後,還須要肯定傳輸層的開發。一個 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。
Netty 設置基本 ok,協議也肯定以後,鏈接的保持就比較重要,不然,第一次發送或者每次發送都要走一次建聯的過程。雖然有 FAST OPEN 的加持,仍是有一些損失。
說到這裏, 可能有些同窗有疑問:
前面咱們說過了,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 的狀況就很重要。
基於咱們本身的狀況,在序列化方式的選擇上:
批量解包圖
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/…
做爲一個 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)