网易首页 > 网易号 > 正文 申请入驻

如何编写高性能的 RPC 框架

0
分享至

在 RPC Benchmark Round 1 中,Turbo 性能炸裂表现强悍,并且在 listUser 这一项目中,取得了 10x dubbo 性能的好成绩。本文将介绍 Turbo 强悍性能背后的原理,并探讨如何编写高性能的 RPC 框架。

过早的优化是万恶之源?

这句话是 The Art of Computer Programming 作者,图灵奖得主 Donald Knuth 大神说的。不过对于框架设计者而言,这句话并不正确。在设计一款高性能的基础框架时,必须始终重视性能优化,并将性能测试贯穿于整个设计开发过程中。

这方面做到极致的类库有 DisruptorJCToolsAgronaDSL-JSON 等等,这几个高性能类库都坚持一个原则:不了解性能的外部类库坚决不用,如果现有的类库不能满足性能要求,那就重新设计一个。作为 Turbo 的设计者,我也尽量坚持这一原则,努力做到 Benchmark 驱动开发。

JMH 让 Benchmark 驱动开发成为可能

在 JMH 出现之前,要对某个类库进行微基准性能测试是一件非常困难的事情。很难保证公平的测试条件,预热次数难以确定,预热效果也不好观察。JMH 的出现让性能测试变得 标准化 简单化,也让 Benchmark 驱动开发成为可能。Turbo 在开发过程中用 JMH 进行了充分的 Benchmark,以确定核心环节的性能开销,选择合适的实现方案。更多关于 JMH 的介绍请参考下面的链接:

  • OpenJDK: jmh:http://openjdk.java.net/projects/code-tools/jmh/

  • JMH - Java Microbenchmark Harness:http://tutorials.jenkov.com/java-performance/jmh.html

  • ImportNew JMH 简介:http://www.importnew.com/12548.html

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个部分。

序列化方案

Java 世界最常用的几款高性能序列化方案有 KryoProtostuffFSTJacksonFastjson。只需要进行一次 Benchmark,然后从这5种序列化方案中选出性能最高的那个就行了。DSL-JSON 使用起来过于繁琐,不在考虑之列。ColferProtocolThrift 因为必须预先定义描述文件,使用起来太麻烦,所以不在考虑之列。至于 Java 自带的序列化方案,早就因为性能问题被大家所抛弃,所以也不考虑。下面的表格列出了在考虑之列的5种序列化方案的性能。

User 序列化+反序列化 性能

frameworkthrpt (ops/ms)sizeprotostuff1654240kryo1288296fst1101263jackson959385fastjson603378

包含15个 User 的 Page 序列化+反序列化 性能

frameworkthrpt (ops/ms)sizekryo1432080fst1183495protostuff983920jackson715711fastjson405606

从这个 benchmark 中可以得出明确的结论:二进制协议的 protostuff kryo fst 要比文本协议的 jackson fastjson 有明显优势;文本协议中,jackson(开启了afterburner) 要比 fastjson 有明显的优势。

无法确定的是:3个二进制协议到底哪个更好一些,毕竟 速度 和 size 对于 RPC 都很重要。直观上 kryo 或许是最佳选择,而且 kryo 也广受各大型系统的青睐。不过最终还是决定把这3个类库都留作备选,通过集成传输模块后的 Benchmark 来决定选用哪个。

frameworkexistUser (ops/ms)createUser (ops/ms)getUser (ops/ms)listUser (ops/ms)protostuff103.9289.5083.3321.17kryo99.2376.7173.8925.68fst102.3376.2478.8123.30

最终的结果也还是各有千秋难以抉择,所以 Turbo 保留了 protostuff 和 kryo 的实现,并允许用户自行替换为自己的实现。

方法调用

可用的 动态方法调用 方案有:Reflection ClassGeneration MethodHandle。Reflection 是最古老的技术,据说性能不佳。ClassGeneration 动态类生成,从原理上说应该是跟直接调用一样的性能。MethodHandle 是从 Java 7 开始出现的技术,据说能达到跟直接调用一样的性能。实际结果如下:

typethrpt (ops/us)direct1062javassist920methodHandle430reflection337

结论非常明显:使用类生成技术的 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/javase/7/docs/api/java/lang/invoke/MethodHandle.html

网络传输

Netty 已经成为事实上的标准,所有主流的项目现在使用的都是 Netty。

MinaGrizzly 已经失去市场,所以也就不用考虑了。还好也不至于这么无聊,Aeron 的闪亮登场让 Netty 多了一个有力的竞争对手。

Aeron 是一个可靠高效的 UDP 单播 UDP 多播和 IPC 消息传递工具。性能是消息传递中的关键。Aeron 的设计旨在达到 高吞吐量 低开销 和 低延迟。实际效果到底如何呢?很遗憾,在 RPC Benchmark Round 1 中的表现一般。跟他们开发团队沟通后,最终确认其无法对超过 64k 的消息进行 zero-copy 处理,我觉得这可能是 Aeron 表现不佳的一个原因。

Aeron 或许更适合 微小消息 极端低延迟 的场景,而不适用于更加通用的 RPC 场景。所以暂时还没有出现能够跟 Netty 一争高下的通用网络传输框架,现阶段 Netty 依然是 RPC 系统的最佳选择。

existUser 判断某个 email 是否存在

frameworkthrpt (ops/ms)avgt (ms)p90 (ms)p99 (ms)p999 (ms)turbo-rpc107.050.280.400.874.06netty99.810.320.400.521.16jupiter73.070.440.661.492.92undertow70.380.451.162.1732.48turbo-rest68.490.441.172.1525.66undertow-async62.650.491.142.4124.84dubbo-kryo57.350.530.671.0211.65rapidoid52.960.611.322.5125.07dubbo52.120.540.670.923.93motan44.960.711.152.4733.39aeron43.460.901.325.1014.29grpc38.970.841.071.316.06thrift27.251.590.1664.87122.83hprose26.241.261.532.018.34springwebflux22.391.422.273.1917.20springboot12.541.682.3813.6333.20

消息格式

我们先来看一下 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 。

MethodParam 简介

MethodParam 才是 Turbo 性能炸裂的真正原因。其基本原理是利用 ClassGeneration 对每个 Method 都生成一个 MethodParam 类,用于对方法参数的封装。这样做的好处有:

  1. 减少基本数据类型的 装箱 拆箱 开销

  2. 序列化时可以省略掉很多类型描述,大大减小 传输消息 的 size

  3. 使 Invoker 可以高效调用 被代理类 的方法

  4. 统一 RPC 和 REST 的数据模型,简化 序列化 反序列化 实现

  5. 大大加快 json 格式数据 反序列化 速度

序列化的进一步优化

大部分 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 出来就行了。

frameworkthrpt (ops/us)ThreadLocal685.418Stormpot272.934HikariCP139.126SegmentLock19.415Vibur4.668CommonsPool21.107CommonsPool0.276

基础类库优化

除了上述的关键流程优化,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/turbo-rpc

  • https://github.com/hank-whu/turbo-rpc

不足之处

  • 有很多优化是毫无价值的,Donald Knuth 大神说得很对

  • 强制必须使用 CompletableFuture 作为返回值导致了一些性能开销

  • 滥用 ClassGeneration,而且并没有考虑类的卸载,这方面需要改进

  • 实现了 UnsafeStringUtils,这是个危险的黑魔法实现,需要重新思考一下

  • 对性能的追求有点走火入魔,导致了很多地方的设计过于复杂

关注微信公众号和今日头条,精彩文章持续更新中。。。。。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
没观众、没提词器、不得插嘴,拜登和特朗普今日辩论,外媒评:最年长美总统与重罪犯的对决

没观众、没提词器、不得插嘴,拜登和特朗普今日辩论,外媒评:最年长美总统与重罪犯的对决

纵相新闻
2024-06-28 11:10:02
取消对华免签,还拒绝中国的高铁,甚至放言:“不欢迎中国人”!

取消对华免签,还拒绝中国的高铁,甚至放言:“不欢迎中国人”!

星辰故事屋
2024-06-18 10:54:05
格鲁吉亚首富为国家队发千万美元奖金,若胜西班牙再发1000万

格鲁吉亚首富为国家队发千万美元奖金,若胜西班牙再发1000万

直播吧
2024-06-28 07:42:15
男生查分后默默去厨房做饭,妈妈一看秒懂:高考估分600只考397分

男生查分后默默去厨房做饭,妈妈一看秒懂:高考估分600只考397分

老王侃趣闻
2024-06-26 19:10:03
孙兴慜现身韩国野球场,主动要求加入野球比赛

孙兴慜现身韩国野球场,主动要求加入野球比赛

直播吧
2024-06-28 15:32:07
中国女篮大爆冷!2米26张子宇无缘国家队,或流浪海外

中国女篮大爆冷!2米26张子宇无缘国家队,或流浪海外

体坛狗哥
2024-06-28 12:59:11
财政部:7月1日起将对塞尔维亚实施自由贸易协定关税减让

财政部:7月1日起将对塞尔维亚实施自由贸易协定关税减让

南方都市报
2024-06-27 22:06:20
悲剧!女子坐月子十天被迫与老公同房,不幸离世,网友坐不住了

悲剧!女子坐月子十天被迫与老公同房,不幸离世,网友坐不住了

热闹的河马
2024-06-28 12:55:06
美国和以色列谈判向乌克兰提供8套爱国者防空系统,俄噩梦来袭

美国和以色列谈判向乌克兰提供8套爱国者防空系统,俄噩梦来袭

山河路口
2024-06-28 12:21:04
北大教授姚洋:支持欧盟对我国新能源车加征关税 不应采取反制措施

北大教授姚洋:支持欧盟对我国新能源车加征关税 不应采取反制措施

户外小阿隋
2024-06-28 10:35:11
产能上来了,打鹅更给力!

产能上来了,打鹅更给力!

凡事一定有办法13119
2024-06-28 11:19:50
汪诘再谈姜萍事件:我错了,但并不羞愧

汪诘再谈姜萍事件:我错了,但并不羞愧

科学声音
2024-06-28 15:18:24
高考出现“神仙卷面”,字迹工整漂亮如印刷,阅卷老师不舍得扣分

高考出现“神仙卷面”,字迹工整漂亮如印刷,阅卷老师不舍得扣分

豆芽妈妈育儿
2024-06-27 18:45:13
骂受伤女子的人,想想歹徒冲上满载孩童的校车,国际上有什么影响

骂受伤女子的人,想想歹徒冲上满载孩童的校车,国际上有什么影响

走读新生
2024-06-26 11:16:08
在美军是否“武力保台”上,特朗普回应亮了,蔡英文赖清德要急

在美军是否“武力保台”上,特朗普回应亮了,蔡英文赖清德要急

行走的大大大羊腿
2024-06-28 14:01:02
姜萍事件惊人内幕!多个部门出丑,草台班子神造闹剧!

姜萍事件惊人内幕!多个部门出丑,草台班子神造闹剧!

阿握看历史
2024-06-24 17:19:23
中国人不骗中国人,TikTok上直播“开窑”忽悠老外疯抢,评论笑死

中国人不骗中国人,TikTok上直播“开窑”忽悠老外疯抢,评论笑死

三楼的猫头鹰
2024-06-27 00:45:11
收评:A股三大指数午后跳水创业板指跌超1% 沪指连跌6周创2018年以来最长周线连跌记录

收评:A股三大指数午后跳水创业板指跌超1% 沪指连跌6周创2018年以来最长周线连跌记录

金融界
2024-06-28 15:21:57
基辅惊现俄特种部队 血战3小时阵亡89人 神秘白人雇佣兵战力惊人

基辅惊现俄特种部队 血战3小时阵亡89人 神秘白人雇佣兵战力惊人

夏天使娱乐
2024-06-28 12:52:04
国防部要求北约停止核讹诈与核胁迫

国防部要求北约停止核讹诈与核胁迫

新京报
2024-06-27 17:00:20
2024-06-28 19:22:44
非常美丽的句子12138
非常美丽的句子12138
每天分享有趣的视频
6063文章数 10253关注度
往期回顾 全部

科技要闻

售价近三万,苹果Vision Pro中国首销

头条要闻

培植个人势力、大搞新型腐败 "新疆虎"李鹏新被逮捕

头条要闻

培植个人势力、大搞新型腐败 "新疆虎"李鹏新被逮捕

体育要闻

哪有什么死亡之组?踢就完了!

娱乐要闻

黄一鸣曝光王思聪聊天内容

财经要闻

A股上半年人均亏损1.2万 中证2000跌23%

汽车要闻

你没看错!广汽丰田今天秀了一把智电技术

态度原创

房产
艺术
旅游
本地
公开课

房产要闻

20亿!又有国企要卖海南资产!

艺术要闻

穿越时空的艺术:《马可·波罗》AI沉浸影片探索人类文明

旅游要闻

上海迪士尼项目遭吐槽,异味难消

本地新闻

冷知识:东北雪糕才是最早的网红雪糕

公开课

连中三元是哪三元?

无障碍浏览 进入关怀版