限流部分
对象
一般是服务提供方做限流,防止异常流量把服务打挂
原因
如果某接口请求量激增,服务机器资源耗尽,影响其他接口,恶性循环
场景
全局限流
单接口限流
调用方限流
接口+调用方限流
接口参数限流
粒度
一般微服务都做了负载均衡,所以单实例维度进行限流也是可以的,B栈的限流就是单实例限流
下面介绍b站目前两种限流器,一个是bbr,一个是令牌桶限流器 \
bbr
首先是bbr限流,采用滑动窗口统计数据,通过综合分析服务的 cpu 使用率、请求通过的 qps 和请求成功的 rt 来做自适应限流保护;针对的场景理论不限制,因为存储的是一个key,key可以按照接口,调用方等维度来传,不过在kratos里面,B栈的HTTP、grpc接口都是用的路径做的key \
下面介绍几个用到的包
git.bilibili.co/platform/go-common/library/container/group/group.go
Group结构体是一个懒加载的容器,容器内存储数据的是一个sync.Map结构,键值对中的value通过结构体的new方法生成,用一个读写锁保证new的读写安全,Group支持初始化,获取,重置,清除操作,之所以说是懒加载具体体现在没有set操作,get的时候如果不存在通过new生成一个value,这样的一个懒加载的工具会被作为 承载一组限流器的容器
// Package group provides a sample lazy load container.
// The group only creating a new object not until the object is needed by user.
// And it will cache all the objects to reduce the creation of object.
package group
import "sync"
// Group is a lazy load container.
type Group struct {
new func() interface{} //方法生成map的value
objs sync.Map //存储数据
sync.RWMutex //读写锁保证new的读写安全
}
// NewGroup news a group container.
初始的时候没有数据,需要赋值new方法
func NewGroup(new func() interface{}) *Group {
if new == nil {
panic("container.group: can't assign a nil to the new function")
}
return &Group{
new: new,
}
}
// Get gets the object by the given key.
func (g *Group) Get(key string) interface{} {
g.RLock()//读锁 安全取出new方法
new := g.new
g.RUnlock()
obj, ok := g.objs.Load(key) //查询key是否存在
if !ok {
obj = new() //不存在通过方法生成对应value 并保存
g.objs.Store(key, obj)
}
return obj
}
// Reset resets the new function and deletes all existing objects.
func (g *Group) Reset(new func() interface{}) {
if new == nil {
panic("container.group: can't assign a nil to the new function")
}
g.Lock()
g.new = new //写锁 更新new方法
g.Unlock()
g.Clear() //并且 删除所有map中存储元素
}
// Clear deletes all objects.
func (g *Group) Clear() {
g.objs.Range(func(key, value interface{}) bool {
g.objs.Delete(key)
return true
})
}
可以看出Group里面可以存储键值对,所以可以用来保存 同一空间下针对key的限流器
下面要介绍的是bbr包,里面是限流器的具体实现,BBR实现了一个限流器,用上面介绍的Group可以承载一组限流器
package bbr
import (
"context"
"math"
"sync"
"sync/atomic"
"time"
"go-common/library/container/group"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/rate/limit"
"go-common/library/stat/metric"
cpustat "go-common/library/stat/sys/cpu"
)
var (
cpu int64
decay = 0.95
//限流器的默认配置
defaultConf = &Config{
Window: time.Second * 10,
WinBucket: 100,
CPUThreshold: 800,
}
cpuProcOnce sync.Once
)
type cpuGetter func() int64
//开启定时job 获取当前CPU
func startCPUProc() {
cpustat.Init()
go cpuproc()
}
func min(l, r uint64) uint64 {
if l < r {
return l
}
return r
}
// cpu = cpuᵗ⁻¹ * decay + cpuᵗ * (1 - decay)
func cpuproc() {
defer func() {
if err := recover(); err != nil {
log.Error("rate.limit.cpuproc() err(%+v)", err)
//可以看出即使panic了,再起一个协程仍然继续获取CPU,可以学习这种job处理方式
go cpuproc()
}
}()
ticker := time.NewTicker(time.Millisecond * 250)
// EMA algorithm: https://blog.csdn.net/m0_38106113/article/details/81542863
for range ticker.C {
stat := &cpustat.Stat{}
cpustat.ReadStat(stat)
stat.Usage = min(stat.Usage, 1000)
prevCPU := atomic.LoadInt64(&cpu)
// 涉及深度学习里面的某种算法,公式在方法上一行注释
curCPU := int64(float64(prevCPU)*decay + float64(stat.Usage)*(1.0-decay))
atomic.StoreInt64(&cpu, curCPU)
}
}
// Stat contains the metrics's snapshot of bbr.
type Stat struct {
CPU int64
InFlight int64
MaxInFlight int64
MinRt int64
MaxPass int64
}
// BBR implements bbr-like limiter.
// It is inspired by sentinel.
// https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81
type BBR struct {
cpu cpuGetter
passStat metric.RollingCounter //统计请求通过数
rtStat metric.RollingCounter //统计接口响应时间
inFlight int64
winBucketPerSec int64 // time.Second / conf.Window / conf.WinBucket,WinBucket表示一个Window包括多少个Bucket
conf *Config //限流器配置
prevDrop atomic.Value //上一次丢掉请求的时间
}
// Config contains configs of bbr limiter. 限流器的配置
type Config struct {
Enabled bool
Window time.Duration
WinBucket int
Rule string
Debug bool
CPUThreshold int64
}
//当前Window中统计每个Bucket中的请求未被限流的个数,取最大值
func (l *BBR) maxPASS() int64 {
val := int64(l.passStat.Reduce(func(iterator metric.Iterator) float64 {
var result = 1.0
for iterator.Next() { //对bucket循环
bucket := iterator.Bucket() //取出bucket
count := 0.0
for _, p := range bucket.Points { //对bucket内 通过请求 求和
count += p
}
result = math.Max(result, count) //取最大值
}
return result
}))
if val == 0 {
return 1
}
return val
}
//当前Window中统计每个Bucket中的请求平均响应时间,取最小值
func (l *BBR) minRT() int64 {
val := l.rtStat.Reduce(func(iterator metric.Iterator) float64 {
var result = math.MaxFloat64
for iterator.Next() {
bucket := iterator.Bucket()
if len(bucket.Points) == 0 {
continue
}
total := 0.0
for _, p := range bucket.Points {
total += p
}
avg := total / float64(bucket.Count) //应该是总响应时间/响应个数
result = math.Min(result, avg)
}
return result
})
return int64(math.Ceil(val))
}
func (l *BBR) maxFlight() int64 {
return int64(math.Floor(float64(l.maxPASS()*l.minRT()*l.winBucketPerSec)/1000.0 + 0.5))
}
//判断是否丢掉这次请求 判断cpu 上次限流时间 当前系统容量
func (l *BBR) shouldDrop() bool {
if l.cpu() < l.conf.CPUThreshold {
prevDrop, ok := l.prevDrop.Load().(time.Time)
if !ok {
return false
}
if time.Since(prevDrop) <= time.Second {
inFlight := atomic.LoadInt64(&l.inFlight)
return inFlight > 1 && inFlight > l.maxFlight()
}
return false
}
inFlight := atomic.LoadInt64(&l.inFlight)
drop := inFlight > 1 && inFlight > l.maxFlight()
if drop {
l.prevDrop.Store(time.Now())
}
return drop
}
// 方法判断是否限流
func (l *BBR) Allow(ctx context.Context, opts ...limit.AllowOption) (func(info limit.DoneInfo), error) {
allowOpts := limit.DefaultAllowOpts()
for _, opt := range opts {
opt.Apply(&allowOpts)
}
if l.shouldDrop() {
return nil, ecode.LimitExceed //返回限流错误码
}
atomic.AddInt64(&l.inFlight, 1)
stime := time.Now()
return func(do limit.DoneInfo) {
rt := int64(math.Ceil(float64(time.Since(stime)) / float64(time.Millisecond)))
l.rtStat.Add(rt)
atomic.AddInt64(&l.inFlight, -1)
switch do.Op {
case limit.Success:
l.passStat.Add(1) //没有限流 就通过统计+1
return
default:
return
}
}, nil
}
//返回一个接口Limiter,实际动态类型是实现了接口的bbr结构体
这个方法作为group里面的new方法返回key对应的限制器的作用,可以看下面的NewGroup方法
func newLimiter(conf *Config) limit.Limiter {
if conf == nil {
conf = defaultConf
}
size := conf.WinBucket
bucketDuration := conf.Window / time.Duration(conf.WinBucket)
passStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: size, BucketDuration: bucketDuration})
rtStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: size, BucketDuration: bucketDuration})
cpu := func() int64 {
return atomic.LoadInt64(&cpu)
}
limiter := &BBR{
cpu: cpu,
conf: conf,
passStat: passStat,
rtStat: rtStat,
winBucketPerSec: int64(time.Second) / (int64(conf.Window) / int64(conf.WinBucket)),
}
return limiter
}
//Group用来装在一组bbr限制器,里面是上面介绍的懒加载的Group结构体
type Group struct {
group *group.Group
}
func NewGroup(conf *Config) *Group {
cpuProcOnce.Do(startCPUProc) //先开启CPU加载的job,限制器工作的时候需要
if conf == nil {
conf = defaultConf
}
group := group.NewGroup(func() interface{} {
return newLimiter(conf) //给Group new方法 ,方法返回一个限制器
})
return &Group{
group: group,
}
}
//group有get方法,内部就是调用结构体Group的get方法获取限制器
func (g *Group) Get(key string) limit.Limiter {
limiter := g.group.Get(key)
return limiter.(limit.Limiter)
}
接下来就在 HTTP中注入限流器的中间件
结构体RateLimiter 里面包含了一个bbr.Group,通过NewRateLimiter方法 初始化,最重要的是Limit()方法返回一个中间件handler,返回的方法里面调用了路径对应的限流器的allow方法判断是否限流
git.bilibili.co/platform/go-common/library/net/http/blademaster/ratelimit.go
package blademaster
import (
"sync/atomic"
"time"
"go-common/library/log"
"go-common/library/rate/limit"
"go-common/library/rate/limit/bbr"
"go-common/library/stat/metric"
)
var (
_metricServerBBR = metric.NewCounterVec(&metric.CounterVecOpts{
Namespace: serverNamespace,
Subsystem: "",
Name: "bbr_total",
Help: "http server bbr total.",
Labels: []string{"url"},
})
)
// RateLimiter bbr middleware.
type RateLimiter struct {
group *bbr.Group
logTime int64
}
// New return a ratelimit middleware.
func NewRateLimiter(conf *bbr.Config) (s *RateLimiter) {
return &RateLimiter{
group: bbr.NewGroup(conf),
logTime: time.Now().UnixNano(),
}
}
// Limit return a bm handler func.
func (b *RateLimiter) Limit() HandlerFunc {
return func(c *Context) {
limiter := b.group.Get(c.RoutePath) //针对路由路径做限流
done, err := limiter.Allow(c) //调用allow方法判断是否限流
b.printStats(c.RoutePath, limiter, err == nil)
if err != nil {
routePath := c.RoutePath
if routePath == "" {
routePath = "/"
}
_metricServerBBR.Inc(routePath[1:])
c.JSON(nil, err)
c.Abort()
return
}
defer func() {
done(limit.DoneInfo{Op: limit.Success})
}()
c.Next()
}
}
在HTTP的newServer方法里面将限流器加入到了中间件中 git.bilibili.co/platform/go-common/library/net/http/blademaster/server.go
// DefaultServer returns an Engine instance with the Recovery, Logger and CSRF middleware already attached.
func DefaultServer(conf *ServerConfig, options ...ServerOption) *Engine {
engine := NewServer(conf, options...)
engine.Use(Recovery(), Trace(), Logger(), CSRF(), Mobile(), AuroraHandler())
if !_httpDisableRateLimit {
engine.Use(NewRateLimiter(nil).Limit()) //返回handler,里面是限流逻辑
}
engine.Use(Mirror())
log.Info("blademaster: rate limit is enabled: %t", !_httpDisableRateLimit)
return engine
}
grpc 的限流,可以看到RateLimiter结构体里面也是有bbr.Group 来做限流逻辑,它的 Limit() 方法也是返回grpc的一个截断器,截断器里面的逻辑执行了对应url的限流器的allow方法判断是否需要限流
git.bilibili.co/platform/go-common/library/net/rpc/warden/ratelimiter/bbr/ratelimiter.go
package bbr
import (
"context"
"sync/atomic"
"time"
"go-common/library/log"
"go-common/library/rate/limit"
"go-common/library/rate/limit/bbr"
"go-common/library/stat/metric"
"google.golang.org/grpc"
)
var (
_metricServerBBR = metric.NewCounterVec(&metric.CounterVecOpts{
Namespace: "grpc_server",
Subsystem: "",
Name: "bbr_total",
Help: "grpc server bbr total.",
Labels: []string{"url"},
})
)
// RateLimiter bbr middleware.
type RateLimiter struct {
group *bbr.Group
logTime int64
}
// New return a ratelimit middleware.
func New(conf *bbr.Config) (s *RateLimiter) {
return &RateLimiter{
group: bbr.NewGroup(conf),
logTime: time.Now().UnixNano(),
}
}
// Limit is a server interceptor that detects and rejects overloaded traffic.
func (b *RateLimiter) Limit() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
uri := args.FullMethod
limiter := b.group.Get(uri) //grpc的请求方法路径作为key做限流
done, err := limiter.Allow(ctx) //是否限流,
b.printStats(uri, limiter, err == nil)
if err != nil { //触发限流
_metricServerBBR.Inc(uri)
return
}
defer func() {
done(limit.DoneInfo{Op: limit.Success})
}()
resp, err = handler(ctx, req)
return
}
}
在grpc 的newServer 方法中,添加了实现限流逻辑的拦截器 git.bilibili.co/platform/go-common/library/net/rpc/warden/server.go
// NewServer returns a new blank Server instance with a default server interceptor.
func NewServer(conf *ServerConfig, opt ...grpc.ServerOption) (s *Server) {
if conf == nil {
s = new(Server)
......
opt = append(opt, keepParam, grpc.UnaryInterceptor(s.interceptor))
s.server = grpc.NewServer(opt...)
if !_grpcDisableRateLimit {
s.Use(bbr.New(nil).Limit()) //这里添加了限流器功能的拦截器
}
log.Info("warden: rate limit is enabled: %t", !_grpcDisableRateLimit)
s.Use(mirrorhandler.WardenServerInterceptor())
s.Use(auroragrpc.ServerInterceptor())
return
}
这是HTTP的限流器实现大概图解,可以看出最外层是ratelimiter结构,里面有Group结构,group里面有键值对承载一组限流器,限流器有allow方法处理限流逻辑
HTTP 和 grpc 里面限流结构体是一样的,对比代码很多东西是相同的
但是limit返回的不一样,grpc是截断器 HTTP返回的是中间件
总结
其实bbr也可以实现 接口,调用方或者其他维度的限流,这个都是可以扩展的,但是在kratos上层 的http ,grpc应用都是只采用了针对接口的限流,不同接口在数据上区别体现在于每一个bbr结构底层参数都不一样,性能比较好的滑动窗口里面统计的响应时间更短,处理更快,在执行接口对应的allow方法中计算公式的参数值是不一样的
令牌桶
也是单实例限流组件,针对rule对应的key限制频次,应该是不考虑cpu等参数的,单纯的限制QPS
git.bilibili.co/platform/go-common/library/net/rpc/warden/ratelimiter/tokenbucket/limiter.go \
在服务newserver的时候使用下面包提供的方法返回相关中间件 new的时候传入配置,配置里面说明限制类型和限制针对的key,阈值等
// Limiter controls how frequently events are allowed to happen.
type Limiter struct {
rbs map[string]*ruleBucket
}
// Config limitter conf.
type Config []*Limit
// Limit limit conf.
type Limit struct {
Rule string //限制类型
Key string //针对的key 比如接口名
Limit rate.Limit
Burst int
}
// New return Limiter. 自已自定义rule传入
func New(conf Config, rs ...Rule) (l *Limiter) {
// 默认内置 rule
rs = append(rs, RuleCaller{}, RuleMethod{}, RuleMethodCaller{})
rbs := make(map[string]*ruleBucket, len(rs))
for _, r := range rs {
if _, ok := rbs[r.Name()]; ok {
continue
}
rb := newRuleBucket(r)
rbs[r.Name()] = rb
}
l = &Limiter{rbs}
l.Reload(conf)
return
}
// Limit is a server interceptor that detects and rejects overloaded traffic.
func (l *Limiter) Limit() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
ok, name, key := l.Allow(ctx, req, args)
if !ok {
_metricServerTB.Inc(name, key)
err = ecode.LimitExceed
log.Warn("warden tokenbucket limit name(%s) key(%s)", name, key)
return
}
resp, err = handler(ctx, req)
return
}
}
func (rb *ruleBucket) Reload(conf Config) {
olm, _ := rb.limMap.Load().(limiterMap)
lm := make(limiterMap, len(conf))
for _, v := range conf {
k := v.Key
if l, ok := olm[k]; ok && l.Limit() == v.Limit && l.Burst() == v.Burst {
lm[k] = l
} else {
lm[k] = rate.NewLimiter(v.fix())
}
}
rb.limMap.Store(lm)
}
具体使用如下
// New new grpc server
func New(svc *service.Service, rateConf rate.Config) (wsvr *warden.Server, err error) {
rm := rate.New(rateConf)
wsvr.Use(rm.Limit()) //use中间件
// quota
quotaLimit := quota.New(conf.Conf.Quota)
wsvr.Use(quotaLimit.Limit())
//这里注册服务逻辑省略
if wsvr, err = wsvr.Start(); err != nil {
return
}
return
}
配置示例如下
`[[RateLimit]]`
`rule = ``"method_caller"`
`key = ``"/live.x.v1.Guard/Get|live.live.test"` `# 用 | 分隔 method 和 caller`
`limit = 1000.0 ``# 注意一定要有小数点`
`burst = 1000`
`[[RateLimit]]`
`rule = ``"caller"`
`key = ``"live.live.test"`
`limit = 1000.0`
`burst = 1000`
`[[RateLimit]]`
`rule = ``"method"`
`key = ``"/live.x.v1.Guard/Get"`
`limit = 1000.0`
`burst = 1000`
可以看出一共提供三种限流,针对方法的,方法加caller的,caller的,这三种分别提供了三种结构体,提供LimitKey方法来解析请求参数中对应的key
// RuleCaller 以 caller 限流的规则
type RuleCaller struct{}
// Name 规则名
func (r RuleCaller) Name() string {
return "caller"
}
// LimitKey 组合 key
func (r RuleCaller) LimitKey(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo) string {
caller := metadata.String(ctx, metadata.Caller)
if caller == "" {
caller = "no_user"
}
return caller
}
// RuleMethod 以 method 限流的规则
type RuleMethod struct{}
// Name 规则名
func (r RuleMethod) Name() string {
return "method"
}
// LimitKey 组合 key
func (r RuleMethod) LimitKey(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo) string {
return args.FullMethod
}
// RuleMethodCaller 组合 method caller 限流的规则
type RuleMethodCaller struct {
rc RuleCaller
rm RuleMethod
}
// Name 规则名
func (r RuleMethodCaller) Name() string {
return "method_caller"
}
// LimitKey 组合 key
func (r RuleMethodCaller) LimitKey(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo) string {
rmKey := r.rm.LimitKey(ctx, req, args)
if rmKey == "" {
return ""
}
return fmt.Sprintf("%s|%s", rmKey, r.rc.LimitKey(ctx, req, args))
}
最终找到key对应的limiter来判断是否限流
参数粒度的限流,用自定义rule
// Rule 定义限流规则
type Rule interface {
// 返回规则名
Name() string
// 从请求中抽取 limiterMap 的 key,抽不出返回空字符串
LimitKey(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo) string
}
rule是一个接口,上面三个也是实现了这个接口 从而进行三种方式的限流,所以自定义一个类型的限流只要满足接口即可,这种能力下我们可以实现更细粒度的限流,比如某个接口的请求参数特定值来限流(比如接口请求传递了请求来源参数,我想要限制来源是1的请求),在LimitKey方法中通过类型断言提取出来源参数的值返回,配置文件里面的key填想要限流的参数,如果提取值和配置的一致就能拿到对应的限流器,就可以进行参数特定值粒度的限流了,这个粒度真的很细很定制化了
熔断部分
对象
可以是调用方也可以是服务方,调用方发现下游有异常节点,熔断防止雪崩,采用降级逻辑
原因
可以下游节点异常,继续调用可以回引起雪崩
场景
接口熔断
目前熔断是根据 请求的成功率来判断熔断与否
目前框架中集成的熔断有http熔断,grpc熔断,db熔断这3种,HTTP熔断器的粒度是每个URL一个熔断器;grpc熔断器的粒度是每个method一个熔断器;db熔断器的粒度是每个Addr一个熔断器,该Addr是通过DSN解析出来的Addr;用户可以自定义熔断器
目前kratos支持熔断的能力,用户可以自定义熔断器
git.bilibili.co/platform/go-common/library/net/netutil/breaker/breaker.go
这里也用到了上面说的懒加载容器Group来承载一组熔断器,NewGroup方法里面给group结构体添加了new方法,可以返回一个熔断器实例,newBreaker方法返回一个实现了接口breaker的结构体实例,这个实例具体实现了熔断逻辑,这里面的方法和上面差不多,不累述
package breaker
import (
"sync"
"time"
"go-common/library/container/group"
xtime "go-common/library/time"
)
// Config broker config.
type Config struct {
SwitchOff bool // breaker switch,default off.
// Hystrix
Ratio float32
Sleep xtime.Duration
// Google
K float64
Window xtime.Duration
Bucket int
Request int64
}
// Breaker is a CircuitBreaker pattern.
// FIXME on int32 atomic.LoadInt32(&b.on) == _switchOn
type Breaker interface {
Allow() error
MarkSuccess()
MarkFailed()
}
type Group struct {
group *group.Group
}
const (
// StateOpen when circuit breaker open, request not allowed, after sleep
// some duration, allow one single request for testing the health, if ok
// then state reset to closed, if not continue the step.
StateOpen int32 = iota
// StateClosed when circuit breaker closed, request allowed, the breaker
// calc the succeed ratio, if request num greater request setting and
// ratio lower than the setting ratio, then reset state to open.
StateClosed
// StateHalfopen when circuit breaker open, after slepp some duration, allow
// one request, but not state closed.
StateHalfopen
//_switchOn int32 = iota
// _switchOff
)
var (
_mu sync.RWMutex
_conf = &Config{
Window: xtime.Duration(3 * time.Second),
Bucket: 10,
Request: 100,
Sleep: xtime.Duration(500 * time.Millisecond),
Ratio: 0.5,
// Percentage of failures must be lower than 33.33%
K: 1.5,
// Pattern: "",
}
_group = NewGroup(_conf)
)
// newBreaker new a breaker.
func newBreaker(c *Config) (b Breaker) {
// factory
return newSRE(c)
}
func NewGroup(conf *Config) *Group {
if conf == nil {
conf = _conf
} else {
conf.fix()
}
group := group.NewGroup(func() interface{} {
return newBreaker(conf)
})
return &Group{
group: group,
}
}
// Get get a breaker by a specified key, if breaker not exists then make a new one.
func (g *Group) Get(key string) Breaker {
brk := g.group.Get(key)
return brk.(Breaker)
}
// Reload reload the group by specified config, this may let all inner breaker
// reset to a new one.
func (g *Group) Reload(conf *Config) {
if conf == nil {
return
}
conf.fix()
g.group.Reset(func() interface{} {
return newBreaker(conf)
})
}
具体实现breaker接口的结构体在
git.bilibili.co/platform/go-common/library/net/netutil/breaker/sre_breaker.go
sreBreaker结构体实现了上面的Breaker接口,他的allow 方法里面有具体的处理熔断逻辑,
newSRE 方法返回一个动态类型是sreBreaker 的Breaker接口,重点在allow里面的逻辑
package breaker
import (
"math"
"math/rand"
"sync"
"sync/atomic"
"time"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/stat/metric"
)
// sreBreaker is a sre CircuitBreaker pattern.
type sreBreaker struct {
stat metric.RollingCounter
k float64
request int64
state int32
r *rand.Rand
rLock sync.Mutex
}
func newSRE(c *Config) Breaker {
counterOpts := metric.RollingCounterOpts{
Size: c.Bucket,
BucketDuration: time.Duration(int64(c.Window) / int64(c.Bucket)),
}
stat := metric.NewRollingCounter(counterOpts)
return &sreBreaker{
stat: stat,
r: rand.New(rand.NewSource(time.Now().UnixNano())),
request: c.Request,
k: c.K,
state: StateClosed,
}
}
func (b *sreBreaker) summary() (success int64, total int64) {
b.stat.Reduce(func(iterator metric.Iterator) float64 {
for iterator.Next() {
bucket := iterator.Bucket()
total += bucket.Count
for _, p := range bucket.Points {
success += int64(p)
}
}
return 0
})
return
}
func (b *sreBreaker) Allow() error {
success, total := b.summary()
k := b.k * float64(success)
if log.V(5) {
log.Info("breaker: request: %d, succee: %d, fail: %d", total, success, total-success)
}
// check overflow requests = K * success
请求总数小于一定阈值 或者成功率大于某个阈值都不会熔断
if total < b.request || float64(total) < k {
//如果现在开关是开的,说明不允许请求通过,现在就要修改为关闭,允许通过
if atomic.LoadInt32(&b.state) == StateOpen {
atomic.CompareAndSwapInt32(&b.state, StateOpen, StateClosed)
}
return nil
}
//现在情况不允许请求通过,需要把开关打开
if atomic.LoadInt32(&b.state) == StateClosed {
atomic.CompareAndSwapInt32(&b.state, StateClosed, StateOpen)
}
dr := math.Max(0, (float64(total)-k)/float64(total+1))
drop := b.trueOnProba(dr)
if log.V(5) {
log.Info("breaker: drop ratio: %f, drop: %v", dr, drop)
}
if drop {
return ecode.ServiceUnavailable
}
//即使超过了阈值,通过上面的trueOnProba方法 还是会有一部分请求到这里,正常情况而不是返回熔断错误
return nil
}
//应该是在请求结果返回的时候调用下面这两个函数,来统计成功和失败的情况
func (b *sreBreaker) MarkSuccess() {
b.stat.Add(1)
}
func (b *sreBreaker) MarkFailed() {
// NOTE: when client reject requets locally, continue add counter let the
// drop ratio higher.
b.stat.Add(0)
}
func (b *sreBreaker) trueOnProba(proba float64) (truth bool) {
b.rLock.Lock()
truth = b.r.Float64() < proba
b.rLock.Unlock()
return
}
熔断的具体使用是在 数据库连接有用到
// NewMySQL new db and retry connection when has error.
func NewMySQL(c *Config) (db *DB) {
if c.QueryTimeout == 0 || c.ExecTimeout == 0 || c.TranTimeout == 0 {
panic("mysql must be set query/execute/transction timeout")
}
db, err := Open(c)
if err != nil {
log.Error("open mysql error(%v)", err)
panic(err)
}
return
}
// Open opens a database specified by its database driver name and a
// driver-specific data source name, usually consisting of at least a database
// name and connection information.
func Open(c *Config) (*DB, error) {
if c.SlowLog == 0 {
c.SlowLog = xtime.Duration(_slowLogDuration)
}
db := new(DB)
d, err := connect(c, c.DSN)
if err != nil {
return nil, err
}
addr := parseDSNAddr(c.DSN)
brkGroup := breaker.NewGroup(c.Breaker)
brk := brkGroup.Get(addr) //给写连接 设置熔断器
w := &conn{DB: d, breaker: brk, conf: c, addr: addr}
rs := make([]*conn, 0, len(c.ReadDSN))
for _, rd := range c.ReadDSN {
d, err := connect(c, rd)
if err != nil {
return nil, err
}
addr = parseDSNAddr(rd)
brk := brkGroup.Get(addr) //给每一个读连接也设置了熔断器
r := &conn{DB: d, breaker: brk, conf: c, addr: addr}
rs = append(rs, r)
}
db.write = w
db.read = rs
db.master = &DB{write: db.write}
return db, nil
}
grpc的newclient里面也设置了熔断器
// NewClient xroom grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (*Client, error) {
...
client := warden.NewClient(cfg, opts...)
conn, err := client.Dial(context.Background(), "discovery://default/"+AppID)
if err != nil {
return nil, err
}
cli := &Client{}
return cli, nil
}
上面的warden.NewClient 实现方法
func NewClient(conf *ClientConfig, opt ...grpc.DialOption) *Client {
resolver.Register(discovery.Builder())
c := new(Client)
if err := c.SetConfig(conf); err != nil {
panic(err)
}
return c
}
上面的c.SetConfig(conf) 实现
func (c *Client) SetConfig(conf *ClientConfig) (err error) {
...
//这里设置了一个熔断器
if c.breaker == nil {
c.breaker = breaker.NewGroup(conf.Breaker)
} else {
c.breaker.Reload(conf.Breaker)
}
c.mutex.Unlock()
return nil
}
参考限流
https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81
限流的一些算法
一年后追加: 感觉下面解释不清楚,看这片文章
zhuanlan.zhihu.com/p/228412634
计数器算法
-种比较简单的限流实现算法,在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略,当进入下一个时间周期时进行访问次数的清零。
滑动窗口算法
滑动窗口算法的原理是在固定窗口中分割出多个小时间窗口,分别在每个小时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的所有小时间窗口总的计数即可
令牌桶限流算法
也是网络中流量整形或速率限制常用的一种算法。令牌桶算法以一个恒定的速率向桶里放入令牌,如果有新的请求进来希望进行处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。令牌桶算法限制的是流入速率(漏桶是流出速率),允许一定规模的突增流量,最大速率受限于桶的容量和令牌生成速率。可以支持一定程度的突发流量,更适合具有突发特性的流量(这个可以对比漏桶限流,速率是固定的就不适合突发,突发来了可能会被卡住)
漏桶限流算法
桶以固定的速率流出流量,无论水龙头进入的流量是多少,都不改变流出速率。上图中,水龙头处存在突发流量,一共进入 30Mb 数据,分布不均匀,对系统有冲击。经过漏桶算法处理,漏桶以 3 Mbps 速率持续流出数据,为系统做了很好的缓冲。
漏桶算法中,如果桶未满,可以持续接收流量;如果桶已满,流量溢出,后续的流量将无法入桶,会被丢弃。漏桶算法限制的是流出速率,无论流入是多少,都能保证后续系统的请求是平稳的。后续系统感知的流量相对固定,但可能在系统仍有能力处理更多流量的时候,也会被漏桶限制住
是网络中流量整形(Traffic Shaping)和速率限制(Rate Limiting)常用的一种算法。漏桶算法调控了访问流量,使得突发流量可以被整形、去毛刺,为系统提供稳定的访问流量