一文讲透自适应微服务熔断的原理和实现

// 请求失败

Reject(reason string)

}

Breaker interface {

// 熔断器名称

Name() string

// 熔断方法,执行请求时必须手动上报执行结果

// 适用于简单无需自定义快速失败,无需自定义判定请求结果的场景

// 相当于手动挡。。。

Allow() (Promise, error)

// 熔断方法,自动上报执行结果

// 自动挡。。。

Do(req func() error) error

// 熔断方法

// acceptable - 支持自定义判定执行结果

DoWithAcceptable(req func() error, acceptable Acceptable) error

// 熔断方法

// fallback - 支持自定义快速失败

DoWithFallback(req func() error, fallback func(err error) error) error

// 熔断方法

// fallback - 支持自定义快速失败

// acceptable - 支持自定义判定执行结果

DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error

}

熔断器实现

=====

circuitBreaker 继承 throttle,实际上这里相当于静态代理,代理模式可以在不改变原有对象的基础上增强功能,后面我们会看到 go-zero 这样做的原因是为了收集熔断器错误数据,也就是为了实现可观测性。

熔断器实现采用静态代理模式,看起来稍微有点绕脑。

一文讲透自适应微服务熔断的原理和实现

// 熔断器结构体

circuitBreaker struct {

name string

// 实际上 circuitBreaker熔断功能都代理给 throttle来实现

throttle

}// 熔断器接口

throttle interface {

// 熔断方法

allow() (Promise, error)

// 熔断方法

// DoXXX()方法最终都会该方法

doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error

}

func (cb *circuitBreaker) Allow() (Promise, error) {

return cb.throttle.allow()

}

func (cb *circuitBreaker) Do(req func() error) error {

return cb.throttle.doReq(req, nil, defaultAcceptable)

}

func (cb *circuitBreaker) DoWithAcceptable(req func() error, acceptable Acceptable) error {

return cb.throttle.doReq(req, nil, acceptable)

}

func (cb *circuitBreaker) DoWithFallback(req func() error, fallback func(err error) error) error {

return cb.throttle.doReq(req, fallback, defaultAcceptable)

}

func (cb *circuitBreaker) DoWithFallbackAcceptable(req func() error, fallback func(err error) error,

acceptable Acceptable) error {

return cb.throttle.doReq(req, fallback, acceptable)

}

throttle 接口实现类:

loggedThrottle 增加了为了收集错误日志的滚动窗口,目的是为了收集当请求失败时的错误日志。

// 带日志功能的熔断器

type loggedThrottle struct {

// 名称

name string

// 代理对象

internalThrottle

// 滚动窗口,滚动收集数据,相当于环形数组

errWin *errorWindow

}

// 熔断方法

func (lt loggedThrottle) allow() (Promise, error) {

promise, err := lt.internalThrottle.allow()

return promiseWithReason{

promise: promise,

errWin: lt.errWin,

}, lt.logError(err)

}

// 熔断方法

func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {

return lt.logError(lt.internalThrottle.doReq(req, fallback, func(err error) bool {

accept := acceptable(err)

if !accept {

lt.errWin.add(err.Error())

}

return accept

}))

}

func (lt loggedThrottle) logError(err error) error {

if err == ErrServiceUnavailable {

// if circuit open, not possible to have empty error window

stat.Report(fmt.Sprintf(

“proc(%s/%d), callee: %s, breaker is open and requests dropped\nlast errors:\n%s”,

proc.ProcessName(), proc.Pid(), lt.name, lt.errWin))

}

return err

}

错误日志收集 errorWindow

==================

errorWindow 是一个环形数组,新数据不断滚动覆盖最旧的数据,通过取余实现。

// 滚动窗口

type errorWindow struct {

reasons [numHistoryReasons]string

index int

count int

lock sync.Mutex

}

// 添加数据

func (ew *errorWindow) add(reason string) {

ew.lock.Lock()

// 添加错误日志

ew.reasons[ew.index] = fmt.Sprintf(“%s %s”, timex.Time().Format(timeFormat), reason)

// 更新index,为下一次写入数据做准备

// 这里用的取模实现了滚动功能

ew.index = (ew.index + 1) % numHistoryReasons

// 统计数量

ew.count = mathx.MinInt(ew.count+1, numHistoryReasons)

ew.lock.Unlock()

}

// 格式化错误日志

func (ew *errorWindow) String() string {

var reasons []string

ew.lock.Lock()

// reverse order

for i := ew.index - 1; i >= ew.index-ew.count; i-- {

reasons = append(reasons, ew.reasons[(i+numHistoryReasons)%numHistoryReasons])

}

ew.lock.Unlock()

return strings.Join(reasons, “\n”)

}

看到这里我们还没看到实际的熔断器实现,实际上真正的熔断操作被代理给了 internalThrottle 对象。

internalThrottle interface {

allow() (internalPromise, error)

doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error

}

internalThrottle 接口实现 googleBreaker 结构体定义

=========================================

type googleBreaker struct {

// 敏感度,go-zero中默认值为1.5

k float64

// 滑动窗口,用于记录最近一段时间内的请求总数,成功总数

stat *collection.RollingWindow

// 概率生成器

// 随机产生0.0-1.0之间的双精度浮点数

proba *mathx.Proba

}

可以看到熔断器属性其实非常简单,数据统计采用的是滑动时间窗口来实现。

RollingWindow 滑动窗口

==================

滑动窗口属于比较通用的数据结构,常用于最近一段时间内的行为数据统计。

它的实现非常有意思,尤其是如何模拟窗口滑动过程。

先来看滑动窗口的结构体定义:

RollingWindow struct {

// 互斥锁

lock sync.RWMutex

// 滑动窗口数量

size int

// 窗口,数据容器

win *window

// 滑动窗口单元时间间隔

interval time.Duration

// 游标,用于定位当前应该写入哪个bucket

offset int

// 汇总数据时,是否忽略当前正在写入桶的数据

// 某些场景下因为当前正在写入的桶数据并没有经过完整的窗口时间间隔

// 可能导致当前桶的统计并不准确

ignoreCurrent bool

// 最后写入桶的时间

// 用于计算下一次写入数据间隔最后一次写入数据的之间

// 经过了多少个时间间隔

lastTime time.Duration

}

一文讲透自适应微服务熔断的原理和实现

window 是数据的实际存储位置,其实就是一个数组,提供向指定 offset 添加数据与清除操作。数组里面按照 internal 时间间隔分隔成多个 bucket。

// 时间窗口

type window struct {

// 桶

// 一个桶标识一个时间间隔

buckets []*Bucket

// 窗口大小

size int

}

// 添加数据

// offset - 游标,定位写入bucket位置

// v - 行为数据

func (w *window) add(offset int, v float64) {

w.buckets[offset%w.size].add(v)

}

// 汇总数据

// fn - 自定义的bucket统计函数

func (w *window) reduce(start, count int, fn func(b *Bucket)) {

for i := 0; i < count; i++ {

fn(w.buckets[(start+i)%w.size])

}

}

// 清理特定bucket

func (w *window) resetBucket(offset int) {

w.buckets[offset%w.size].reset()

}

// 桶

type Bucket struct {

// 当前桶内值之和

Sum float64

//当前桶的add总次数

Count int64

}

// 向桶添加数据

func (b *Bucket) add(v float64) {

// 求和

b.Sum += v

// 次数+1

b.Count++

}

// 桶数据清零

func (b *Bucket) reset() {

b.Sum = 0

b.Count = 0

}

window 添加数据:

  1. 计算当前时间距离上次添加时间经过了多少个 时间间隔,实际上就是过期了几个 bucket。

  2. 清理过期桶的数据

  3. 更新 offset,更新 offset 的过程实际上就是在模拟窗口滑动

  4. 添加数据

一文讲透自适应微服务熔断的原理和实现

// 添加数据

func (rw *RollingWindow) Add(v float64) {

rw.lock.Lock()

defer rw.lock.Unlock()

// 获取当前写入的下标

rw.updateOffset()

// 添加数据

rw.win.add(rw.offset, v)

}

// 计算当前距离最后写入数据经过多少个单元时间间隔

// 实际上指的就是经过多少个桶

func (rw *RollingWindow) span() int {

offset := int(timex.Since(rw.lastTime) / rw.interval)

if 0 <= offset && offset < rw.size {

return offset

}

// 大于时间窗口时 返回窗口大小即可

return rw.size

}

// 更新当前时间的offset

// 实现窗口滑动

func (rw *RollingWindow) updateOffset() {

// 经过span个桶的时间

span := rw.span()

// 还在同一单元时间内不需要更新

if span <= 0 {

return

}

offset := rw.offset

// 既然经过了span个桶的时间没有写入数据

// 那么这些桶内的数据就不应该继续保留了,属于过期数据清空即可

// 可以看到这里全部用的 % 取余操作,可以实现按照下标周期性写入

// 如果超出下标了那就从头开始写,确保新数据一定能够正常写入

// 类似循环数组的效果

for i := 0; i < span; i++ {

rw.win.resetBucket((offset + i + 1) % rw.size)

}

// 更新offset

rw.offset = (offset + span) % rw.size

now := timex.Now()

// 更新操作时间

// 这里很有意思

rw.lastTime = now - (now-rw.lastTime)%rw.interval

}

window 统计数据:

// 归纳汇总数据

func (rw *RollingWindow) Reduce(fn func(b *Bucket)) {

rw.lock.RLock()

defer rw.lock.RUnlock()

var diff int

span := rw.span()

// 当前时间截止前,未过期桶的数量

if span == 0 && rw.ignoreCurrent {

diff = rw.size - 1

} else {

diff = rw.size - span

}

if diff > 0 {

// rw.offset - rw.offset+span之间的桶数据是过期的不应该计入统计

offset := (rw.offset + span + 1) % rw.size

// 汇总数据

rw.win.reduce(offset, diff, fn)

}

}

googleBreaker 判断是否应该熔断

======================

  1. 收集滑动窗口内的统计数据

  2. 计算熔断概率

// 按照最近一段时间的请求数据计算是否熔断

func (b *googleBreaker) accept() error {

// 获取最近一段时间的统计数据

accepts, total := b.history()

// 计算动态熔断概率

weightedAccepts := b.k * float64(accepts)

// https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101

dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1))

// 概率为0,通过

if dropRatio <= 0 {

return nil

}

// 随机产生0.0-1.0之间的随机数与上面计算出来的熔断概率相比较

// 如果随机数比熔断概率小则进行熔断

if b.proba.TrueOnProba(dropRatio) {

return ErrServiceUnavailable

}

return nil

}

googleBreaker 熔断逻辑实现

====================

熔断器对外暴露两种类型的方法

  1. 简单场景直接判断对象是否被熔断,执行请求后必须需手动上报执行结果至熔断器。

func (b *googleBreaker) allow() (internalPromise, error)

  1. 复杂场景下支持自定义快速失败,自定义判定请求是否成功的熔断方法,自动上报执行结果至熔断器。

func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error

Acceptable 参数目的是自定义判断请求是否成功。

Acceptable func(err error) bool

// 熔断方法

// 返回一个promise异步回调对象,可由开发者自行决定是否上报结果到熔断器

func (b *googleBreaker) allow() (internalPromise, error) {

if err := b.accept(); err != nil {

return nil, err

}

return googlePromise{

b: b,

}, nil

}

// 熔断方法

// req - 熔断对象方法

// fallback - 自定义快速失败函数,可对熔断产生的err进行包装后返回

// acceptable - 对本次未熔断时执行请求的结果进行自定义的判定,比如可以针对http.code,rpc.code,body.code

func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {

// 判定是否熔断

if err := b.accept(); err != nil {

// 熔断中,如果有自定义的fallback则执行

if fallback != nil {

return fallback(err)

}

return err

}

// 如果执行req()过程发生了panic,依然判定本次执行失败上报至熔断器

defer func() {

if e := recover(); e != nil {

b.markFailure()

panic(e)

}

}()

// 执行请求

err := req()

// 判定请求成功

if acceptable(err) {

b.markSuccess()

} else {

b.markFailure()

}

return err

}

// 上报成功

func (b *googleBreaker) markSuccess() {

b.stat.Add(1)

}

// 上报失败

func (b *googleBreaker) markFailure() {

b.stat.Add(0)

}

// 统计数据

func (b *googleBreaker) history() (accepts, total int64) {

最后总结

ActiveMQ+Kafka+RabbitMQ学习笔记PDF

image.png

  • RabbitMQ实战指南

image.png

  • 手写RocketMQ笔记

image.png

  • 手写“Kafka笔记”

image

关于分布式,限流+缓存+缓存,这三大技术(包含:ZooKeeper+Nginx+MongoDB+memcached+Redis+ActiveMQ+Kafka+RabbitMQ)等等。这些相关的面试也好,还有手写以及学习的笔记PDF,都是啃透分布式技术必不可少的宝藏。以上的每一个专题每一个小分类都有相关的介绍,并且小编也已经将其整理成PDF啦

// 如果执行req()过程发生了panic,依然判定本次执行失败上报至熔断器

defer func() {

if e := recover(); e != nil {

b.markFailure()

panic(e)

}

}()

// 执行请求

err := req()

// 判定请求成功

if acceptable(err) {

b.markSuccess()

} else {

b.markFailure()

}

return err

}

// 上报成功

func (b *googleBreaker) markSuccess() {

b.stat.Add(1)

}

// 上报失败

func (b *googleBreaker) markFailure() {

b.stat.Add(0)

}

// 统计数据

func (b *googleBreaker) history() (accepts, total int64) {

最后总结

ActiveMQ+Kafka+RabbitMQ学习笔记PDF

[外链图片转存中…(img-j4sKi7YY-1718786578765)]

  • RabbitMQ实战指南

[外链图片转存中…(img-XuMveO5i-1718786578766)]

  • 手写RocketMQ笔记

[外链图片转存中…(img-BiKsABER-1718786578766)]

  • 手写“Kafka笔记”

[外链图片转存中…(img-5ssD2158-1718786578767)]

关于分布式,限流+缓存+缓存,这三大技术(包含:ZooKeeper+Nginx+MongoDB+memcached+Redis+ActiveMQ+Kafka+RabbitMQ)等等。这些相关的面试也好,还有手写以及学习的笔记PDF,都是啃透分布式技术必不可少的宝藏。以上的每一个专题每一个小分类都有相关的介绍,并且小编也已经将其整理成PDF啦

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值