微服务就是将复杂的大应用拆分成小的应用,这样做的好处是各个应用之间独立开发、测试、上线,互不影响。但是服务拆分之后,带来的问题也很多,我们需要保障微服务的正常运行,就需要进行服务治理。常用手段有:鉴权、限流、降级、熔断等。

其中,限流是指对某个接口的调用频率进行限制,防止接口调用频率过快导致线程资源被耗尽从而导致整个系统响应变慢。限流在很多业务中都有应用,比如:秒杀、双11。当用户请求量过大时,就会拒绝后续接口请求,保障系统的稳定性。

接口限流的实现思路是:统计某个时间段内的接口调用次数,当调用次数超过设置的阈值时,就进行限流限制接口访问。

常见的限流算法有:固定时间窗口算法、滑动时间窗口算法、令牌桶算法、漏桶算法等,下面我们将一一介绍每种算法的实现思路和代码实现。

一、固定时间窗口限流算法

1、算法概述

固定时间窗口限流算法的思路就是:确定一段时间段,在该时间段内统计接口的调用次数,来判断是否限流。

实现步骤如下:
选定一个时间起点,当接口请求到来时,

  • 接口访问次数小于阈值,可以访问,接口访问次数 + 1;
  • 接口访问次数大于阈值,拒绝该时间段内后续访问进行限流,接口访问次数不变;
  • 进入下一个时间窗口之后,计数器清零,时间起点设置为当前时间,这样就进入下一个时间窗口。

示意图如下:

​ (图片来源:https://time.geekbang.org/column/article/80388?utm_term=zeusNGLWQ&utm_source=xiangqingye&utm_medium=geektime&utm_campaign=end&utm_content=xiangqingyelink1104,下图同上)

这种限流算法的缺点是:无法应对两个时间窗口临界时间内的突发流量。

如下图:假设要求每秒钟接口请求次数不超过100,在第1s时间窗口内接口请求次数为100,但是都集中在最后10ms;第2s时间窗口内接口请求次数也为100,都集中在前10ms内;两个时间窗口请求次数都小于100,满足要求。但是在两个10ms内接口请求次数=200 > 100。如果这个次数不是200,是2000万,可能就会导致系统崩溃。

2、代码实现

public class FixedWindowRateLimitAlg implements RateLimitAlg {// msprivate static final long LOCK_EXPIRE_TIME = 200L;private Stopwatch stopWatch;// 限流计数器private AtomicInteger counter = new AtomicInteger(0);private final int limit;private Lock lock = new ReentrantLock();public FixedWindowRateLimitAlg(int limit) {this(limit, Stopwatch.createStarted());}public FixedWindowRateLimitAlg(int limit, Stopwatch stopWatch) {this.limit = limit;this.stopWatch = stopWatch;}@Overridepublic boolean tryAcquire() throws InterruptedException {int currentCount = counter.incrementAndGet();// 未达到限流if (currentCount < limit) {return true;}// 使用固定时间窗口统计当前窗口请求数// 请求到来时,加锁进行计数器统计工作try {if (lock.tryLock(LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS)) {// 如果超过这个时间窗口, 则计数器counter归零, stopWatch, 窗口进入下一个窗口if (stopWatch.elapsed(TimeUnit.MILLISECONDS) > TimeUnit.SECONDS.toMillis(1)) {counter.set(0);stopWatch.reset();}// 不超过, 则当前时间窗口内的计数器counter+1currentCount = counter.incrementAndGet();return currentCount < limit;}} catch (InterruptedException e) {System.out.println("tryAcquire() wait lock too long:" + LOCK_EXPIRE_TIME + " ms");throw new InterruptedException("tryAcquire() wait lock too long:" + LOCK_EXPIRE_TIME + " ms");} finally {lock.unlock();}// 出现异常 不能影响接口正常请求return true;}}

二、滑动时间窗口限流算法

1、算法概述

固定时间窗口限流算法无法处理两个时间窗口临界值流量突增的情况。为了解决这个问题,我们可以稍微优化下固定时间窗口限流算法,通过限制任意时间窗口内(比如:1S)接口请求数都不超过某个阈值,这个优化后的算法就叫做滑动时间窗口限流算法。

滑动时间窗口限流算法将一个大的时间窗口分成粒度更小的时间窗口,每个子窗口独立统计次数。每经过一个子窗口的时间,整体窗口就向右滑动一格。


如上图所示,假设要求每分钟通过次数不超过100次,将1分钟分成6个10s的单元格。
第一个图中假设最后1个10s内(序号:6)通过请求次数为100次,第二个图中假设第1个10s (序号:7)内请求次数也通过100次。由于是滑动窗口,第一个窗口向右移动一格后,在第二个滑动窗口内,序号6、7两者加起来的请求次数为200>100,所以限流,从而解决了固定时间窗口无法处理两个窗口临界值的问题。

虽然滑动时间窗口算法可以保证任意时间窗口内接口请求次数不超过阈值,但是仍然无法避免更细粒度流量突增的场景,比如在某个10s内的单元格内流量突增无法立即被限流。同时,使用滑动窗口算法时,流量曲线如下,无法达到平滑过渡的效果,无法控制流量速度

2、算法实现

可以使用循环队列来实现滑动时间窗口限流算法:

假设限流规则是任意1s内,接口请求数不超过N次。

创建一个N+1 (循环队列本身会浪费一个存储单元,所以是N+1)的循环队列,用来记录1S内的请求。

当有新的请求到来时,

  • 将与该请求的时间间隔超过1s的请求从队列中移除(移动head指针);
  • 再看循环队列中是否有空闲位置,如果有,则把新请求存储在队列尾部(tail指针所在位置,同时移动tail指针);
  • 如果循环队列尾部没有空闲位置,说明这个1s内的请求次数已经超过限流次数N,拒绝后续服务。

算法实现的示意图如下:

假设1S内请求次数不能超过6次,整个队列分成(6+1)个单元格。

  • 18:060代表 18s 60ms的时候,第一个请求到来,此时队列为空,于是存储在第一个单元格内(即head指针指向的位置);
  • 同样,18:123、18:336、18:569、18:702、18:906分别为第2、3、4、5、6个请求,均在1s间隔内且队列都有空闲位置,于是依次存储到对应单元格内;
  • 当19:003请求到来时,没有与其超过1s间隔的请求,所以不需要移除其他请求;由于队列尾部没有空闲位置,说明1S内的请求次数已经超过6次,拒绝该请求访问;
  • 当19:406到来时,与其超过1S间隔的请求有18:060、18:123、18:336,该3个请求需要移除队列(逆时针移动head指针3个单元格),同时tail指针逆时针移动一格后,存放当前请求19:406;

3、伪代码实现

/*** @author: wanggenshen* @date: 2020/6/29 21:56.* @description: 循环队列实现滑动窗口限流算法*/
public class SlidingWindowLimiter {private int windowSize;private CircularQueue queue;public SlidingWindowLimiter(int windowSize) {this.windowSize = windowSize;queue = new CircularQueue(windowSize);}public boolean tryAcquire(long now) {// 判断是否有间隔1s的请求, 有则移除队列while (queue.prevNode() != -1 && now - queue.prevNode() > 1000) {System.out.println("超过1S间隔, 移除超过间隔的节点: " + queue.prevNode() + "当前时间: " + now + ", 间隔: " + (now - queue.prevNode()));queue.dequeue();}// 队列已满, 拒绝访问if (queue.isFull()) {System.out.println("队列已满, now: " + now);return false;}queue.enqueue(now);return true;}static class CircularQueue {/*** 每次请求的时间戳*/private long[] timeQueue;/*** 队列大小*/private int size;/*** 头指针*/private int headIndex;/*** 尾指针*/private int tailIndex;public CircularQueue(int size) {// 循环队列尾部指针多占用一个单元格timeQueue = new long[size + 1];this.size = size + 1;}/*** 入队*/public void enqueue (long timestamp) {// 队列已满if (isFull()) {throw new RuntimeException("Exceed queue size.");}timeQueue[tailIndex] = timestamp;tailIndex = (tailIndex + 1) % size;}/*** 出队** @return*/public long dequeue () {// 队列为空if (isEmpty()) {return -1;}long timestamp = timeQueue[headIndex];headIndex = (headIndex + 1) % size;return timestamp;}public long prevNode() {// 队列为空if (isEmpty()) {return -1;}return timeQueue[headIndex];}public boolean isFull() {return (tailIndex + 1) % size == headIndex;}public boolean isEmpty() {return tailIndex == headIndex;}}}

三、漏桶算法

1、算法概述

实际上,当请求数超过阈值时,我们不希望后续流量被全部限流,而是希望将流量控制在一定速度内。
漏桶算法就是基于流控来控制流量。

如下图所示,调用方请求比作是水龙头出的水,水桶出的水是比作是接口提供方处理的请求。当水龙头出水速度大于桶里的水流出速度(类似接口调用请求频率过快),水直接溢出(类似请求直接被限流)。通过这种方法不仅能保证流量不会超过阈值,同时保证接口的请求数以稳定的速度去处理。

漏桶算法的优点在于能够控制接口提供方的接口被匀速处理;缺点在于设置的速率不当会影响接口处理的效率。

2、代码实现

伪代码如下:

/*** @author: wanggenshen* @date: 2020/6/29 21:00.* @description: 漏桶限流算法*/
public class LeakyBucketLimiter {/*** 桶内剩余的水*/private long left;/*** 桶的容量*/private long capacity;/*** 一桶水漏完的时间*/private long duration;/*** 桶漏水的速率, capacity = duration*velocity*/private double velocity;/*** 上一次成功放入水桶的时间*/private long lastUpdateTime;public boolean acquire() {long now = System.currentTimeMillis();// 剩余的水量 - 桶匀速漏出去的水left = Math.max(0, left - (long)((now - lastUpdateTime) * velocity));// 当前水桶再加一单位水没有溢出, 则可以继续访问if (left++ <= capacity) {lastUpdateTime = now;return true;} else {return false;}}
}

四、令牌桶算法

1、算法概述

令牌桶算法的实现原理是:

以恒定速率生成令牌放进令牌桶,令牌桶满了的时候就丢弃不再放入令牌桶;

如果想要处理请求,就需要从令牌桶中取一个令牌。能取出令牌则去处理请求;没有令牌则拒绝请求。

令牌桶算法与漏桶算法很类似,最主要的区别在于:

  • 漏桶算法输入速率不定,但是输出速率恒定;令牌桶算法输出速率可以根据流量大小进行调整;

  • 从接口处理者的角度看,漏桶算法只能以固定频率去处理请求(比如每秒只能处理1个请求,如果此时来了10个请求,漏桶需要花10s处理完);而令牌桶算法可以处理突发流量,比如来了20个请求,如果令牌桶中有>=20个令牌,那么处理者就可以一下子全部处理这20个请求;

2、伪代码实现

/*** @author: wanggenshen* @date: 2020/6/29 21:00.* @description: 令牌桶限流算法*/
public class TokenBucketLimiter {/*** 令牌桶桶内剩余的令牌*/private long left;/*** 令牌桶的容量*/private long capacity;/*** 一桶水漏完的时间*/private long duration;/*** 令牌桶生产令牌的速率, capacity = duration*velocity*/private double velocity;/*** 上一次拿走令牌的时间*/private long lastUpdateTime;public boolean acquire() {long now = System.currentTimeMillis();// 令牌桶余量 =  【上一次令牌桶剩余的令牌】+ 【(上一次拿走令牌到现在的时间段) * 每个单位时间生产令牌的速率 】// 生产出的令牌 超过令牌桶的容量时, 则舍弃left = Math.min(capacity, left + (long)((now - lastUpdateTime) * velocity));// 若当前能够成功领取令牌, 则可以访问if (left-- >= 0) {lastUpdateTime = now;return true;} else {return false;}}
}

生产环境下可以考虑使用Guava提供的令牌桶算法实现类: RateLimiter来进行限流,RateLimiter的实现是线程安全的。

五、分布式限流

生产环境下服务基本上分布式部署,那么在对服务进行限流时需要考虑到分布式限流。

最简单的做法是给每台应用服务器平均分配流控阈值,将分布式限流转换为单机限流。如总流量不超过1000次,那么5个服务实例,每个实例请求数不能超过200次。但是如果遇到流量不均匀(比如一台机器流量一直是10、另外几台> 200)、或者有一台宕机,那么另外几台平均下来就是250>200,这种做法不是很好。

常见的实现思路有两种:

  • 中心化:使用一个第三方服务统一存储所有服务实例的调用次数,由其去判断是否进行限流。这种方式需要注意第三方服务宕机导致不可用问题。这个时候可以退化成单机流控。
  • 去中心化:每个服务单独保存同一份流控数据,但是很难做到保持状态一致,即CAP中的C。

一般使用中心化这种思路。

1、TokenServer 流控

Sentinel提供了TokenServer,作为一个独立服务来统计总调用量、判断单个请求是否允许访问。应用服务器每次接收到请求后,都要与TokenServer进行一次通信,判断该次请求能否访问。

这种实现方式的好处是:由TokenServer集中管理每个服务实例的总调用量,服务实例不用关心请求的统计工作;

缺点是:非常依赖于TokenServer的性能,因为需要与其进行网络通信。同时需要关系TokenServer服务的单节点故障问题。

2、存储式流控

存储式流控是每个服务请求到来时,从第三方存储(如Redis、MySQL)读取接口请求数、然后再将请求数更新回缓存;

拿到请求数后由每个服务实例自己去判断是否需要限流。

总结

要设计一个高性能、高可靠性的分布式流控性能需要考虑网络通信、加锁同步等对性能带来的影响,同时也需要考虑分布式环境的可靠性。


参考:https://mp.weixin.qq.com/s/joP22Z8zblcDBAV1keSdJw

【限流01】限流算法理论篇相关推荐

  1. 一步步教你轻松学朴素贝叶斯模型算法理论篇1

    一步步教你轻松学朴素贝叶斯模型理论篇1 (白宁超2018年9月3日17:51:32) 导读:朴素贝叶斯模型是机器学习常用的模型算法之一,其在文本分类方面简单易行,且取得不错的分类效果.所以很受欢迎,对 ...

  2. KNN 算法-理论篇-如何给电影进行分类

    公号:码农充电站pro 主页:https://codeshellme.github.io 目录 1,准备电影数据 2,用KNN 算法处理分类问题 3,用KNN 算法处理回归问题 4,总结 KNN 算法 ...

  3. 模拟退火算法——理论篇

    模拟退火算法(Simulated Annealing,SA)是模拟物理退火求解组合问题的算法,核心是要理解Metropolis 采样算法,具有算法简单.适用范围广.可靠性高等特点. 图片来自网络 1 ...

  4. 模拟退火算法——仿真篇

    理论部分不再赘述,详情请查看我以往文章. (19条消息) 模拟退火算法--理论篇_talkAC的博客-CSDN博客 1 仿真问题 旅行商问题(TSP问题). 假设1个旅行商要对31个省会城市进行拜访, ...

  5. svm手写数字识别_KNN 算法实战篇如何识别手写数字

    上篇文章介绍了KNN 算法的原理,今天来介绍如何使用KNN 算法识别手写数字? 1,手写数字数据集 手写数字数据集是一个用于图像处理的数据集,这些数据描绘了 [0, 9] 的数字,我们可以用KNN 算 ...

  6. 【限流02】限流算法实战篇 - 手撸一个单机版Http接口通用限流框架

    本文将从需求的背景.需求分析.框架设计.框架实现几个层面一步一步去实现一个单机版的Http接口通用限流框架. 一.限流框架分析 1.需求背景 微服务系统中,我们开发的接口可能会提供给很多不同的系统去调 ...

  7. 限流的两种算法以及相关的实现方法

    令牌桶算法限流 限流 限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机.常用的限流算法有令牌桶和和漏桶,而Google开源项目Guava中的R ...

  8. 高可用系统设计 | 分布式限流策略:计数器算法、漏桶算法、令牌桶算法

    文章目录 限流 什么是限流? 分布式限流 限流算法 计数器算法 固定窗口计数器 滑动窗口计数器 漏桶算法 令牌桶算法 限流 什么是限流? 限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已 ...

  9. 令牌桶算法和漏桶算法python_限流之漏桶算法与令牌桶算法

    在开发高并发系统时有三把利器用来保护系统:缓存.降级和限流 缓存:缓存的目的是提升系统访问速度和增大系统处理容量 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降 ...

最新文章

  1. Spring中@Value用法收集
  2. 学习python第四天内容回顾
  3. sts (eclipse)安装配置lombok
  4. Android—EventBus使用与源码分析
  5. 【安卓开发 】Android初级开发(八)WebView网页
  6. Jenkins的一些代码
  7. 多层感知机 深度神经网络_使用深度神经网络和合同感知损失的能源产量预测...
  8. java json 修改字段_JSON文件-Java:编辑/更新字段值
  9. 《Java并发性和多线程介绍》-Java TheadLocal
  10. 关于Js下拉导航的解释
  11. 函数遍历IOS中block的使用
  12. iOS开发篇——OC之NSNumber数字对象讲解
  13. DOS 常用命令大全
  14. ssm项目之Bookstrap创建页面并分页查询
  15. 没有鼠标怎么打开笔记本的触摸板
  16. 记录POJO类、DO、DTO、BO概念
  17. 在计算机上最常用的英语单词,计算机常用英语单词
  18. 计算几何 - 你绝对找不到比这更好的计算几何
  19. envi5.6处理gf3(SAR)详细过程记录
  20. 微信小程序-appId, 真机调试,上线

热门文章

  1. Ninja构建系统初探
  2. 福建农十林大的计算机专业怎么样,福建农林大学计算机与信息学院
  3. SQL 计算每个月的工作天数
  4. Arduino应用——PWM控制直流电机风扇
  5. linux高级编程基础系列:线程间通信
  6. 【uniapp】小程序中使用css实现一个带框的加减号
  7. 埃及分数 (迭代加深入门)
  8. 尽管颓废了一年,但我仍未放弃梦想「2021年终总结」
  9. Epson机器人程序---点位控制(1)
  10. CF(935C - Fifa and Fafa)