从2.7.0-2.7.5版本,Dubbo调用链路是如何提高30%性能的

前言

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

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

consumer端缓存ConsumerModel

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

public void checkAndUpdateSubConfigs() {
    /** * 省略无关代码 */
    // 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-start
    ServiceRepository repository = ApplicationModel.getServiceRepository();
    ServiceDescriptor serviceDescriptor = repository.registerService(interfaceClass);
    repository.registerConsumer(
            serviceMetadata.getServiceKey(),
            serviceDescriptor,
            this,
            null,
            serviceMetadata);
    // part2-end
    /** * 省略无关代码 */
}
复制代码

provider端缓存ProviderModel

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

private void doExportUrls() {
    // part1-start
    ServiceRepository repository = ApplicationModel.getServiceRepository();
    ServiceDescriptor serviceDescriptor = 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中缓存的元数据计算结果。ide

  • 初始化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对于方法级别元数据获取操做的内存分配

public class URL implements Serializable {
    /** * 省略无关代码 */
    private final Map<String, String> parameters;
    private final Map<String, Map<String, String>> methodParameters;
    private volatile transient Map<String, Map<String, Number>> methodNumbers;
  
    public static Map<String, Map<String, String>> toMethodParameters(Map<String, String> parameters) {
        Map<String, Map<String, String>> methodParameters = new HashMap<>();
        if (parameters == null) {
            return methodParameters;
        }

        String methodsString = parameters.get(METHODS_KEY);
        if (StringUtils.isNotEmpty(methodsString)) {
            String[] methods = methodsString.split(",");
            for (Map.Entry<String, String> entry : parameters.entrySet()) {
                String key = entry.getKey();
                for (String method : methods) {
                    String methodPrefix = method + '.';
                    if (key.startsWith(methodPrefix)) {
                        String realKey = key.substring(methodPrefix.length());
                        URL.putMethodParameter(method, realKey, entry.getValue(), methodParameters);
                    }
                }
            }
        } else {
            for (Map.Entry<String, String> entry : parameters.entrySet()) {
                String key = entry.getKey();
                int methodSeparator = key.indexOf('.');
                if (methodSeparator > 0) {
                    String method = key.substring(0, methodSeparator);
                    String realKey = key.substring(methodSeparator + 1);
                    URL.putMethodParameter(method, realKey, entry.getValue(), methodParameters);
                }
            }
        }
        return methodParameters;
    }
    /** * 省略无关代码 */
}

复制代码

在url中parameters属性携带着服务相关的一些元数据配置,好比group、timeout等配置,包括方法级别的配置,还有服务相关的methods、interface等元数据,下图是我跑的一个demo,其中展现了parameters的一些内容:性能

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

减小URL.getAddress的对象分配

旧版本代码ui

public class URL implements Serializable {
    /** * 省略无关代码 */
    private final String host;
    private final int port;
    public String getAddress(String host, int port) {
        return port <= 0 ? host : host + ':' + port;
    }
    /** * 省略无关代码 */
}
复制代码

新版本代码this

public class URL implements Serializable {
    /** * 省略无关代码 */
    private transient String address;
    private final String host;
    private final int port;
    private static 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也是针对对象分配的优化,原理和这个差很少,这里就不一一列举了。

送福利区域

扫描下方二维码关注公众号【加点代码调调味】
点击菜单栏获取免费49篇的《Dubbo源码解析》系列文章

加点代码调调味

foot
相关文章
相关标签/搜索