美文网首页
限流系统稳定基石

限流系统稳定基石

作者: 左小星 | 来源:发表于2019-01-26 17:34 被阅读0次

为什么要限流

  秒杀时特别热卖的商品,会有很多人来抢(不排除黄牛),不进行限流,有可能把缓存和DB全部拖垮从而造成雪崩。
  微博上的热搜,那流量可想而知。
  比如业务方发布了一个活动,拼团支付一分钱,抽百万大奖,这是拼团支付流量很大,影响了其他业务的正常支付,这个时候需要对各个业务方做单独限流。
  业务方提出需求,用户在单位时间内访问某个页面的次数不能超过多少,这时也需要限流。
  限流应用场景很多,就不一一列举了,这些应用场景应该单独抽取出来,做成一个流控服务。

常见的限流有 : 单机限流,分布式限流,计数器限流。

单机限流

  单机限流的常见算法 漏桶算法、令牌桶算法、计数器算法

漏桶算法

  由图片可知,桶的漏出速率是一定的,请求达到桶的容量之后就会拒绝请求。漏桶算法有一定的缺点,就是当系统可以承受这么大的压力时,请求放过的量还是这么多,不能有效的利用系统资源。


漏桶算法

令牌桶算法

  如下图所示,请求过来以后先去桶里拿令牌,拿不到令牌之后则拒绝请求。令牌桶允许一定量的突发流量,请求空闲时预热一部分令牌,新请求无需等待


令牌桶算法

  guava 的RateLimiter默认就是令牌桶实现,提供了SmoothWarmingUp(预热限流),SmoothWarmingUp(突发限流)两种实现。
几个关键的成员变量

  // 当前桶里存放多少个令牌
  double storedPermits;
  // 最大可以存放多少令牌
  double maxPermits;
  // 获取一个令牌需要的时间,比如限流qps是5,那此值就是 1/5 = 200ms
  double stableIntervalMicros;
  // 记录最近一个可以获取令牌的时间。
  long nextFreeTicketMicros = 0L;
public double acquire(int permits) {
    // 拿 permits 个令牌要等待多长时间
    long microsToWait = reserve(permits);
    // 其实就是sleep
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }

接下来看一下reserve方法的实现

final long reserve(int permits) {
    checkPermits(permits);
    // mutex方法使用了double check,这在多线程编程里是很常见的
    synchronized (mutex()) {
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
  }

reserveAndGetWaitLength 调用了reserveEarliestAvailable方法,最主要的实现就是在这个方法里了。没看源码之前总以为是有一个后台线程在不断的往桶里面放令牌,其实是每次拿令牌的时候才去更新桶里的令牌数量,这个实现很精髓。

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
   // 这个方法的作用是,假如有很长时间都没有线程来拿令牌了,则更新令牌桶里的令牌数量
    resync(nowMicros);
    long returnValue = nextFreeTicketMicros;
    // 取要求拿几个令牌 和 现有存储令牌的 最小值
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    double freshPermits = requiredPermits - storedPermitsToSpend;
    // 计算等待时间,storedPermitsToWaitTime 在SmoothBursty实现里返回0
    // 当拿的令牌数量小于桶中剩余令牌数量时,等待时间为0。
    long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
        + (long) (freshPermits * stableIntervalMicros);
    // 更新剩余令牌数量和最近一个可用令牌的时间
    this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
  }

返回的这个returnValue 大于0的话,当前线程就sleep多长时间。

private void resync(long nowMicros) {
    // 当前时间大于最后一个令牌可用时间,说明有一段时间没有线程来拿令牌了
    if (nowMicros > nextFreeTicketMicros) {
      // 更新存储令牌的数量,计算方式如下
      storedPermits = min(maxPermits,
          storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros);
      nextFreeTicketMicros = nowMicros;
    }
  }

acquire方法介绍完了,接下来在看一下 tryAcquire 方法的实现。

public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
    long timeoutMicros = max(unit.toMicros(timeout), 0);
    checkPermits(permits);
    long microsToWait;
    // acquire 和 tryAcquire 方法在此处都是加锁线程安全的。
    synchronized (mutex()) {
      long nowMicros = stopwatch.readMicros();
      // 看能否获取锁
      if (!canAcquire(nowMicros, timeoutMicros)) {
        return false;
      } else {
        // 接下来就是调用 reserveEarliestAvailable方法,上面已经介绍过了
        microsToWait = reserveAndGetWaitLength(permits, nowMicros);
      }
    }
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return true;
  }
// timeoutMicros 参数就是最长等待多长时间
private boolean canAcquire(long nowMicros, long timeoutMicros) {
    // queryEarliestAvailable 返回的是 nextFreeTicketMicros
    // 最近一次可以获取令牌的时间 - 等待时间 < 当前时间
    return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
 }

guava的RateLimiter源码就大致解析到这里

计数器算法

这个实现比较简单,demo如下

// 设置1秒钟过期
LoadingCache<String, AtomicLong> cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<String, AtomicLong>() {
            @Override
            public AtomicLong load(String key) throws Exception {
                // load 方法内部加了 synchorized 代码块修饰,在高并发下key失效,也只有一个线程调用此方法去加载初始值放到cache,其余线程从cache获取
                return new AtomicLong(0);
            }
        });
 if (cache.get("key").incrementAndGet() < 限流大小) {
      // pass
  } else {
      // reject
  }

单机限流还可以根据 线程池,每个限流方法配置不同的线程池,做到很好的线程隔离。也可以使用信号量,hystrix实现。

分布式限流

原因

  现在服务部署都是集群方式,使用比如dubbo等RPC框架,还有使用nginx做反向代理,有可能流量请求到物理机上不均匀,还有就是机器的性能不相同,如果使用单机限流的话,性能好的物理机资源会大大的浪费。

应用

  • 支付服务是核心服务,会有多个业务方来调,这时候要多各个业务方做限流,防止由于某个业务方支付量激增时影响其他业务方支付。
  • 限制 百分之多少的用户请求时直接降级。
  • 限制用户一天访问多少次,等等。

实现

  • 分布式限流可以基于redis,对限流的key做incr操作,并设置过期值,当incr的值大于限流值之后,直接reject调。
  • 可以单独部署一个令牌服务,每次请求时客户端去令牌服务端拿对应的令牌,令牌服务做成HA高可用,这样就不强依赖redis。(阿里开源的sentinel分布式限流就是这么做的)
  • 也可以使用nginx做限流,nginx+lua脚本实现。

  下面简单画了一下基于redis的集群限流图片


redis限流

高效率与高可用

  • 集群限流强依赖于第三方(比如redis),当网络放生抖动时,会使一个请求的时间大大加长,这对于业务方来说是不可忍受的。可以对请求redis的时间做熔断降级(异步写redis),当redis不可用时,集群限流降级为单机限流(集群限流值/机器总数)。
  • 客户端也可以一次拿多个令牌,这样就不会频繁请求redis了。
  • 读取redis的从库,写入主库,这样有可能存在主从延时导致多放请求进来。

相关文章

  • 限流系统稳定基石

    为什么要限流   秒杀时特别热卖的商品,会有很多人来抢(不排除黄牛),不进行限流,有可能把缓存和DB全部拖垮从而造...

  • 分布式限流

    RateLimiter 限流普及 限流是对出入流量进行控制 , 防止大量流入,导致资源不足,系统不稳定。限流系统是...

  • 经典面试题——让你设计一个限流的系统怎么做?

    保障服务稳定的三大利器:熔断降级、服务限流和故障模拟。限流系统是当前很多系统都需要考虑的场景。首先在Nginx层面...

  • 限流总结

    限流是对系统的出入流量进行控制,防止大流量出入,导致资源不足,系统不稳定。 常见限流方式 1、限制瞬时并发数2、限...

  • 微服务架构下的分布式限流方案思考

    1.微服务限流 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。缓存、降级和限流是保护微服务系统运行稳定性...

  • 日志那些事儿——日志Logger漫谈

    前言 最近在关注限流、降级、监控等系统稳定性方面的技术,反复牵涉到的几个技术名词是日志log,Aop切片。 限流降...

  • Spring Cloud Gateway -- 熔断限流

    微服务系统中熔断限流环节,对保护系统的稳定性起到了很大的作用,作为网关,Spring Cloud Gateway也...

  • 秒杀系统之二:接口限流(令牌桶和漏斗算法)

    3. 接口限流 限流:是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行...

  • 系统设计:关于高可用系统的一些技术方案

    [TOC] 系统设计:关于高可用系统的一些技术方案 可靠的系统是业务稳定、快速发展的基石。那么,如何做到系统高可靠...

  • 聊聊高并发系统限流特技-2

    转载来自开涛的聊聊高并发系统限流特技-2 上一篇《聊聊高并发系统限流特技-1》讲了限流算法、应用级限流、分布式限流...

网友评论

      本文标题:限流系统稳定基石

      本文链接:https://www.haomeiwen.com/subject/sruijqtx.html