整個流程中對性能影響比較大的環節有:序列化[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
包含15個 User 的 Page 序列化+反序列化 性能
framework thrpt (ops/ms) size數據庫
從這個 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
最終的結果也仍是各有千秋難以抉擇,因此 Turbo 保留了 protostuff 和 kryo 的實現,並容許用戶自行替換爲本身的實現。數組
可用的 動態方法調用 方案有:Reflection ClassGeneration MethodHandle。Reflection 是最古老的技術,聽說性能不佳。ClassGeneration 動態類生成,從原理上說應該是跟直接調用同樣的性能。MethodHandle 是從 Java 7 開始出現的技術,聽說能達到跟直接調用同樣的性能。實際結果以下:安全
type thrpt (ops/us)性能優化
結論很是明顯:使用類生成技術的 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)
咱們先來看一下 Dubbo 的消息格式
能夠說是很是經典的設計,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 在消息格式上作出了很是大的改變。
public boolean verifyUser(String email, String pwd) 大體的內存佈局:
|int|int|實際的參數|
高效多了,只用了 4 byte 就作到了 方法 和 參數 的定義。大大減少了 傳輸數據 的 size,同時 int 類型的 serviceId 也下降了 Invoker 的查找開銷。
看到這裏,有同窗可能會問:那豈不是要爲每一個方法定義一個惟一 id ? 答案是不須要的,Turbo 解決了這一問題,詳情參考 TurboConnectService 。
推薦一個交流學習羣:575745314 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多:
MethodParam 纔是 Turbo 性能炸裂的真正緣由。其基本原理是利用 ClassGeneration 對每一個 Method 都生成一個MethodParam 類,用於對方法參數的封裝。這樣作的好處有:
大部分 RPC 框架的 序列化 反序列化 過程都須要一箇中間的 bytes
而 Turbo 砍掉了中間的 bytes,直接操做 ByteBuf,實現了 序列化 反序列化 的 zero-copy,大大減小了 內存分配 內存複製 的開銷。具體實現請參考 ProtostuffSerializer 和 Codec。
對於已知類型和已知字段,Turbo 都儘可能採用 手工序列化 手工反序列化 的方式來處理,以進一步減小性能開銷。
常見的幾個 ObjectPool 實現性能都不好,反而很容易成爲性能瓶頸。Stormpot 性能強悍,不過存在偶爾死鎖的問題,並且做者也中止維護了。HikariCP 性能不錯,不過其自己是一款數據庫鏈接池,用做 ObjectPool 並不稱手。個人建議是儘可能避免使用 ObjectPool,轉而使用替代技術。更重要的是 Netty 的 Channel 是線程安全的,並不須要使用 ObjectPool 來管理。只須要一個簡單的容器來存儲 Channel,用的時候使用 負載均衡策略 選出一個 Channel 出來就好了。
framework thrpt (ops/us)
除了上述的關鍵流程優化,Turbo 還作了大量基礎類庫的優化
上面的內容僅介紹了做者認爲重要的東西,更多內容請直接查看 Turbo 源碼
https://gitee.com/hank-whu/tu...
https://github.com/hank-whu/t...