最近考虑把工作中手头的一个鉴权服务加上限流的能力,由于接入业务方越来越多,服务的负载也越来越大,除了扩容外,服务本身也需要有限流降级的自我保护能力,避免被瞬时的流量高峰击垮,从而保障服务的高可用性。

sentinel-go 是阿里开源的限流熔断组件库,已经通过双十一的考验,可以保证该组件的稳定性和有效性,毕竟有大厂的背书,所以我们也考虑使用该组件。

初始化

下载 sentinel-go

1
go get -v -u github.com/alibaba/sentinel-golang

初始化函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 创建默认配置对象
config.NewDefaultConfig() 
// 从环境变量指定的配置文件以及环境变量中读取相应配置来初始化 Sentinel,若环境变量不存在则使用默认值。
api.InitDefault()
// 从给定的 YAML 文件中读取相应配置来初始化 Sentinel。
api.Init(configPath string)
// 用户硬编码配置对象*config.Entity来初始化Sentinel。
api.InitWithConfig(confEntity *config.Entity)
// 使用自定义的 parser 解析器来加载配置,内部还是调用的 InitWithConfig
api.InitWithParser(configBytes []byte, parser func([]byte) (*config.Entity, error))

通用配置项加载策略和配置项可以参考 配置方式使用文档

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import (
	"github.com/alibaba/sentinel-golang/api"
)

func initSentinel() {
	err := api.Init(confPath)
	if err != nil {
		// 初始化 Sentinel 失败
	}
}

配置规则

流控规则定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type Rule struct {
	// 规则的唯一 ID,可选配置
    ID string `json:"id,omitempty"`
	// 资源名,规则的作用目标
    Resource               string                 `json:"resource"`
    // 当前流量控制器的 Token 计算策略。Direct 表示直接使用字段 Threshold 作为阈值;WarmUp 表示使用预热方式计算 Token 的阈值。
    // v1.0.2 新增 MemoryAdaptive,表示根据内存使用情况自适应限流阈值
    TokenCalculateStrategy TokenCalculateStrategy `json:"tokenCalculateStrategy"`
    // 表示流量控制器的控制策略;Reject表示超过阈值直接拒绝,Throttling表示匀速排队。
	ControlBehavior        ControlBehavior        `json:"controlBehavior"`
	// 表示流控阈值;如果字段 StatIntervalInMs 是1000(也就是1秒),那么Threshold就表示QPS,流量控制器也就会依据资源的QPS来做流控。
    Threshold        float64          `json:"threshold"`
    // 调用关系限流策略,CurrentResource表示使用当前规则的resource做流控;AssociatedResource表示使用关联的resource做流控,关联的resource在字段 RefResource 定义;
    RelationStrategy RelationStrategy `json:"relationStrategy"`
    // 关联的resource;
	RefResource      string           `json:"refResource"`
    // 匀速排队的最大等待时间,该字段仅仅对 Throttling ControlBehavior生效;
    MaxQueueingTimeMs uint32 `json:"maxQueueingTimeMs"`
    // 预热的时间长度,该字段仅仅对 WarmUp 的TokenCalculateStrategy生效;
    WarmUpPeriodSec   uint32 `json:"warmUpPeriodSec"`
    // 预热的因子,默认是3,该值的设置会影响预热的速度,该字段仅仅对 WarmUp 的TokenCalculateStrategy生效;
	WarmUpColdFactor  uint32 `json:"warmUpColdFactor"`
	// 规则对应的流量控制器的独立统计结构的统计周期。如果StatIntervalInMs是1000,也就是统计QPS。
	StatIntervalInMs uint32 `json:"statIntervalInMs"`

    // v1.0.2 新增,当 TokenCalculateStrategy 为 MemoryAdaptive 时生效
    // 自适应流量控制算法相关参数
    // 限制条件:LowMemUsageThreshold > HighMemUsageThreshold && MemHighWaterMarkBytes > MemLowWaterMarkBytes
    // 当前使用内存 <= MemLowWaterMarkBytes,则 threshold = LowMemUsageThreshold
    // 当前使用内存 >= MemHighWaterMarkBytes,则threshold = HighMemUsageThreshold
    // 当前使用内存在 (MemLowWaterMarkBytes, MemHighWaterMarkBytes) 之间,threshold 也在(HighMemUsageThreshold, LowMemUsageThreshold)之间
	LowMemUsageThreshold  int64 `json:"lowMemUsageThreshold"`
	HighMemUsageThreshold int64 `json:"highMemUsageThreshold"`
	MemLowWaterMarkBytes  int64 `json:"memLowWaterMarkBytes"`
	MemHighWaterMarkBytes int64 `json:"memHighWaterMarkBytes"`
}

这里特别强调一下 StatIntervalInMs 和 Threshold 这两个字段,这两个字段决定了流量控制器的灵敏度。以 Direct + Reject 的流控策略为例,流量控制器的行为就是在 StatIntervalInMs 周期内,允许的最大请求数量是 Threshold。

比如,如果 StatIntervalInMs 是 10000,Threshold 是 10000,那么流量控制器的行为就是 10s 内运行最多 10000 次访问。

更多流控配置项说明可以查看官方文档

常见场景的规则配置

基于 QPS 对某个资源限流
1
2
3
4
5
6
7
{
    Resource:                "some-test",
    TokenCalculateStrategy:  flow.Direct,
    ControlBehavior:         flow.Reject,
    Threshold:               500,
    StatIntervalInMs:        1000,
}

上面示例中的 5 个字段是必填的,其中 StatIntervalInMs 必须是 1000,表示统计周期是 1s,那么 Threshold 所配置的值也就是 QPS 的阈值。

基于一定统计间隔时间来控制总的请求数

这个场景就是想在一定统计周期内控制请求的总量,比如 StatIntervalInMs 配置 10000,Threshold 配置 10000,这种配置意思就是控制 10s 内最大请求数是 10000。

1
2
3
4
5
6
7
{
    Resource:                "some-test",
    TokenCalculateStrategy:  flow.Direct,
    ControlBehavior:         flow.Reject,
    Threshold:               10000,
    StatIntervalInMs:        10000,
}

注意:这种流控配置对于脉冲类型的流量抵抗力很弱,有极大潜在风险压垮系统。

毫秒级别流控

针对一些流量曲在毫秒级别波动非常大的场景(类似于脉冲),建议 StatIntervalInMs 的配置在毫秒级别,除非特殊场景,建议配置的值为 100ms 的倍数,比如 100,200 这种。这种相当于缩小了统计周期,将 QPS 的周期缩小了10倍,控制周期降低到了 100ms。这种配置能够很好的应对脉冲流量,保障系统稳定性。

1
2
3
4
5
6
7
{
    Resource:                "some-test",
    TokenCalculateStrategy:  flow.Direct,
    ControlBehavior:         flow.Reject,
    Threshold:               80,
    StatIntervalInMs:        100,
}

上面限制了 100ms 的阈值是 80,实际 QPS 大概是 800。

注意:这种配置也是有缺点的,脉冲流量很大可能造成有损(会拒绝很多流量)。

脉冲流量无损

针对前面第三点场景,如果既想控制流量曲线,又想无损,一般做法是通过匀速排队的控制策略,平滑掉流量。

1
2
3
4
5
6
7
8
{
    Resource:                "some-test",
    TokenCalculateStrategy:  flow.Direct,
    ControlBehavior:         flow.Throttling,
    Threshold:               800,
    MaxQueueingTimeMs:       10,
    StatIntervalInMs:        1000,
}

定义资源(埋点)

使用 Sentinel 的 Entry API 将业务逻辑封装起来,这一步称为“埋点”。每个埋点都有一个资源名称(resource),代表触发了这个资源的调用或访问。

埋点 API 位于 api 包中:

1
Entry(resource string, opts ...Option) (*base.SentinelEntry, *base.BlockError)

其中 resource 代表埋点资源名,opts 代表埋点配置。

这里需要注意的是,返回值参数列表的第一个和第二个参数是互斥的,也就是说:

  • 如果 Entry 执行 pass,那么 Sentinel 会返回(*base.SentinelEntry, nil);
  • 如果 Entry 执行 blocked,那么 Sentinel 会返回(nil, *base.BlockError);

目前支持以下埋点配置:

1
2
3
4
5
6
7
8
WithTrafficType(entryType base.TrafficType)
WithResourceType(resourceType base.ResourceType)
WithBatchCount(batchCount uint32) 
WithFlag(flag int32)
WithArgs(args ...interface{})
WithSlotChain(chain *base.SlotChain)
WithAttachment(key interface{}, value interface{})
WithAttachments(data map[interface{}]interface{})

比较常用是 WithTrafficType,标记该埋点资源的流量类型,其中 Inbound 参数代表入口流量,Outbound 参数代表出口流量。若不指定,默认为 Outbound。

埋点 API 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import (
	sentinel "github.com/alibaba/sentinel-golang/api"
)

// sentinel 配置代码省略...

// Entry 方法用于埋点
e, b := sentinel.Entry("your-resource-name", sentinel.WithTrafficType(base.Inbound))
if b != nil {
	// 请求被流控,可以从 BlockError 中获取限流详情
	// block 后不需要进行 Exit()
} else {
	// 请求可以通过,在此处编写您的业务逻辑
	// 务必保证业务逻辑结束后 Exit,切记切记切记!!!
	e.Exit()
}

若该次调用被拒绝,则 Entry API 会返回 BlockError 代表被 Sentinel 限流。BlockError 提供了限流原因以及触发的规则等信息,可以方便开发者获取相关信息进行记录和处理。

配置数据源

硬编码方式

Sentinel 支持原始的硬编码方式加载规则,可以通过各个模块的 LoadRules(rules) 函数加载规则。以流控规则为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
_, err = flow.LoadRules([]*flow.Rule{
	{
		Resource:               "some-test",
		Threshold:              10,
		TokenCalculateStrategy: flow.Direct,
		ControlBehavior:        flow.Reject,
	},
})
if err != nil {
	// 加载规则失败,进行相关处理
}

动态数据源

Sentinel 提供动态数据源接口进行扩展,用户可以通过动态文件、etcd、consul、nacos 等配置中心来动态地配置规则,开发者也可使用其提供的接口扩展配置数据源。

官方动态数据源开源库https://github.com/sentinel-group/sentinel-go-datasources

框架适配器

Sentinel 也提供了常用 Go 微服务框架的集成模块,包括:Gin、Echo、gRPC、go-micro、dubbo-go。

官方适配器开源库https://github.com/sentinel-group/sentinel-go-adapters

参考

sentinel-go 官方文档

https://github.com/alibaba/sentinel-golang

enjoys