Sentinel 的热点参数限流是如何实现的?
Sentinel 的热点参数限流(ParamFlow)是为了防止由于某个具体参数(如某个商品 ID、某个用户 ID)的突发高并发请求导致系统崩溃而设计的。
与 Sentinel 普通的基于资源的限流(使用滑动时间窗口 LeapArray)不同,热点参数限流在底层实现上有着显著的区别。它的核心实现原理可以概括为:LRU 缓存淘汰算法 + 令牌桶算法(动态计算)。
下面从核心组件、数据结构、算法细节等几个维度来深入剖析它的实现机制。
1. 为什么不能用普通的滑动窗口?
如果使用普通的滑动窗口统计每个参数的 QPS,假设有一个接口带有 userId 参数,系统里有千万级别的活跃用户,如果为每一个 userId 都创建一个滑动窗口(LeapArray),会导致极其严重的内存溢出(OOM)。
因此,热点参数限流必须解决“无限的参数值空间”与“有限的内存资源”之间的矛盾。
2. 核心数据结构:LRU Map
为了解决内存问题,Sentinel 引入了 LRU(Least Recently Used,最近最少使用) 策略。
Sentinel 使用了 Google Guava 内部非常著名的 ConcurrentLinkedHashMap 的变体(ConcurrentLinkedHashMapWl)作为底层存储结构。
- 作用: 只保留最近被访问到的、最活跃的参数的统计信息。
- 机制: 当不同的参数值(如不同的 userId)数量超过设定的最大容量(默认是 100000,或由规则指定),最久未被访问的参数的统计信息会被移除。未被记录的参数说明并发不高,不需要限流;只有真正被频繁访问的“热点”参数才会一直留在缓存中并受到限流控制。
3. 核心算法:令牌桶(Token Bucket)的惰性计算
在热点参数限流中,Sentinel 使用的是令牌桶算法,并且采用的是“惰性计算”(Lazy Calculation)的方式,而不是启动一个后台线程定时去发放令牌(后台线程去遍历几万个参数发令牌极其消耗 CPU)。
惰性计算的过程解析:
当一个携带具体参数(比如 userId=123)的请求到达时,底层在 ParamFlowChecker.passCheck() 方法中执行以下逻辑:
- 获取当前时间:获取当前系统的时间戳。
- 查询 LRU 缓存:从 LRU Map 中查找
userId=123对应的上次添加令牌的时间(lastAddTokenTime)和当前剩余令牌数(oldQps)。 - 计算时间差与应发令牌数:
- 如果当前时间与上次添加令牌的时间在同一个统计周期(通常是 1 秒)内,说明不需要新增令牌。
- 如果超过了时间间隔,则计算这段时间内应该生成的令牌数:
新增令牌数 = (当前时间 - 上次添加时间) * (限流阈值 / 1000ms)
- 更新令牌桶:
当前令牌数 = 旧令牌数 + 新增令牌数- 注意:当前令牌数不能超过配置的最大阈值(桶的容量)。
- 扣减令牌与判断:
- 如果当前令牌数 请求需要的令牌数(通常是 1),则允许通过,扣减令牌,并更新 LRU 缓存中的“剩余令牌数”和“最后发放时间”。
- 如果令牌不够,则拒绝请求(抛出
ParamFlowException)。
以上逻辑通过 CAS(Compare-And-Swap)或加锁操作保证并发安全。
4. 源码执行链路
当你的代码执行 SphU.entry(resourceName, EntryType.IN, count, args) 时,包含参数的 args 会被传入 Sentinel 的责任链。
- 请求进入
ParamFlowSlot(热点参数限流槽)。 ParamFlowSlot会检查当前资源是否配置了ParamFlowRule(热点规则)。- 如果配置了,会根据规则中的
paramIdx(参数索引)从args数组中提取出具体的参数值。 - 调用
ParamFlowChecker.passCheck(resourceWrapper, rule, count, value)进行核心校验。 - 校验通过则放行进入下一个 Slot,不通过则抛出异常。
5. 特殊处理:参数例外项(Exception Items)
在实际业务中,我们经常遇到这种需求:“普通商品限流 10 QPS,但 iPhone 14(ID=999)是大促商品,单独给它限流 10000 QPS”。
Sentinel 热点限流支持参数例外项(paramFlowItemList)。
在 ParamFlowChecker 的源码中,逻辑是这样的:
- 首先检查当前提取到的参数值,是否存在于例外项 Map 中。
- 如果存在,则直接使用例外项配置的特定阈值去进行上述的令牌桶计算。
- 如果不存在,才使用规则中配置的全局默认阈值进行计算。
6. 线程数限流模式
除了 QPS 模式,热点参数也支持线程数(Thread Count)限流。
相比于 QPS 的令牌桶,线程数的实现非常简单:
- 依然使用 LRU Map 存储每个参数的当前并发线程数。
- 请求进入时,在 LRU Map 中找到对应参数,原子性地增加计数(
AtomicInteger.incrementAndGet)。 - 如果计数超过阈值,直接拒绝。
- 请求结束退出(
exit)时,原子性地减少计数。
总结
Sentinel 热点参数限流的实现精髓在于:
- 防 OOM:放弃了标准限流的 LeapArray 数组,使用基于
ConcurrentLinkedHashMap的 LRU Cache 来存储状态,只统计真正的热点参数。 - 高性能:放弃了后台定时发令牌的模式,采用基于请求驱动的令牌桶惰性计算(Lazy Token Bucket)算法,极大降低了 CPU 消耗。
- 灵活性:内置支持“例外参数”配置,能够满足精细化运营和大促场景的诉求。