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

打卡内容
- 把今天或者昨天工作中的内容做笔记,尽可能多输出,先保证自己看懂即可
- 加练挑战:选最近读的一篇文章/一个文档/书一本,写个分享笔记,字数、形式不限
素材积累一二三
对于今天的课程内容,自己十分赞同,自己日常积累知识也是通过这三个途径去做的。
- 个人工作经验积累素材
- 日常工作中,一个项目的发展往往都经历:需求评审->方案设计->方案评审->开发设计->开发测试->联调验收这几个环节。这个过程中会积累很多内容,比如方案设计时经常会先去调研方案,对比各种方案之间的差异性,并结合需求设计出适合本项目的方案,这个过程会沉淀很多素材。
- 互动积累经验
- 目前自己还没有到与读者互动的阶段,不过在日常工作中会经常和周边的技术大佬沟通交流,了解别人做的内容,或者从别人那学习自己想知道的内容。或者从公司或者外网搜索相关的技术文章,从中找到自己想要的知识和答案。这个过程提升很多,学习成本也比较低,非常有用。
- 不断学习倒闭自己输入
- 说实话工作之后,已经很少有机会能有一块完整的时间去从论文、书本等途径去获取新知识了,这个过程学习成本相对较高,因为这个学习过程是给自己充电的过程,往往没有明确的目的,所以做起来自驱性就比较差,不过通过这次训练营,也确实意识到,如果带着输出的目的,自己会更努力去做。
结合今天的课程主题,以及上节课自己分享的内容,这里简单分享一下自己进一步对“限流”的学习总结。
限流你该知道的那些事儿
继之前遇到的限流问题,自己进一步去看了一些“限流”相关的内容,看了周志明老师软件架构课中《限流的目标与模式》一节、以及其他技术博客,自己做一些总结,从三个问题去分享“限流的那些事儿”。
什么是限流?
限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,对于服务提供方的一种保护手段。通过限流功能,我们可以通过控制限流指标的方式,以避免被瞬时的流量高峰冲垮,从而保障系统的高可用性。
这里简单介绍几种常见的限流指标。
- 每秒事务数(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限流组件,就是基于令牌桶算法实现的。
总结
- 固定窗口限流算法实现简单,性能也高,但会有临界问题;
- 滑动窗口限流算法解决了临界问题,但当流量达到阈值时会瞬间掐断流量,导致流量不够平滑;
- 想要流量平滑限流,则考虑漏桶算法,漏桶算法通常配置一个FIFO的队列做漏桶。但漏桶算法出水速率固定,即使某个时刻下游处理能力过剩,也不能充分利用资源,无法做到动态调节。
- 在下游处理能力充分的情况下,如果想要短时间突发流量可以立马处理,那么令牌漏桶算法就是不二之选,令牌桶以固定的速率v 产生令牌放入一个固定容量为n的桶中,当请求到达时尝试从桶中获取令牌。当桶满时,允许最大瞬时流量就为n;当桶内没有剩余令牌时,则限流流量速率最低,即为令牌生成的速率v。
浙公网安备 33010602011771号