要怎樣纔可以完美的編寫高性能RPC框架

RPC 的主要流程

  1. 客戶端 獲取到 UserService 接口的 Refer: userServiceRefer
  2. 客戶端 調用 userServiceRefer.verifyUser(email, pwd)
  3. 客戶端 獲取到 請求方法 和 請求數據
  4. 客戶端 把 請求方法 和 請求數據 序列化爲 傳輸數據
  5. 進行網絡傳輸
  6. 服務端 獲取到 傳輸數據
  7. 服務端 反序列化獲取到 請求方法 和 請求數據
  8. 服務端 獲取到 UserService 的 Invoker: userServiceInvoker
  9. 服務端 userServiceInvoker 調用 userServiceImpl.verifyUser(email, pwd) 獲取到
    響應結果
  10. 服務端 把 響應結果 序列化爲 傳輸數據
  11. 進行網絡傳輸
  12. 客戶端 接收到 傳輸數據
  13. 客戶端 反序列化獲取到 響應結果
  14. 客戶端 userServiceRefer.verifyUser(email, pwd) 返回 響應結果

整個流程中對性能影響比較大的環節有:序列化[4, 7, 10, 13],方法調用[2, 3, 8, 9, 14],網絡傳輸[5, 6, 11, 12]。本文後續內容將着重介紹這3個部分。html

序列化方案

Java 世界最經常使用的幾款高性能序列化方案有 Kryo Protostuff FST Jackson Fastjson。只須要進行一次 Benchmark,而後從這5種序列化方案中選出性能最高的那個就好了。DSL-JSON 使用起來過於繁瑣,不在考慮之列。Colfer Protocol Thrift 由於必須預先定義描述文件,使用起來太麻煩,因此不在考慮之列。至於 Java 自帶的序列化方案,早就由於性能問題被你們所拋棄,因此也不考慮。下面的表格列出了在考慮之列的5種序列化方案的性能。java

User 序列化+反序列化 性能git

framework thrpt (ops/ms) sizegithub

clipboard.png

包含15個 User 的 Page 序列化+反序列化 性能
framework thrpt (ops/ms) size數據庫

clipboard.png

從這個 benchmark 中能夠得出明確的結論:二進制協議的 protostuff kryo fst 要比文本協議的 jackson fastjson 有明顯優點;文本協議中,jackson(開啓了afterburner) 要比 fastjson 有明顯的優點。
沒法肯定的是:3個二進制協議到底哪一個更好一些,畢竟 速度 和 size 對於 RPC 都很重要。直觀上 kryo 或許是最佳選擇,並且 kryo 也廣受各大型系統的青睞。不過最終仍是決定把這3個類庫都留做備選,經過集成傳輸模塊後的 Benchmark 來決定選用哪一個。json

framework existUser (ops/ms) createUser (ops/ms) getUser (ops/ms) listUser (ops/ms)api

clipboard.png
最終的結果也仍是各有千秋難以抉擇,因此 Turbo 保留了 protostuff 和 kryo 的實現,並容許用戶自行替換爲本身的實現。數組

方法調用

可用的 動態方法調用 方案有:Reflection ClassGeneration MethodHandle。Reflection 是最古老的技術,聽說性能不佳。ClassGeneration 動態類生成,從原理上說應該是跟直接調用同樣的性能。MethodHandle 是從 Java 7 開始出現的技術,聽說能達到跟直接調用同樣的性能。實際結果以下:安全

type thrpt (ops/us)性能優化

clipboard.png

結論很是明顯:使用類生成技術的 javassist 跟直接調用幾乎同樣的性能,就用 javassist 了。
MethodHandle 表現並無宣傳的那麼好,怎麼回事?原來 MethodHandle 只有在明確知道調用 參數數量 參數類型 的狀況下才能調用高性能的 invokeExact(Object... args),因此它並不適合做爲動態調用的方案。

As is usual with virtual methods, source-level calls to invokeExact and invoke compile to an invokevirtual instruction. More unusually, the compiler must record the actual argument types, and may not perform method invocation conversions on the arguments. Instead, it must push them on the stack according to their own unconverted types. The method handle object itself is pushed on the stack before the arguments. The compiler then calls the method handle with a symbolic type descriptor which describes the argument and return types.
refer: https://docs.oracle.com/javas...

網絡傳輸

Netty 已經成爲事實上的標準,全部主流的項目如今使用的都是 Netty。Mina Grizzly 已經失去市場,因此也就不用考慮了。還好也不至於這麼無聊,Aeron 的閃亮登場讓 Netty 多了一個有力的競爭對手。Aeron 是一個可靠高效的 UDP 單播 UDP 多播和 IPC 消息傳遞工具。性能是消息傳遞中的關鍵。Aeron 的設計旨在達到 高吞吐量 低開銷 和 低延遲。實際效果到底如何呢?很遺憾,在 RPC Benchmark Round 1 中的表現通常。跟他們開發團隊溝通後,最終確認其沒法對超過 64k 的消息進行 zero-copy 處理,我以爲這多是 Aeron 表現不佳的一個緣由。Aeron 或許更適合 微小消息 極端低延遲 的場景,而不適用於更加通用的 RPC 場景。因此暫時尚未出現可以跟 Netty 一爭高下的通用網絡傳輸框架,現階段 Netty 依然是 RPC 系統的最佳選擇。

existUser 判斷某個 email 是否存在
framework thrpt (ops/ms) avgt (ms) p90 (ms) p99 (ms) p999 (ms)

clipboard.png

消息格式

咱們先來看一下 Dubbo 的消息格式

clipboard.png

能夠說是很是經典的設計,Client 必須告知 Server 要調用的 方法名稱 參數類型 參數。Server 獲取到這3個參數後,經過 方法名稱 com.alibaba.service.auth.UserService.verifyUser 和 參數類型 (String, String) 獲取到 Invoker,而後經過 Invoker 實際調用 userServiceImpl 的 verifyUser(String, String) 方法。其餘的衆多 RPC 框架也都採起了這一經典設計。
可是,這是正確的作法嗎?固然不是,這種作法很是浪費空間,每次請求消息體的大概內存佈局應該是下面的樣子。 public boolean verifyUser(String email, String pwd) 大體的內存佈局:

|com.alibaba.service.auth.UserService.verifyUser|java.lang.String,java.lang.String|實際的參數|

囉裏囉嗦的,浪費了 80 byte 來定義 方法 和 參數,並無比 http+json 的方式高效多少。實際的 性能測試 也證實了這一點,undertow+jackson 要比 dubbo motan 的成績都要好。
那什麼纔是正確的作法?Turbo 在消息格式上作出了很是大的改變。

clipboard.png

public boolean verifyUser(String email, String pwd) 大體的內存佈局:

|int|int|實際的參數|

高效多了,只用了 4 byte 就作到了 方法 和 參數 的定義。大大減少了 傳輸數據 的 size,同時 int 類型的 serviceId 也下降了 Invoker 的查找開銷。
看到這裏,有同窗可能會問:那豈不是要爲每一個方法定義一個惟一 id ? 答案是不須要的,Turbo 解決了這一問題,詳情參考 TurboConnectService 。

推薦一個交流學習羣:575745314 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多:
圖片描述

MethodParam 簡介

MethodParam 纔是 Turbo 性能炸裂的真正緣由。其基本原理是利用 ClassGeneration 對每一個 Method 都生成一個MethodParam 類,用於對方法參數的封裝。這樣作的好處有:

  1. 減小基本數據類型的 裝箱 拆箱 開銷
  2. 序列化時能夠省略掉不少類型描述,大大減少 傳輸消息 的 size
  3. 使 Invoker 能夠高效調用 被代理類 的方法
  4. 統一 RPC 和 REST 的數據模型,簡化 序列化 反序列化 實現
  5. 大大加快 json 格式數據 反序列化 速度

clipboard.png

序列化的進一步優化

大部分 RPC 框架的 序列化 反序列化 過程都須要一箇中間的 bytes

  • 序列化過程:User > bytes > ByteBuf
  • 反序列化過程:ByteBuf > bytes > User

而 Turbo 砍掉了中間的 bytes,直接操做 ByteBuf,實現了 序列化 反序列化 的 zero-copy,大大減小了 內存分配 內存複製 的開銷。具體實現請參考 ProtostuffSerializer 和 Codec。
對於已知類型和已知字段,Turbo 都儘可能採用 手工序列化 手工反序列化 的方式來處理,以進一步減小性能開銷。

ObjectPool

常見的幾個 ObjectPool 實現性能都不好,反而很容易成爲性能瓶頸。Stormpot 性能強悍,不過存在偶爾死鎖的問題,並且做者也中止維護了。HikariCP 性能不錯,不過其自己是一款數據庫鏈接池,用做 ObjectPool 並不稱手。個人建議是儘可能避免使用 ObjectPool,轉而使用替代技術。更重要的是 Netty 的 Channel 是線程安全的,並不須要使用 ObjectPool 來管理。只須要一個簡單的容器來存儲 Channel,用的時候使用 負載均衡策略 選出一個 Channel 出來就好了。

framework thrpt (ops/us)

clipboard.png

基礎類庫優化

除了上述的關鍵流程優化,Turbo 還作了大量基礎類庫的優化

  • AtomicMuiltInteger 多個 int 的原子性操做
  • ConcurrentArrayList 無鎖併發 List 實現,比 CopyOnWriteArrayList 的寫入開銷低,O(1)
    vs O(n)
  • ConcurrentIntToObjectArrayMap 以 int 數組爲底層實現的無鎖併發Map,讀多寫少狀況下接近直接訪問字段的性能,讀多寫多狀況下是 ConcurrentHashMap 性能的 5x
  • ConcurrentIntegerSequencer 快速序號生成器,併發環境下是 AtomicInteger 性能的10x
  • ObjectId 全局惟一 id 生成器,是 Java 自帶 UUID 性能的 200x
  • HexUtils 查表 + 批量操做,是 Netty 和 Guava 實現的 2x~5x
  • URLEncodeUtils 基於 HexUtils 實現,是 Java 和 Commons 實現的 2x,Guava 實現的 1.1x
    (Guava 只有 urlEncode 實現,無 urlDecode 實現)
  • ByteBufUtils 實現了高效的 ZigZag 寫入操做,最高可達一般實現的 4x

上面的內容僅介紹了做者認爲重要的東西,更多內容請直接查看 Turbo 源碼
https://gitee.com/hank-whu/tu...
https://github.com/hank-whu/t...

不足之處

  • 有不少優化是毫無價值的,Donald Knuth 大神說得很對
  • 強制必須使用 CompletableFuture 做爲返回值致使了一些性能開銷
  • 濫用 ClassGeneration,並且並無考慮類的卸載,這方面須要改進
  • 實現了 UnsafeStringUtils,這是個危險的黑魔法實現,須要從新思考下
  • 對性能的追求有點走火入魔,致使了不少地方的設計過於複雜
相關文章
相關標籤/搜索