dubbo服务订阅_具体过程

742 阅读9分钟

参考资料:

总结

主线:参数检查,配置Map -> 构建URL对象,按照协议生成Invoker -> 动态代理封装Invoker

具体过程:

  1. 检查各种配置,把参数添加到Map里面
  2. 把map转成参数字符串拼接到URL对象上。根据URL上的协议,利用dubbo spi机制调用对应Protocol#refer创建Invoker
    1. RegistryProtocol#refer,获取注册中心实例,注册消费者信息,以zk为例,在协议/服务接口/consumers目录下创建consumer节点。接着,订阅provider、configurators和routers目录,订阅完成会触发DubboProtocol#refer
    2. DubboProtocol#refer,初始化客户端连接,默认是NettyClient,返回DubboInvoker
  3. 如果多个URL对象,先遍历生成Invoker对象,然后经过StaticDirectory封装一下,使用Cluster#join合并,对外暴露一个Invoker,方便调用
  4. 利用动态代理,封装Invoker,屏蔽调用细节,返回代理对象

image-20201223215858204

一些思考

这段内容源自Dubbo系列之服务订阅(2),从宏观的角度讲述了调用远程服务的过程和细节,看完这个会对服务订阅有了初步认识。

  1. 如果要实现远程服务的调用,那么就必须建立网络连接。因为我们需要与特定的目标通信,所以使用TCP连接,除此以外,还要打开并监听端口,我们封装一个类实现这些操作,叫做ExchangeClient

  2. 拿到ExchangeClient后,我们就可以远程连接。但是,我们更加方便沟通交流,还需要定义一个协议,比如dubbo。于是,搞了个DubboInvoker

  3. 可以通信了,但是,我还想在本地增加一些过滤器。那就通过责任链模式,把Filter与DubboInvoker连接起来,返回一个ProtocolFilterWrapper

  4. 同理,如果还想加上一些监听器,同样再进行一次封装,返回ListenerInvokerWrapper

  5. 考虑到一个服务有多个提供者,我给每个提供者都创建一个ListenerInvokerWrapper,如下所示

    Demoservice::196.254.324.1 ListenerInvokerWrapper1 Demoservice::196.254.324.2 ListenerInvokerWrapper2 Demoservice::196.254.324.3 ListenerInvokerWrapper3

  6. 这样本地创建太费事,我们引入注册中心。直接把服务注册到注册中心,然后消费者从注册中心拉取服务对象。拉取时,这些服务都在某个目录下,我们称为RegistryDirectory,这个服务目录包含所有远程服务调用对象ListenerInvokerWrapper。我们在RegistryDirectory弄一个Map,叫做urlInvokerMap,保存所有的远程服务调用对象,key是Demoservice::ip

  7. 我们可以在本地通过调用RegistryDirectoy对象,实现远程通信。但是每次调用都需要手动选择调用哪个,非常麻烦,而且造成流量不均。想到这里,我们封装一个负载均衡类来解决,叫做LoadBalance,定义一个select方法,LoadBalance.select(registryDirectory)自动选择一个服务调用。负载均衡策略包括:一致性Hash、随机、轮询、最少活跃数、最短响应时间

  8. 虽然可以自动调用,但是调用失败能不能重试,或者直接失败。那我们把具备这种能力的服务调用,称为集群(Cluster),定义个join方法,Cluster.join(registry),返回一个可以快速失败或者重试的服务调用对象

  9. 目前,我们已经拥有一个XXXclusterInvoker对象,它具有快速失败或者重试等功能,且具有负载均衡算法的远程服务调用对象。但是,我还想在远程服务调用失败后,可以在本地模拟调用,返回一个mock对象,那么我们对XXXclusterInvoker重新封装,命名为MockClusterInvoker,具有Mock功能,持有服务目录RegistryDirectory和XXXclusterInvoker对象

  10. 至此,我们的MockClusterInvoker已经比较完善。不过,为了屏蔽远程调用的细节,使得用户可以像本地调用般无感知,我们通过jdk动态代理返回代理对象,在拦截方法invoke中调用MockClusterInvoker的方法,完成远程调用。

最终在消费者端呈现出一个远程服务代理对象如下图:

image-20201223164509212

服务订阅入口

经过上篇文章的分析,无论是xml方式,还是注解方式,服务订阅入口都是ReferenceConfig#get()。我们发现,这个方法核心是内部调用init方法。

public synchronized T get() {
    if (destroyed) {
        throw new IllegalStateException("Already destroyed!");
    }
    if (ref == null) {
        init();
    }
    return ref;
}

由于init方法实在太长,大部分都是检查配置,然后往Map里面填充各种属性,下图是部分代码,以及构建完的map内容

image-20201223171318772

image-20201224101027544

private void init() {
    // .... 省略代码
    
    StaticContext.getSystemContext().putAll(attributes);
    // 重点:创建代理对象
    ref = createProxy(map);
    ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods());
    ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel); 
}

createProxy

接下来是重点方法com.alibaba.dubbo.config.ReferenceConfig#createProxy,由于方法同样比较长,我会删除掉部分非重点代码。

/**
  * 核心方法,通过配置参数的元信息,创建一个代理对象
  *
  **/
private T createProxy(Map<String, String> map) {
    // 创建临时URL
    // temp://localhost?application=bm-officeAuto-asset&check=false&dubbo=2.8.4.BM-SNAPSHOT...
    URL tmpUrl = new URL("temp", "localhost", 0, map);
    final boolean isJvmRefer;
    
    // 这一段都是用来判断是否引用本地服务 
    if (isInjvm() == null) {
        // 指定URL的情况下,不做本地引用
        if (url != null && url.length() > 0) {
            isJvmRefer = false;
        } else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {
            // 默认情况下如果本地有服务暴露,则引用本地服务.
            isJvmRefer = true;
        } else {
            isJvmRefer = false;
        }
    } else {
        isJvmRefer = isInjvm().booleanValue();
    }
	
    
    if (isJvmRefer) {
        // 构建本地协议的URL
        URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
   		// 本地服务引入,就是去之前服务暴露的exported
        invoker = refprotocol.refer(interfaceClass, url);
    } else {
        
        // 用户指定url,有可能是点对点直连,也有可能是注册中心地址 
        if (url != null && url.length() > 0) { 
            // 获取到Reference配置的url,多个url用分号分隔(;)
            String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
            if (us != null && us.length > 0) {
                for (String u : us) {
                    URL url = URL.valueOf(u);
                    if (url.getPath() == null || url.getPath().length() == 0) {
                        url = url.setPath(interfaceName);
                    }
                    if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                        // 如果是注册中心的url,将map转成查询字符串,然后使用refer作为参数的key拼接到url上。即xx?refer=map查询字符串
                        urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                    } else {
                        // 如果是点对点,合并url,移除服务提供者的一些配置
                        urls.add(ClusterUtils.mergeUrl(url, map));
                    }
                }
            }
        } else { 
            // 不满足上面两种情况,获取注册中心地址。与服务暴露不同,参数是false,表示不是provider
            List<URL> us = loadRegistries(false);
            if (us != null && !us.isEmpty()) {
                for (URL u : us) {
                    URL monitorUrl = loadMonitor(u);
                    if (monitorUrl != null) {
                        map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
                    }
                    // 将map转成查询字符串,然后使用refer作为参数的key拼接到url上。即xx?refer=map查询字符串
                    urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                }
            }
            ....
        }
		
        /********  往上都是在构建URL对象,把map作为参数 *************/
        
        /********  往下就是根据URL的协议,调用refer方法,创建Invoker *************/
        if (urls.size() == 1) {
            // 只有一个url,直接转成invoker
            invoker = refprotocol.refer(interfaceClass, urls.get(0));
        } else {
            List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
            URL registryURL = null;
            for (URL url : urls) {
                // 多个url,各自转成invoker后保存到集合
                invokers.add(refprotocol.refer(interfaceClass, url));
                if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                    registryURL = url; // use last registry url
                }
            }
            
            // 创建StaticDirectory实例,并由cluster对多个invoker进行合并,只暴露出一个invoker,便于使用
            if (registryURL != null) { // registry url is available
                // use AvailableCluster only when register's cluster is available
                URL u = registryURL.addParameterIfAbsent(Constants.CLUSTER_KEY, AvailableCluster.NAME);
                invoker = cluster.join(new StaticDirectory(u, invokers));
            } else { // not a registry url
                invoker = cluster.join(new StaticDirectory(invokers));
            }
        }
    }

    ......
        
    // 通过动态代理封装Invoker,返回代理对象
    // create service proxy
    return (T) proxyFactory.getProxy(invoker);
}

小结:

createProxy()方法主要做的事情:

1.先获取一个或多个注册中心的URL,然后把map转成字符串作为url的参数,key是refer;(这一步和服务暴露类似,构建URL对象,注册自身信息)

2.根据URL对象上的协议,调用对应的protocol.refer方法得到invoker

3.把invoker添加到集合中,使用StaticDirectory封装,通过Cluster进行合并,对外暴露出一个invoker

4.使用动态代理,封装invoker调用细节,返回代理对象

image-20201223203814517

protocol#refer

我们已经知道大致的服务引入流程,不过,还有些细节还需要继续发掘,比如生成的Invoker到底是什么,消费者又是怎么获取提供者的数据。要解决这些疑惑,都离不开protocol#refer,根据协议不同,可以分为Registry类型的Protocol,和非Registry类型的Protocol。

RegistryProtocol#refer

我们先来看下com.alibaba.dubbo.registry.integration.RegistryProtocol#refer

@Override
@SuppressWarnings("unchecked")
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    // 获取registry的参数值,设置为URL协议头
    // 未替换前:registry://ip:port/path?registry=zookeeper&key=value
    // 替换后:zookeeper://ip:port/path?key=value
    url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY))
        																	.removeParameter(Constants.REGISTRY_KEY);
    // 获取注册中心实例,一般是ZookeeperRegistry,包括zkClient、registryUrl
    Registry registry = registryFactory.getRegistry(url);
    
    if (RegistryService.class.equals(type)) {
        return proxyFactory.getInvoker((T) registry, type, url);
    }

  	// .... 省略部分代码
    
    // 真正的refer
    return doRefer(cluster, registry, type, url);
}
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    // 创建RegistryDirectory,就是我们之前提到的服务目录
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    // 设置注册中心实例
    directory.setRegistry(registry);
    // 设置动态生成的Protocol$Adaptive
    directory.setProtocol(protocol);

    // all attributes of REFER_KEY
    // 获取url上的参数,封装成map
    Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
    
    // 构建订阅URL,以consumer//开头
    // url:consumer://10.0.0.113/cn.com.bluemoon.service.portal.service.PortalAppService?
    // application=bm-officeAuto-asset&check=false
    // &interface=cn.com.bluemoon.service.portal.service.PortalAppService&methods=insertUserRoleTable,queryAPIList
    // &pid=7252&revision=0.0.8&sendmsg=false&side=consumer&timeout=60000&timestamp=1608728137056
    URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, 
                               parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);

    if (!Constants.ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(Constants.REGISTER_KEY, true)) {
        URL registeredConsumerUrl = getRegisteredConsumerUrl(subscribeUrl, url);
        // 向注册中心注册消费者,实际上就是在consumers目录下创建新节点
        registry.register(registeredConsumerUrl);
        directory.setRegisteredConsumerUrl(registeredConsumerUrl);
    }
	
    // 订阅服务,并且会拉取远程服务Invoker到directory对象的urlInvokerMap
    // 监听providers目录、configurators目录和routers目录。订阅完,会触发DubboProtocol#refer方法
    directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
                                                  Constants.PROVIDERS_CATEGORY
                                                  + "," + Constants.CONFIGURATORS_CATEGORY
                                                  + "," + Constants.ROUTERS_CATEGORY));

    // 封装多个Invoker,对外暴露一个,方便使用。得到是MockClusterInvoker
    Invoker invoker = cluster.join(directory);
    // 在提供者消费者服务表中记录这个信息
    ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
    return invoker;
}

小结:

RegistryProtocol#refer方法主要是获取注册中心实例,接着调用deRefer方法,生成一个服务目录RegistryDirectory,然后向注册中心注册自身信息,以zk为例,在服务接口的目录下创建consumer节点。接着,监听providers、configurators和routers节点,然后RegistryDirectory会拉取节点的最新数据,触发DubboProtocol#refer方法,生成DubboInvoker,消费者与提供者建立TCP连接。

具体订阅过程参考:Dubbo系列之服务订阅(3)

DubboProtocol#refer

com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#refer

@Override
public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
    optimizeSerialization(url);
    // create rpc invoker.
    // 根据url获取远程客户端,返回一个DubboInvoker
    // 关键是getClients()
    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 service_share_connect = false;
    int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
    // if not configured, connection is shared, otherwise, one connection for one service
    if (connections == 0) {
        service_share_connect = true;
        connections = 1;
    }

    ExchangeClient[] clients = new ExchangeClient[connections];
    for (int i = 0; i < clients.length; i++) {
        if (service_share_connect) {
            // 获取共享的客户端。过程是通过远程地址找到client,如果远程地址还没有客户端就执行initClients()
            clients[i] = getSharedClient(url);
        } else {
            // 得到初始化的客户端
            clients[i] = initClient(url);
        }
    }
    return clients;
}

/**
  * Create new connection
  */
private ExchangeClient initClient(URL url) {

    // client type setting.
    // 获取客户端类型,默认是netty
    String str = url.getParameter(Constants.CLIENT_KEY, 
                                  url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_CLIENT));

    url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
    // enable heartbeat by default
    url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));

    // BIO is not allowed since it has severe performance issue.
    .......

    ExchangeClient client;
    try {
        // connection should be lazy
        if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) {
            // 懒连接
            client = new LazyConnectExchangeClient(url, requestHandler);
        } else {
            // 连接远程
            client = Exchangers.connect(url, requestHandler);
        }
    } catch (RemotingException e) {
        throw new RpcException("Fail to create remoting client for service(" + url + "): " + e.getMessage(), e);
    }
    return client;
}

看下获取到的clients:

image-20201223214136332

小结:

DubboProtocol#refer主要是获取到远程客户端,然后封装返回一个DubboInvoker。