-
Notifications
You must be signed in to change notification settings - Fork 22
流量控制
流量控制 (Flow Control) 模块,基于令牌桶 (Token Bucket) 的思想,监控资源 (Resource) 的统计指标,然后根据 Token 计算策略来计算资源的可用 Token (也就是流量的阈值),然后根据流量控制策略对请求进行控制,避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
Sentinel 的流量控制实现代码参考:https://github1s.com/sentinel-group/sentinel-rust/tree/main/sentinel/core/flow
Sentinel 通过定义流控规则来实现对 Resource 的流量控制。Sentinel 内部会在加载流控规则(flow::Rule
)时候,将每个规则转换成流量控制器 (flow::TrafficShapingController
)。 每个流量控制器实例都会有自己独立的统计结构,这里统计结构是一个滑动窗口。Sentinel 内部会尽可能复用 Resource 级别的全局滑动窗口,如果流控规则的统计设置没法复用 Resource 的全局统计结构,那么Sentinel会为流量控制器创建一个全新的私有的滑动窗口,然后通过 flow::StandaloneStatSlot
这个统计 Slot 来维护统计指标。
Sentinel 的流量控制组件对 Resource 的检查结果要么通过,要么会 block,对于 block 的流量相当于拒绝。
流控规则的定义如下:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
pub id: Id,
pub resource: String,
pub ref_resource: String,
pub calculate_strategy: CalculateStrategy,
pub control_strategy: ControlStrategy,
pub relation_strategy: RelationStrategy,
pub threshold: f64,
pub max_queueing_time_ms: u32,
pub stat_interval_ms: u32,
pub warm_up_period_sec: u32,
pub warm_up_cold_factor: u32,
pub low_mem_usage_threshold: u64,
pub high_mem_usage_threshold: u64,
pub mem_low_water_mark: u64,
pub mem_high_water_mark: u64,
}
一条流控规则主要由下面的参数组成,我们可以组合这些元素来实现不同的限流效果:
-
id
:规则的唯一标识,使用 API 加载规则时,可省略,默认为随机的 uuid;使用标签宏加载规则时,默认为资源名(同时,标签宏仅支持单一规则)。 -
resource
:资源名,即规则的作用目标。 -
ref_resource
:关联的资源名。 -
calculate_strategy
: 当前流量控制器的 Token 计算策略,是一个CalculateStrategy
类型的枚举变量。Direct
表示直接使用字段threshold
作为阈值;WarmUp
表示使用预热方式计算Token的阈值;MemoryAdaptive
表示根据内存使用情况计算。 -
control_strategy
: 表示流量控制器的控制策略,是一个ControlStrategy
类型的枚举变量。Reject
表示超过阈值直接拒绝,Throttling
表示匀速排队。 -
relation_strategy
: 调用关系限流策略,是一个RelationStrategy
的枚举变量。CurrentResource
表示使用当前规则的resource 做流控;AssociatedResource
表示使用关联的 resource 做流控,关联的 resource 在字段ref_resource
定义。 -
threshold
: 表示流控阈值。如果字段stat_interval_ms
是1000 (也就是1秒),那么threshold
就表示 QPS,流量控制器也就会依据资源的 QPS 来做流控。 -
两个与
CalCulateStrategy::WarmUp
相关的参数:warm_up_period_sec
:,预热的时间长度;warm_up_cold_factor
,预热的因子,默认是3,该值的设置会影响预热的速度。 -
max_queueing_time_ms
: 匀速排队的最大等待时间,该字段仅在控制策略为ControlStrategy::Throttling
时生效。 -
stat_interval_ms
: 规则对应的流量控制器的独立统计结构的统计周期。如果设为1000
ms,也就是统计 QPS。当用户指定的stat_interval_ms
无法复用全局的统计数据时,会自动创建专属于资源的统计数据。 -
四个与
CalculateStrategy::MemoryAdaptive
相关的参数:low_mem_usage_threshold
,high_mem_usage_threshold
,mem_low_water_mark
,mem_high_water_mark
,分别表示内存占用低时的流量阈值,内存占用高时的流量阈值,低内存占用阈值,高内存占用阈值。当内存占用位于区间内部时,通过对两侧的流量阈值线性插值确定此时的流量阈值。当流量用户需要保证参数满足low_mem_usage_threshold > high_mem_usage_threshold && mem_high_water_mark > mem_low_water_mark
。
这里特别强调一下 stat_interval_ms
和 threshold
这两个字段,这两个字段决定了流量控制器的灵敏度。以 CalculateStrategy::Direct
+ ControlStrategy::Reject
的流控策略为例,流量控制器的行为就是在 stat_interval_ms
周期内,允许的最大请求数量是threshold
,超出后直接拒绝。比如,如果 stat_interval_ms
是 10000,threshold
是 10000,那么流量控制器的行为就是 10s 内运行最多 10000 次访问。
Sentinel 的流量控制策略由规则中的 CalculateStrategy
和 ControlStrategy
两个字段决定。CalculateStrategy
表示流量控制器的 Token 计算方式,目前 Sentinel 支持三种:
-
Direct
表示直接使用规则中的Threshold
表示当前统计周期内的最大 Token 数量。 -
WarmUp
表示通过预热的方式计算当前统计周期内的最大 Token 数量,预热的计算方式会根据规则中的字段warm_up_period_sec
和warm_up_cold_factor
来决定预热的曲线。 -
MemoryAdaptive
表示通过内存占用情况,自适应确定阈值(目前自适应策略为线性插值)。
WarmUp 方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。这块设计和 Java版本 类似。 通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
字段 ControlStrategy
表示流量控制器的控制行为,目前 Sentinel 支持两种:
-
Reject
:表示如果当前统计周期内,统计结构统计的请求数超过了阈值,就直接拒绝。 -
Throttling
:表示匀速排队的统计策略。它的中心思想是,以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间,间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过(排队等待处理);若预期的通过时间超出最大排队时长,则直接拒接这个请求。
匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
以下规则代表每 100ms 最多通过一个请求,多余的请求将会排队等待通过,若排队时队列长度大于 500ms 则直接拒绝:
{
resource: "some-test".into(),
calculate_strategy: CalculateStrategy::Direct,
control_strategy: ControlStrategy::Throttling, // 流控效果为匀速排队
threshold: 10, // 请求的间隔控制在 1000/10=100 ms
max_queueing_time_ms: 500, // 最长排队等待时间
}
上面 threshold
是10,Sentinel 默认使用 1s 作为控制周期,表示 1 秒内 10 个请求匀速排队,所以排队时间就是 1000ms/10 = 100ms。
max_queueing_time_ms
设为 0 时代表不允许排队,只控制请求时间间隔,多余的请求将会直接拒绝(注意这仍然与对请求时间间隔没有限制的 Direct
控制策略不同)。
每个流量控制器实例都会有自己独立的统计结构。流量控制器的统计结构由规则中的 stat_interval_ms
字段设置,stat_interval_ms
表示统计结构的统计周期。Sentinel 默认会为每个 resource 创建一个全局的滑动窗口统计结构,这个全局的统计结构默认是一个总时长为 10s, 20 个元素的数组,也就是每个统计窗口的长度是 500ms。
流量控制器实例会尽可能复用这个 resource 级别的全局统计结构,复用逻辑原则是:优先复用 Resource 级别的全局统计结构,如果不可复用,就重新创建一个独立的滑动窗口统计结构 (BucketLeapArray
),具体的逻辑细节如下:
- 如果
stat_interval_ms
大于全局滑动窗口的间隔 (默认10s),那么将不可复用全局统计结构。Sentinel会给流量控制器创建一个长度是stat_interval_ms
,窗口数是1的全新统计结构,这个全新的统计结构由 Sentinel 内部的StandaloneStatSlot
来维护统计。 - 如果
stat_interval_ms
小于全局滑动窗口的窗口长度 (默认是500ms), 那么将不可复用全局统计结构。Sentinel会给流量控制器创建一个长度是stat_interval_ms
,窗口数是 1 的全新统计结构,这个全新的统计结构由 Sentinel 内部的StandaloneStatSlot
来维护统计。 - 如果
stat_interval_ms
在区间 [全局滑动窗口的窗口长度,全局滑动窗口的间隔] 之间,首先需要计算窗口数:如果stat_interval_ms
可以被全局滑动窗口的窗口长度 (默认是500ms) 整除,那么窗口数就为stat_interval_ms
/global_statistic_bucket_length_ms
,如果不可整除,格子数是 1。然后会调用base::check_validity_for_reuse_statistic
函数来判断当前统计结构间隔和格子数是否可以复用全局统计结构。如果可以复用,就会基于 resource 级别的全局统计结构ResourceNode
创建SlidingWindow
,SlidingWindow
是一个只可读的结构,而且读的数据是通过聚合ResourceNode
数据得到的。如果不可复用,就使用统计结构间隔和格子数创建全新的滑动窗口 (BucketLeapArray
)。
Sentinel 支持关联流量控制策略。当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢。
在流控模块(flow)中,Sentinel 会为每条流控规则(flow::Rule
)生成相应的流量调配器(flow::traffic_shaping::Controller
)。Controller
会包含两个 trait object,分别为:
-
flow::traffic_shaping::Calculator
: 根据规则的计算策略计算出当前的流量阈值(如部分策略在一段时间内逐步抬升 QPS 阈值)。 -
flow::traffic_shaping::Checker
: 根据阈值执行相应的检查和调配策略,返回base::TokenResult
指示如何进行调配。
基于对某个资源访问的QPS来做流控,这个是非常常见的场景。流控规则的配置示例如下:
{
resource: "some-test".into(),
calculate_strategy: CalculateStrategy::Direct,
control_strategy: CalculateStrategy::Reject,
threshold: 500,
stat_interval_ms: 1000,
..Default::default()
}
上面样例中的5个字段是必填的。其中 stat_interval_ms 必须是 1000,表示统计周期是 1s,那么 threshold 所配置的值也就是 QPS 的阈值。
这个场景就是想在一定统计周期内控制请求的总量。比如 stat_interval_ms
配置 10000,threshold
配置 10000,这种配置意思就是控制 10s 内最大请求数是 10000。样例:
{
resource: "some-test".into(),
calculate_strategy: CalculateStrategy::Direct,
control_strategy: CalculateStrategy::Reject,
threshold: 10000,
stat_interval_ms: 10000,
..Default::default()
}
注意:这种流控配置对于脉冲类型的流量抵抗力很弱,有极大潜在风险压垮系统。比如流量表现形式是10s内请求数最大是9800,实际上流量可能是脉冲形式,比如下图:
这种类型的流量,其实在前一秒的 QPS 达到了4500,很可能直接把系统打挂了。对于这种类型流量除非是抗脉冲场景,一般建议使用毫秒级别的流控。
注意:这种大周期的配置其实也有好处,就是能够做到流量的无损,前提是保证系统能够抗住这种周期内的脉冲流量,当然如果流量曲线在秒级别比较平顺,也就不存在脉冲问题,我们是建议统计周期可以稍微调大。
针对一些流量曲在毫秒级别波动非常大的场景 (类似于脉冲),建议 stat_interval_ms
的配置在毫秒级别,除非特殊场景,建议配置的值为 100ms 的倍数,比如 100,200 这种。这种相当于缩小了统计周期,将 QPS 的周期缩小了 10 倍,控制周期降低到了100ms。这种配置能够很好的应对脉冲流量,保障系统稳定性,比如下面这个例子:
{
resource: "some-test".into(),
calculate_strategy: CalculateStrategy::Direct,
control_strategy: CalculateStrategy::Reject,
threshold: 80,
stat_interval_ms: 100,
..Default::default()
}
上面限制了 100ms 的阈值是 80,实际 QPS 大概是 800。
注意:这种配置也是有缺点的,脉冲流量很大可能造成有损 (会拒绝很多流量)。
针对前面第三点场景,如果既想控制流量曲线,又想无损,一般做法是通过匀速排队的控制策略,平滑掉流量。