1.常见限流算法
1. 固定时间窗口算法(Fixed Window Counter)
这是最简单粗暴的算法。
-
原理:将时间划分为固定的窗口(比如从0点到1点)。在每个窗口内维护一个计数器,每来一个请求就加1。如果计数器超过了阈值(比如1000),则拒绝后续请求。当时间进入下一个窗口(比如1点到2点),计数器清零重置。
-
致命缺点:临界突发问题。假设窗口大小为1秒,限流100QPS。如果在第1秒的后100ms来了100个请求,第2秒的前100ms又来了100个请求,那么在这200ms内系统承受了200个请求,但每个窗口内的计数都刚好是100,并没有触发限流。这就导致了流量“突刺”,系统可能会被瞬间冲垮。
2. 滑动时间窗口算法(Sliding Window Counter)
这是对固定窗口算法的改进,也是Sentinel默认统计流量的方式。
-
原理:它不再有固定的、生硬的窗口边界,而是将时间窗口划分成更小的格子(比如把1秒分成2个500ms的格子)。每当有请求到来,窗口会随着时间滑动,窗口内统计的数据是当前时刻往前推一个完整窗口周期(比如1秒)内所有格子的数据总和。
-
优点:因为窗口是平滑滑动的,所以解决了固定窗口的临界突发问题。在上面的例子中,第2秒前100ms的请求会滑动到包含第1秒后100ms的窗口内,此时总和是200,超过了阈值,就会被限流。
-
代价:因为需要记录每个小格子的数据,所以内存和性能开销比固定窗口要大。
3. 漏桶算法(Leaky Bucket)
这个算法强调“整形”,让流量输出变得非常均匀。
-
原理:可以想象成一个底部有一个小洞的水桶。水(请求)可以以任意速度流入桶中,但水会以固定的速率从洞中流出(被处理)。如果桶满了,新流入的水(请求)就会溢出(被拒绝)。
-
核心特点:无论流入流量多大,流出速率都是恒定的。这能很好地保护后端系统,避免瞬时高峰。
-
适用场景:适合需要绝对平滑流量的场景,比如数据库批量写入、文件导出等,可以防止瞬间流量压垮数据库。
-
局限性:因为它强制匀速,所以无法应对突发流量。即使系统当前完全空闲,也无法处理突然涌来的一波请求(它们只能在桶里排队),实时性稍差。
4. 令牌桶算法(Token Bucket)
这是最常用、也最灵活的算法,Sentinel的预热功能就是基于它实现的。
-
原理:有一个桶,里面放着令牌。系统会以固定的速率往桶里添加令牌。当请求到来时,必须从桶里获取一个令牌,如果拿到令牌就通过,否则就被拒绝。如果桶里的令牌积攒满了,多余的令牌就会被丢弃。
-
核心特点:它允许一定程度的突发流量。因为如果系统空闲了一段时间,桶里会积攒很多令牌。当突发流量到来时,请求可以一次性拿走所有积攒的令牌,从而快速处理峰值流量,之后再回归到正常的令牌生成速率。
-
适用场景:非常通用。既能通过速率限制保护系统,又能利用令牌积攒的特性应对业务高峰,是实际工程中使用最广泛的算法之一。
| 算法 | 流量是否均匀 | 能否应对突发流量 | 实现复杂度 | 典型应用 |
|---|---|---|---|---|
| 固定时间窗口 | 否 | 不能(有临界问题) | 极低 | 简单计数场景(不推荐) |
| 滑动时间窗口 | 否 | 不能(精确计数但无缓冲) | 较高 | Sentinel默认限流统计,精确控制QPS |
| 漏桶算法 | 是(绝对均匀) | 不能(只能排队) | 中等 | 流量整形,平滑输出(如消息队列消费) |
| 令牌桶算法 | 否(允许瞬时突发) | 能(积攒令牌应对) | 中等 | 通用限流(如Guava RateLimiter、Sentinel预热) |
2.Sentinel限流机制
1.总体介绍
阿里的sentinel,sentinel基于滑动时间窗口算法统计调用数据,提供了如下限流实现
- 快速失败(DefaultController),基于滑动时间窗口的统计数据,如果当前QPS已达到阈值则立即限流,严格保证流量小于限流阈值,属于 “滑动时间窗口限流算法”的特点。
- 排队等待(RateLimiterController),根据限流阈值计算请求放行的时间间隔,如果过两个请求之间的间隔时间过短,则计算第二个请求需要等待的时间,并通过Thread.sleep让第二请求线程进行等待。如果第N请求的等待时间大于最大等待时间,则该请求被限流。这种以固定速率释放请求,且允许一定数量的请求堆积,属于“漏桶限流算法”的特点。
- WarmUp(WarmUpController、WarmUpRateLimiterController),内部实现参考了guava的RateLimiter,但是逻辑比guava更加复杂,功能也更加强大。可以确定的是WarmUp使用的是“令牌桶限流算法”。
Sentinel 的滑动窗口没有用 Redis ZSet 的 ZREMRANGEBYSCORE 这类操作,因为那是分布式环境下的解决方案(比如配合 Redis 做集群限流)。而 Sentinel 本质上是本地限流组件(运行在应用进程内的 JVM 中),它的设计原则就是极致的高性能,绝不会依赖远程 Redis 的网络 IO 或复杂数据结构。
Sentinel 的滑动窗口实现,正是“分成了很多个小窗口”(专业术语叫 LeapArray,即“跳跃数组”)。它通过空间换时间和数组+时间戳取模的方式,把性能开销降到了极低。
拆解一下它的核心原理:
1. 核心数据结构:环形数组
Sentinel 没有用链表或树,而是使用了一个固定长度的环形数组。
-
假设你设置的统计时长是 1 秒,采样窗口数量(
sampleCount)默认为 2。那么数组长度就是 2,每个格子代表 500 毫秒。 -
数组在初始化时就创建好了,不会频繁创建和销毁对象,避免了 GC(垃圾回收)压力。
2. 定位算法:时间戳取模(无锁竞争)
当请求进来时,Sentinel 会计算当前时间属于哪个格子,定位逻辑极其轻量:
-
计算当前时间的 Bucket ID(窗口索引):
(当前时间戳 / 窗口长度) % 数组长度。 -
由于是环形数组,后面的时间可能会覆盖前面的格子(比如第 3 个 500ms 会覆盖第 1 个 500ms 的位置)。
3. 解决“覆盖”问题:复用而非删除
旧数据怎么处理?
它不会去删除旧数据,而是直接复用(Reset)。
当请求定位到某个数组格子时,它会检查该格子里的时间戳:
-
如果格子的时间戳和当前时间属于同一个窗口期 -> 直接在该格子上累加计数(原子 CAS 操作)。
-
如果格子的时间戳已经过期(属于上一个周期) -> 直接重置该格子的所有数据(将计数置为 0,时间戳更新为当前时间),然后写入新数据。
关键点:整个过程只有数组下标访问和 CAS(比较并交换)原子操作,完全没有 ZSet 那种排序、插入、删除的 O(logN) 或 O(N) 开销,也没有任何网络 IO。
为什么说“分成小窗口”远比“ZSet”高效?
| 对比维度 | Redis ZSet (分布式方案) | Sentinel 本地环形数组 |
|---|---|---|
| 依赖 | 依赖网络、Redis 内存、序列化 | 纯本地内存,无外部依赖 |
| 时间复杂度 | ZREMRANGEBYSCORE 复杂度 O(logN+M),数据量大了有性能损耗 | 数组下标访问 O(1),极快 |
| 内存开销 | 每个请求都要存一个 member,数据量大会膨胀 | 只有固定 2 个或少量对象,内存恒定不变 |
| GC 压力 | 频繁创建/销毁 ZSet 元素,触发 GC | 对象复用,几乎没有 GC 压力 |
2.滑动窗口工作原理
LeapArray 统计数据的基本思路:
创建一个长度为 n 的数组,数组元素就是窗口;
每个窗口包装了 1 个指标桶,桶中存放了该窗口时间范围内对应的请求统计数据;
可以想象成一个环形数组在时间轴上向右滚动;
请求到达时,会命中数组中的一个窗口,该请求的数据就会存到命中的这个窗口包含的指标桶中;
当数组转满一圈时,会回到数组的开头;
此时下标为 0 的元素需要重复使用,它里面的窗口数据过期了,需要重置,然后再使用。
总结
Sentinel 就是分成了固定数量的小窗口(默认 2 个),通过环形数组复用的方式替代了删除操作。
这样做的好处是:
-
毫秒级响应:计算过程就是几次整数运算。
-
无 GC 干扰:数组长度固定,对象不增不减。
-
精度可控:如果你需要更精确的统计(比如想统计到 100ms 精度),可以通过
sampleCount调大数组长度(比如设为 10),但这会稍微增加内存,默认的 2 个(500ms 精度)在性能和精度上达到了最好的平衡。
2.StatisticNode
简单来说,StatisticNode 就是 Sentinel 进行实时流量统计的“数据收集器”和“仓库”。
为了让你更直观地理解,可以把它看作是一个高性能的“仪表盘”——每一个请求的到来、通过、拒绝、耗时等信息,都会实时上报并记录在 StatisticNode 里。当限流规则(比如 QPS 不能超过 100)需要判断时,Sentinel 就会直接从这个节点里查询“过去 1 秒内有多少个请求”,然后决定是否放行。
它在 Sentinel 的架构中承担了两个最核心的职能:
1. 它是“滑动窗口”的载体
我们刚才聊到的滑动窗口(LeapArray),其实就是 StatisticNode 里的核心成员变量。
-
StatisticNode内部维护了两个滑动窗口数组:-
rollingCounterInSecond:用于秒级统计(QPS、响应时间等)。它默认将 1 秒分成 2 个 500ms 的格子。 -
rollingCounterInMinute:用于分钟级统计(用于判断是否达到熔断降级的慢调用比例等)。它将 1 分钟分成 60 个格子(每个格子 1 秒)。
-
-
所以,当你问“基于 QPS 限流的数据结构是什么”时,本质上就是
StatisticNode里的rollingCounterInSecond这个滑动窗口。
2. 它是“多维度指标”的统计中枢
StatisticNode 不仅仅统计总请求数,它会将请求分类统计。在它的代码逻辑里,每次请求进来都会调用 addPassRequest(记录通过数)或 addBlockRequest(记录拒绝数)等方法。
它主要维护了以下几类计数器(通过滑动窗口中的 MetricBucket 存储):
-
pass:通过的请求数(用于计算 QPS)。 -
block:被限流/降级拦截的请求数。 -
success:业务逻辑执行成功的请求数(用于计算异常比例)。 -
rt(Response Time):请求的响应耗时(用于计算平均 RT)。 -
exception:业务异常数。
它在架构中的位置(便于理解)
你可以把 Sentinel 的工作流想象成这样:
-
请求进入 -> 2.
StatisticNode记录请求(+1) -> 3. 查询StatisticNode过去 1 秒的总数 -> 4. 判断是否超过阈值 -> 5. 放行或拒绝。
而 StatisticNode 通常不会单独存在,它会被另一个概念 ProcessorSlotChain(处理器插槽链) 所持有。在默认的调用链中,StatisticNode 属于 StatisticSlot 这个插槽来管理。
总结一句话
StatisticNode 就是 Sentinel 用来承载“滑动窗口算法”,并对 QPS、RT、异常率等所有实时指标进行线程安全统计的“本地内存数据节点”。 如果没有它,Sentinel 就无法知道当前的流量到底有多大,限流也就无从谈起。
StatisticNode数据结构
public class StatisticNode implements Node {
/**
* 保存最近 1 秒内的统计数据
* 每个桶(bucket)500ms,共 2 个桶
*/
private transient volatile Metric rollingCounterInSecond =
new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL);
/**
* 保存最近 60 秒的统计数据
* windowLengthInMs 被特意设置为 1000 毫秒,即每个桶代表 1 秒
* 共 60 个桶,这样可以获得每秒精确的统计信息
*/
private transient Metric rollingCounterInMinute =
new ArrayMetric(60, 60 * 1000, false);
// 省略其他字段和方法...
}
作者:得物技术
链接:https://juejin.cn/post/7610636104946270227
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.ArrayMetric 是 StatisticNode 的“底层引擎”或“数据管家”。
如果用一个比喻来理解:
-
StatisticNode是“前台接待员”,负责告诉外界:“我能提供 QPS、RT 等数据”。 -
ArrayMetric是“后台数据库引擎”,负责真正地执行“数据写到哪里”和“从哪个格子读取数据”。
StatisticNode 持有(包含)ArrayMetric 的引用,而 ArrayMetric 持有(包含)我们上一轮讨论的核心数据结构——滑动窗口(LeapArray)。
数据结构
public class ArrayMetric implements Metric {
/**
* 滑动窗口数组
*/
private final LeapArray<MetricBucket> data;
public ArrayMetric(int sampleCount, int intervalInMs) {
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}
public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
if (enableOccupy) {
// 可抢占的滑动窗口,支持借用未来窗口的配额
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
} else {
// 普通滑动窗口
this.data = new BucketLeapArray(sampleCount, intervalInMs);
}
}
}
1. 它们的核心区别与职责
| 组件 | 核心职责 | 面向对象 |
|---|---|---|
StatisticNode | 指标分类与业务逻辑。它定义了我们有哪些指标(pass、block、rt、success 等),并提供 addPassRequest()、successQps() 这类业务含义明确的 API。 | 给 Sentinel 限流规则判断时调用(面向业务)。 |
ArrayMetric | 数据读写实现。它屏蔽了底层滑动窗口操作的复杂性(比如数组下标计算、CAS 更新、过期数据重置)。它提供的 API 是通用的,比如 add(MetricEvent, int)。 | 给 StatisticNode 调用,执行具体的存储与统计(面向技术实现)。 |
LeapArray | 存储实体。它是一个抽象父类,真正存放数据的是其子类(如 OccupiableBucketLeapArray)。它负责维护环形数组和每个格子(MetricBucket)。 | 被 ArrayMetric 调用,操作底层的数组和对象。 |
2. 为什么要设计成 StatisticNode -> ArrayMetric -> LeapArray 三层?
这是典型的单一职责原则设计,好处很明显:
-
StatisticNode不用关心“数据怎么存”,它只关心“存什么数据”和“对外提供什么数据”。这使得它很“轻”,业务语义清晰。 -
ArrayMetric作为中间层,封装了所有对滑动窗口的增删改查操作。它让上层(StatisticNode)可以像操作一个普通的Map一样存取数据,而无需关心底层是数组还是链表。 -
LeapArray专注于最底层、最高性能的环形数组复用算法。如果未来要换一种存储结构(虽然不太可能),也只需要修改ArrayMetric中的实现,而完全不影响StatisticNode。

7938

被折叠的 条评论
为什么被折叠?



