Dubbo2.7.x与3.0的优化改进

本文涉及的产品
性能测试 PTS,5000VUM额度
简介: ### 环境配置 - Client与服务端分处两台ECS,配置为8C8G - OS:alios7.x86_64  - JDK:ajdk-8_6_11-b380 - 启动参数:-Xms1g -Xmx1g - 压测配置:同步调用(最常见的调用方式),200个客户端并发线程,预热30s,压测时长600s (10分钟) ### 若干解释 1. 本次压测是单对单的同步调用,

环境配置

  • Client与服务端分处两台ECS,配置为8C8G
  • OS:alios7.x86_64 
  • JDK:ajdk-8_6_11-b380
  • 启动参数:-Xms1g -Xmx1g
  • 压测配置:同步调用(最常见的调用方式),200个客户端并发线程,预热30s,压测时长600s (10分钟)

若干解释

  1. 本次压测是单对单的同步调用,也就是一台client发送请求到server,建立的网络连接是一个,按照Netty的处理策略是一个Eventloop线程在工作处理io事件,这样的测试条件可能会导致io线程成为瓶颈。减轻io线程的工作负荷(比如把序列化和反序列化等逻辑从io线程中腾挪到biz线程阶段)在此背景下是一个优化点,但是考虑到真实使用场景下,会是多个连接,io层(也就是网络层)一般不会是瓶颈,所以减轻io的技巧不具备太多实用价值。
  2. 服务端的线程策略是默认的dubbo策略,即io线程转给biz线程的两阶段,这一策略在服务端是纯cpu逻辑的性能测试场景下会弱化,但考虑到其普适性,我们还是坚持以此策略为测试。相比较其他RPC库可能是不一样的,也是影响性能的重要因素。
  3. 网络上有一些rpc库/框架的性能比较文章或者代码库,但是基本是单纯的简单调用,也就是侧重在网络性能的分析比较。但是像Dubbo/HSF更多的考虑是软负载,服务治理等功能特性的支持,甚至扩展机制。在简单测试中肯定会吃亏。从这个角度看,单纯一味追求性能的RPC框架未必是好的。
  4. 最后也是重要一点,rpc测试中必须考虑RT,一方面RT对于业务应用的意义相比较TPS显得一样重要,RT延迟会受到多方面因素(比如GC,网络质量等)的影响,同时,RT的质量(也就是RT的正太分布图,比如99%的RT是8ms以内,而最长的rt可能是50ms,这个在很多场景下也是无法被接受)也是比较重要的考量;  另一方面,在压测过程中始终达不到系统资源(一般是网络和CPU)的极限,这时就会出现优化改进后tps基本没提升,但RT成为衡量因素。
  5. 测试中用到了JMH,JFR/JMC,这两个东东还不错,尤其是JMH不仅方便压测,也有自带一些profiler。http://psy-lob-saw.blogspot.com/2016/02/why-most-sampling-java-profilers-are.html
  6. 本文中多个优化点离不开@陆龟 的帮助和支持,尤其是从优化点8开始均有@陆龟提供。当然,现在的Dubbo优化也只是刚开始。

优化点及数据

数据解释

以下面出现的第一张图为例。

  • 第二行Client.createUser: 在createUser这个用例下,吞吐量(thrpt)是每毫秒42.819,也就是qps是42819。
  • 第四行Client.createUser:在createUser这个用例下,平均RT是4.852ms,误差在0.008ms。
  • 第九行Client**·createUser·p0.99:在createUser这个用例下,99%请求的RT都是在12.665ms以内
  • 第十二行Client**·createUser·p1.00:在createUser这个用例下,100%请求的RT都是在73.138ms以内,也就说最大RT的那一次调用是73.138。

未优化的情形

  • Dubbo 2.7.1-release

image.png

  • Dubbo 2.6.5 release

image.png

  • Dubbo 3.0.0-SNAPSHOT

image.png

优化1:3.0版本Load高吞吐量低的问题

3.0的当前版本是相当让人惊讶的结果,没办法重复跑了几遍,都是如此。服务端OK,客户端的load非常高。
image.png
排查过程:考虑到load极高而CPU,而事实上CPU(无论是sys,还是us)没有沾满100%,先从thread分析。
image.png
从stackprofiler来分析,问题很明显:在AsyncToSynInvoker中调用了CompletableFuture(简称CF)的get方法。打开CF的相关代码,会发现先通过自旋2的8次方再进入阻塞队列进行等待。在RPC的场景下,网络等待一般都是ms级别的,这样自旋存在着浪费CPU的问题,更适合的方式是通过信号锁的方式来处理。
image.png

  • 解决方法: 其实Google了很多CF的文章,想找到一种比较好的使用方式,也参照了些代码实现,最后还是认为是以前HSF中基于Guava的Future实现更合适。
  • 优化效果:性能由6300tps提升到38458tps,提升了510%,具体见截图数据。

image.png 

优化2:FGC问题

以上的CF使用问题虽然解决了一大问题,但是性能还是不及2.x版本,尤其是在rt方面,从上面的数据截图可以看出,在2.7.1版本上p99的rt在12.66ms,而3.0版本上的p99达到了44ms,这差距实在是无语。。。。

特别感谢@刘军在优化1时也在本地跑了一下,本地也跑出来OutofMemory。。。

本人在性能环境做了GC方面的压测,在接近1小时的压测过程内,没有遇到内存溢出,但是基本上每三秒就会一次FGC,这应该是导致RT增加了2倍多的原因
image.png

由于无outmemeryerror,同时加上FGC是由于old区的满引起,所以猜测是对象的生命周期太长,导致无法在NEW区中快速回收。通过几次查看HEAP的对象实例,发下排名前几位且与Dubbo相关的对象是DefaultFuture
image.png

  • 问题根源:在timeout的task中持有了DefaultFuture这个比较重的对象,而timeoutTask是和超时时间一样生命市场的对象。通过优化TimeoutTask对象,解决此问题。
  • 优化效果:fgc解除,rt提升明显。见下图数据:old区基本不增长了,FGC也不再随着时间推移而增长。最关键的是rt由之前的44ms降到了13ms,基本接近2.7.1版本。

image.png

image.png

以上两个问题其实更多是一种BUG存在的问题排查,经过优化1和优化2后,当前最新版本从qps和rt两方面基本上达到了Dubbo的正常性能水平。顺便提一下,这也再次印证本人之前说的:没有rt的qps性能测试都是扯淡!!

优化3:返回类型的改进

根据jmh的stackprofiler结果看,每次结果返回都需要通过java reflection api获取returnType,这里的计算比较多。见截图。
image.png

优化后的结果见下图,性能从39797提升到41784,有5%的提升。
image.png

优化4:判断invokerMode的优化

  0.0%   6.1% sun.reflect.Reflection.getCallerClass
              java.lang.Class.getMethod
              org.apache.dubbo.rpc.support.RpcUtils.getReturnType
              org.apache.dubbo.rpc.support.RpcUtils.isReturnTypeFuture
              org.apache.dubbo.rpc.support.RpcUtils.getInvokeMode
              org.apache.dubbo.rpc.protocol.AbstractInvoker.invoke
              org.apache.dubbo.rpc.protocol.AsyncToSyncInvoker.invoke
              org.apache.dubbo.monitor.support.MonitorFilter.invoke
              org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke
              org.apache.dubbo.rpc.protocol.dubbo.filter.FutureFilter.invoke
              org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke
              org.apache.dubbo.rpc.filter.ConsumerContextFilter.invoke
              org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke
              org.apache.dubbo.rpc.listener.ListenerInvokerWrapper.invoke
              org.apache.dubbo.rpc.proxy.InvokerInvocationHandler.invoke
              org.apache.dubbo.common.bytecode.proxy3.getUser

优化后从41784提升到43659,提升在5%,同时RT也提升到13ms内,具体见下图数据。
image.png

优化5:消费端使用发起同步调用时去除额外的共享线程池

当前Dubbo的实现在同步调用方式下,对于response的处理是单独创建了一个线程池做处理,即 “I/O -> 共享线程池 -> 业务线程” 的切换,这样就额外增加了一次Context Switch。经过@刘军(陆龟)的优化改进,做一下实现。

  • 同步:“I/O -> 业务线程“
  • 异步:保持不变,“I/O -> 共享线程池”

image.png

看完结果,跟预期的tps与rt都提升相比较,还是有些疑惑:基本上没有提升的迹象。分析下来,有两个原因可以稍作解释:一是这一块的改动比较复杂,需要关注比较细致的并发技巧,现在的改进还不足,需要进一步完善;二是当前阶段的瓶颈不在于此,也就是说Context Switch的频繁对于当前测试的影响微不足道。

优化6:消费端实现rpc模块和Remoting模块的Future桥接

问题发现:通过jstack发现,io线程有比较多的比例是耗费在CF的complete过程,如下截图。
image.png

解决方案:remoting层复用rpc层的CompletableFuture来解决。性能提高了10%以上。
image.png

优化7:char[]的分配导致gc

image.png

从JProfiler allocation hot spot 也反映出URL相关操作分配占比较高
image.png

注:除GC意外,可能由于分配导致的耗时操作
image.png

优化8:AsyncRpcResult.thenApplyWithContext重复执行

AsyncRpcResult.thenApplyWithContext在每个filter重复执行,占用较大比例的CPU时间
可能和频繁操作RpcContext.get/setContext有关(consumer单线程场景比例较高,可能和provider端threadlocal初始化有关)
image.png

image.png

优化9:YGC频率较高

请求过程中避免一些重复的对象创建,除了5中提到URL操作导致String分配外,考虑Request、Response的复用与加速回收
      如利用Netty的Recycler机制等。

优化10:序列化从I/O线程切到业务线程池

  1. decode.in.io=true
  • 从provider端I/O thread状态来看,在32并发的情况下,decode在I/O对provider端还是有较大的runnable集中期的,可以考虑调整试一试

image.png

  • 消费端可以尝试调整I/O线程数量,或者减少I/O线程中可能的阻塞/耗时操作

image.png

目前发现的存在于I/O线程中慢操作有:

  • Future.complete()状态转换 park -> unpark
  • ConcurrentHashMap.put("LAST_READ_TIME", long);造成的锁竞争,参见 优化11
  • 业务线程send调用channel.isConnect(),与I/O线程read等造成竞争,打破了Netty关于channel绑定单线程的模型,见下图

image.png

优化11:NetUtils.getLocalHost()

比如ConsumerContextFilter中有调用

public static String getLocalHost() {
    InetAddress address = getLocalAddress();    return address == null ? Constants.LOCALHOST_VALUE :     address.getHostAddress();
}

优化12: 避免每次RpcContext.getContext(),注意保存引用

这个问题经充分预热后可能会好很多。
这点从theyApplyWithContext中也能反映出来
image.png

优化13: 业务线程、I/O线程同时更新Channel.ConcurrentHashMap导致的锁竞争

此点尚存义
attributes.put(LAST_READ_TIME, long),没必要做到精确,可使用普通HashMap
心跳避免更新HashMap?
image.png

image.png

相关实践学习
通过性能测试PTS对云服务器ECS进行规格选择与性能压测
本文为您介绍如何利用性能测试PTS对云服务器ECS进行规格选择与性能压测。
目录
相关文章
|
XML Dubbo Java
Dubbo 3 Spring相关优化
Spring Context Initialization首先,我们先来看一下Spring context初始化主要流程,如下图所示: 相关代码:org.springframework.context.support.AbstractApplicationContext#refresh()简单描述一下每个步骤包含的内容:创建BeanFactory:读取加载XML/注解定义的BeanDefiniti
1024 3
Dubbo 3 Spring相关优化
|
5月前
|
缓存 负载均衡 监控
dubbo3如何优化
dubbo3如何优化
113 1
|
存储 缓存 Kubernetes
Dubbo-kubernetes 基于 Informer 服务发现优化之路
优化为 Informer 后,Dubbo 的服务发现不用每次直接调用 kube-apiserver,减小了 kube-apiserver 的压力,也大大减少了响应时间,助力 Dubbo 从传统架构迁移到 Kubernetes 中。
211 8
Dubbo-kubernetes 基于 Informer 服务发现优化之路
|
负载均衡 Dubbo Cloud Native
Dubbo 2.7.5在线程模型上的优化(上)
Dubbo 2.7.5在线程模型上的优化(上)
640 0
|
存储 缓存 Kubernetes
Dubbo-kubernetes 基于Informer服务发现优化之路
在 Kubernetes(简称 K8S,一个可移植容器的编排管理工具)体系中,etcd 存储集群的数据信息,kube-apiserver作为统一入口,任何对数据的操作都必须经过 kube-apiserver。因此 Dubbo 想要以 Kubernetes 作为注册中心,必须调用 kube-apiserver 获取服务地址列表,那是以什么样的机制保持信息的可靠性、实时性、顺序性、高性能呢?答案就是基于List/Watch的Informer组件。
Dubbo-kubernetes 基于Informer服务发现优化之路
|
Dubbo 应用服务中间件
Dubbo 这波优化好像不够彻底啊?(下)
Dubbo 这波优化好像不够彻底啊?(下)
Dubbo 这波优化好像不够彻底啊?(下)
|
Dubbo 应用服务中间件
Dubbo 这波优化好像不够彻底啊?(中)
Dubbo 这波优化好像不够彻底啊?(中)
Dubbo 这波优化好像不够彻底啊?(中)
|
消息中间件 Dubbo IDE
Dubbo 这波优化好像不够彻底啊?(上)
Dubbo 这波优化好像不够彻底啊?(上)
Dubbo 这波优化好像不够彻底啊?(上)
|
自然语言处理 Dubbo Cloud Native
Dubbo 2.7.6在线程模型上的优化(下)
Dubbo 2.7.6在线程模型上的优化(下)
358 0
|
Kubernetes Dubbo Cloud Native
开发者社区精选直播合集(二十九)| Apache Dubbo 优化探索之旅
Dubbo 3.0 作为三位一体架构的首推方案,在集团内被寄予了厚望。它完美融合了内部 HSF 的特性,天然拥有高性能、高可用的核心能力,我们期望用它来解决内部落地问题,做到技术栈统一。
开发者社区精选直播合集(二十九)|  Apache Dubbo 优化探索之旅