1.限流

为什么要限流?

正常来说,一个员工A他每天能够处理的工作是10个,突然某一天来了100个工作量,这时候,如果员工A还处理100个,只有一种可能,这个员工被压垮。

如果我们能预先知道会有100个任务会来,我们通过增加员工数或定义消息队列等等来临时解决。

但是我们很多时候无法预料这些意外的。根据墨菲定律,坏事往往会接踵而来,有可能某个点挂了会引起全局的挂掉(雪崩)。因此我们不得不对我们的系统做一些保护措施。限流是其中之一。

针对秒杀这类场景,我们也可以做一些限流措施,而不影响到系统全局

思路:限速,我们可能第一个想到的应该是,我通过一个计数器,进行计数,如果超过了计数器阀值,表示速度太快了。一秒一个计数器

这样有个问题就是:粒度太大了,不均匀,针对1秒以下的,没法辨析。

我们能不能把粒度拆细了,1秒拆成10个100毫秒。每一个100毫秒有一个计数器。了解TCP/IP的应该知道,TCP/IP为了增加传输速度和控制传输速度,有个叫“滑动窗口协议”。

就算拆得再细,也无法解决匀速限制速度的问题。

           而且还有个临界点问题,假如,一秒限制10个请求,在第1秒和第2秒之间,第1秒后半段时间10个请求,第2秒前半段10个请求,那第1秒后半段+第2秒前半段时间组成的一秒钟里就有20个请求,没有起到限速的作用

在生活中,如果一桶有一个细眼,我们往里面装水,可以看到水是一滴一滴匀速的下落的,哪我们能不能通过程序来实现这种方式呢。

思路:桶为容器,一滴水为一请求。如果桶满了就拒绝请求,没满处理请求

限流熔断属于微服务的什么功能 什么是限流式熔断器_缓存

         因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率。

         对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

         什么意思呢?就是说我服务前面闲了很久,突然来了很多请求(在桶的容量内),我得快速的把这些处理了。

思路:匀速的产生令牌,往桶里面丢,每次请求来,看是否有多余的令牌。如果有获取令牌执行正常业务,偌没有限速

限流熔断属于微服务的什么功能 什么是限流式熔断器_限流熔断属于微服务的什么功能_02

 

简单的基于GUAVA的令牌桶的使用:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import GUAVA.concurrent.BaseRateLimiter;

/**
 * 有很多任务,但希望每秒不超过N个
 *
 * @data 2019年5月22日 下午2:40:31
 * @author ztq
 **/
public class BaseRateLimiterTest {
 public static void main(String[] args) {
  // 2代表1秒最多多少个
  BaseRateLimiter rateLimiter = BaseRateLimiter.create(2);

  List<Runnable> tasks = new ArrayList<>(10);
  for (int i = 0; i < 9; i++) {
   tasks.add(new UserRequest(i));
  }
  // 建立线程池
  ExecutorService threadPool = Executors.newCachedThreadPool();

  for (Runnable runnable : tasks) {
   // rateLimiter.acquire()该方法会阻塞线程,直到令牌桶中能取到令牌为止才继续向下执行,并返回等待的时间。
   System.out.println("等待时间: " + rateLimiter.acquire());
   // 开始执行
   threadPool.execute(runnable);
  }

  for (Runnable runnable : tasks) {
   // 判断能否在500毫秒内得到令牌,如果不能则立即返回false,不会阻塞程序
   System.out.println("超时是否获取令牌: " + (rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS) ? "成功" : "失败"));
   // 开始执行
   threadPool.execute(runnable);
  }
 }

 // 执行的任务
 private static class UserRequest implements Runnable {
  private int id;

  public UserRequest(int id) {
   this.id = id;
  }

  public void run() {
   System.out.println(id);
  }
 }
}

比如这样一个需求,我作为客户端要向kafka生产数据,而kafka的消费者则源源不断的消费数据,并将消费的数据全部请求到web服务器。当业务量巨大的时候,每秒可能有上万条数据产生,如果生产者直接生产数据的话极有可能把web服务器拖垮

读取本地文件,获得数据,将数据放入到队列中,以备调用。读取文件的舒服非常之快。每秒大概可以读取1g的数据。如果直接将数据全部塞到队列中。会造成内存溢出等问题

实现QPS速率的最简单的方式就是记住上一次请求的最后授权时间,然后保证1/QPS秒内不允许请求进入.比如QPS=5,如果我们保证最后一个被授权请求之后的200ms的时间内没有请求被授权,那么我们就达到了预期的速率.如果一个请求现在过来但是最后一个被授权请求是在100ms之前,那么我们就要求当前这个请求等待100ms.按照这个思路,请求15个新令牌(许可证)就需要3秒.

 

对一个每秒产生一个令牌的RateLimiter,每有一个没有使用令牌的一秒,我们就将storedPermits加1,如果RateLimiter在10秒都没有使用,则storedPermits变成10.0.这个时候,一个请求到来并请求三个令牌(acquire(3)),我们将从storedPermits中的令牌为其服务,storedPermits变为7.0.这个请求之后立马又有一个请求到来并请求10个令牌,我们将从storedPermits剩余的7个令牌给这个请求,剩下还需要三个令牌,我们将从RateLimiter新产生的令牌中获取.我们已经知道,RateLimiter每秒新产生1个令牌,就是说上面这个请求还需要的3个请求就要求其等待3秒.

        想象一个RateLimiter每秒产生一个令牌,现在完全没有使用(处于初始状态),限制一个昂贵的请求acquire(100)过来.如果我们选择让这个请求等待100秒再允许其执行,这显然很荒谬.我们为什么什么也不做而只是傻傻的等待100秒,一个更好的做法是允许这个请求立即执行(和acquire(1)没有区别),然后将随后到来的请求推迟到正确的时间点.这种策略,我们允许这个昂贵的任务立即执行,并将随后到来的请求推迟100秒.这种策略就是让任务的执行和等待同时进行.

所以,允许先消费后付款。意思是它可以来一个请求一次性取走几个或者剩下所有的令牌。甚至更多,但是后面的请求就得为上一次请求买单,它需要等待桶中的令牌补齐之后才能继续获取令牌。

/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
* 当这个RateLimiter使用不足(即请求到来速度小于permitsPerSecond),会囤积最多permitsPerSecond个请求
*/
public static RateLimiter create(double permitsPerSecond);
/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 还包含一个热身期(warmup period),热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率
* 同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态
*
* 设计这个的意图是为了满足那种资源提供方需要热身时间,而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
* 参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间
*/
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit);

 

限流熔断属于微服务的什么功能 什么是限流式熔断器_服务提供者_03

 

 

2.熔断

起因:

服务雪崩效应:是一种因 服务提供者 的不可用导致 服务调用者 的不可用,并将不可用 逐渐放大 的过程。

            如:A为服务提供者, B为A的服务调用者, CDB的服务调用者. 当A的不可用,引起B的不可用,并将不可用逐渐放大CD时, 服务雪崩就形成了

               

限流熔断属于微服务的什么功能 什么是限流式熔断器_限流熔断属于微服务的什么功能_04

            1、当A处理变的很慢时,B发送的请求就会变成无效请求,每次都会等返回结果。等来的却是超时结果。

            2、A的问题积攒,就会导致B处理效率变慢,同理C和D也会变的不可用。问题越来越大,所有的服务不可用。

雪崩原因:

   

产生的因素:

产生原因

解决方法 

硬件原因

1、硬件损坏造成的服务器主机宕机
2、网络硬件故障造成的服务提供者的不可访问

 

资源耗尽

同步等待造成的资源耗尽;使用 同步调用 时,会产生大量的等待线程占用系统资源

一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态,造成服务雪崩效应产生

1、资源隔离:主要是对调用服务的线程池进行隔离.
2、对依赖服务进行分类
    依赖服务分为: 强依赖和若依赖. 强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不会导致当前业务的中止.
3、不可用服务的调用快速失败
    一般通过 超时机制, 熔断器 和熔断后的 降级方法 来实现

流量原因

1、用户大量请求:在秒杀、大和节假日,如果准备不充分,用户发起大量请求造成服务提供者的不可用

 

2、用户重试:

3、代码逻辑重试:

1、用户交互限流

     采用加载动画,提高用户的忍耐等待时间.

     提交按钮添加强制等待时间机制

网关限流

 3、关闭重试

 4、服务自动扩容

代码逻辑

程序Bug

 

缓存击穿

缓存击穿:缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时:

大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用 

1、缓存预加载

2、同步改为异步刷新

 解决方案:

    1) 使用Hystrix预防服务雪崩

    2) Netflix的  Hystrix  是一个帮助解决分布式系统交互时超时处理和容错的类库, 它同样拥有保护系统的能力

    3) Hystrix的设计原则包括:资源隔离、熔断器、命令模式

    介绍:https://github.com/Netflix/Hystrix/wiki/Configuration

              

简单的熔断器的应用:

1、首先引入hystrix-core的依赖:

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
    <version>1.5.12</version>
</dependency>

 

2、创建熔断器的类,并且继承HystrixCommand类,用这个类的实例化对象去访问服务提供者。

public class DelayCommand extends HystrixCommand<Code> {
    private Envelope envelope;
    private String Id;
    private String defId;

    private static HystrixThreadPoolProperties.Setter newPropertes = HystrixThreadPoolProperties.Setter()// 创建setter
            .withMaxQueueSize(Constant.EOT_QUEUE_CAPACITY)// 设置队列长度
            .withCoreSize(Constant.EOT_THREAD_POOL_CORE * 4)// 设置核心线程数量
            .withQueueSizeRejectionThreshold(Constant.EOT_QUEUE_CAPACITY)
            .withMaximumSize(Constant.EOT_THREAD_POOL_CORE * 32);// 设置最大线程数量

    private static HystrixCommandProperties.Setter CommandProperties = HystrixCommandProperties.Setter()// 创建setter
            .withCircuitBreakerSleepWindowInMilliseconds(Constant.HYSTRIX_BREAKER_MILLISECOND)// 设置熔断器打开时间
            .withExecutionTimeoutInMilliseconds(Constant.HYSTRIX_TIMEOUT_MILLISECOND)// 设置run超时时间
            .withCircuitBreakerErrorThresholdPercentage(50);

    public DelayCommand(Envelope envelope, String flowId, String defId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(envelope.getDefCode() + "_regist"))// 域名作为GroupKey,必须有的参数
                .andCommandKey(HystrixCommandKey.Factory.asKey(envelope.getDefCode() + "_regist"))// itype作为CommandKey
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(envelope.getDefCode() + "_regist"))// itype作为线程池名称
                .andThreadPoolPropertiesDefaults(newPropertes).andCommandPropertiesDefaults(CommandProperties));
        this.envelope = envelope;
        this.flowId = Id;
        this.defId = defId;
    }

    // 调用服务提供者的服务,及我们利用熔断器去发送Http调用其服务

    @Override
    protected EotCode run() throws Exception {
        EotCode result = HttpSyncTransmitter.getInstance().post("http://localhost:8080/xxx",
                envelope, Id, defId);
        return result;

    }

    @Override
    protected EotCode getFallback() {
        // fallcallBack为空则检查当前熔断状态,打开则生成熔断拒绝报文;
        if (this.circuitBreaker.isOpen()) {
            // 熔断器开启
            LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=Hystrix熔断器启动,请求拒绝", null);
            return EotCode.ERROR_HYTRIX_OPEN;
        } else if (this.isResponseThreadPoolRejected()) {
            // 线程池拒绝,队列已满
            LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=Hystrix服务拒绝,请求过多", null);
            return EotCode.ERROR_HYTRIX_QUEUE_FULL;
        } else if (this.isResponseTimedOut()) {
            // 发生超时
            LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=Hystrix等待响应超时", null);
            return EotCode.ERROR_HYTRIX_TIMEOUT;
        } else if (this.isFailedExecution()) {
            // run方法执行失败
            Throwable e = this.getFailedExecutionException();
            if (e == null) {
                // 执行失败,未获取到异常
                LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=无异常失败", null);
            } else {
                // 执行失败,发现异常
                LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=" + ThrowUtil.cause(e), null);
            }
            return EotCode.ERROR_HYTRIX_EXECUTE_FAIL;
        } else {
            // 未知异常
            LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=未知原因", null);
            return EotCode.ERROR_HYTRIX_UNKNOWN_EXCEPTION;
        }
    }

}