随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
文档
资源的定义:可以是应用、接口、函数或一段代码,目的是保护这段资源的运行。
InitDefault()
:从环境变量指定的配置文件以及环境变量中读取相应配置来初始化 Sentinel,若环境变量不存在则使用默认值。Init(configPath string)
:从给定的 YAML
文件中读取相应配置来初始化 Sentinel。InitWithConfig(confEntity *config.Entity)
: 用户硬编码配置对象*config.Entity
来初始化Sentinel。**埋点(定义资源):**每个埋点都有一个资源名称,代表触发了这个资源的调用和访问
Entry(resource string, opts ...Option) (*base.SentinelEntry, *base.BlockError)
resource表示资源名称,opts表示埋点配置。返回值列表的第一个参数和第二个参数是互斥的,如果pass的话,就会返回(*base.SentinelEntry, nil);
如果执行blocked
,那么Sentinel
会返回(nil, *base.BlockError)
埋点配置:
WithTrafficType(entryType base.TrafficType)
:标记该埋点资源的流量类型,其中 Inbound
代表入口流量,Outbound
代表出口流量。若不指定,默认为 Outbound
。WithResourceType(resourceType base.ResourceType)
:标记该埋点资源的分类。WithAcquireCount(acquireCount uint32)
:标记每次触发该埋点计为几次调用(可以理解为 batch count
)。若不指定,默认为 1。WithArgs(args ...interface{})
:埋点携带的参数列表,为热点参数统计预留。WithSlotChain(chain *base.SlotChain)
:埋点执行的检查的slotchain
,若不指定,默认使用全局slotchain
示例:在这里假设业务逻辑是很复杂的函数,记作ExcuteSteps
,所以在Exit的时候可以使用defer
import (
sentinel "github.com/alibaba/sentinel-golang/api"
)
// Entry 方法用于埋点
e, b := sentinel.Entry("resource-name", sentinel.WithTrafficType(base.Inbound))
if b != nil {
// 请求被流控,可以从 BlockError 中获取限流详情
// block 后不需要进行 Exit()
} else {
// 请求可以通过
// 务必保证业务逻辑结束后 Exit
defer e.Exit()
}
ExcuteSteps()
规则配置:支持硬编码方式或是动态数据源
流控规则为例
_, err = flow.LoadRules([]*flow.Rule{
{
Resource: "some-test",
Threshold: 10,
TokenCalculateStrategy: flow.Direct,
ControlBehavior: flow.Reject,
},
})
if err != nil {
// 加载规则失败,进行相关处理
}
Sentinel Go支持 流控规则、流量隔离规则(并发控制)、熔断规则、自适应过载保护规则和热点参数流控规则。
基于QPS限流的完整示例
import (
sentinel "github.com/alibaba/sentinel-golang/api"
)
func main() {
// 务必先进行初始化
err := sentinel.InitDefault()
if err != nil {
log.Fatal(err)
}
// 配置一条限流规则
_, err = flow.LoadRules([]*flow.Rule{
{
Resource: "some-test",
Threshold: 10,
TokenCalculateStrategy: flow.Direct,
ControlBehavior: flow.Reject,
},
})
if err != nil {
fmt.Println(err)
return
}
ch := make(chan struct{})
for i := 0; i < 10; i++ {
go func() {
for {
// 埋点逻辑,埋点资源名为 some-test
e, b := sentinel.Entry("some-test")
if b != nil {
// 请求被拒绝,在此处进行处理
time.Sleep(time.Duration(rand.Uint64() % 10) * time.Millisecond)
} else {
// 请求允许通过,此处编写业务逻辑
fmt.Println(util.CurrentTimeMillis(), "Passed")
time.Sleep(time.Duration(rand.Uint64() % 10) * time.Millisecond)
// 务必保证业务结束后调用 Exit
e.Exit()
}
}
}()
}
<-ch
}
流量控制是监控资源的统计指标,根据token
计算策略来计算资源的可用token
(阈值),根据流量控制策略对请求进行控制,避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
Sentinel
通过定义流控规则来实现对 Resource
的流量控制。在Sentinel
内部会在加载流控规则时候将每个 flow.Rule
都会被转换成流量控制器(TrafficShapingController)
。 每个流量控制器实例都会有自己独立的统计结构,这里统计结构是一个滑动窗口。Sentinel
内部会尽可能复用 Resource
级别的全局滑动窗口,如果流控规则的统计设置没法复用Resource
的全局统计结构,那么Sentinel
为流量控制器创建一个全新的私有的滑动窗口,然后通过 flow.StandaloneStatSlot
这个统计Slot
来维护统计指标。
type Rule struct {
ID string `json:"id,omitempty"` //ID表示规则的唯一ID(可选)
Resource string `json:"resource"` //资源名字
TokenCalculateStrategy TokenCalculateStrategy `json:"tokenCalculateStrategy"` //当前流量控制器的Token计算策略。Direct表示直接使用字段 Threshold 作为阈值;WarmUp表示使用预热方式计算Token的阈值。
ControlBehavior ControlBehavior `json:"controlBehavior"`//表示流量控制器的控制策略;Reject表示超过阈值直接拒绝,Throttling表示匀速排队。
Threshold float64 `json:"threshold"`//表示流控阈值
RelationStrategy RelationStrategy `json:"relationStrategy"`// 调用关系限流策略
RefResource string `json:"refResource"` //关联的resource
MaxQueueingTimeMs uint32 `json:"maxQueueingTimeMs"` //匀速排队的最大等待时间
WarmUpPeriodSec uint32 `json:"warmUpPeriodSec"` //预热的时间长度
WarmUpColdFactor uint32 `json:"warmUpColdFactor"`//预热的因子,默认是3,该值的设置会影响预热的速度
StatIntervalInMs uint32 `json:"statIntervalInMs"`//规则对应的流量控制器的独立统计结构的统计周期
}
补充:
如果字段StatIntervalInMs
是1000(也就是1秒),那么Threshold
就表示QPS
,流量控制器也就会依据资源的QPS
来做流控。
RelationStrategy
: 调用关系限流策略,CurrentResource
表示使用当前规则的resource
做流控;AssociatedResource
表示使用关联的resource
做流控,关联的resource
在字段 RefResource
定义;
这里特别强调一下 StatIntervalInMs
和 Threshold
这两个字段,这两个字段决定了流量控制器的灵敏度。以 Direct + Reject
的流控策略为例,流量控制器的行为就是在 StatIntervalInMs
周期内,允许的最大请求数量是Threshold
。比如如果 StatIntervalInMs
是 10000,Threshold
是10000,那么流量控制器的行为就是控制该资源10s内运行最多10000次访问。
由规则里的TokenCalculateStrategy
和 ControlBehavior
两个字段决定。
TokenCalculateStrategy
表示流量控制器的Token计算方式:
Direct
表示直接使用规则中的 Threshold
表示当前统计周期内的最大Token
数量。WarmUp
表示通过预热的方式计算当前统计周期内的最大Token
数量,预热的计算方式会根据规则中的字段 WarmUpPeriodSec
和 WarmUpColdFactor
来决定预热的曲线。字段 ControlBehavior
表示表示流量控制器的控制行为:
Reject
:表示如果当前统计周期内,统计结构统计的请求数超过了阈值,就直接拒绝。Throttling
:表示匀速排队的统计策略。它的中心思想是,以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout
时间,则该请求会等待直到预设时间到来通过(排队等待处理);若预期的通过时间超出最大排队时长,则直接拒接这个请求。主要用于处理间隔性突发的流量,例如消息队列,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。每个流量控制器实例都会有自己独立的统计结构。流量控制器的统计结构由规则中的 StatIntervalInMs
字段设置,StatIntervalInMs
表示统计结构的统计周期。Sentinel 默认会为每个Resource创建一个全局的滑动窗口统计结构,这个全局的统计结构默认是一个间隔为10s,20个格子的滑动窗口,也就是每个统计窗口长度是500ms。
流量控制器实例会尽可能复用这个Resource级别的全局统计结构,复用逻辑原则是:优先复用Resource级别的全局统计结构,如果不可复用,就重新创建一个独立的滑动窗口统计结构(BucketLeapArray),具体的逻辑细节如下:
StatIntervalInMs
大于全局滑动窗口的间隔(默认10s),那么将不可复用全局统计结构。Sentinel会给流量控制器创建一个长度是StatIntervalInMs
,格子数是1的全新统计结构,这个全新的统计结构由Sentinel内部的StandaloneStatSlot
来维护统计。StatIntervalInMs
小于全局滑动窗口的窗口长度(默认是500ms), 那么将不可复用全局统计结构。Sentinel会给流量控制器创建一个长度是StatIntervalInMs
,格子数是1的全新统计结构,这个全新的统计结构由Sentinel内部的StandaloneStatSlot
来维护统计。StatIntervalInMs
在集合[全局滑动窗口的窗口长度,全局滑动窗口的间隔]之间,首先需要计算格子数:如果StatIntervalInMs
可以被全局滑动窗口的窗口长度(默认是500ms)整除,那么格子数就为 StatIntervalInMs
/GlobalStatisticBucketLengthInMs
,如果不可整除,格子数是1。然后会调用 core/base/CheckValidityForReuseStatistic
函数来判断当前统计结构间隔和格子数是否可以复用全局统计结构。如果可以复用,就会基于resource级别的全局统计结构ResourceNode
创建SlidingWindow
,SlidingWindow是一个虚拟结构,SlidingWindow只可读,而且读的数据是通过聚合ResourceNode
数据得到的。如果不可复用,就使用统计结构间隔和格子数创建全新的滑动窗口(BucketLeapArray)。CheckValidityForReuseStatistic函数参考:https://github.com/alibaba/sentinel-golang/blob/master/core/base/stat.go#func CheckValidityForReuseStatistic
基于规则创建统计结构的逻辑参考:https://github.com/alibaba/sentinel-golang/blob/master/core/flow/rule_manager.go#func generateStatFor
{
Resource: "some-test",
TokenCalculateStrategy: flow.Direct,
ControlBehavior: flow.Reject,
Threshold: 500,
StatIntervalInMs: 1000,
}
StatIntervalInMs
必须是1000,表示统计周期是1s,那么Threshold
所配置的值也就是QPS
的阈值
{
Resource: "some-test",
TokenCalculateStrategy: flow.Direct,
ControlBehavior: flow.Reject,
Threshold: 10000,
StatIntervalInMs: 10000,
}
在一定统计周期内控制请求的总量。比如上面的例子指的是在10000ms(10s)
里的最大请求数是10000
。但是对于脉冲类型流量的抵抗力很弱!
如果一些流量在毫秒级别的波动很大,那建议StatIntervalInMs
的配置在毫秒级别,除非特殊场景,建议配置的值为100ms
的倍数,比如100,200这种。这种相当于缩小了统计周期,将QPS
的周期缩小了10倍,控制周期降低到了100ms。这种配置能够很好的应对脉冲流量,保障系统稳定性,比如sample:
{
Resource: "some-test",
TokenCalculateStrategy: flow.Direct,
ControlBehavior: flow.Reject,
Threshold: 80,
StatIntervalInMs: 100,
}
上面限制了100ms
的阈值是80,实际QPS
大概是800。
注意:这种配置也是有缺点的,脉冲流量很大可能造成有损(会拒绝很多流量)。
针对前面第三点场景,如果既想控制流量曲线,又想无损,一般做法是通过匀速排队的控制策略,平滑掉流量。
Sentinel 支持关联流量控制策略。当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢。
示例
现在的分布式架构里,一个服务常常会调用第三方服务,比如另一个RPC接口、数据库、API等。如果依赖的外部服务出现了不稳定的情况,比如请求的响应时间变长,那么服务自身调用第三方服务的响应时间也会变长,自身的线程可能会产生堆积,耗尽业务自身的线程池,最终服务本身也变得不可用。因此我们需要对不稳定的 弱依赖服务调用 进行 熔断降级,暂时切断不稳定的服务调用,避免局部不稳定因素导致整个分布式系统的雪崩。熔断降级作为保护服务自身的手段,通常在**客户端(调用端)**进行配置。
比如A服务需要调用B服务的接口,在A服务里需要配置熔断器,避免B服务不稳定从而拖垮A服务。
衡量下游服务质量,指标是RT、异常数量和异常比例等等。支持三种熔断策略:慢调用比例熔断、异常比例熔断和异常数量熔断。
用户通过设置熔断规则(Rule)来给资源添加熔断器。Sentinel会将每一个熔断规则转换成对应的熔断器,熔断器对用户是不可见的。Sentinel 的每个熔断器都会有自己独立的统计结构。
熔断器的整体检查逻辑可以用几点来精简概括:
静默期: 三种熔断策略都支持静默期,静默期指的是最小的静默请求数,在一个统计周期里,如果对资源的请求数小于设置的静默数,熔断器不会基于统计值去更改熔断器的状态。
比如在统计周期刚开始的时候,第一个请求恰好是慢请求,这时候慢调用比例是100%,显然不合理。静默期提高了熔断器的精准性且降低了误判的可能性。
支持的几种熔断策略:(前提都是不在静默期)
**慢调用比例策略:**慢调用的比例大于设置的阈值,需要设置RT临界值(最大响应时间),对该资源访问的响应时间大于该阈值即为慢调用。
错误比例策略: 统计周期内资源请求访问异常的比例大于阈值。
错误计数策略: 请求访问异常数大于设定的阈值。
如果规则指定熔断器策略采用错误比例或则错误计数,那么为了统计错误比例或错误计数,需要调用API:
api.TraceError(entry, err)
埋点每个请求的业务异常。
// Rule encompasses the fields of circuit breaking rule.
type Rule struct {
Id string `json:"id,omitempty"`//全局唯一ID 可选
Resource string `json:"resource"` //资源名称
Strategy Strategy `json:"strategy"` //熔断策略
RetryTimeoutMs uint32 `json:"retryTimeoutMs"`//熔断出发后持续时间 ms
MinRequestAmount uint64 `json:"minRequestAmount"` //静默数量
StatIntervalMs uint32 `json:"statIntervalMs"` //统计的时间窗口长度 ms
MaxAllowedRtMs uint64 `json:"maxAllowedRtMs"` //最大响应时间
Threshold float64 `json:"threshold"` //阈值
}
补充说明:
Strategy
: 熔断策略,目前支持SlowRequestRatio
、ErrorRatio
、ErrorCount
三种;
选择以慢调用比例
(SlowRequestRatio) 作为阈值,需要设置允许的最大响应时间
(MaxAllowedRtMs),**请求的响应时间大于该值则统计为慢调用**。通过
Threshold字段设置触发熔断的慢调用比例,取值范围为
[0.0, 1.0]。规则配置后,**在单位统计时长内请求数目大于设置的最小请求数目(静默期),并且慢调用的比例大于阈值**,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求响应时间小于设置的最大
RT则结束熔断,若大于设置的最大
RT` 则会再次被熔断。(ErrorRatio)
作为阈值,需要设置触发熔断的异常比例(Threshold)
,取值范围为 [0.0, 1.0]
。规则配置后,在单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求没有错误则结束熔断,否则会再次被熔断。代码中可以通过 api.TraceError(entry, err)
函数来记录 error
。Threshold
: 对于慢调用熔断策略,Threshold
表示是慢调用比例的阈值(小数表示,比如0.1表示10%),也就是如果当前资源的慢调用比例如果高于Threshold
,那么熔断器就会断开;否则保持闭合状态。 对于错误比例策略,Threshold
表示的是错误比例的阈值(小数表示,比如0.1表示10%)。对于错误数策略,Threshold
是错误计数的阈值
Resource
、Strategy
、RetryTimeoutMs
、MinRequestAmount
、StatIntervalMs
、Threshold
每个规则都必设的字段,MaxAllowedRtMs
是慢调用比例熔断规则必设的字段。
MaxAllowedRtMs
字段仅仅对慢调用比例 (SlowRequestRatio)
策略有效,对其余策略均属于无效字段。
StatIntervalMs
表示熔断器的统计周期,单位是毫秒,这个值我们不建议设置的太大或则太小,一般情况下设置10秒左右都OK,当然也要根据实际情况来适当调整。
RetryTimeoutMs
的设置需要根据实际情况设置探测周期,一般情况下设置10秒左右都OK,当然也要根据实际情况来适当调整。
配置参考
// 慢调用比例规则
rule1 := &Rule{
Resource: "abc",
Strategy: SlowRequestRatio,
RetryTimeoutMs: 5000,
MinRequestAmount: 10,
StatIntervalMs: 10000,
MaxAllowedRtMs: 20,
Threshold: 0.1,
},
// 错误比例规则
rule1 := &Rule{
Resource: "abc",
Strategy: ErrorRatio,
RetryTimeoutMs: 5000,
MinRequestAmount: 10,
StatIntervalMs: 10000,
Threshold: 0.1,
},
// 错误计数规则
rule1 := &Rule{
Resource: "abc",
Strategy: ErrorCount,
RetryTimeoutMs: 5000,
MinRequestAmount: 10,
StatIntervalMs: 10000,
Threshold: 100,
},
熔断器一般用于应用对外部资源访问时的保护措施。这里简单描述一些场景:
demo
并发隔离控制是基于资源访问的并发协程数来控制对资源的访问,也就是控制资源访问的最大并发数,避免因为资源的异常导致协程耗尽。
// Rule describes the concurrency num control, that is similar to semaphore
type Rule struct {
ID string `json:"id,omitempty"`
Resource string `json:"resource"` //资源名称
MetricType MetricType `json:"metricType"`//目标度量类型
Threshold uint32 `json:"threshold"`//阈值
}
配置示例:其中Concurrency
表示并发数,如果资源的当前并发数超过阈值,资源将不可访问。
r1 := &Rule{
Resource: "abc",
MetricType: Concurrency,
Threshold: 100,
}
在客户端(调用端) 做一层软隔离(并发隔离控制),达到对资源访问的并发控制的目的
demo
package main
import (
sentinel "github.com/alibaba/sentinel-golang/api"
"github.com/alibaba/sentinel-golang/core/config"
"github.com/alibaba/sentinel-golang/core/isolation"
"github.com/alibaba/sentinel-golang/logging"
"math/rand"
"os"
"testing"
"time"
)
func Test_concurrency_limitation(t *testing.T) {
cfg := config.NewDefaultConfig() //生成默认配置
cfg.Sentinel.Log.Logger = logging.NewConsoleLogger() //控制台日志
err := sentinel.InitWithConfig(cfg) //初始化
if err != nil {
logging.Error(err, "fail")
os.Exit(1)
}
logging.ResetGlobalLoggerLevel(logging.DebugLevel) //设置日志级别 debug
ch := make(chan struct{}) //通道
r1 := &isolation.Rule{ //定义规则
Resource: "abc",
MetricType: isolation.Concurrency, //并发数
Threshold: 12, //阈值
} //并发数>12触发熔断器
_, err = isolation.LoadRules([]*isolation.Rule{r1})
if err != nil {
logging.Error(err, "fail")
os.Exit(1)
}
for i := 0; i < 15; i++ { //开辟15个协程
go func() {
for {
e, b := sentinel.Entry("abc", sentinel.WithBatchCount(1)) //WithBatchCount 表示每次占用的资源个数
if b != nil { //限流
logging.Info("[Isolation] Blocked", "reason", b.BlockType().String(), "rule", b.TriggeredRule(), "snapshot", b.TriggeredValue())
time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)
} else { //通过 处理业务逻辑
logging.Info("[Isolation] Passed")
time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)
e.Exit() //务必要写!
}
}
}()
}
<-ch //保证主进程不退出
}
阿里 双11 同款,流量防卫兵 Sentinel go 源码解读
Sentinel-Go 源码系列(一)|开篇
Sentinel Go 核心统计结构滑动窗口的深度解析