dubbo调用链路是怎么提升性能30%的?【2.7.0~2.7.5】
创始人
2025-05-28 10:48:24

Dubbo 2.7.5版本发布也有快两个月了,从2.7.0到2.7.5,Dubbo在性能优化上也做了不少事情,据官方的压测结果,在QPS层面,从2.7.0到2.7.5,Dubbo 单次RPC调用链路性能提升了 30%,本文就带大家看一看Dubbo做了哪些改动来提升RPC 调用链路性能。

服务元数据静态化,减少链路计算


consumer端缓存ConsumerModel

我们知道当一个服务启动时,如果配置了服务引用相关的配置时,在consumer端会生成所引用服务的代理对象,有关服务引用过程的源码解析可以直接扫下方二维码关注公众号【加点代码调调味】获取。在生成服务的代理对象前会做一些校验配置以及更新配置等工作,它们主要是在ReferenceConfig的#checkAndUpdateSubConfigs()方法中实现的。在2.7.5版本中,该方法被新增了一些加载服务元数据的逻辑,下面来看看下面这部分源码:

publicvoidcheckAndUpdateSubConfigs() {

/**

* 省略无关代码

*/// part1-start//init serivceMetadata

serviceMetadata.setVersion(version);

serviceMetadata.setGroup(group);

serviceMetadata.setDefaultGroup(group);

serviceMetadata.setServiceType(getActualInterface());

serviceMetadata.setServiceInterfaceName(interfaceName);

// TODO, uncomment this line once service key is unified

serviceMetadata.setServiceKey(URL.buildKey(interfaceName, group, version));

// part2-startServiceRepositoryrepository= ApplicationModel.getServiceRepository();

ServiceDescriptorserviceDescriptor= repository.registerService(interfaceClass);

repository.registerConsumer(

serviceMetadata.getServiceKey(),

serviceDescriptor,

this,

null,

serviceMetadata);

// part2-end/**

* 省略无关代码

*/

}

provider端缓存ProviderModel

当服务启动时,必然会经过doExportUrls方法,具体的服务暴露过程源码分析可以直接扫下方二维码关注公众号【加点代码调调味】获取。跟consumer端一样,在新版本中添加了加载和计算元数据的逻辑。看下面源码:

privatevoiddoExportUrls() {

// part1-startServiceRepositoryrepository= ApplicationModel.getServiceRepository();

ServiceDescriptorserviceDescriptor= repository.registerService(getInterfaceClass());

repository.registerProvider(

getUniqueServiceName(),

ref,

serviceDescriptor,

this,

serviceMetadata

);

// part1-end/**

* 省略无关代码

*/

}

看到这里还是一头雾水吧,不要放弃,接着往下看:

可以看到consumer端的part1部分只是赋值一些服务相关的元数据,而consumer端的part2部分以及provider端的part1部分则是引入了ServiceRepository,ServiceRepository是服务仓库,它封装了服务相关的元数据、consumer端的元数据模型ConsumerModel以及provider端的元数据模型ProviderModel,ConsumerModel和ProviderModel两个模型,分别封装了consumer端和provider端的配置,比如ConsumerModel就封装了referenceConfig、methodConfig等。在part2部分最重要的就是实例化了一个ServiceRepository对象,然后将version、group、服务类型、服务接口名等元数据放入ServiceRepository对象中,保证在进程启动阶段服务元数据尽量做一些计算,做一些元数据以及计算结果的缓存,(比如生成方法参数描述parameterDesc数据等),从而在RPC调用链路上需要用到相关元数据时可以直接能直接拿到计算结果。在下面几个地方用到了ServiceRepository中缓存的元数据计算结果。

  • 初始化RpcInvocation时:无论是consumer端的调用链路中还是provider端的调用链路中,RpcInvocation一直都是整个调用链路内携带元数据的载体,举个例子:可以看源码中RpcInvocation有一个方法initParameterDesc(),这其中是赋值parameterDesc、compatibleParamSignatures、returnTypes这三个属性,但是这三个数据都是直接从ServiceRepository中直接获取的,并不是在初始化RpcInvocation时再计算这三个值。

  • 解码时:当provider端对请求进行解码时,会解析需要调用的方法签名,包括方法的参数类型、返回类型,现在这些内容都已经做了缓存,所以无需再重新解析,只要直接从ServiceRepository中获取即可,具体的源码可以看DecodeableRpcInvocation的#decode(Channel channel, InputStream input)方法。

减少调用过程中的 URL 操作产生的内存分配


除了在consumer端以及provider端在各自的调用链路中提升性能外,减少调用过程中的URL操作产生的内存分配动作也是一个优化的点。在2.7.x以前的版本,也就是还没有元数据中心的版本,统一模型URL携带的内容极其多,这会导致在网络中数据的传输中数据包太大,影响响应时间。而在2.7.x版本缩减了URL中的相关内容,让URL只关注服务定位相关的元数据,比如protocol、host、port等。下面来看看在最新的版本中是如何做到减少调用过程中的 URL 操作产生的内存分配的。主要从以下几个方面着手:

  • 减少了URL对于方法级别元数据获取操作的内存分配

  • 减少URL.getAddress的对象分配

减少了URL对于方法级别元数据获取操作的内存分配

publicclassURLimplementsSerializable {

/**

* 省略无关代码

*/privatefinal Map parameters;

privatefinal Map> methodParameters;

privatevolatiletransient Map> methodNumbers;

publicstatic Map> toMethodParameters(Map parameters) {

Map> methodParameters = newHashMap<>();

if (parameters == null) {

return methodParameters;

}

StringmethodsString= parameters.get(METHODS_KEY);

if (StringUtils.isNotEmpty(methodsString)) {

String[] methods = methodsString.split(",");

for (Map.Entry entry : parameters.entrySet()) {

Stringkey= entry.getKey();

for (String method : methods) {

StringmethodPrefix= method + '.';

if (key.startsWith(methodPrefix)) {

StringrealKey= key.substring(methodPrefix.length());

URL.putMethodParameter(method, realKey, entry.getValue(), methodParameters);

}

}

}

} else {

for (Map.Entry entry : parameters.entrySet()) {

Stringkey= entry.getKey();

intmethodSeparator= key.indexOf('.');

if (methodSeparator > 0) {

Stringmethod= key.substring(0, methodSeparator);

StringrealKey= key.substring(methodSeparator + 1);

URL.putMethodParameter(method, realKey, entry.getValue(), methodParameters);

}

}

}

return methodParameters;

}

/**

* 省略无关代码

*/

}

在url中parameters属性携带着服务相关的一些元数据配置,比如group、timeout等配置,包括方法级别的配置,还有服务相关的methods、interface等元数据

在新版本中将方法相关的元数据通过一个map维护在url中,减少对字符串的操作,因为每次需要获取方法的元数据时,都需要从parameters中获取对应的值,比如获取sayHello.timeout的值,然后还要根据“.”分割获取该配置的key为timeout,最终才能获取到sayHello这个方法的timeout配置,现在方法级别的元数据直接维护在url中,就不需要每次都进行字符串操作,并且还添加了缓存methodNumbers,加快二次获取的速度。

减少URL.getAddress的对象分配

旧版本代码

publicclassURLimplementsSerializable {

/**

* 省略无关代码

*/privatefinal String host;

privatefinalint port;

public String getAddress(String host, int port) {

return port <= 0 ? host : host + ':' + port;

}

/**

* 省略无关代码

*/

}

新版本代码

publicclassURLimplementsSerializable {

/**

* 省略无关代码

*/privatetransient String address;

privatefinal String host;

privatefinalint port;

privatestatic String getAddress(String host, int port) {

return port <= 0 ? host : host + ':' + port;

}

public String getAddress() {

if (address == null) {

address = getAddress(host, port);

}

return address;

}

/**

* 省略无关代码

*/

}

从源码看,在URL中新增了address这个属性,在获取address时只做一次对象分配,而不需要像原来每次调用getAddress方法时都做一些字符串拼接,由于拼接的host和port都是一个String类型的对象,所以在拼接的时候并不会被在编译期间就优化,而是会创建一个StringBuilder对象来进行拼接,这样每次获取address就会带来对象的内存分配的性能损耗。

除了对于URL.getAddress的对象分配的优化外,在2.7.5版本发布后,还有几个pull request也是针对对象分配的优化,原理和这个差不多

相关内容

热门资讯

华宝中证智能制造主题ETF净值... 华宝中证智能制造主题交易型开放式指数证券投资基金(简称:华宝中证智能制造主题ETF,代码516800...
汇添富中证沪港深张江自主创新5... 汇添富中证沪港深张江自主创新50交易型开放式指数证券投资基金(简称:汇添富中证沪港深张江自主创新50...
海富通精选贰号混合净值上涨2.... 海富通精选贰号混合型证券投资基金(简称:海富通精选贰号混合,代码519015)公布12月22日最新净...
浦银安盛价值成长混合A净值上涨... 浦银安盛价值成长混合型证券投资基金(简称:浦银安盛价值成长混合A,代码519110)公布12月22日...
万家颐达A净值上涨1.55% 万家颐达灵活配置混合型证券投资基金(简称:万家颐达A,代码519197)公布12月22日最新净值,上...