7、Dubbo源码系列-服务调用方流程分析

277 阅读2分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

经过前面几篇文章分析,Dubbo的服务提供方终于正常启动并可以处理请求了,今天就带着大家一起看下Dubbo的服务调用方的启动流程

一、消费Demo

public class ConsumerApplication {
    public static void main(String[] args) {
        ReferenceConfig<DemoService> reference = new ReferenceConfig<>();
        reference.setInterface(DemoService.class);

        DubboBootstrap bootstrap = DubboBootstrap.getInstance();
        bootstrap
                .application(new ApplicationConfig("dubbo-demo-api-consumer"))
                .registry(new RegistryConfig("zookeeper://127.0.0.1:2181"))
                .reference(reference)
                .start();

        String message = ReferenceConfigCache.getCache().get(reference).sayHello("dubbo");
        System.out.println("Application_sayHello_" + message);
    }
}

以API调用为例,可以看到,消费端通过ReferenceConfig的API来执行对应的服务调用。

二、时序图

image.png 老规矩,有图先看图,以上就是客户端调用方整体流程。首先通过ReferenceConfigCache类获取ReferenceConfig,内部对ReferenceConfig做了个缓存,优先从对应的缓存中获取,否则执行ReferenceConfig init方法进行初始化,同前文分析的其他流程一样,依然是调用的Protocol$Adaptive类的方法,经历过层层Wrapper到达RegistryProtocol类,执行其doRefer方法获取到对应的invoke。

三、RegistryProtocol-doRefer

    private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
        ... ...  
        // 1、建立路由规则
        directory.buildRouterChain(subscribeUrl);
        // 2、订阅服务提供方者地址
        directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
                PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));

        // 3、包装容错策略到invoker
        Invoker invoker = cluster.join(directory);
        return invoker;
    }

doRefer的核心逻辑如上,首先创建对应的路由规则,毕竟服务端肯定不是单点服务,一般来说都是分布式部署,因此要确定,用哪种路由策略来选择对应的服务端机器;然后向ZK订阅对应的服务端地址,以便客户端能感知到服务端机器的上下线;最后生成invoker对象,并绑定对应的容错策略;

3.1.创建路由规则

查看directory.buildRouterChain实现如下:

    public void buildRouterChain(URL url) {
        this.setRouterChain(RouterChain.buildChain(url));
    }
    
    public static <T> RouterChain<T> buildChain(URL url) {
        return new RouterChain<>(url);
    }

跟踪RouterChain初始化方法可以看到,默认四种routers实现,分别是MockInvokersSelector、TagRouter、AppRouter、ServiceRouter image.png 后面选择集群容错策略后,发起服务调用前,会调用routerChain.route,获取负载均衡后的invoek列表(本文先不展开讲了,分析负载均衡时候的时候在展开)

3.2 订阅服务提供方地址

3.2.1.时序图

image.png

3.2.2.步骤2

步骤2主要从zk获取服务提供者列表,获取到后会调用notify方法通知RegistryProtocol image.png 本地Debug发现实际URL是有三个,查看对应的时间戳发现,第一条才是我本地启动的服务方。另外两条看时间戳应该是脏数据,通过看协议头也能发现,一个的dubbo,一个是empty。

3.2.2.步骤12

主要是将服务提供者地址URL转换成invoker对象,这里最终调用的是protocol.refer方法,阅读过前文的同学想必对这个对象已经很熟悉了,没错,实际调用的就是就是Protocol$Adaptive方法,依然是经历层层Wrapper,最终调用到DubboProtocol相关方法。

    private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
            ... ...
            invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
            ... ...
    }

首先调用DubboProtocol的父类refer方法,内部会调用抽象方法protocolBindingRefer,查看DubboProtocol实现,getClients方法用来创建消费端的NettyClient对象

    @Override
    public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
        // create rpc invoker.
        DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
        invokers.add(invoker);

        return invoker;
    }
    private ExchangeClient[] getClients(URL url) {
        // whether to share connection

        boolean useShareConnect = false;

        int connections = url.getParameter(CONNECTIONS_KEY, 0);
        List<ReferenceCountExchangeClient> shareClients = null;
        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;
    }

这里关注下useShareConnect变量,用来表示是否共享链接。默认是共享的,也就是说引用同一台机器上的多个服务时,可以复用一个Netty链接,同时初始化client的时候,可以选择是否懒加载创建链接,默认是客户端启动的时候就和服务端建立连接。

3.3 选择容错策略

当directory.subscribe方法执行成功后,此时就把服务提供者提供的RUL信息转成了Invoker列表,并保存到了RegistryDerectory里。下面就来看看cluster.join的实现,查看源码发现cluster对象本身也是个SPI注入点,默认实现为FailoverCluster,查看FailoverCluster实现如下

    @Override
    public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
        return new FailoverClusterInvoker<>(directory);
    }

同时dubbo会对cluster使用wrapper类MockClusterWrapper进行增强,也就是说cluster.join用的cluster对象实际为MockClusterWrapper对象,MockClusterWrapper对象内部调用了FailoverCluster方法。

public class MockClusterWrapper implements Cluster {

    private Cluster cluster;

    public MockClusterWrapper(Cluster cluster) {
        this.cluster = cluster;
    }

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new MockClusterInvoker<T>(directory,
                this.cluster.join(directory));
    }

}

MockClusterWrapper,顾名思义,用来实现mock功能。若是开启Mock则须要过滤出MockInvoker后执行服务降级,这里就不展开讲了。

回到容错机制,可以看到dubbo提供了很多集群容错的方式

image.png

  • Failover Cluster
    • 失败重试,可通过retries=x设置重试次数,通常用于幂等性的读操作。
  • Failsafe Cluster
    • 快速失败,出现异常时,立刻报错。通常用于非幂等的写操作。
  • Failback Cluste
    • 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作
  • Forking Cluster
    • 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源
  • Broadcast Cluste
    • 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息

四、小结

本文主要分析了服务调用方相关实现,从ReferenceConfig一直分析到了Netty客户端启动,其中涉及到路由策略选择、invoke注册、集群容错策略等,有些知识点由于篇幅限制,暂未展开讲解,后面将会为大家一一呈现。