整个流程中对性能影响比较大的环节有:序列化[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...