dubbo 源码学习笔记 (六) —— 集群模块

来源:互联网 发布:大蚂蚁软件 编辑:程序博客网 时间:2024/05/16 10:33

欢迎访问我的个人博客休息的风

dubbo集群模块主要实现了集群容错和负载均衡功能。官网上有对此模块功能进行一个概述如下:


                                      

  1. 这里的 Invoker 是 Provider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息
  2. Directory 代表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
  3. Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
  4. Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等
  5. LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选

先来看下整体的类图情况如下图(看不清可在新标签页中查看)


主要关注实现Cluster、LoadBalance、Invoker这三个接口的图。这里首先介绍下框架有的集群容错模式及负载均衡策略。

集群容错的模式有以下六种:

  • Failover Cluster(默认模式)
失败自动切换,当出现失败,重试其它服务器 1。通常用于读操作,但重试会带来更长延迟
  • Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
  • Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
  • Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
  • Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源
  • Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错 2。通常用于通知所有提供者更新缓存或日志等本地资源信息。


负载均衡的策略有以下四种:

  • Random LoadBalance(默认模式)
随机,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
  • RoundRobin LoadBalance
轮循,按公约后的权重设置轮循比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
  • LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
  • ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。


本文将从Failover Cluster集群容错模式和Random LoadBalance负载均衡策略这两个默认的配置入手,分析源码,了解其整个实现过程。

这一调用的入口在RegistryProtocol.doRefer方法中,会去执行“cluster.join(directory)”这一代码。根据SPI机制获取默认的Failover Cluster模式,在这个模式里,同样也会默认采用Random LoadBalance策略。具体我们来看下源码。

public <T> Invoker<T> join(Directory<T> directory) throws RpcException {    return new FailoverClusterInvoker<T>(directory);}
FailoverCluster类中会去创建一个Invoker,具体的调用过程将在这个类的doInvoke中处理。过程定义在AbstractClusterInvoker父类中。

public Result invoke(final Invocation invocation) throws RpcException {    checkWhetherDestroyed();    LoadBalance loadbalance;    //RegistryDirectory处理通知响应(notify)时,将注册中心最新的列表接取下来,存放在静态变量缓存中    //这里的invokers列表,调用directory.list(invocation),从RegistryDirectory的methodInvokerMap中获取的    List<Invoker<T>> invokers = list(invocation);    //通过SPI机制获取loadbalance    if (invokers != null && invokers.size() > 0) {        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()                .getMethodParameter(invocation.getMethodName(), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));    } else {        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE);    }    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);    //再到子类中具体处理调用过程    return doInvoke(invocation, invokers, loadbalance);}
具体子类的调用过程,这里是FailoverClusterInvoker:

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {    List<Invoker<T>> copyinvokers = invokers;    checkInvokers(copyinvokers, invocation);    int len = getUrl().getMethodParameter(invocation.getMethodName(), 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++) {        //重试时,进行重新选择,避免重试时invoker列表已发生变化.        //注意:如果列表发生了变化,那么invoked判断会失效,因为invoker示例已经改变        if (i > 0) {            checkWhetherDestroyed();            copyinvokers = list(invocation);            //重新检查一下            checkInvokers(copyinvokers, invocation);        }        //使用loadbalance负载均衡选择哪个服务invoker去调用        Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);        invoked.add(invoker);        RpcContext.getContext().setInvokers((List) invoked);        try {            //调用执行调用链            Result result = invoker.invoke(invocation);            //省略一些代码。。。。。}
这个调用处理如果失败,默认会重试三次。AbstractClusterInvoker父类中对于选择哪个invoker定义的过程:

private Invoker<T> doselect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {    if (invokers == null || invokers.size() == 0)        return null;    if (invokers.size() == 1)        return invokers.get(0);    // 如果只有两个invoker,退化成轮循    if (invokers.size() == 2 && selected != null && selected.size() > 0) {        return selected.get(0) == invokers.get(0) ? invokers.get(1) : invokers.get(0);    }    //真正调用负载策略处理的入口    Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);    //如果 selected中包含(优先判断) 或者 不可用&&availablecheck=true 则重试.    if ((selected != null && selected.contains(invoker))            || (!invoker.isAvailable() && getUrl() != null && availablecheck)) {        try {            Invoker<T> rinvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);            if (rinvoker != null) {                invoker = rinvoker;            } else {                //看下第一次选的位置,如果不是最后,选+1位置.                int index = invokers.indexOf(invoker);                try {                    //最后在避免碰撞                    invoker = index < invokers.size() - 1 ? invokers.get(index + 1) : invoker;                //省略代码 。。。。    }    return invoker;}
如果只有两个invoker,会退化成轮循;否则从具体负载均衡选择invoker,选择后还需进行是否可用的判断,如果不可用再重新选择。在RandomLoadBalance处理具体选择哪个invoker如下:

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {    int length = invokers.size(); // 总个数    int totalWeight = 0; // 总权重    boolean sameWeight = true; // 权重是否都一样    for (int i = 0; i < length; i++) {        int weight = getWeight(invokers.get(i), invocation);        totalWeight += weight; // 累计总权重        if (sameWeight && i > 0                && weight != getWeight(invokers.get(i - 1), invocation)) {            sameWeight = false; // 计算所有权重是否一样        }    }    if (totalWeight > 0 && !sameWeight) {        // 如果权重不相同且权重大于0则按总权重数随机        int offset = random.nextInt(totalWeight);        // 并确定随机值落在哪个片断上        for (int i = 0; i < length; i++) {            offset -= getWeight(invokers.get(i), invocation);            if (offset < 0) {                return invokers.get(i);            }        }    }    // 如果权重相同或权重为0则均等随机    return invokers.get(random.nextInt(length));}
选完后如果这个invoker不可用,则需要得选,重选的代码在AbstractClusterInvoker父类中:

private Invoker<T> reselect(LoadBalance loadbalance, Invocation invocation,                            List<Invoker<T>> invokers, List<Invoker<T>> selected, boolean availablecheck)        throws RpcException {    //预先分配一个,这个列表是一定会用到的.    List<Invoker<T>> reselectInvokers = new ArrayList<Invoker<T>>(invokers.size() > 1 ? (invokers.size() - 1) : invokers.size());    //先从非select中选    if (availablecheck) { //选isAvailable 的非select        for (Invoker<T> invoker : invokers) {            if (invoker.isAvailable()) {                if (selected == null || !selected.contains(invoker)) {                    reselectInvokers.add(invoker);                }            }        }        if (reselectInvokers.size() > 0) {            return loadbalance.select(reselectInvokers, getUrl(), invocation);        }    } else { //选全部非select        for (Invoker<T> invoker : invokers) {            if (selected == null || !selected.contains(invoker)) {                reselectInvokers.add(invoker);            }        }        if (reselectInvokers.size() > 0) {            return loadbalance.select(reselectInvokers, getUrl(), invocation);        }    }    //最后从select中选可用的.     {        if (selected != null) {            for (Invoker<T> invoker : selected) {                if ((invoker.isAvailable()) //优先选available                        && !reselectInvokers.contains(invoker)) {                    reselectInvokers.add(invoker);                }            }        }        if (reselectInvokers.size() > 0) {            return loadbalance.select(reselectInvokers, getUrl(), invocation);        }    }    return null;}
至此,总结一下这个过程。首先从RegistryProtocol.doRefer的cluster.join为入口,开始进行集群容错。默认的容错模式为FailoverCluster。调用过程如果不成功,则重试三次;从RegistryDirectory中获取注册中心的invoker等信息,之后选择默认的负载均衡策略RandomLoadBalance进行invoker的选择。选择之后还需判断invoker是否可用,不可用还需重选。这样,一个大概的容错和负载均衡的过程就完成了。