dubbo泛化调用性能优化

背景

为了优化APP服务端开发及后端服务接入的灵活性,无线后端组开发了网关服务。在性能测试过程中发现了两个性能瓶颈点:

(1) 日志级别配置错误导致dubbo框架内部日志逻辑耗时过长;

(2) dubbo泛化调用比API调用(spring配置)耗时多,导致和之前的haitao-mobile(sp.kaola.com后端服务)性能差距较大。

基于上述情况情况,APP服务端对dubbo泛化调用进行了优化并在dubbok-3.0.5.RC release,优化后的性能已达到API调用并在某些返回数据量场景下超过API调用。目前网关一期已经上线并开始投入使用,欢迎大家接入。

泛化调用介绍

泛接口调用方式主要用于客户端没有API接口及模型类的情况下,调用服务端的服务。目前网关通过构造服务端需要的相关参数,通过泛化调用访问RPC接口并返回。泛化调用示例代码如下:

ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(new ApplicationConfig("kaola-gateway-compose"));
RegistryConfig registry = new RegistryConfig();
registry.setAddress("xxxx");
registry.setClient("curator");
referenceConfig.setRegistry(registry);
referenceConfig.setInterface("xxxxx");
referenceConfig.setGroup("xxxx");
referenceConfig.setVersion("xxxx");
referenceConfig.setGeneric(“raw.return”);
referenceConfig.setCluster("failfast");
referenceConfig.setCheck(false);
referenceConfig.setFilter("gatewayConsumerRequestFilter");
referenceConfig.get();
Object obj = referenceConfig.get().$invoke("$methodName", new String[1], new String[1]);

性能瓶颈及优化

在性能测试过程中发现,随着后端dubbo服务返回数据量增加网关泛化调用的整体耗时相对于API调用会成倍下降。通过使用VisualVM排查发现泛化调用过程中dubbo服务端的GenericFilter在把调用结果返回给dubbo客户端之前,对数据进行了一次POJO对象转换,这个地方就是导致泛化调用随着返回数据量增加耗时比API调用慢的根本原因。VisualVM抓取的快照如下:

具体代码截图如下:

所以,接下来优化的目标就是去掉GenericFilter中多出的一次POJO对象转换。

从截图上面的第一个框中代码中可以看出,此时已经通过dubbo动态代理调用了目标服务方法并返回了Result(RpcResult)。接着对结果进行POJO转换并返回一个新的RpcResult对象。 按照前面的目标,客户端在初始化ReferenceConfig时setGeneric(“raw.return”),服务端的GenericFilter通过判断generic值是否为 raw.return”来直接返回调用结果,不进行POJO转换。优化后的GenericFilter部分代码截图如下:

这样修改后,带来了新的问题:泛化调用耗时比之前慢了将近十倍!

同样,使用VisualVM抓取调用快照发现新的问题点:SerializerFactory 在获取序列化对象时,多次执行Class.forname方法且每次执行都抛异常并打异常日志。这样,返回的数据量越大执行Class.forname类加载并抛异常的次数越多,性能自然急剧下降。

SerializerFactory 类中Class.forname的代码截图如下:

下面简要分析一下dubbo客户端拿到服务端返回的结果、hessian协议解析和反序列化过程:

1、客户端通过Netty拿到服务端返回的数据后,经过协议包头大小及dubbo协议中的其他字段(如序列化类型)等一系列解析到达DecodeableRpcResult类中的decode(Channel channel, InputStream input)方法。此方法中通过判断flag的类型分别执行对应的逻辑,此时正常的返回会走到case DubboCodec.RESPONSE_VALUE:下的逻辑,然后通过hessian2的readObject根据hessian协议反序列化服务端返回的结果值。DecodeableRpcResult代码截图如下:

2、根据返回的byte数据,Hessian2Input类解析hessian2协议对应的字节头(如M代表Map对象数据,I代表int类型数据),获取对应的反序列化工具对象。代码截图如下:

3、反序列化数据,经过dubbo客户端一系列的处理最终返回给调用方。

服务端去除POJO对象转换直接返回给客户端后,由于数据中没有class字段(POJO对象转换过程中会在原来的基础上加一个class字段),会走到上图中第二个红框中的,readObjectInstance方法。最终通过SerializerFactory类getDeserializer(String type)方法,用Class.forname尝试加载类。由于客户端泛化调用没有对应的JAR包,最终抛异常同时被catch到并打日志。后续逻辑会返回null,并默认使用MapDeserializer对象作为反序列化工具对象。代码截图如下:

网关泛化调用过程只需要把结果用JSON直接返回,所以使用默认的MapDeserializer反序列化工具对象对结果返回不会有影响。所以此时重点要做的就是优化Class.forname。这里优化的方法是把不能被加载的类放到LRU cache中,第二次再访问时直接跳过Class.forname,这样大大提高了泛化调用的性能。SerializerFactory类中Class,forname优化代码如下:

总结以上优化点:

  • dubbo服务端的GenericFilter中通过客户端传的标识直接返回调用结果,不用POJO对象转换;
  • 在SerializerFactory中增加LRU cache保存不能被classload加载的类,避免多次加载增加耗时。

优化前后性能测试对比数据


1、网关泛化调用优化前后性能测试对比数据

优化前:


优化后:


2、优化后网关和haitao-mobile( sp.kaola.com后端服务)不同返回数据量性能压测对比

417KB数据

网关

haitao-mobile

261KB数据

网关


haitao-mobile

105KB数据

网关


haitao-mobile

53KB数据

网关


haitao-mobile

12KB数据

网关


haitao-mobile


网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者杜敬兵授权发布。