Dubbo的集群容错与负载均衡策略及自定义

Dubbo的集群容错策略spring

正常状况下,当咱们进行系统设计时候,不只要考虑正常逻辑下代码该如何走,还要考虑异常状况下代码逻辑应该怎么走。当服务消费方调用服务提供方的服务出现错误时候,Dubbo提供了多种容错方案,缺省模式为failover,也就是失败重试。缓存

Dubbo提供的集群容错模式安全

下面看下Dubbo提供的集群容错模式:服务器

Failover Cluster:失败重试并发

当服务消费方调用服务提供者失败后自动切换到其余服务提供者服务器进行重试。这一般用于读操做或者具备幂等的写操做,须要注意的是重试会带来更长延迟。可经过 retries="2" 来设置重试次数(不含第一次)。app

接口级别配置重试次数方法 <dubbo:reference retries="2" /> ,如上配置当服务消费方调用服务失败后,会再重试两次,也就是说最多会作三次调用,这里的配置对该接口的全部方法生效。固然你也能够针对某个方法配置重试次数以下:负载均衡

 

<dubbo:reference>
    <dubbo:method name="sayHello" retries="2" />
</dubbo:reference>

 

Failfast Cluster:快速失败dom

当服务消费方调用服务提供者失败后,当即报错,也就是只调用一次。一般这种模式用于非幂等性的写操做。ide

Failsafe Cluster:失败安全ui

当服务消费者调用服务出现异常时,直接忽略异常。这种模式一般用于写入审计日志等操做。

Failback Cluster:失败自动恢复

当服务消费端用服务出现异常后,在后台记录失败的请求,并按照必定的策略后期再进行重试。这种模式一般用于消息通知操做。

Forking Cluster:并行调用

当消费方调用一个接口方法后,Dubbo Client会并行调用多个服务提供者的服务,只要一个成功即返回。这种模式一般用于实时性要求较高的读操做,但须要浪费更多服务资源。可经过 forks="2" 来设置最大并行数。

Broadcast Cluster:广播调用

当消费者调用一个接口方法后,Dubbo Client会逐个调用全部服务提供者,任意一台调用异常则此次调用就标志失败。这种模式一般用于通知全部提供者更新缓存或日志等本地资源信息。

如上,Dubbo自己提供了丰富的集群容错模式,可是若是您有定制化需求,能够根据Dubbo提供的扩展接口Cluster进行定制。在后面的消费方启动流程章节会讲解什么时候/如何使用的集群容错。

失败重试策略实现分析

Dubbo中具体实现失败重试的是FailoverClusterInvoker类,这里咱们看下具体实现,主要看下doInvoke代码:

复制代码
public Result doInvoke(Invocation invocation,final List<Invoker<T>> invokers,LoadBalance loadbalance) throws RpcException{
    // (1) 全部服务提供者
    List<Invoker<T>> copyinvokers = invokers;
    checkInvokers(copyinvokers,invocation);

    // (2)获取重试次数
    int len  = getUrl().getMethodParameter(invocation.getMethodName(),Constants.RETRIES_KEY,Constants.DEFAULT_RETRIES) + 1;
    if(len <= 0){
        len = 1;
    }
    
    // (3)使用循环,失败重试
    RpcException le = null;    // last exception
    List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size());
    Set<String> providers = new HashSet<String>();
    for(int i=0;i<len;i++){
        // 重试时,进行从新选择,避免重试时invoker列表已发生变化
        // 注意:若是列表发生了变化,那么invoked判断会失效,由于invoker示例已经改变
        if(i > 0){
            // (3.1)
            checkWhetherDestroyed();    // 若是当前实例已经被销毁,则抛出异常
            // (3.2) 从新获取全部服务提供者
            copyinvokers = list(invocation);
            // (3.3) 从新检查一下
            checkInvokers(copyinvokers,invocation);
        }
        // (3.4) 选择负载均衡策略
        Invoker<T> invoker = select(loadbalance,invocation,copyinvokers,invoked);
        invoked.add(invoker);
        RpcContext.getContext().setInvokers((List)invoked);
        // (3.5) 具体发起远程调用
        try{
            Result result = invoker.invoke(invocation);
            if(le != null && logger.isWarnEnabled()){
                ...
            }
            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("抛出异常...");
}
复制代码
  • 如上代码(2)从url参数里面获取设置的重试次数,若是用户没有设置则取默认的值,默认是重试2,这里须要注意的是代码(2)是获取配置重试次数又+1了。这说明 总共调用次数=重试次数+1 (1是正常调用)。
  • 代码(3)循环重复试用,若是第一次调用成功则直接跳出循环返回,不然循环重试。第一次调用时不会走代码(3.1)(3.2)(3.3)。若是第一次调用出现异常,则会循环,这时候i=1,因此会执行代码(3.1)检查是否有线程调用了当前ReferenceConfig的destroy()方法,销毁了当前消费者。若是当前消费者实例已经被消费,那么重试就没有意义了,因此会抛出RpcException异常。
  • 若是当前消费者实例没被销毁,则执行代码(3.2)从新获取当前服务提供者列表,这是由于从第一次调开始到线程可能提供者列表已经变化了,获取列表后,而后执行(3.2)又一次进行了校验。校验经过则执行(3.4),根据负载均衡策略选择一个服务提供者,再次尝试调用。负载均衡策略的选择下节会讲解。

 Dubbo的负载均衡策略

当服务提供方是集群的时候,为了不大量请求一直落到一个或几个服务提供方机器上,从而使这些机器负载很高,甚至打死,须要作必定的负载均衡策略。Dubbo提供了多种均衡策略,缺省为random,也就是每次随机调用一台服务提供者的机器。

Dubbo提供的负载均衡策略

  • Random LoadBalance:随机策略。按照几率设置权重,比较均匀,而且能够动态调节提供者的权重。
  • RoundRobin LoadBalance:轮询策略。轮询,按公约后的权重设置轮询比率。会存在执行比较慢的服务提供者堆积请求的状况,好比一个机器执行的很是慢,可是机器没有挂调用(若是挂了,那么当前机器会从Zookeeper的服务列表删除),当不少新的请求到达该机器后,因为以前的请求尚未处理完毕,会致使新的请求被堆积,长此以往,全部消费者调用这台机器上的请求都被阻塞。
  • LeastActive LoadBalance:最少活跃调用数。若是每一个提供者的活跃数相同,则随机选择一个。在每一个服务提供者里面维护者一个活跃数计数器,用来记录当前同时处理请求的个数,也就是并发处理任务的个数。因此若是这个值越小说明当前服务提供者处理的速度很快或者当前机器的负载比较低,因此路由选择时候就选择该活跃度最小的机器。若是一个服务提供者处理速度很慢,因为堆积,那么同时处理的请求就比较多,也就是活跃调用数目越大,这也使得慢的提供者收到更少请求,由于越慢的提供者的活跃度愈来愈大。
  • ConsistentHash LoadBalance:一致性Hash策略。一致性Hash,能够保证相同参数的请求老是发到同一提供者,当某一台提供者挂了时,本来发往该提供者的请求,基于虚拟节点,平摊到其余提供者,不会引发剧烈变更。

在spring boot dubbo中,它能够经过参数spring.dubbo.reference.loadbalance=consistentHash启用,可是一致性哈希有一个并非很合理的地方,它是将相同参数的请求发送到相同提供者,默认为第一个参数。若是全部接口的第一个参数为公共入参还好,若是是采用泛型或继承设计,这就比较悲剧了,咱们就遇到这个问题。分析源码,可知它是调用com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector#select生成的,以下:

        public Invoker<T> select(Invocation invocation) {
            String key = toKey(invocation.getArguments());
            byte[] digest = md5(key);
            Invoker<T> invoker = sekectForKey(hash(digest, 0));
            return invoker;
        }

        private String toKey(Object[] args) {
            StringBuilder buf = new StringBuilder();
            for (int i : argumentIndex) {
                if (i >= 0 && i < args.length) {
                    buf.append(args[i]);
                }
            }
            return buf.toString();
        }

因此,咱们彻底能够经过将toKey暴露给业务自定义实现,以下:

        public Invoker<T> select(Invocation invocation) {
            String key = toKey(invocation.getArguments());

            // zjhua 自定义一致性哈希判断用的key,用于指定请求发送到特定节点
            if (rpcLoadBalance == null) {
                rpcLoadBalance = SpringContextHolder.getBean(RpcLoadBalance.class);
            }

            if (rpcLoadBalance != null) {
                key = rpcLoadBalance.toKey(invocation,invocation.getArguments());
            }
            byte[] digest = md5(key);
            Invoker<T> invoker = sekectForKey(hash(digest, 0));
            return invoker;
        }

这样就能够根据特定接口/参数值业务本身决定是否须要将请求发送到相同节点了。

相关文章
相关标签/搜索