整個流程中對性能影響比較大的環節有:序列化[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
framework thrpt (ops/ms) sizegit
protostuff | 1654 | 240 |
kryo | 1288 | 296 |
fst | 1101 | 263 |
jackson | 959 | 385 |
fastjson | 603 | 378 |
framework thrpt (ops/ms) sizegithub
kryo | 143 | 2080 |
fst | 118 | 3495 |
protostuff | 98 | 3920 |
jackson | 71 | 5711 |
fastjson | 40 | 5606 |
從這個 benchmark 中能夠得出明確的結論:二進制協議的 protostuff kryo fst 要比文本協議的 jackson fastjson 有明顯優點;文本協議中,jackson(開啓了afterburner) 要比 fastjson 有明顯的優點。web
沒法肯定的是:3個二進制協議到底哪一個更好一些,畢竟 速度 和 size 對於 RPC 都很重要。直觀上 kryo 或許是最佳選擇,並且 kryo 也廣受各大型系統的青睞。不過最終仍是決定把這3個類庫都留做備選,經過集成傳輸模塊後的 Benchmark 來決定選用哪一個。spring
framework existUser (ops/ms) createUser (ops/ms) getUser (ops/ms) listUser (ops/ms)typescript
protostuff | 103.92 | 89.50 | 83.33 | 21.17 |
kryo | 99.23 | 76.71 | 73.89 | 25.68 |
fst | 102.33 | 76.24 | 78.81 | 23.30 |
最終的結果也仍是各有千秋難以抉擇,因此 Turbo 保留了 protostuff 和 kryo 的實現,並容許用戶自行替換爲本身的實現。數據庫
可用的 動態方法調用 方案有:Reflection ClassGeneration MethodHandle。Reflection 是最古老的技術,聽說性能不佳。ClassGeneration 動態類生成,從原理上說應該是跟直接調用同樣的性能。MethodHandle 是從 Java 7 開始出現的技術,聽說能達到跟直接調用同樣的性能。實際結果以下:
apache
type thrpt (ops/us)json
direct | 1062 |
javassist | 920 |
methodHandle | 430 |
reflection | 337 |
結論很是明顯:使用類生成技術的 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: docs.oracle.com/javase/7/do…
Netty 已經成爲事實上的標準,全部主流的項目如今使用的都是 Netty。Mina Grizzly 已經失去市場,因此也就不用考慮了。還好也不至於這麼無聊,Aeron 的閃亮登場讓 Netty 多了一個有力的競爭對手。
。實際效果到底如何呢?很遺憾,在 RPC Benchmark Round 1 中的表現通常。跟他們開發團隊溝通後,最終確認其沒法對超過 64k 的消息進行 zero-copy 處理,我以爲這多是 Aeron 表現不佳的一個緣由。Aeron 或許更適合 微小消息 極端低延遲 的場景,而不適用於更加通用的 RPC 場景。因此暫時尚未出現可以跟 Netty 一爭高下的通用網絡傳輸框架,現階段 Netty 依然是 RPC 系統的最佳選擇。
framework thrpt (ops/ms) avgt (ms) p90 (ms) p99 (ms) p999 (ms)
turbo-rpc | 107.05 | 0.28 | 0.40 | 0.87 | 4.06 |
netty
|
99.81
|
0.32
|
0.40
|
0.52
|
1.16
|
jupiter | 73.07 | 0.44 | 0.66 | 1.49 | 2.92 |
undertow | 70.38 | 0.45 | 1.16 | 2.17 | 32.48 |
turbo-rest | 68.49 | 0.44 | 1.17 | 2.15 | 25.66 |
undertow-async | 62.65 | 0.49 | 1.14 | 2.41 | 24.84 |
dubbo-kryo | 57.35 | 0.53 | 0.67 | 1.02 | 11.65 |
rapidoid | 52.96 | 0.61 | 1.32 | 2.51 | 25.07 |
dubbo | 52.12 | 0.54 | 0.67 | 0.92 | 3.93 |
motan | 44.96 | 0.71 | 1.15 | 2.47 | 33.39 |
aeron
|
43.46
|
0.90
|
1.32
|
5.10
|
14.29
|
grpc | 38.97 | 0.84 | 1.07 | 1.31 | 6.06 |
thrift | 27.25 | 1.59 | 0.16 | 64.87 | 122.83 |
hprose | 26.24 | 1.26 | 1.53 | 2.01 | 8.34 |
springwebflux | 22.39 | 1.42 | 2.27 | 3.19 | 17.20 |
springboot | 12.54 | 1.68 | 2.38 | 13.63 | 33.20 |
public class RpcInvocation implements Invocation, Serializable {
private String methodName;
private Class<?>[] parameterTypes;
private Object[] arguments;
...
}
複製代碼
能夠說是很是經典的設計,Client 必須告知 Server 要調用的 方法名稱 參數類型 參數。Server 獲取到這3個參數後,經過
和
獲取到 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 classRequestimplementsSerializable{
private int requestId;
private int serviceId;
private MethodParam methodParam;
...
}
複製代碼
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 類,用於對方法參數的封裝。這樣作的好處有:
//方法 test(long id, int value) 將會生成下面的 MethodParam 類:
public class TestService_test_2_MethodParam implements MethodParam {
private long id;
private int value;
public long $param0() { return this.id; }
public int $param1() { return this.value; }
//... getters and setters
publicTestService_test_2_MethodParam(long id, int value){
this.id = id;
this.value= value;
}
}
複製代碼
複製代碼
而 Turbo 砍掉了中間的 bytes,直接操做 ByteBuf,實現了 序列化 反序列化 的 zero-copy,大大減小了 內存分配 內存複製 的開銷。具體實現請參考 ProtostuffSerializer 和 Codec。
對於已知類型和已知字段,Turbo 都儘可能採用 手工序列化 手工反序列化 的方式來處理,以進一步減小性能開銷。
framework thrpt (ops/us)
ThreadLocal | 685.418 |
Stormpot | 272.934 |
HikariCP | 139.126 |
SegmentLock | 19.415 |
Vibur | 4.668 |
CommonsPool2 | 1.107 |
CommonsPool | 0.276 |
除了上述的關鍵流程優化,Turbo 還作了大量基礎類庫的優化
上面的內容僅介紹了做者認爲重要的東西,更多內容請直接查看 Turbo 源碼