Dubbo 的集群容错和负载均衡
线上的服务一般是要集群部署的,集群的话就要考虑消费者选择哪一个提供者调用,调用失败了怎么办。这两点对应的就是负载均衡和集群容错了。
invoker
在 Dubbo 中 invoker 其实就是一个具有调用功能的对象,在服务暴露端封装的就是真实的服务实现,把真实的服务实现封装一下变成一个 invoker。
在服务发现端就是从注册中心得到服务提供者的配置信息,然后一条配置信息对应封装成一个 invoker,这个 invoker 就具备远程调用能力,当然要是走的是 injvm 协议那真实走的还是本地的调用。
然后还有个 ClusterInvoker ,它也是个 invoker ,它封装了服务引入生成的 invoker 们,赋予其集群容错等能力,这个 invoker 就是暴露给消费者调用的 invoker。
所以说 Dubbo 就是搞了个统一模型,将能调用的服务的对象都封装成 invoker。
这里主要讲的是服务消费者这边的事情,因为集群容错是消费者端实现的。
服务目录
服务目录也就是 Directory,可以理解为服务的目录,但实际上它是一堆 invoker 的集合。
服务的提供者都会集群部署,所有同样的服务一般都会有多个提供者,服务目录就负责管理这些服务提供者,当需要选择服务提供者时,就直接在服务目录中通过负载均衡算法挑选出一个即可。
而服务提供者们也不是一成不变的,比如集群中增加了一台服务提供者,那么相应的服务目录就需要添加一个 invoker,下线了一台服务提供者,目录里面也需要删除对应的 invoker,修改了配置也一样得更新。
所以这个服务目录其实还实现了监听注册中心的功能(指的是 RegistryDirectory )。
使用一个抽象类来实现 Directory 接口,抽象类会实现一些公共方法,并且定义好逻辑,然后具体的实现由子类来完成,可以看到有两个子类,分别是 StaticDirectory 和 RegistryDirectory。
RegistryDirectory
RegistryDirectory ,是一个动态目录。它实现了 NotifyListener 接口,所以它可以监听注册中心的变化,当服务中心的配置发生变化之后, RegistryDirectory 就可以收到变更通知,然后根据配置刷新其 Invoker 列表。
RegistryDirectory 一共有三大作用:
-
获取 invoker 列表:RegistryDirectory 实现的父类抽象方法 doList,其目的就是得到 invoker 列表,而其内部的实现主要是做了层方法名的过滤,通过方法名找到对应的 invokers。
@Override public List<Invoker<T>> doList(Invocation invocation) { if (forbidden) { // 1. No service provider 2. Service providers are disabled throw new RpcException(...); } List<Invoker<T>> invokers = null; // 拿到 方法名 和 Invoker 的映射 Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) { String methodName = RpcUtils.getMethodName(invocation); Object[] args = RpcUtils.getArguments(invocation); if (args != null && args.length > 0 && args[0] != null && (args[0] instanceof String || args[0].getClass().isEnum())) { invokers = localMethodInvokerMap.get(methodName + "." + args[0]); // The routing can be enumerated according to the first parameter } if (invokers == null) { invokers = localMethodInvokerMap.get(methodName); } if (invokers == null) { invokers = localMethodInvokerMap.get(Constants.ANY_VALUE); } if (invokers == null) { Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator(); if (iterator.hasNext()) { invokers = iterator.next(); } } } // 经过方法名过滤返回 invokers return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers; }
-
监听注册中心的变化:通过实现 NotifyListener 接口能感知到注册中心的数据变更,这其实是在服务引入的时候就订阅的。
RegistryDirectory 定义了三种集合,分别是 invokerUrls 、routerUrls 、configuratorUrls 分别处理相应的配置变化,然后对应转化成对象。
@Override public synchronized void notify(List<URL> urls) { List<URL> invokerUrls = new ArrayList<URL>(); List<URL> routerUrls = new ArrayList<URL>(); List<URL> configuratorUrls = new ArrayList<URL>(); for (URL url : urls) { // 按要求添加到上面三个 list 中 ... } // configurators if (configuratorUrls != null && !configuratorUrls.isEmpty()) { this.configurators = toConfigurators(configuratorUrls); } // routers if (routerUrls != null && !routerUrls.isEmpty()) { List<Router> routers = toRouters(routerUrls); if (routers != null) { // null - do nothing setRouters(routers); } } List<Configurator> localConfigurators = this.configurators; // local reference // merge override parameters this.overrideDirectoryUrl = directoryUrl; if (localConfigurators != null && !localConfigurators.isEmpty()) { for (Configurator configurator : localConfigurators) { this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl); } } // providers refreshInvoker(invokerUrls); }
-
刷新 invokers:其实就是根据监听变更的 invokerUrls 做一波操作,
refreshInvoker(invokerUrls)
, 根据配置更新 invokers。private void refreshInvoker(List<URL> invokerUrls) { if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) { // 如果只有一个 invokerUrl 并且协议是 empty,则清楚所有的 invoker this.forbidden = true; // Forbid to access this.methodInvokerMap = null; // Set the method invoker map to null destroyAllInvokers(); // Close all invokers } else { // 根据 URL 生成 invoker map,然后方法名对应 invoker 的 map,再销毁无效的 invoker ... Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // Change method name to map Invoker Map ... this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap; this.urlInvokerMap = newUrlInvokerMap; try { destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker } catch (Exception e) { logger.warn("destroyUnusedInvokers error. ", e); } } }
先根据 invokerUrls 数量和协议头是否是 empty,来决定是否禁用所有 invokers,如果不禁用,则将 url 转成 Invoker,得到 <url, Invoker> 的映射关系。
然后再进行转换,得到 <方法名, Invoker 列表> 映射关系,再将同一个组的 Invoker 进行合并,并将合并结果赋值给 methodInvokerMap,这个 methodInvokerMap 就是上面 doList 中使用的那个 Map。
所以是在 refreshInvoker 的时候构造 methodInvokerMap,然后在调用的时候再读 methodInvokerMap,最后再销毁无用的 invoker。
StaticDirectory
StaticDirectory,这个是用在多注册中心的时候,它是一个静态目录,即固定的不会增减的,所有 Invoker 是通过构造器来传入。
可以简单的理解成在单注册中心下我们配置的一条 reference 可能对应有多个 provider,然后生成多个 invoker,我们将它们存入 RegistryDirectory 中进行管理,为了便于调用再对外只暴露出一个 invoker 来封装内部的多 invoker 情况。
那多个注册中心就会有多个已经封装好了的 invoker ,这又面临了选择了,于是我们用 StaticDirectory 再来存入这些 invoker 进行管理,也再封装起来对外只暴露出一个 invoker 便于调用。
之所以是静态的是因为多注册中心是写在配置里面的,不像服务可以动态变更。
StaticDirectory 的内部逻辑非常的简单,就是一个 list 存储了这些 invokers,然后实现父类的方法也就单纯的返回这个 list 不做任何操作。
@Override
protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
return invokers;
}
服务路由
服务路由其实就是路由规则,它规定了服务消费者可以调用哪些服务提供者,Dubbo 一共有三种路由分别是:条件路由 ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter。
条件路由
条件路由是两个条件组成的,是这么个格式 [服务消费者匹配条件] => [服务提供者匹配条件]
,举个例子官网的例子就是 host = 10.20.153.10 => host = 10.20.153.11
。该条规则表示 IP 为 10.20.153.10
的服务消费者只可调用 IP 为 10.20.153.11
机器上的服务,不可调用其他机器上的服务。
路由的配置一样是通过 RegistryDirectory 的 notify 更新和构造的,然后路由的调用在是刷新 invoker 的时候,具体是在调用 toMethodInvokers
的时候会进行服务级别的路由和方法级别的路由。
private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) {
..