Dubbo源码解析十二:集群容错

在这里插入图片描述

介绍

在实际生产环境中,为了保证服务稳定可靠的运行,我们需要将一个相同的服务部署多个实例。然而远程服务并不是每时每刻都能正常运行,当某个服务的调用出现异常,需要自动容错。

集群容错的流程如下图
在这里插入图片描述
Directory接口:服务目录接口,包含了每个服务的Invoker集合,并且这个集合是动态的。是后续集群容错,路由,负载均衡的基础
Cluster接口:集群容错的接口,当Consumer的调用某些Provider失败时,能将请求转发到那些正常的Provider节点上
Router接口:路由接口,按照用户指定的规则匹配出符合条件的Provider
LoadBalance接口:负载均衡接口,按照指定的负载均衡策略从Provider集合中选择一个来处理请求

Directory(服务目录)

在这里插入图片描述
Directory(服务目录)代表了多个Invoker(对于消费端来说,每个Invoker代表一个服务提供者)

RegistryDirectory:Invoker列表是根据注册中心的推送而进行变化的,实现了NotifyListener接口,当注册中心监听的路径发生变化的时候,会回调NotifyListener#notify方法,这样就能更新Invoker列表
StaticDirectory:当使用来多注册中心时,把所有注册中心的invoker列表汇集到一个invoker列表中

下面是服务引入过程中,订阅zookeeper服务,生成Invoker的过程。因为这个过程和本节相关性比较大,所以放到本节来分析
在这里插入图片描述
我们来分析一下Invoker的生成过程,顺便分析一下RegistryDirectory中的Invoker为什么能动态刷新

在这里插入图片描述
在这里插入图片描述
下图是ZookeeperRegistry#doSubscribe的实现,第一次订阅或者节点发生变化,都会执行ZookeeperRegistry#notify方法,这个方法会回调RegistryDirectory#notify方法,并更新缓存
在这里插入图片描述
RegistryDirectory#refreshOverrideAndInvoker(providerURLs)
方法就是根据providerURLs生成Invoker的过程

主要逻辑如下:

  1. 只有一个服务提供者,且协议为empty则会禁用改服务
  2. 根据providerURLs生成新的Invokers,如果某个providerURL的Invoker已经存在则不重新生成,否则生成Invoker
  3. 销毁旧的providerURLs生成的Invoker(被新的providerURL用了的Invoker不会销毁哈)

大概逻辑如下图
在这里插入图片描述

Cluster

在这里插入图片描述
在服务引入的过程中,Cluster会把多个Invoker合并,只暴露出一个Invoker让调用方使用

// RegistryProtocol#doRefer
Invoker invoker = cluster.join(directory);

Cluster包含了集群容错策略,最终使用的容错策略由Dubbo SPI来决定,默认是FailoverCluster

扫描二维码关注公众号,回复: 12271880 查看本文章
实现类 解释
AvailableCluster 找到一个可用的节点,直接发起调用
FailoverCluster 失败重试(默认)
FailfastCluster 快速失败
FailsafeCluster 安全失败
FailbackCluster 失败自动恢复
ForkingCluster 并行调用
BroadcastCluster 广播调用
@SPI(FailoverCluster.NAME)
public interface Cluster {
    
    

    @Adaptive
    <T> Invoker<T> join(Directory<T> directory) throws RpcException;

}
public class FailoverCluster implements Cluster {
    
    

    public final static String NAME = "failover";

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

}

FailoverCluster#join方法只是简单的返回了一个FailoverClusterInvoker,其他集群容错的策略和这个一样,都是返回对应的Invoker

在这里插入图片描述
所有的集群容错的Invoker都实现了AbstractClusterInvoker接口

AbstractClusterInvoker接口主要抽象了如下两部分的逻辑

  1. 根据路由配置过滤出符合条件的Invoker
  2. 初始化负载均衡策略,对List<Invoker> invokers进行负载均衡

分析一下FailoverClusterInvoker的实现,其他Invoker的实现和这个类似,有兴趣的可以看看

public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {
    
    

    @Override
    @SuppressWarnings({
    
    "unchecked", "rawtypes"})
    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    
    
        List<Invoker<T>> copyInvokers = invokers;
        checkInvokers(copyInvokers, invocation);
        String methodName = RpcUtils.getMethodName(invocation);
        // 获取重试次数
        int len = getUrl().getMethodParameter(methodName, Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
        if (len <= 0) {
    
    
            len = 1;
        }
        // retry loop.
        RpcException le = null; // last exception.
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
        Set<String> providers = new HashSet<String>(len);
        // 循环调用,失败重试
        for (int i = 0; i < len; i++) {
    
    
            //Reselect before retry to avoid a change of candidate `invokers`.
            //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
            if (i > 0) {
    
    
                // 当前实例已经被销毁,则抛出异常
                checkWhetherDestroyed();
                // 重新获取服务提供者
                copyInvokers = list(invocation);
                // check again
                // 重新检查一下
                checkInvokers(copyInvokers, invocation);
            }
            // 通过负载均衡选择 Invoker,已经调用过的不会再选择
            Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
            invoked.add(invoker);
            RpcContext.getContext().setInvokers((List) invoked);
            try {
    
    
                // 发起远程调用
                Result result = invoker.invoke(invocation);
                if (le != null && logger.isWarnEnabled()) {
    
    
                    logger.warn("Although retry the method " + methodName
                            + " in the service " + getInterface().getName()
                            + " was successful by the provider " + invoker.getUrl().getAddress()
                            + ", but there have been failed providers " + providers
                            + " (" + providers.size() + "/" + copyInvokers.size()
                            + ") from the registry " + directory.getUrl().getAddress()
                            + " on the consumer " + NetUtils.getLocalHost()
                            + " using the dubbo version " + Version.getVersion() + ". Last error is: "
                            + le.getMessage(), le);
                }
                return result;
            } catch (RpcException e) {
    
    
                // 业务类的异常直接跑出来
                if (e.isBiz()) {
    
     // biz exception.
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
    
    
                le = new RpcException(e.getMessage(), e);
            } finally {
    
    
                providers.add(invoker.getUrl().getAddress());
            }
        }
        throw new RpcException(le.getCode(), "Failed to invoke the method "
                + methodName + " in the service " + getInterface().getName()
                + ". Tried " + len + " times of the providers " + providers
                + " (" + providers.size() + "/" + copyInvokers.size()
                + ") from the registry " + directory.getUrl().getAddress()
                + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
                + Version.getVersion() + ". Last error is: "
                + le.getMessage(), le.getCause() != null ? le.getCause() : le);
    }

}

执行调用的时候,doInvoke方法会执行,其中invokers(路由过后的Invokers),loadbalance(负载均衡策略)都是由AbstractClusterInvoker根据配置决定的,doInvoke调用成功则直接返回,否则遍历invokers列表继续遍历,直到超过重试次数。如果此时还没有调用成功,则会抛出RpcException

Router(路由)

从Directory中根据调用信息找到的Invoker并不能直接拿来调用,需要经过路由规则过滤后的Invoker才直接发起调用

配置如下路由规则,表示ip为 172.22.3.1的服务调用方,只会调用ip为172.22.3.2的服务

host = 172.22.3.1 => host = 172.22.3.2

路由规则可以看官方文档:
http://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule/

路由分为如下三种

  1. 条件路由:使用Dubbo定义的语法规则去写路由规则
  2. 文件路由:框架从文件中读取路由规则
  3. 脚本路由:使用jdk自身的脚本解析引擎解析路由规则脚本

条件路由用的最多,简单介绍一下条件路由的实现

public class ConditionRouter extends AbstractRouter {
    
    

    protected Map<String, MatchPair> whenCondition;
    protected Map<String, MatchPair> thenCondition;

}

路由时主要和whenCondition和thenCondition打交道,在构造函数中会初始化这2个map

路由条件如下时,生成的2个map如下

host != 4.4.4.4 & host = 2.2.2.2,1.1.1.1,3.3.3.3 & method = sayHello => host = 1.2.3.4 & host != 4.4.4.4

在这里插入图片描述
在这里插入图片描述
下面就是执行路由逻辑的部分,matenWhen和matenThen的方法就是利用上面初始化好的whenCondition和thenCondition进行匹配的过程,不具体分析了,对实现感兴趣的话可以调试一下官方对这个类写的Test类(ctrl + shift + t 快捷键即可快速到达对应的Test类)

// ConditionRouter
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
        throws RpcException {
    
    
    // 不生效
    if (!enabled) {
    
    
        return invokers;
    }

    if (CollectionUtils.isEmpty(invokers)) {
    
    
        return invokers;
    }
    try {
    
    
        // 没有whenRule匹配,返回所有
        if (!matchWhen(url, invocation)) {
    
    
            return invokers;
        }
        List<Invoker<T>> result = new ArrayList<Invoker<T>>();
        // 没有thenRule,则表明服务消费者在黑名单中,返回空列表
        if (thenCondition == null) {
    
    
            logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
            return result;
        }
        for (Invoker<T> invoker : invokers) {
    
    
            // 匹配成功
            if (matchThen(invoker.getUrl(), url)) {
    
    
                result.add(invoker);
            }
        }
        if (!result.isEmpty()) {
    
    
            // result不为空,直接返回
            return result;
        } else if (force) {
    
    
            // result为空 force=true 强制返回空列表
            logger.warn("The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(Constants.RULE_KEY));
            return result;
        }
    } catch (Throwable t) {
    
    
        logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);
    }
    // result为空,force=false 返回所有Invoker列表
    return invokers;
}

LoadBalance(负载均衡)

经过路由规则过滤后的Invoker,如果只有一个,就可以直接发起调用了。如果有多个,此时就涉及到负载均衡策略了,Dubbo提供了如下策略,如果不能满足你的需求,你可以自定义LoadBalance接口的实现

实现类 解释
RandomLoadBalance 随机策略(默认)
RoundRobinLoadBalance 轮询策略
LeastActiveLoadBalance 最少活跃调用数
ConsistentHashLoadBalance 一致性hash策略

在这里插入图片描述
AbstractLoadBalance主要提供了一个方法getWeight,根据服务的启动时间,给服务定一个权重。

主要思路如下

我们来分析一下RandomLoadBalance

思路如下:
RandomLoadBalance 是加权随机算法的具体实现,它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字3会落到服务器 A 对应的区间上,此时返回服务器 A 即可。权重越大的机器,在坐标轴上对应的区间范围就越大,因此随机数生成器生成的数字就会有更大的概率落到此区间内。只要随机数生成器产生的随机数分布性很好,在经过多次选择后,每个服务器被选中的次数比例接近其权重比例。比如,经过一万次选择后,服务器 A 被选中的次数大约为5000次,服务器 B 被选中的次数约为3000次,服务器 C 被选中的次数约为2000次。

public class RandomLoadBalance extends AbstractLoadBalance {
    
    

    public static final String NAME = "random";

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    
    
        // Number of invokers
        int length = invokers.size();
        // Every invoker has the same weight?
        boolean sameWeight = true;
        // the weight of every invokers
        int[] weights = new int[length];
        // the first invoker's weight
        // 获取第一个服务的权重
        int firstWeight = getWeight(invokers.get(0), invocation);
        weights[0] = firstWeight;
        // The sum of weights
        int totalWeight = firstWeight;
        // 下面这个循环有两个作用
        // 1. 计算总权重
        // 2. 检测所有服务的权重是否相同
        for (int i = 1; i < length; i++) {
    
    
            int weight = getWeight(invokers.get(i), invocation);
            // save for later use
            weights[i] = weight;
            // Sum
            // 类加权重
            totalWeight += weight;
            if (sameWeight && weight != firstWeight) {
    
    
                sameWeight = false;
            }
        }
        // 下面的 if 分支主要用于获取随机数,并计算随机数落在哪个区间上
        if (totalWeight > 0 && !sameWeight) {
    
    
            // If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
            // 随机获取一个 [0, totalWeight) 区间内的数字
            int offset = ThreadLocalRandom.current().nextInt(totalWeight);
            // Return a invoker based on the random value.
            // 循环让 offset 数减去服务提供者权重值,当 offset 小于0时,返回相应的 Invoker。
            // 举例说明一下,我们有 servers = [A, B, C],weights = [5, 3, 2],offset = 7。
            // 第一次循环,offset - 5 = 2 > 0,即 offset > 5,
            // 表明其不会落在服务器 A 对应的区间上。
            // 第二次循环,offset - 3 = -1 < 0,即 5 < offset < 8,
            // 表明其会落在服务器 B 对应的区间上
            for (int i = 0; i < length; i++) {
    
    
                offset -= weights[i];
                if (offset < 0) {
    
    
                    return invokers.get(i);
                }
            }
        }
        // 如果所有服务提供者权重值相同,此时直接随机返回一个即可
        // If all invokers have the same weight value or totalWeight=0, return evenly.
        return invokers.get(ThreadLocalRandom.current().nextInt(length));
    }

}

其他的你可以看一下官网的分析,写的很清楚
http://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/

参考博客

服务目录
[0]http://dubbo.apache.org/zh/docs/v2.7/dev/source/directory/
负载均衡
[1]http://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/

猜你喜欢

转载自blog.csdn.net/zzti_erlie/article/details/109696511
今日推荐