没想到,为了一个限流我写了1万字!(三)

发布于 2022-6-10 16:03
浏览
0收藏

 

填充令牌的逻辑如下:

  1. 拿到当前的时间,然后去掉毫秒数,得到的就是秒级时间
  2. 判断时间小于这里就是为了控制每秒丢一次令牌
  3. 然后就是coolDownTokens去计算我们的冷启动/预热是怎么计算填充令牌的
  4. 后面计算当前剩下的令牌数这个就不说了,减去上一次消耗的就是桶里剩下的令牌
protected void syncToken(long passQps) {
  long currentTime = TimeUtil.currentTimeMillis();
  //去掉当前时间的毫秒
  currentTime = currentTime - currentTime % 1000;
  long oldLastFillTime = lastFilledTime.get();
  //控制每秒填充一次令牌
  if (currentTime <= oldLastFillTime) {
    return;
  }
  //当前的令牌数量
  long oldValue = storedTokens.get();
  //获取新的令牌数量,包含添加令牌的逻辑,这就是预热的逻辑
  long newValue = coolDownTokens(currentTime, passQps);
  if (storedTokens.compareAndSet(oldValue, newValue)) {
    //存储的令牌数量当然要减去上一次消耗的令牌
    long currentValue = storedTokens.addAndGet(0 - passQps);
    if (currentValue < 0) {
      storedTokens.set(0L);
    }
    lastFilledTime.set(currentTime);
  }

}
  1. 最开始的事实因为lastFilledTime和oldValue都是0,所以根据当前时间戳会得到一个非常大的数字,最后和maxToken取小的话就得到了最大的令牌数,所以第一次初始化的时候就会生成maxToken的令牌
  2. 之后我们假设系统的QPS一开始很低,然后突然飙高。所以开始的时候回一直走到高于警戒线的逻辑里去,然后passQps又很低,所以会一直处于把令牌桶填满的状态(currentTime - lastFilledTime.get()会一直都是1000,也就是1秒),所以每次都会填充最大QPScount数量的令牌
  3. 然后突增流量来了,QPS瞬间很高,慢慢地令牌数量就会消耗到警戒线之下,走到我们if的逻辑里去,然后去按照count数量增加令牌
private long coolDownTokens(long currentTime, long passQps) {
  long oldValue = storedTokens.get();
  long newValue = oldValue;

  //水位低于警戒线,就生成令牌
  if (oldValue < warningToken) {
    //如果桶中令牌低于警戒线,根据上一次的时间差,得到新的令牌数,因为去掉了毫秒,1秒生成的令牌就是阈值count
    //第一次都是0的话,会生成count数量的令牌
    newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
  } else if (oldValue > warningToken) {
    //反之,如果是高于警戒线,要判断QPS。因为QPS越高,生成令牌就要越慢,QPS低的话生成令牌要越快
    if (passQps < (int)count / coldFactor) {
      newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
    }
  }
  //不要超过最大令牌数
  return Math.min(newValue, maxToken);
}

上面的逻辑理顺之后,我们就可以继续看限流的部分逻辑:

  1. 令牌计算的逻辑完成,然后判断是不是超过警戒线,按照上面的说法,低QPS的状态肯定是一直超过的,所以会根据斜率来计算出一个warningQps,因为我们处于冷启动的状态,所以这个阶段就是要根据斜率来计算出一个QPS数量,让流量慢慢地达到系统能承受的峰值。举个例子,如果count是100,那么在QPS很低的情况下,令牌桶一直处于满状态,但是系统会控制QPS,实际通过的QPS就是warningQps,根据算法可能只有10或者20(怎么算的不影响理解)。QPS主键提高的时候,aboveToken再逐渐变小,整个warningQps就在逐渐变大,直到走到警戒线之下,到了else逻辑里。
  2. 流量突增的情况,就是else逻辑里低于警戒线的情况,我们令牌桶在不停地根据count去增加令牌,这时候消耗令牌的速度超过我们生成令牌的速度,可能就会导致一直处于警戒线之下,这时候判断当然就需要根据最高QPS去判断限流了。
 long restToken = storedTokens.get();
 if (restToken >= warningToken) {
  //当前的令牌超过警戒线,获得超过警戒线的令牌数
  long aboveToken = restToken - warningToken;
  // 消耗的速度要比warning快,但是要比慢
  // current interval = restToken*slope+1/count
  double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
  if (passQps + acquireCount <= warningQps) {
   return true;
  }
 } else {
  if (passQps + acquireCount <= count) {
   return true;
  }
 }

所以,按照低QPS到突增高QPS的流程,来想象一下这个过程:

  1. 刚开始,系统的QPS非常低,初始化我们就直接把令牌桶塞满了
  2. 然后这个低QPS的状态持续了一段时间,因为我们一直会填充最大QPS数量的令牌(因为取最小值,所以其实桶里令牌基本不会有变化),所以令牌桶一直处于满的状态,整个系统的限流也处于一个比较低的水平
    这以上的部分一直处于警戒线之上,实际上就是叫做冷启动/预热的过程。
  3. 接着系统的QPS突然激增,令牌消耗速度太快,就算我们每次增加最大QPS数量的令牌任然无法维持消耗,所以桶里的令牌在不断低减少,这个时候,冷启动阶段的限制QPS也在不断地提高,最后直到桶里的令牌低于警戒线
  4. 低于警戒线之后,系统就会按照最高QPS去限流,这个过程就是系统在逐渐达到最高限流的过程
    那这样一来,实际就达到了我们处理突增流量的目的,整个系统在漫漫地适应突然飙高的QPS,然后最终达到系统的QPS阈值。
  5. 最后,如果QPS回复正常,那么又会逐渐回到警戒线之上,就回到了最开始的过程。

没想到,为了一个限流我写了1万字!(三)-开源基础软件社区

总结

因为算法如果单独说的话都比较简单,一说大家都可以听明白,不需要几个字就能说明白,所以还是得弄点源码看看别人是怎么玩的,所以尽管我很讨厌放源码,但是还是不得不干。

光靠别人说一点其实有点看不明白,按照顺序读一遍的话心里就有数了。

那源码的话最难以理解的就是令牌桶的实现了,说实话那几个计算的逻辑我看了好几遍不知道他算的什么鬼,但是思想我们理解就行了,其他的逻辑相对来说就比较容易理解。

 

文章转自公众号:艾小仙

分类
标签
已于2022-6-10 16:03:51修改
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐