常见限流算法与Sentinel限流机制

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基于滑动时间窗口算法统计调用数据,提供了如下限流实现

  1. 快速失败(DefaultController),基于滑动时间窗口的统计数据,如果当前QPS已达到阈值则立即限流,严格保证流量小于限流阈值,属于 “滑动时间窗口限流算法”的特点。
  2. 排队等待(RateLimiterController),根据限流阈值计算请求放行的时间间隔,如果过两个请求之间的间隔时间过短,则计算第二个请求需要等待的时间,并通过Thread.sleep让第二请求线程进行等待。如果第N请求的等待时间大于最大等待时间,则该请求被限流。这种以固定速率释放请求,且允许一定数量的请求堆积,属于“漏桶限流算法”的特点。
  3. 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 个),通过环形数组复用的方式替代了删除操作。

这样做的好处是:

  1. 毫秒级响应:计算过程就是几次整数运算。

  2. 无 GC 干扰:数组长度固定,对象不增不减。

  3. 精度可控:如果你需要更精确的统计(比如想统计到 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 的工作流想象成这样:

  1. 请求进入 -> 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指标分类与业务逻辑。它定义了我们有哪些指标(passblockrtsuccess 等),并提供 addPassRequest()successQps() 这类业务含义明确的 API。给 Sentinel 限流规则判断时调用(面向业务)。
ArrayMetric数据读写实现。它屏蔽了底层滑动窗口操作的复杂性(比如数组下标计算、CAS 更新、过期数据重置)。它提供的 API 是通用的,比如 add(MetricEvent, int)给 StatisticNode 调用,执行具体的存储与统计(面向技术实现)。
LeapArray存储实体。它是一个抽象父类,真正存放数据的是其子类(如 OccupiableBucketLeapArray)。它负责维护环形数组和每个格子(MetricBucket)。被 ArrayMetric 调用,操作底层的数组和对象

2. 为什么要设计成 StatisticNode -> ArrayMetric -> LeapArray 三层?

这是典型的单一职责原则设计,好处很明显:

  1. StatisticNode 不用关心“数据怎么存”,它只关心“存什么数据”和“对外提供什么数据”。这使得它很“轻”,业务语义清晰。

  2. ArrayMetric 作为中间层,封装了所有对滑动窗口的增删改查操作。它让上层(StatisticNode)可以像操作一个普通的 Map 一样存取数据,而无需关心底层是数组还是链表。

  3. LeapArray 专注于最底层、最高性能的环形数组复用算法。如果未来要换一种存储结构(虽然不太可能),也只需要修改 ArrayMetric 中的实现,而完全不影响 StatisticNode。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值