dubbo源码系列7-集群容错之服务目录 Directory

一、前沿

服务导出和服务引用都已经讲完了,接下来就该分析服务的调用过程,但在服务调用过程中有一个非常重要的模块需要先学习一下,即集群容错模块,集群容错模块包含四个部分,分别是服务目录 Directory、服务路由 Router、集群 Cluster 和负载均衡 LoadBalance,下面会分四个章节分别为大家讲解。,首先我们看 服务目录 Directory。

定义:服务目录 directory 是 Invoker 集合,且这个集合中的元素会随注册中心的变化而进行动态调整

服务目录 directory 中存储了一些和 provider 有关的信息,通过服务目录,consumer 可获取到 provider 的信息,比如 ip、端口、服务协议等。通过这些信息,consumer 就可通过 Netty 等客户端进行远程调用。在一个服务集群中,provider 数量并不是一成不变的,如果集群中新增了一台机器,相应地在 directory 中就要新增一条 provider 记录。或者,如果 provider 的配置修改了,directory 中原有的记录也要做相应的更新。这样看的话,directory 和注册中心的功能确实是差不多的。实际上 directory 在获取注册中心的服务配置信息后,会为每条配置信息生成一个 Invoker 对象,并把这个 Invoker 对象存储起来,这个 Invoker 才是 directory 最终持有的对象。Invoker 有什么用呢?看名字就知道了,这是一个具有远程调用功能的对象

二、服务目录结构

服务目录 directory 的总结构如下图:

从上图中可以看出 Directory 的具体实现类只有两个,分别为 StaticDirectory RegistryDirectory,它们都继承了 AbstractDirectory 类,而 AbstractDirectory 实现了 Directory 接口,Directory 接口中定义了一个非常重要的方法,即 list(Invocation invocation),用于列举 Invoker 列表

Directory 接口又继承了 Node 接口,Node 接口中定义了一个非常重要的方法,即 getUrl(),获取配置信息 URL,实现该接口的类可以向外提供配置信息,dubbo 中像 Registry、Monitor、Invoker 等均继承了这个接口

RegistryDirectory 不仅实现了 Directory 接口,还实现了 NotifyListener 接口,这样子的话,当注册中心节点信息发生变化后,RegistryDirectory 可以通过此接口方法得到变更信息,并根据变更信息动态调整内部 Invoker 列表

三、Directory 源码

从服务目录结构中得知,分析 Directory 源码只需要分析三个类即可,分别是 AbstractDirectory(抽象类)、StaticDirectory(实现类) 和 RegistryDirectory (实现类)

3.1 AbstractDirectory

AbstractDirectory 封装了 Invoker 列举流程,具体的列举逻辑则由子类实现,这是典型的模板模式。接下来我们先来看一下 AbstractDirectory 的源码:

    public AbstractDirectory(URL url, URL consumerUrl, RouterChain<T> routerChain) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }

        if (url.getProtocol().equals(REGISTRY_PROTOCOL)) {
            // protocol 协议是 registry 时,解码获取 refer 参数值并设置到 url 中,同时移除 monitor 参数
            Map<String, String> queryMap = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
            this.url = url.addParameters(queryMap).removeParameter(MONITOR_KEY);
        } else {
            this.url = url;
        }

        this.consumerUrl = consumerUrl;
        // 设置路由链
        setRouterChain(routerChain);
    }

    @Override
    public List<Invoker<T>> list(Invocation invocation) throws RpcException {
        if (destroyed) {
            throw new RpcException("Directory already destroyed .url: " + getUrl());
        }

        // doList 是模板方法,由子类实现具体的业务逻辑
        return doList(invocation);
    }

逻辑相当简单,这里不在赘述了,下面看 StaticDirectory 源码

3.2 StaticDirectory

StaticDirectory 即静态服务目录,顾名思义,它内部存放的 Invoker 是不会变动的。所以,理论上它和不可变 List 的功能很相似。下面我们来看一下这个类具体源码:
 

public class StaticDirectory<T> extends AbstractDirectory<T> {
    private static final Logger logger = LoggerFactory.getLogger(StaticDirectory.class);

    private final List<Invoker<T>> invokers;

    public StaticDirectory(List<Invoker<T>> invokers) {
        this(null, invokers, null);
    }

    public StaticDirectory(List<Invoker<T>> invokers, RouterChain<T> routerChain) {
        this(null, invokers, routerChain);
    }

    public StaticDirectory(URL url, List<Invoker<T>> invokers) {
        this(url, invokers, null);
    }

    public StaticDirectory(URL url, List<Invoker<T>> invokers, RouterChain<T> routerChain) {
        super(url == null && CollectionUtils.isNotEmpty(invokers) ? invokers.get(0).getUrl() : url, routerChain);
        if (CollectionUtils.isEmpty(invokers)) {
            throw new IllegalArgumentException("invokers == null");
        }
        this.invokers = invokers;
    }

    @Override
    public Class<T> getInterface() {
        // 获取接口类
        return invokers.get(0).getInterface();
    }

    // 检测 directory 是否可用
    @Override
    public boolean isAvailable() {
        if (isDestroyed()) {
            return false;
        }
        for (Invoker<T> invoker : invokers) {
            if (invoker.isAvailable()) {
                // 只要有一个 invoker 可用,就认为当前 directory 是可用的
                return true;
            }
        }
        return false;
    }

    // 销毁 directory 目录
    @Override
    public void destroy() {
        if (isDestroyed()) {
            return;
        }
        super.destroy();
        // 遍历销毁 directory 目录下的所有 invoker
        for (Invoker<T> invoker : invokers) {
            invoker.destroy();
        }
        invokers.clear();
    }

    // 构建路由链
    public void buildRouterChain() {
        RouterChain<T> routerChain = RouterChain.buildChain(getUrl());
        routerChain.setInvokers(invokers);
        this.setRouterChain(routerChain);
    }

    @Override
    protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
        List<Invoker<T>> finalInvokers = invokers;
        if (routerChain != null) {
            try {
                // 经过路由链过滤掉不需要的 invoker
                finalInvokers = routerChain.route(getConsumerUrl(), invocation);
            } catch (Throwable t) {
                logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
            }
        }
        // 返回 invoker 列表
        return finalInvokers == null ? Collections.emptyList() : finalInvokers;
    }

}

StaticDirectory 源码中没有什么复杂的逻辑,唯一需要做的就是通过路由链对 Invoker 进行了过滤,这个在后面章节再讲,其他内容都通俗易懂,不做过多的赘述了

3.3  RegistryDirectory

RegistryDirectory 是一种动态服务目录,它实现了 NotifyListener 接口。当注册中心服务配置发生变化后,RegistryDirectory 可收到与当前服务相关的变化。收到变更通知后,RegistryDirectory 可根据配置变更信息刷新 Invoker 列表。RegistryDirectory 中有三个比较重要的逻辑,如下:

1)、列举 Invoker 列表

2)、接收服务配置变更通知

3)、刷新存储的 Invoker 列表

接下来按照 1、2、3 的顺序分别为大家分析一下源码

3.3.1 列举 Invoker 列表

doList 方法实现了列举 Invoker 列表,源码如下:

    @Override
    public List<Invoker<T>> doList(Invocation invocation) {
        if (forbidden) {
            // 1. No service provider 2. Service providers are disabled
            // provider 关闭或禁用了服务,此时抛出 No provider 异常
            throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "No provider available from registry " +
                    getUrl().getAddress() + " for service " + getConsumerUrl().getServiceKey() + " on consumer " +
                    NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() +
                    ", please check status of providers(disabled, not registered or in blacklist).");
        }

        if (multiGroup) {
            // 多个 group 的话直接返回 invoker 列表
            return this.invokers == null ? Collections.emptyList() : this.invokers;
        }

        List<Invoker<T>> invokers = null;
        try {
            // Get invokers from cache, only runtime routers will be executed.
            // route 路由器职责链过滤满足条件的 Invoker 列表
            invokers = routerChain.route(getConsumerUrl(), invocation);
        } catch (Throwable t) {
            logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
        }

        return invokers == null ? Collections.emptyList() : invokers;
    }

这里主要逻辑也是通过 route 路由器链过滤满足条件的 invoker 列表返回,简单易懂,这里不在赘述

3.3.2 接收服务配置变更通知
RegistryDirectory 是一个动态服务目录,会随注册中心配置的变化进行动态调整。因为 RegistryDirectory 实现了 NotifyListener 接口,通过这个接口可以获取到注册中心变更通知,具体实现逻辑在 notify 方法中

notify 方法源码如下:

    @Override
    public synchronized void notify(List<URL> urls) {
        // 定义三个 集合,分别为 provider 的 configurators URL、routers URL 和 providers URL
        Map<String, List<URL>> categoryUrls = urls.stream()
                .filter(Objects::nonNull)
                .filter(this::isValidCategory)
                .filter(this::isNotCompatibleFor26x)
                .collect(Collectors.groupingBy(url -> {
                    if (UrlUtils.isConfigurator(url)) {
                        // configurators URL
                        return CONFIGURATORS_CATEGORY;
                    } else if (UrlUtils.isRoute(url)) {
                        // routers URL
                        return ROUTERS_CATEGORY;
                    } else if (UrlUtils.isProvider(url)) {
                        // providers URL
                        return PROVIDERS_CATEGORY;
                    }
                    return "";
                }));

        List<URL> configuratorURLs = categoryUrls.getOrDefault(CONFIGURATORS_CATEGORY, Collections.emptyList());
        // configurators URL 转成 Configurator
        this.configurators = Configurator.toConfigurators(configuratorURLs).orElse(this.configurators);

        List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
        // routes URL 转成 Router
        toRouters(routerURLs).ifPresent(this::addRouters);

        // providers
        // 提供者 URL 列表
        List<URL> providerURLs = categoryUrls.getOrDefault(PROVIDERS_CATEGORY, Collections.emptyList());
        // 刷新 invoker 列表
        refreshOverrideAndInvoker(providerURLs);
    }

notify 方法主要实现了以下逻辑:

1)、根据url中的 protocol 或者 category 参数对 URL 分类存储

2)、toConfigurators 和 toRouters 方法分别将 url 转成了 Configurator 和 Router

3)、根据提供者 url 列表刷新 invoker 列表

接下来的重点就是分析 刷新 invoker 列表 源码

3.3.3  刷新 invoker 列表

刷新 invoker 列表逻辑主要在 refreshOverrideAndInvoker 方法中,是保证 RegistryDirectory 随注册中心变化而变化的关键所在,整个过程源码如下:

    private void refreshOverrideAndInvoker(List<URL> urls) {
        // mock zookeeper://xxx?mock=return null
        // 获取配置 overrideDirectoryUrl 值
        overrideDirectoryUrl();
        // 刷新 invoker 列表
        refreshInvoker(urls);
    }


    /**
     * Convert the invokerURL list to the Invoker Map. The rules of the conversion are as follows:
     * <ol>
     * <li> If URL has been converted to invoker, it is no longer re-referenced and obtained directly from the cache,
     * and notice that any parameter changes in the URL will be re-referenced.</li>
     * <li>If the incoming invoker list is not empty, it means that it is the latest invoker list.</li>
     * <li>If the list of incoming invokerUrl is empty, It means that the rule is only a override rule or a route
     * rule, which needs to be re-contrasted to decide whether to re-reference.</li>
     * </ol>
     *
     * @param invokerUrls this parameter can't be null
     */
    // TODO: 2017/8/31 FIXME The thread pool should be used to refresh the address, otherwise the task may be accumulated.
    private void refreshInvoker(List<URL> invokerUrls) {
        Assert.notNull(invokerUrls, "invokerUrls should not be null");

        // invokerUrls 中只有一个元素 && url 的协议头 Protocol 为 empty,表示禁用所有服务
        if (invokerUrls.size() == 1
                && invokerUrls.get(0) != null
                && EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
            // 禁用服务标识
            this.forbidden = true; // Forbid to access
            // 空 invoker 列表
            this.invokers = Collections.emptyList();
            routerChain.setInvokers(this.invokers);
            // 销毁所有的 invoker
            destroyAllInvokers(); // Close all invokers
        } else {
            // Allow to access
            this.forbidden = false;
            // local reference
            // 原来的 invoker 映射map
            Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap;
            if (invokerUrls == Collections.<URL>emptyList()) {
                invokerUrls = new ArrayList<>();
            }
            if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
                // 添加缓存 url 到 invokerUrls 中
                invokerUrls.addAll(this.cachedInvokerUrls);
            } else {
                this.cachedInvokerUrls = new HashSet<>();
                //Cached invoker urls, convenient for comparison
                // 缓存 invokerUrls
                this.cachedInvokerUrls.addAll(invokerUrls);
            }
            if (invokerUrls.isEmpty()) {
                return;
            }
            // Translate url list to Invoker map
            // 将 url 转换成 url 到 invoker 映射 map,map 中 url 为 key,value 为 invoker
            Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);

            /**
             * If the calculation is wrong, it is not processed.
             *
             * 1. The protocol configured by the client is inconsistent with the protocol of the server.
             *    eg: consumer protocol = dubbo, provider only has other protocol services(rest).
             * 2. The registration center is not robust and pushes illegal specification data.
             *
             */
            if (CollectionUtils.isEmptyMap(newUrlInvokerMap)) {
                logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :" + invokerUrls.size() + ", invoker.size :0. urls :" + invokerUrls
                        .toString()));
                return;
            }

            List<Invoker<T>> newInvokers = Collections.unmodifiableList(new ArrayList<>(newUrlInvokerMap.values()));
            // pre-route and build cache, notice that route cache should build on original Invoker list.
            // toMergeMethodInvokerMap() will wrap some invokers having different groups, those wrapped invokers not should be routed.
            routerChain.setInvokers(newInvokers);
            // 多个 group 时需要合并 invoker
            this.invokers = multiGroup ? toMergeInvokerList(newInvokers) : newInvokers;
            this.urlInvokerMap = newUrlInvokerMap;

            try {
                // Close the unused Invoker
                // 销毁不需要的 invoker
                destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap);
            } catch (Exception e) {
                logger.warn("destroyUnusedInvokers error. ", e);
            }
        }
    }

刷新 invoker 列表主要实现了以下逻辑:

1)、根据入参 invokerUrls 的数量和 url 的协议头判断是否需要禁用所有服务,若需要 forbidden 设为 true,并销毁所有的 Invoker

2)、将 url 转换成 invoker,并得到 <url, Invoker> 的映射关系

3)、合并多个组的 invoker,并赋值给 invokers 变量

4)、销毁无用的 invoker,避免 consumer 调用下线的 provider

下面分析一下 toInvokers 方法,即 url 转换成 invoker,并得到 <url, Invoker> 的映射关系的过程,源码如下:

    /**
     * Turn urls into invokers, and if url has been refer, will not re-reference.
     *
     * @param urls
     * @return invokers
     */
    private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
        Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<>();
        if (urls == null || urls.isEmpty()) {
            // 列表为空,直接返回
            return newUrlInvokerMap;
        }
        Set<String> keys = new HashSet<>();
        // 获取 consumer 端配置的协议
        String queryProtocols = this.queryMap.get(PROTOCOL_KEY);
        for (URL providerUrl : urls) {
            // If protocol is configured at the reference side, only the matching protocol is selected
            // 如果在 consumer 端配置了协议,则只选择匹配的协议
            if (queryProtocols != null && queryProtocols.length() > 0) {
                boolean accept = false;
                String[] acceptProtocols = queryProtocols.split(",");
                for (String acceptProtocol : acceptProtocols) {
                    // provider 协议是否被 consumer 协议支持
                    if (providerUrl.getProtocol().equals(acceptProtocol)) {
                        accept = true;
                        break;
                    }
                }
                if (!accept) {
                    // provider 协议不被 consumer 协议支持,则忽略此 url
                    continue;
                }
            }
            // 忽略 empty 协议
            if (EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
                continue;
            }
            // 通过 SPI 检测 provider 端协议是否被 consumer 端支持,不支持则抛出异常
            if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
                logger.error(new IllegalStateException("Unsupported protocol " + providerUrl.getProtocol() +
                        " in notified url: " + providerUrl + " from registry " + getUrl().getAddress() +
                        " to consumer " + NetUtils.getLocalHost() + ", supported protocol: " +
                        ExtensionLoader.getExtensionLoader(Protocol.class).getSupportedExtensions()));
                continue;
            }
            // 合并 url 参数,顺序是 : override > -D > Consumer > Provider
            URL url = mergeUrl(providerUrl);

            // The parameter urls are sorted
            // url 参数拼接的字符串且被排序了,
            // 例如:dubbo://192.168.1.247:20887/org.apache.dubbo.config.spring.api.DemoService?anyhost=true&application=service-class&
            // bean.name=org.apache.dubbo.config.spring.api.DemoService&bind.ip=192.168.1.247&bind.port=20887&
            // class=org.apache.dubbo.config.spring.impl.DemoServiceImpl&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&
            // interface=org.apache.dubbo.config.spring.api.DemoService&methods=sayName,getBox&owner=world&pid=24316&register=true&
            // release=&side=provider&timestamp=1572405725011
            String key = url.toFullString();
            if (keys.contains(key)) {
                // Repeated url
                // 忽略重复 key
                continue;
            }
            keys.add(key);
            // Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again
            // 原来本地缓存的 <url, Invoker>
            Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference
            // 获取现有的 url 对应的 invoker
            Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
            // Not in the cache, refer again
            // 原来缓存没有或者provider配置变化了
            if (invoker == null) {
                try {
                    boolean enabled = true;
                    if (url.hasParameter(DISABLED_KEY)) {
                        // 获取 url 中 disable 配置值,取反,然后赋值给 enable 变量
                        enabled = !url.getParameter(DISABLED_KEY, false);
                    } else {
                        // 获取 url 中 enable 配置值
                        enabled = url.getParameter(ENABLED_KEY, true);
                    }
                    if (enabled) {
                        // 调用 Protocol 的 refer 方法构建获取 Invoker,
                        // 具体调用流程是 refer(ProtocolListenerWrapper) -> refer(ProtocolFilterWrapper) -> refer(AbstractProtocol)
                        invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
                    }
                } catch (Throwable t) {
                    logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
                }
                if (invoker != null) {
                    // Put new invoker in cache
                    // 新 invoker 放入缓存
                    newUrlInvokerMap.put(key, invoker);
                }
            } else {
                newUrlInvokerMap.put(key, invoker);
            }
        }
        keys.clear();
        return newUrlInvokerMap;
    }

toInvokers 方法主要实现了以下逻辑:

1)、忽略 provider 协议不被 consumer 支持的 和 empty 协议的 url

2)、合并 url 参数,顺序是 : override > -D > Consumer > Provider

3)、获取本地缓存 invoker,如果缓存没有或者provider配置变化了 && provider 没有被禁用,则构建新的 invoker 并放入缓存 newUrlInvokerMap 中

接下来讲解 toMergeInvokerList 方法,即 多个组的 invoker 合并逻辑,源码如下:

    private List<Invoker<T>> toMergeInvokerList(List<Invoker<T>> invokers) {
        List<Invoker<T>> mergedInvokers = new ArrayList<>();
        Map<String, List<Invoker<T>>> groupMap = new HashMap<>();
        // 遍历 invoker 进行分组
        for (Invoker<T> invoker : invokers) {
            // url 中获取 group 配置
            String group = invoker.getUrl().getParameter(GROUP_KEY, "");
            groupMap.computeIfAbsent(group, k -> new ArrayList<>());
            // invoker 放入同一组中
            groupMap.get(group).add(invoker);
        }

        if (groupMap.size() == 1) {
            // 只有一个分组,直接取出值
            mergedInvokers.addAll(groupMap.values().iterator().next());
        } else if (groupMap.size() > 1) {
            // 多个分组时,遍历分组中的 invoker列表,并调用集群的 join 方法合并每个组的 invoker 列表
            for (List<Invoker<T>> groupList : groupMap.values()) {
                StaticDirectory<T> staticDirectory = new StaticDirectory<>(groupList);
                staticDirectory.buildRouterChain();
                // 通过集群类 的 join 方法合并每个分组对应的 Invoker 列表
                mergedInvokers.add(CLUSTER.join(staticDirectory));
            }
        } else {
            mergedInvokers = invokers;
        }
        return mergedInvokers;
    }

多个组合并 invoker 列表逻辑很简单,代码注释已经很详细了,这里不在赘述

最后分析一下 destroyUnusedInvokers 方法,即 销毁无用的 invoker,源码如下:

    /**
     * Check whether the invoker in the cache needs to be destroyed
     * If set attribute of url: refer.autodestroy=false, the invokers will only increase without decreasing,there may be a refer leak
     *
     * @param oldUrlInvokerMap
     * @param newUrlInvokerMap
     */
    private void destroyUnusedInvokers(Map<String, Invoker<T>> oldUrlInvokerMap, Map<String, Invoker<T>> newUrlInvokerMap) {
        if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
            // 新的 invoker 为空,表明禁用了所有的 provider,这里销毁所有的 invoker
            destroyAllInvokers();
            return;
        }
        // check deleted invoker
        // 记录需要被销毁的 invoker 列表
        List<String> deleted = null;
        if (oldUrlInvokerMap != null) {
            Collection<Invoker<T>> newInvokers = newUrlInvokerMap.values();
            for (Map.Entry<String, Invoker<T>> entry : oldUrlInvokerMap.entrySet()) {
                // 新的 invoker 列表中不包含原有的 invoker,则该 invoker 需要被销毁
                if (!newInvokers.contains(entry.getValue())) {
                    if (deleted == null) {
                        deleted = new ArrayList<>();
                    }
                    // 该 invoker 添加到销毁列表中
                    deleted.add(entry.getKey());
                }
            }
        }

        // 销毁列表中有数据,需要销毁 invoker
        if (deleted != null) {
            for (String url : deleted) {
                if (url != null) {
                    // 缓存中移除要销毁的 invoker
                    Invoker<T> invoker = oldUrlInvokerMap.remove(url);
                    if (invoker != null) {
                        try {
                            // 销毁 invoker
                            invoker.destroy();
                            if (logger.isDebugEnabled()) {
                                logger.debug("destroy invoker[" + invoker.getUrl() + "] success. ");
                            }
                        } catch (Exception e) {
                            logger.warn("destroy invoker[" + invoker.getUrl() + "] failed. " + e.getMessage(), e);
                        }
                    }
                }
            }
        }
    }

到此关于 Invoker 列表的刷新逻辑就分析了,这里对整个过程做下简单总结,如下:

1)、检测入参是否仅包含一个 url,且 url 协议头为 empty

2)、若第一步检测结果为 true,表示禁用所有服务,此时销毁所有的 Invoker

3)、若第一步检测结果为 false,此时将 url 转换成 invoker,并得到 <url, Invoker> 的映射关系

4)、合并多组 Invoker

5)、销毁无用 Invoker

四、总结

本文详细的介绍了服务目录 directory 有关的源码,逻辑相对来说比较简单,但是要让本地服务目录和注册中心服务配置保持一致的话,还是需要做很多的事情的。服务目录 directory 是 Dubbo 集群容错的一部分,也是比较基础的部分,看起来也比较容易理解,希望大家掌握住

参考:

https://dubbo.apache.org/zh-cn/docs/source_code_guide/directory.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值