Dubbo服务引用流程

319 阅读9分钟

1. 前言

DubboBootstrap启动时,首先会通过initialize()方法完成初始化,装配各种Config对象,为后续的服务暴露和引用准备好环境。

ReferenceConfig是Dubbo对服务引用的描述类,它记录了需要引用的服务的协议、版本、服务名称、服务地址等信息,get()方法可以获取接口对象,接口是不能实例化的,这个对象其实是Dubbo创建的代理对象,Dubbo会在生成的代理对象中帮我们处理RPC调用、集群容错、负载均衡等复杂的逻辑。 ​

DubboBootstrap初始化结束后,会调用DubboBootstrap#referServices()方法引用依赖的服务,其实就是从ConfigManager提取出所有的ReferenceConfig对象,然后挨个调用get()方法创建代理对象,以它为入口分析即可。

2. 源码分析

在这里插入图片描述 ref属性记录了Dubbo生成的代理对象,get()方法会判断ref是否已经生成,已经生成就直接返回,否则会调用init()方法进行初始化,初始化的过程中会创建代理对象。

public synchronized T get() {
    if (destroyed) {
        throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
    }
    if (ref == null) {
        init();
    }
    return ref;
}

init()方法主要做了以下事情:

  1. 检查和更新配置
  2. 检查stub、local
  3. 创建代理对象
  4. 分发init事件

我们重点关注1和3,尤其是第三步,在这个创建的代理对象中,实现了Dubbo RPC调用的所有核心逻辑。

2.1 checkAndUpdateSubConfigs()

和ServiceConfig类似,ReferenceConfig在引用服务前也需要对相关配置进行检查和更新。 首先是如果没有指定相关配置,使用ConsumerConfig的默认配置,以及相关的Check和自身属性的刷新。

// 使用ConsumerConfig默认配置
completeCompoundConfigs(consumer);
if (consumer != null) {
    if (StringUtils.isEmpty(registryIds)) {
        setRegistryIds(consumer.getRegistryIds());
    }
}
// ConsumerConfig不存在会自动创建
checkDefault();
// 自身属性根据优先级刷新
this.refresh();

然后是通用服务相关的处理:

if (getGeneric() == null && getConsumer() != null) {
    setGeneric(getConsumer().getGeneric());
}
if (ProtocolUtils.isGeneric(generic)) {// 通用服务
    interfaceClass = GenericService.class;
} else {
    try {
        interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                                       .getContextClassLoader());
    } catch (ClassNotFoundException e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
    checkInterfaceAndMethods(interfaceClass, getMethods());
}

初始化服务元数据:

serviceMetadata.setVersion(getVersion());
serviceMetadata.setGroup(getGroup());
serviceMetadata.setDefaultGroup(getGroup());
serviceMetadata.setServiceType(getActualInterface());
serviceMetadata.setServiceInterfaceName(interfaceName);
serviceMetadata.setServiceKey(URL.buildKey(interfaceName, group, version));

要消费的服务注册到本地ServiceRepository:

ServiceRepository repository = ApplicationModel.getServiceRepository();
ServiceDescriptor serviceDescriptor = repository.registerService(interfaceClass);
repository.registerConsumer(
    serviceMetadata.getServiceKey(),
    serviceDescriptor,
    this,
    null,
    serviceMetadata);

resolveFile()方法会解析dubbo.resolve.file文件,判断是否需要点对点直连Provider。然后就是对自身进行一个Check。

// 解析dubbo.resolve.file,直连Provider
resolveFile();
ConfigValidationUtils.validateReferenceConfig(this);
postProcessConfig();

2.2 createProxy()

上述操作完成后,接下来会进行Stub、Local和Mock的校验,再然后就是开始构建引用服务所需的参数,使用HashMap存储,最终调用createProxy()开始创建代理对象。

Map<String, String> map = new HashMap<String, String>();
// 端点 表明是消费端
map.put(SIDE_KEY, CONSUMER_SIDE);
// 运行时参数
ReferenceConfigBase.appendRuntimeParameters(map);
map.put(INTERFACE_KEY, interfaceName);
AbstractConfig.appendParameters(map, getMetrics());
AbstractConfig.appendParameters(map, getApplication());
AbstractConfig.appendParameters(map, getModule());
......
// 创建代理对象
ref = createProxy(map);

如果在同一个JVM里,Consumer要引用的服务自身已经提供了,那么Dubbo会尝试直接引用同一个JVM内的服务,这样可以避免网络传输带来的性能损耗,shouldJvmRefer()方法判断是否应该这么做。

if (shouldJvmRefer(map)) {
    // 优先引用同一个JVM内的Provider
    URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
    invoker = REF_PROTOCOL.refer(interfaceClass, url);
    if (logger.isInfoEnabled()) {
        logger.info("Using injvm service " + interfaceClass.getName());
    }
}

大多数情况下,引用的还是远程服务,所以接下来要分析的才是重点。 如果配置了点对点直连的url,则会开始解析它:

if (url != null && url.length() > 0) {
    // ,分割 点对点直连url
    String[] us = SEMICOLON_SPLIT_PATTERN.split(url);
    if (us != null && us.length > 0) {
        for (String u : us) {
            URL url = URL.valueOf(u);
            if (StringUtils.isEmpty(url.getPath())) {
                url = url.setPath(interfaceName);
            }
            // 解析,保存到urls集合
            if (UrlUtils.isRegistry(url)) {
                urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
            } else {
                urls.add(ClusterUtils.mergeUrl(url, map));
            }
        }
    }
}

一般很少配置直连url,没有配置的话Dubbo就会基于注册中心去引用服务。

if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {// 非injvm协议
    checkRegistry();
    // 加载注册中心URL
    List<URL> us = ConfigValidationUtils.loadRegistries(this, false);
    if (CollectionUtils.isNotEmpty(us)) {
        for (URL u : us) {
            URL monitorUrl = ConfigValidationUtils.loadMonitor(this, u);
            if (monitorUrl != null) {
                map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
            }
            // 给注册中心URL添加要引用的服务参数:refer
            urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
        }
    }
    if (urls.isEmpty()) {
        throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
    }
}

不论如何,最终解析出的urls就是要引用的服务URL列表,可能一个也可能多个,我们目前只看一个url的情况。

if (urls.size() == 1) {
    // 通过协议引用服务
    invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
}

2.2.1 构建Invoker

Protocol#refer()方法可以引用远程服务,得到Invoker对象,Consumer可以对Invoker发起服务调用,这个调用可能是远程调用,也可能是集群调用,取决于子类如何实现。

如果是点对点直连Provider,那就是根据协议创建对应的Invoker,例如DubboInvoker,这种就是简单的远程调用,没有服务重试、负载均衡的功能。 如果配置了多个点对点的直连url,相当于有多个Provider可以调用,Dubbo会创建StaticDirectory,它代表一个静态的服务目录,Provider的数量是明确的,此时基于Cluster#join()方法创建ClusterInvoker对象,它拥有集群容错的功能。

不过一般来说,上面两种方法都很少用,更多的是基于注册中心来实现自动的服务注册与发现。这种情况下,URL的协议就是registry,对应的方法是RegistryProtocol#refer()

Protocol接口有两个包装类:ProtocolFilterWrapper和ProtocolListenerWrapper,在根据URL协议调用具体的refer()方法前会先经过这两个包装类,这是由Dubbo的SPI机制决定的。前者的作用是为Invoker构建FilterChian,执行过滤器逻辑;后者的作用是在服务引用和销毁时触发相应的事件。 ​

基于注册中心引用服务的流程是这样子的,先解析注册中心URL,利用SPI加载对应的Registry实现类,例如NacosRegistry。然后根据cluster参数加载对应的集群容错策略,默认是FailoverCluster。再创建RegistryDirectory,它是基于注册中心的服务调用目录,通过它可以知道都有哪些Provider可以调用。最终调用Cluster#join()创建对应的ClusterInvoker实现。

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    // 重写URL,获取注册中心URL
    url = getRegistryUrl(url);
    Registry registry = registryFactory.getRegistry(url);
    if (RegistryService.class.equals(type)) {
        return proxyFactory.getInvoker((T) registry, type, url);
    }
    // SPI加载Cluster
    Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY));
    // 引用服务
    return doRefer(cluster, registry, type, url);
}

关于RegistryDirectory,这里有两个方法需要关注。buildRouterChain()方法用于构建RouterChain,每个Directory都有一条Router路由链,Dubbo的路由机制可以根据路由规则对Provider进行筛选。subscribe()方法会订阅服务,当Provider服务发生变更时会收到通知,触发notify()方法,notify()方法里会将ProviderUrls转换成对应的Invoker,这个待会细说。

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    Map<String, String> parameters = new HashMap<String, String>(directory.getConsumerUrl().getParameters());
    URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
    if (directory.isShouldRegister()) {
        directory.setRegisteredConsumerUrl(subscribeUrl);
        // 注册Consumer
        registry.register(directory.getRegisteredConsumerUrl());
    }
    directory.buildRouterChain(subscribeUrl);
    // 订阅服务,服务变更时触发notify()
    directory.subscribe(toSubscribeUrl(subscribeUrl));
    // 创建Invoker对象
    Invoker<T> invoker = cluster.join(directory);
    List<RegistryProtocolListener> listeners = findRegistryProtocolListeners(url);
    if (CollectionUtils.isEmpty(listeners)) {
        return invoker;
    }
    RegistryInvokerWrapper<T> registryInvokerWrapper = new RegistryInvokerWrapper<>(directory, cluster, invoker);
    for (RegistryProtocolListener listener : listeners) {
        listener.onRefer(this, registryInvokerWrapper);
    }
    return registryInvokerWrapper;
}

Cluster#join()方法用于创建ClusterInvoker,它拥有集群容错的能力,我们看下创建过程。doJoin()是子类实现的创建对应ClusterInvoker的方法,buildClusterInterceptors()方法会加载ClusterInterceptor拦截器,构建拦截器链,拦截ClusterInvoker的invoke()方法。

public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
    /**
      * doJoin:创建ClusterInvoker
      * buildClusterInterceptors:构建Cluter拦截器链
      */
    return buildClusterInterceptors(doJoin(directory), directory.getUrl().getParameter(REFERENCE_INTERCEPTOR_KEY));
}

2.2.2 订阅服务

Cluster创建ClusterInvoker是依赖Directory的,ClusterInvoker不做具体的服务调用,只是在Invoker的基础上增加了集群容错的能力。ClusterInvoker会通过Directory过滤出可调用的服务列表,然后通过LoadBalance最终选出一个服务进行调用,一旦调用失败就要进行容错处理,例如FailoverClusterInvoker会进行重试。

public Result invoke(final Invocation invocation) throws RpcException {
    checkWhetherDestroyed();
    Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
    if (contextAttachments != null && contextAttachments.size() != 0) {
        ((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
    }
    // 通过Directory过滤服务列表
    List<Invoker<T>> invokers = list(invocation);
    // 初始化 负载均衡
    LoadBalance loadbalance = initLoadBalance(invokers, invocation);
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    
    return doInvoke(invocation, invokers, loadbalance);
}

Directory是如何获取invokers的呢?Directory接口有两个实现类:StaticDirectory和RegistryDirectory,前者用于点对点直连时已经明确Provider的场景,后者是基于注册中心的,适用于Provider不断变化的场景。 还记得之前说过的吗?RegistryDirectory创建完,会调用subscribe()方法订阅服务,这样当Provider发生变化时会收到通知并触发notify()方法,这里就会自动将ProviderUrl转换成对应的Invoker,方法是RegistryDirectory#toInvokers(),方法较长,下面是精简后的大致意思:

// 协议引用获得Invoker
invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
// 存储到Map容器
newUrlInvokerMap.put(key, invoker);

例如,当Provider使用dubbo协议时,就会调用DubboProtocol#protocolBindingRefer()方法,创建DubboInvoker并返回。在创建DubboInvoker的过程中,就会去和服务端建立连接。

public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
    optimizeSerialization(url);
    DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
    invokers.add(invoker);
    return invoker;
}

getClients()方法会获取客户端ExchangeClient对象,和服务端建立连接。可以看到它是一个数组,这意味着Consumer可以跟同一个Provider同时建立多个连接,通过参数connections进行配置。默认情况下,该值为0,Consumer只会建立一条连接,所有人都共用这条连接。一般不建议修改该值,除非是一对一压测时,可以通过调大它来增强网络传输的能力。

private ExchangeClient[] getClients(URL url) {
    // 使用共享连接
    boolean useShareConnect = false;
    // 连接数,=0代表使用共享连接,否则创建独占连接
    int connections = url.getParameter(CONNECTIONS_KEY, 0);
    List<ReferenceCountExchangeClient> shareClients = null;
    // if not configured, connection is shared, otherwise, one connection for one service
    if (connections == 0) {
        useShareConnect = true;        String shareConnectionsStr = url.getParameter(SHARE_CONNECTIONS_KEY, (String) null);
        connections = Integer.parseInt(StringUtils.isBlank(shareConnectionsStr) ? ConfigUtils.getProperty(SHARE_CONNECTIONS_KEY,
                                                                                                          DEFAULT_SHARE_CONNECTIONS) : shareConnectionsStr);
        shareClients = getSharedClient(url, connections);
    }
    ExchangeClient[] clients = new ExchangeClient[connections];
    for (int i = 0; i < clients.length; i++) {
        if (useShareConnect) {
            clients[i] = shareClients.get(i);
        } else {
            // 初始化客户端
            clients[i] = initClient(url);
        }
    }
    return clients;
}

initClient()方法会创建ExchangeClient并和服务端建立连接,这涉及到网络传输层,Dubbo会根据clienttransporter参数加载对应的实现,默认是netty,即创建NettyClient。在NettyClient的构造函数中,会创建Bootstrap并设置EventLoopGroup、编排ChannelHandlerPipeline等,最终调用Bootstrap#connect()连接服务端,这部分是Netty的代码,这里就不贴了。 DubboInvoker在执行invoke调用时,会从ExchangeClient数组中轮询一个出来,然后发送网络请求。

2.2.3 创建代理对象

通过Protocol#refer()得到的Invoker对象并不能直接使用,开发者面向接口编程,我们并不关心Invoker对象,我们需要的是接口实例。 所以,Dubbo还需要基于Invoker来创建接口的代理对象,这个并不难,通过JDK动态代理、字节码技术都能实现。 ProxyFactory#getProxy()方法用来获取Invoker的代理对象,它有两个实现类JdkProxyFactory和JavassistProxyFactory,前者使用JDK动态代理,后者使用javassist字节码技术,Dubbo默认使用后者。

// create service proxy
return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
public class JavassistProxyFactory extends AbstractProxyFactory {
    @Override
    @SuppressWarnings("unchecked")
    public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }
}

我们对代理对象发起的调用,最终会调用InvokerInvocationHandler#invoke(),非Object继承来的方法,会发起RPC调用。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    ......
    RpcInvocation rpcInvocation = new RpcInvocation(method, invoker.getInterface().getName(), args);
    String serviceKey = invoker.getUrl().getServiceKey();
    rpcInvocation.setTargetServiceUniqueName(serviceKey);
    if (consumerModel != null) {
        rpcInvocation.put(Constants.CONSUMER_MODEL, consumerModel);
        rpcInvocation.put(Constants.METHOD_MODEL, consumerModel.getMethodModel(method));
    }
    return invoker.invoke(rpcInvocation).recreate();
}

3. 总结

Dubbo引用服务,首先会对相关的配置进行检查和更新,然后引用服务创建Invoker,最终基于Invoker创建代理对象,这是大致的流程。 在引用服务时,会判断自身是否已经提供了该服务,如果是会优先引用自身,跳过网络传输,提高效率。 引用远程服务时,如果是基于注册中心的,那么会创建RegistryDirectory,它会去订阅相关服务,然后将ProviderUrls转换成对应的Invoker,例如DubboInvoker,DubboInvoker创建时会自动创建NettyClient和服务端建立连接,DubboInvoker在执行invoke调用时会轮训出NettyClient发送网络请求。 invokers转换完毕,开始构建RouterChain执行路由逻辑,list()方法会进行路由过滤。 最后根据配置的集群容错策略加载对应的Cluster实现,Cluster会基于RegistryDirectory创建ClusterInvoker,ClusterInvoker本身不实现服务调用的逻辑,它只是在Invoker的基础上增加了集群容错的能力。