21天技术人写作行动训练营-Day04素材积累-限流你该知道的那些事儿

如何积累素材

img

打卡内容

  • 把今天或者昨天工作中的内容做笔记,尽可能多输出,先保证自己看懂即可
  • 加练挑战:选最近读的一篇文章/一个文档/书一本,写个分享笔记,字数、形式不限

素材积累一二三

对于今天的课程内容,自己十分赞同,自己日常积累知识也是通过这三个途径去做的。

  1. 个人工作经验积累素材
    • 日常工作中,一个项目的发展往往都经历:需求评审->方案设计->方案评审->开发设计->开发测试->联调验收这几个环节。这个过程中会积累很多内容,比如方案设计时经常会先去调研方案,对比各种方案之间的差异性,并结合需求设计出适合本项目的方案,这个过程会沉淀很多素材。
  2. 互动积累经验
    • 目前自己还没有到与读者互动的阶段,不过在日常工作中会经常和周边的技术大佬沟通交流,了解别人做的内容,或者从别人那学习自己想知道的内容。或者从公司或者外网搜索相关的技术文章,从中找到自己想要的知识和答案。这个过程提升很多,学习成本也比较低,非常有用。
  3. 不断学习倒闭自己输入
    • 说实话工作之后,已经很少有机会能有一块完整的时间去从论文、书本等途径去获取新知识了,这个过程学习成本相对较高,因为这个学习过程是给自己充电的过程,往往没有明确的目的,所以做起来自驱性就比较差,不过通过这次训练营,也确实意识到,如果带着输出的目的,自己会更努力去做。

结合今天的课程主题,以及上节课自己分享的内容,这里简单分享一下自己进一步对“限流”的学习总结。

限流你该知道的那些事儿

继之前遇到的限流问题,自己进一步去看了一些“限流”相关的内容,看了周志明老师软件架构课中《限流的目标与模式》一节、以及其他技术博客,自己做一些总结,从三个问题去分享“限流的那些事儿”。

什么是限流?

限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,对于服务提供方的一种保护手段。通过限流功能,我们可以通过控制限流指标的方式,以避免被瞬时的流量高峰冲垮,从而保障系统的高可用性。

这里简单介绍几种常见的限流指标

  • 每秒事务数(Transactions per Second, TPS)

TPS是衡量信息系统吞吐量的最终标准。“事务”可以理解为一个逻辑上具备原子性的业务操作。

  • 每秒请求数(Hits per Second, HPS)

HPS是指每秒从客户端发向服务端的请求数。如果只要一个请求就能完成一笔业务,那么HPS与TPS是等价的,但在一些场景中(尤其常见于网页中),一笔业务可能需要多次请求才能完成。

  • 每秒查询数(Queries per Second, QPS)

QPS是指一台服务器能够响应的查询次数。如果只有一台服务器来应答请求,那么QPS与HPS是等价的,但在分布式系统中,一个请求的响应,往往要由后台多个服务节点共同协作来完成。

选择哪一种指标作为限流依据,需要根据业务系统实际情况、以及具体的限流场景来定。

超额流量如何处理?

当设定了限流指标后,对于超出指标的流量又该如何处理?

超额流量可以有不同的处理策略,可以选择直接返回失败,或者被迫使它们进入降级逻辑,这种策略统一被称为否决式限流;也可以让请求排队等待,暂时阻塞一段时间后继续处理,这种则被称为阻塞式限流

如何设计限流?

目前常见的限流算法有四种:固定窗口限流算法、滑动窗口限流算法、漏桶算法、令牌桶算法。其中前两种通常适用于否决式限流,对于超过阈值的流量会强制失败或者降级,很难进行阻塞等待处理;后两种通常适用于阻塞式限流,能够对流量曲线进行整形,达到削峰填谷的效果。

固定窗口限流算法

将单位时间段作为一个窗口,并维护一个计数器,计数器记录这个窗口接收请求的次数。

  • 当次数小于限流阈值,就允许访问,并且计数器+1;
  • 当次数大于限流阈值,就拒绝访问;
  • 当时间窗口过去后,计数器清零。

举个🌰:假设单位时间是1秒,限流阈值为3。在单位时间1秒内,每来一个请求,就计数器+1,如果计数器累加的次数超过限流阈值3,则拒绝后续的请求。等1秒结束后,计数器清零,重新开始计数,流程如下:

算法伪代码:

/**
 * 固定窗口限流算法
 */
boolean fixedWindowsTryAcquire() {
  long currentTime = System.currentTimeMillis();	//获取当前时间
  if (currentTime - lastRequestTime > windowUnit) {	//检查是否在时间窗口内
    counter = 0;	//计数器清零
    lastRequestTime = currentTime;	//开启新的时间窗口
  }
  if (counter < threshold) {	//小于限流阈值
    counter++;	//计数器加1
    return true;
  }
  return false;
}

算法缺点:

有很明显的临界问题:依旧以上图为例,限流阈值为3个请求,单位时间窗口为1秒,如果在单位时间内的前0.7-1s和1-1.3s,分别并发3个请求。虽然在各自单位时间内都没有超过阈值,但是考虑0.7-1.3s,则并发数就达到6了,已经超过单位时间1s内不超过3阈值的定义了,未起到限流作用。

滑动窗口限流算法

滑动窗口限流算法解决了固定窗口临界值的问题。它将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。

举个🌰:假设单位时间为1s,滑动窗口算法将1s划分为5个小周期,每个小周期为0.2s。每过0.2s,时间窗口就会往右滑动一格。每个小周期,都有自己独立的计数器,如果请求是0.25s到达的,那么0.2-0.4s对应的计数器就会加1。流程如下:

进一步分析滑动窗口算法如何解决临界问题的呢?

依旧假设1s内的限流阈值还是3个请求,假如在0.8-1.0s内来了3个请求,落在褐色格子内。时间过了1.0s这个点后,又来了3个请求,落在紫色格子里。如果是固定窗口算法,则紫色格子里的流量是不会被限流的,但是滑动窗口算法,每过一个小周期(0.2s),就会右移一个小格。因此,当整个窗口右移一格后,当前的单位时间段是0.2-1.2s,这个区域的请求数已经超过限流阈值3了,就会触发限流,紫色格子中的请求都将被拒绝。

从上面过程可以看出当滑动窗口的格子周期划分的越多,那么滑动窗口的滚动就越平滑,限流的统计也就越精准。

算法伪代码:

/**
 * 单位时间划分的小周期(单位时间1分钟,10s一个小格子窗口,共6个格子)
 */
private int SUB_CYCLE = 10;

/**
 * 每分钟限流请求数
 */
private int thresholdPerMin = 100;

/**
 * 计数器,key为小格子窗口的开始时间值秒,value为当前窗口的计数
 */
private final TreeMap<Long, Integer> counters = new TreeMap<>();

/**
 * 滑动窗口限流算法
 */
boolean slidingWindowsTryAcquire() {
  //获取当前时间在哪个小周期窗口
  long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE;
  int currentWindowNum = countCurrentWindow(currentWindowTime);	//当前单位时间窗口总请求数
  
  //超过阈值限流
  if (currentWindowNum >= thresholdPerMin) {
    return false;
  }
  
  counters.get(currentWindowTime) ++;
  return true;
}

/**
 * 统计当前单位时间窗口的请求数
 */
private int countCurrentWindow(long currentWindowTime) {
  //计算窗口开始位置,为最近一分钟内所在的位置
  long startTime = currentWindowTime - SUB_CYCLE * (60s / SUB_CYCLE - 1);
  int count = 0;
  
  //遍历存储的计数器
  Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
  while (iterator.hasNext()) {
    Map.Entry<Long, Integer> entry = iterator.next();
    //删除无效过期的子窗口计数器
    if (entry.getKey() < startTime) {
      iterator.remove();
    } else {
      //累加当前窗口的所有计数之和
      count = count + entry.getValue();
    }
  }
  return count;
}

有个问题:滑动窗口算法能否精准控制任意给定时间窗口T内的访问量不大于N?

答案是否定的。举个🌰:如上伪代码,单位时间为1分钟,将1分钟分成6个10秒的子窗口,限流阈值为100,假设请求的速率是20次/秒,从0:05时刻开始进入,那么在0:05-0:10时间段内会放进100个请求,同时接下来的请求都会被限流,直到1:00时刻窗口滑动,在1:00-1:05时刻继续放进100个请求。如果把0:05-1:05看做一个单位时间窗口,那么这个窗口内实际的请求量是200,超出了限流阈值100。(= = 又回到了临界问题!)

所以滑动窗口的格子周期划分的越细致,则限流的准确性就越高。

漏桶算法

原来很简单,就是模拟漏水注水的过程,将流量比作是水流,可以按任意速率往漏桶中注入,漏桶控制以固定的速率出水。当水超过桶的容量时,会溢出丢弃。因为桶容量不变,所以保证了整体的出水速率,即将湍急的流量缓冲了一下,并整形按固定速率输出。

  • 流入的水滴,可以看做是访问系统的请求,这个流入速率是不确定的;
  • 桶的容量一般表示系统所能处理的请求数;
  • 如果桶的容量满了,就达到限流的阈值,就会丢弃水滴(拒绝请求);
  • 流出的水滴,是恒定过滤的,对应服务按照固定的速率处理请求。

算法伪代码:

/**
 * 每秒处理数(出水率)
 */
private long rate;

/**
 * 当前剩余水量
 */
private long currentWater;

/**
 * 最后刷新时间
 */
private long refreshTime;

/**
 * 桶容量
 */
private long capacity;

/**
 * 漏桶算法
 */
boolean leakybucketLimitTryAcquire() {
  long currentTime = System.currentTimeMillis();	//获取当前时间
  //流出的水量 = (当前时间 - 上次刷新时间) * 出水率
  long outWater = (currentTime -  refreshTime) / 1000 * rate;
  //当前水量 = 之前的桶内水量 - 流出的水量
  long currentWater = Math.max(0, currentWater - outWater);
  refreshTime = currentTime;	//刷新时间
  
  //当前剩余水量还小于桶的容量时,允许请求执行
  if (currentWater < capacity) {
    currentWater ++;
    return true;
  }
  
  //当前剩余水量大于等于桶的容量,限流
  return false;
}

漏桶算法实现起来比较简单,比较困难的是如何确定漏桶的两个参数:桶的大小和水的流出速率

首先是桶的大小。如果桶设置得太大,那服务依然可能遭遇流量过大的冲击,不能完全发挥限流的作用;如果设置得太小,那很可能就会误杀掉一部分正常的请求。

其次流出速率在漏桶算法中一般是个固定值,对于固定拓扑结构的服务是很合适的;但对于突发流量或者说流量突变时,是希望系统能尽量快点处理请求,提升用户体验,这就需要用到令牌桶算法。

令牌桶算法

令牌桶算法原理:

  • 设定一个令牌管理员,根据限流大小,定速往令牌桶里放令牌;
  • 如果令牌数量满了,超过令牌桶容量的限制,就丢弃令牌;
  • 系统在接收到一个用户请求时,都会先去令牌桶取一个令牌,如果拿到令牌,那么就处理这个请求的业务逻辑;
  • 如果拿不到令牌,就直接拒绝这个请求。

算法伪代码:

/**
 * 每秒处理数(放入令牌速率)
 */
private long putTokenRate;

/**
 * 最后刷新时间
 */
private long refreshTime;

/**
 * 令牌桶容量
 */
private long capacity;

/**
 * 当前桶内令牌数
 */
private long currentToken = 0L;

/**
 * 令牌桶算法
 */
boolean tokenBucketTryAcquire() {
  long currentTime = System.currentTimeMillis();	//获取当前时间;
  //生成的令牌 = (当前时间 - 上次刷新时间)* 放入令牌的速率
  long genToken = (currentTime - refreshTime) / 1000 * putTokenRate;
  //当前令牌数量 = 之前的桶内令牌数量 + 放入的令牌数量
  currentToken = Math.min(capacity, genToken + currentToken);
  refreshTime = currentTime;	//刷新时间
  
  //桶内还有令牌,则正常处理请求
  if (currentToken > 0) {
    currentToken --;	//令牌数量-1
    return true;
  }
  return false;
}

如果令牌发放的策略正确,则整个系统即不会拖垮,也能提高机器的利用率。Guava的RateLimiter限流组件,就是基于令牌桶算法实现的。

总结

  1. 固定窗口限流算法实现简单,性能也高,但会有临界问题;
  2. 滑动窗口限流算法解决了临界问题,但当流量达到阈值时会瞬间掐断流量,导致流量不够平滑;
  3. 想要流量平滑限流,则考虑漏桶算法,漏桶算法通常配置一个FIFO的队列做漏桶。但漏桶算法出水速率固定,即使某个时刻下游处理能力过剩,也不能充分利用资源,无法做到动态调节。
  4. 在下游处理能力充分的情况下,如果想要短时间突发流量可以立马处理,那么令牌漏桶算法就是不二之选,令牌桶以固定的速率v 产生令牌放入一个固定容量为n的桶中,当请求到达时尝试从桶中获取令牌。当桶满时,允许最大瞬时流量就为n;当桶内没有剩余令牌时,则限流流量速率最低,即为令牌生成的速率v。
posted @ 2023-12-17 22:13  Rezerol  阅读(54)  评论(0)    收藏  举报