常见的限流实现

常见的限流算法

1、计数器(固定窗口)算法

计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。

此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松实现。

对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题,如下图:

假设1min内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。

2、滑动窗口算法

限流中的滑动窗口可以简单理解为,设定的单位时间就是一个窗口,窗口可以分割多个更小的时间单元,随着时间的推移,窗口会向右移动。比如一个接口一分钟限制调用1000次,1分钟就可以理解为一个窗口,可以把1分钟分割为4个单元格,每个单元格就是15秒。每个时间片段都有独立的计数器,我们在计算整个时间窗口内的请求总数时会累加所有的时间片段内的计数器。时间窗口划分的越细,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

可以把这些小单元看作一个环,可以用一个数组存储,比如:A[0]、A[1]、A[2]、A[3],用一个变量k表示当前窗口,再用一个变量t表示当前窗口的开始时刻,数组的每个元素保存一个计数器,初始时,k=0,t等于当前时刻,因为滑动窗口是1分钟,所以每个小窗口间隔是15秒,收到请求时,先找当前窗口,n=(当前时间 - t)/15作为当前窗口往后多少个窗口,假如n=0,则请求还是落在当前窗口,然后累加4个窗口的计数器,如果小于限流预设值,则可以进去,当前窗口的计数器加1,如果窗口发生了移动,也就是n不等于0,则当前窗口往后直到新当前窗口的计数器要清零,比如n=1,则新当前窗口k=k+1,也就是1,则下标为1的窗口的计数器要清零,然后再累加四个窗口的计数器判断是否能进去,同理,如果n=2,则下标为1和2的窗口的计数器都要清零,如果(当前时间-t)大于1分钟,也就是当前窗口一次性移动了4格或以上,此时所有窗口计数器都都要清零。假如限流为100,A[0]=60,A[1]=20,A[2]=10,当前窗口k=3,则当前窗口的时间范围内,只能再接收10个请求,超过10则限流,直到下个时间窗口也就是k=0时才能重新接收请求,因为A[1]=20,A[2]=10,A[3]=10,所以k=0时,最多只能接收60个请求,可以看出,不会出现固定窗口算法中的临界问题。

3、漏桶算法

漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。

4、令牌桶算法

令牌桶算法生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的,当桶中无令牌可用时,则请求丢弃,这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌。

RateLimiter是基于令牌桶实现的开源工具,下面是RateLimiter的原理及使用。

Guava 提供了两种 RateLimiter 的实现:

  1. SmoothBursty
  2. SmoothWarmingUp

1)RateLimiter对于持续生成令牌,采用的不是定时任务的方式(过于耗费资源,不适合高并发),而是使用延迟计算的方式,即在获取令牌时计算上一次时间nextFreeTicketMicros和当前时间之间的差值,计算这段时间之内按照用户设定的速率可以生产多少令牌;

void resync(long nowMicros) {
    // if nextFreeTicket is in the past, resync to now
    if (nowMicros > nextFreeTicketMicros) {
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();//能够生产多少令牌
      storedPermits = min(maxPermits, storedPermits + newPermits);
      nextFreeTicketMicros = nowMicros;//修改时间
    }
}
  1. storedPermits 当前许可的数量
  2. maxPermits 许可数量的最大值
  3. nextFreeTicketMicros 用于标记下次请求获得许可的时间,其更新的逻辑在 reserveEarliestAvailable中

2)RateLimiter计算延迟时间,即过度消费部分需要由下一次调用买单,因此其具有处理突发流量的能力

reserveEarliestAvailable是一个抽象方法,其实现逻辑在 SmoothRateLimiter 中如下:

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
  resync(nowMicros);//调用resync方法生成令牌
  long returnValue = nextFreeTicketMicros; // 返回的是上次计算的nextFreeTicketMicros
  double storedPermitsToSpend = min(requiredPermits, this.storedPermits); // 可以消费的最大令牌数
  double freshPermits = requiredPermits - storedPermitsToSpend; // 还需要的令牌数(若需要的大于已有的令牌,则下次请求需要延时等待;否则此值为0,即不需要等待)
  long waitMicros =
      storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
          + (long) (freshPermits * stableIntervalMicros); // SmoothBursty为0(SmoothWarmingUp具体计算)+需要等待时间
 
  this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); // 计算此次用了这么多令牌后,下次需要等待到什么时候
  this.storedPermits -= storedPermitsToSpend;//减少消耗掉的令牌
  return returnValue;
}

stableIntervalMicros 两次请求之间的时间差。例如每秒5个许可,那么该值为200ms。

如果每次都只取一个令牌,则requiredPermits=1,则只要可消费令牌数storedPermitsToSpend>=1即可,否则等待一个令牌。

在 SmoothBursty 中,storedPermitsToWaitTime 为 0,也就是说对于已经生成的许可,不需要再等待。

3)父类tryAcquire 方法,对于延迟,RateLimiter使用StopWatch实现不可中断挂起

  public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
    long timeoutMicros = max(unit.toMicros(timeout), 0);
    checkPermits(permits);
    long microsToWait;
    synchronized (mutex()) {
      long nowMicros = stopwatch.readMicros(); // 获取当前时间
      if (!canAcquire(nowMicros, timeoutMicros)) { // 尝试获取许可
        return false;
      } else {
        microsToWait = reserveAndGetWaitLength(permits, nowMicros); // 计算等待许可的时间
      }
    }
    stopwatch.sleepMicrosUninterruptibly(microsToWait); // 等待许可
    return true;
  }

  private boolean canAcquire(long nowMicros, long timeoutMicros) {
    return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
  }

可见当 timeout 为 0 时, microsToWait 总是为 0,所以不会造成阻塞。

queryEarliestAvailable 用于计算最早可以获取许可的时间点,如果该时间点大于 timeout,那么返回 false,也就是无法获得许可。queryEarliestAvailable 是抽象方法,在 SmoothRateLimiter 中也就是nextFreeTicketMicros:

  @Override
  final long queryEarliestAvailable(long nowMicros) {
    return nextFreeTicketMicros;
  }

reserveAndGetWaitLength返回获得令牌需要等待多长时间,即nextFreeTicketMicros(下次请求获得许可的时间)减去当前时间,如果小于零则无需等待。从resync方法看到,如果当前时间大于nextFreeTicketMicros,则nextFreeTicketMicros=当前时间,而reserveEarliestAvailable返回的是nextFreeTicketMicros,reserveAndGetWaitLength返回0,即无需等待,如果当前时间小于nextFreeTicketMicros,resync方法不会起作用,通过reserveEarliestAvailable方法看到,会重新计算nextFreeTicketMicros=nextFreeTicketMicros+申请的令牌数*令牌间隔时间,虽然reserveEarliestAvailable返回的是重新计算之前的nextFreeTicketMicros,但原来的nextFreeTicketMicros是比当前时间大的,所以还是会等待,但等待的时间并不是下一个令牌生成的时间,而是提前消费一个未生成的令牌,但并不是一定能提前消费令牌的,从canAcquire方法可知,如果timeout=0,则nextFreeTicketMicros大于当前时间时即返回false,而不会等待,而且等待的时间(nextFreeTicketMicros-当前时间)不能比超时时间大。

  final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    return max(momentAvailable - nowMicros, 0);
  }

4)剩下的就是3个细节了:

maxPermits计算:

SmoothBursty 中代码如下:

    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      double oldMaxPermits = this.maxPermits;
      maxPermits = maxBurstSeconds * permitsPerSecond;
      if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        // if we don't special-case this, we would get storedPermits == NaN, below
        storedPermits = maxPermits;
      } else {
        storedPermits =
            (oldMaxPermits == 0.0)
                ? 0.0 // initial state
                : storedPermits * maxPermits / oldMaxPermits;
      }
    }

maxBurstSeconds 默认为 1.0,所以 SmoothBursty 最大突发 permitsPerSecond 个许可

在 SmoothWarmingUp 中实现如下:

    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      double oldMaxPermits = maxPermits;
      double coldIntervalMicros = stableIntervalMicros * coldFactor;
      thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;
      maxPermits =
          thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);
      slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);
      if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        // if we don't special-case this, we would get storedPermits == NaN, below
        storedPermits = 0.0;
      } else {
        storedPermits =
            (oldMaxPermits == 0.0)
                ? maxPermits // initial state is cold
                : storedPermits * maxPermits / oldMaxPermits;
      }
    }

coolDownIntervalMicros

在 SmoothBursty 中,coolDownIntervalMicros 就是 stableIntervalMicros,也就是说匀速生成许可。

在 SmoothWarmingUp 中实现如下:

    @Override
    double coolDownIntervalMicros() {
      return warmupPeriodMicros / maxPermits;
    }

storedPermitsToWaitTime

在 SmoothBursty 中,storedPermitsToWaitTime 为 0,也就是说对于已经生成的许可,不需要再等待。

在 SmoothWarmingUp 中实现如下:

    @Override
    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
      double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
      long micros = 0;
      // measuring the integral on the right part of the function (the climbing line)
      if (availablePermitsAboveThreshold > 0.0) {
        double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
        // TODO(cpovirk): Figure out a good name for this variable.
        double length =
            permitsToTime(availablePermitsAboveThreshold)
                + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);
        micros = (long) (permitsAboveThresholdToTake * length / 2.0);
        permitsToTake -= permitsAboveThresholdToTake;
      }
      // measuring the integral on the left part of the function (the horizontal line)
      micros += (long) (stableIntervalMicros * permitsToTake);
      return micros;
    }

    private double permitsToTime(double permits) {
      return stableIntervalMicros + permits * slope;
    }

总的来说,SmoothBursty 比较容易理解,而 SmoothWarmingUp 添加了预热机制,代码不太直观,可以配合下图来理解代码。

  /**
   *          ^ throttling
   *          |
   *    cold  +                  /
   * interval |                 /.
   *          |                / .
   *          |               /  .   ← "warmup period" is the area of the trapezoid between
   *          |              /   .     thresholdPermits and maxPermits
   *          |             /    .
   *          |            /     .
   *          |           /      .
   *   stable +----------/  WARM .
   * interval |          .   UP  .
   *          |          . PERIOD.
   *          |          .       .
   *        0 +----------+-------+--------------→ storedPermits
   *          0 thresholdPermits maxPermits
   */

许可数量从 thresholdPermits 降至 0 的时间周期等于 warmupPeriod/2,因此 thresholdPermits = 0.5 * warmupPeriod / stableInterval

从 maxPermits 降至 thresholdPermits 的时间周期等于 warmupPeriod,因此 maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval)

coolDownIntervalMicros 方法控制许可的生成速度,也就是在 warmupPeriod 周期匀速生成 maxPermits 个许可。

对于已经生成的许可,storedPermitsToWaitTime 方法增加了等待时间,计算方式也就是对上图求个积分,初始态在x轴的右边,然后向左移动,也就是说一开始等待时间比较大,然后逐渐减小,最后平稳。

由于 coldFactor 硬编码为 3.0, 所以预热的“斜率”由 warmupPeriod 控制,warmupPeriod 越大,慢启动速率衰减越平缓,达到稳定的时间也比较长。

以上内容来自https://www.jianshu.com/p/4a78088252f0

### 令牌桶算法的工作原理 令牌桶算法是一种用于流量整形和速率限制的经典算法。它的核心思想是通过一个虚拟的“桶”,并以固定的速率向其中投放一定数量的令牌来控制请求的处理速度[^1]。 具体来说,系统会按照设定的时间间隔不断向桶中添加令牌,直到桶中的令牌数量达到预定义的最大容量为止。每当有一个新的请求到达时,该请求需要尝试从桶中取出一个令牌;如果成功获取到令牌,则允许此请求继续执行业务逻辑;反之,如果没有足够的令牌可供分配,则可以选择让请求排队等待或者直接拒绝服务[^2]。 这种设计使得令牌桶不仅能够有效地平滑数据流、维持稳定的吞吐量,而且还能容忍短时间内的突发访问压力——即当桶中有剩余未使用的令牌积累起来时,在下一波高峰到来之前它们可以被立即消耗掉从而应对突然增加的需求[^3]。 #### Python实现示例 下面是一个简单的基于Python语言模拟令牌桶行为的例子: ```python import time from threading import Lock class TokenBucket: def __init__(self, capacity, rate_per_second): self.capacity = capacity # 桶的最大容量 (单位时间内最多产生的token数目) self.tokens = capacity # 当前桶里的token数量 初始化为capacity self.rate = rate_per_second # token生成速率 单位: tokens/second self.last_time = time.time() # 上一次填充tokens的时间戳 self.lock = Lock() def _refill_tokens(self): now = time.time() elapsed = now - self.last_time new_tokens = elapsed * self.rate if self.tokens + new_tokens >= self.capacity: self.tokens = self.capacity else: self.tokens += new_tokens self.last_time = now def consume(self, number_of_tokens=1): with self.lock: self._refill_tokens() if self.tokens >= number_of_tokens: self.tokens -= number_of_tokens return True return False # 测试代码 if __name__ == "__main__": bucket = TokenBucket(10, 5) # 创建一个容量为10,每秒产生5个token的bucket实例 for i in range(20): if bucket.consume(): print(f"Request {i} allowed.") else: print(f"Request {i} denied due to lack of tokens.") time.sleep(0.2) ``` 上述程序展示了如何创建一个具有指定参数的`TokenBucket`对象,并测试其对于连续请求的响应情况。注意这里为了简化演示过程忽略了多线程环境下的同步问题实际应用中可能还需要考虑更多细节如持久化状态等[^4]。 ### 相关概念解释 - **平均速率**:随着操作持续进行,最终能达到接近于配置好的每秒钟生产多少个令牌这样的稳定输出水平。 - **瞬时速率**:即使短时间内涌入大量请求,只要不超过当前存储上限就能一次性满足这些需求而不违反整体约束条件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值