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

本文详细介绍了微服务中的熔断机制,包括熔断与降级的区别,工作原理,以及常用的熔断组件。重点讨论了自适应熔断的概念,提供了一种基于Google SRE的自适应熔断算法,并通过代码分析展示了如何在Go语言中实现熔断器,包括滑动时间窗口、自定义判定和可观测性的设计。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

熔断是调用方自我保护的机制(客观上也能保护被调用方),熔断对象是外部服务。

降级

降级是被调用方(服务提供者)的防止因自身资源不足导致过载的自我保护机制,降级对象是自身。

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

熔断这一词来源时我们日常生活电路里面的熔断器,当负载过高时(电流过大)保险丝会自行熔断防止电路被烧坏,很多技术都是来自生活场景的提炼。

工作原理

====

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

熔断器一般具有三个状态:

  1. 关闭:默认状态,请求能被到达目标服务,同时统计在窗口时间成功和失败次数,如果达到错误率阈值将会进入断开状态。

  2. 断开:此状态下将会直接返回错误,如果有 fallback 配置则直接调用 fallback 方法。

  3. 半断开:进行断开状态会维护一个超市时间,到达超时时间开始进入 半断开 状态,尝试允许一部门请求正常通过并统计成功数量,如果请求正常则认为此时目标服务已恢复进入 关闭 状态,否则进入 断开 状态。半断开 状态存在的目的在于实现了自我修复,同时防止正在恢复的服务再次被大量打垮。

使用较多的熔断组件:

  1. hystrix circuit breaker(不再维护)

  2. hystrix-go

  3. resilience4j(推荐)

  4. sentinel(推荐)

什么是自适应熔断

========

基于上面提到的熔断器原理,项目中我们要使用好熔断器通常需要准备以下参数:

  1. 错误比例阈值:达到该阈值进入 断开 状态。

  2. 断开状态超时时间:超时后进入 半断开 状态。

  3. 半断开状态允许请求数量。

  4. 窗口时间大小。

实际上可选的配置参数还有非常非常多,参考

https://resilience4j.readme.io/docs/circuitbreaker

对于经验不够丰富的开发人员而言,这些参数设置多少合适心里其实并没有底。

那么有没有一种自适应的熔断算法能让我们不关注参数,只要简单配置就能满足大部分场景?

其实是有的,google sre提供了一种自适应熔断算法来计算丢弃请求的概率:

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

算法参数:

  1. requests:窗口时间内的请求总数

  2. accepts:正常请求数量

  3. K:敏感度,K 越小越容易丢请求,一般推荐 1.5-2 之间

算法解释:

  1. 正常情况下 requests=accepts,所以概率是 0。

  2. 随着正常请求数量减少,当达到 requests == K* accepts 继续请求时,概率 P 会逐渐比 0 大开始按照概率逐渐丢弃一些请求,如果故障严重则丢包会越来越多,假如窗口时间内 accepts==0 则完全熔断。

  3. 当应用逐渐恢复正常时,accepts、requests 同时都在增加,但是 K*accepts 会比 requests 增加的更快,所以概率很快就会归 0,关闭熔断。

代码实现

====

接下来思考一个熔断器如何实现。

初步思路是:

  1. 无论什么熔断器都得依靠指标统计来转换状态,而统计指标一般要求是最近的一段时间内的数据(太久的数据没有参考意义也浪费空间),所以通常采用一个 滑动时间窗口 数据结构 来存储统计数据。同时熔断器的状态也需要依靠指标统计来实现可观测性,我们实现任何系统第一步需要考虑就是可观测性,不然系统就是一个黑盒。

  2. 外部服务请求结果各式各样,所以需要提供一个自定义的判断方法,判断请求是否成功。可能是 http.code 、rpc.code、body.code,熔断器需要实时收集此数据。

  3. 当外部服务被熔断时使用者往往需要自定义快速失败的逻辑,考虑提供自定义的 fallback() 功能。

下面来逐步分析 go-zero 的源码实现:

core/breaker/breaker.go

熔断器接口定义

=======

兵马未动,粮草先行,明确了需求后就可以开始规划定义接口了,接口是我们编码思维抽象的第一步也是最重要的一步。

核心定义包含两种类型的方法:

Allow():需要手动回调请求结果至熔断器,相当于手动挡。

DoXXX():自动回调请求结果至熔断器,相当于自动挡,实际上 DoXXX() 类型方法最后都是调用 DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error

// 自定义判定执行结果

Acceptable func(err error) bool

// 手动回调

Promise interface {

// Accept tells the Breaker that the call is successful.

// 请求成功

Accept()

// Reject tells the Breaker that the call is failed.

// 请求失败

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)

}

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

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

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

MySQL全家桶笔记

还有更多面试复习笔记分享如下

Java架构专题面试复习

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-hDL8tZXf-1713435128875)]

[外链图片转存中…(img-KJsJMe6s-1713435128875)]

[外链图片转存中…(img-LBIKHajO-1713435128876)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

[外链图片转存中…(img-sN78R9yO-1713435128876)]

还有更多面试复习笔记分享如下

[外链图片转存中…(img-ExaTJXhT-1713435128877)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

### 回答1: ESD(Electrostatic Discharge,静电放电)是一种瞬时放电现象,通常是由人体或设备上积累的静电电荷引起的。一般来说,ESD会导致电子设备损坏或误操作,因此必须采取措施来避免ESD。 在设计中,ESD保护应该开始于PCB的物理设计。一个好的物理设计将使ESD泄放的能量尽可能地均匀地分散到整个电路板上。这种物理设计包括有效的接地,涂覆排列PCB层。同时,这也需要考虑到整个系统的电缆结构、机箱接地隔离等因素,从而最大限度地提高整个系统的耐ESD能力。 此外,在设计电路时,还需要考虑到ESD保护措施。主要的保护措施包括使用可靠的ESD保护器件,如TVS器件、瞬变压抑器热释电器件,以保护线路免受ESD的影响。此外,在设计输入、输出供电接口时,还应该采用合适的线路过滤器电容器,以进一步提高系统的ESD耐受性。 最后,测试是ESD保护设计的重要环节。ESD测试可以验证保护设计的有效性,并排除措施上的缺陷。通常,测试人员会使用标准ESD模拟器来模拟真实的ESD事件。在测试过程中,应注意对设备进行预处理,如去静电适当的人体模拟。此外,还应该制定合适的检验标准以确保测试的准确性可重复性。 总之,ESD保护设计至关重要,因为它能够保护电子设备免受静电放电的损害。为了实现可靠的ESD保护,这需要考虑物理设计电路设计,以及有效的测试工具。最后,只有将所有这些因素合理结合,才能实现有效的ESD保护设计。 ### 回答2: ESD(Electrostatic Discharge,静电放电)指的是在两个带有不同电荷的物体接触或者靠近时,电荷之间发生放电的现象。这种放电可以对各种电子元器件电路造成损害,从而影响设备的性能寿命。 ESD的原理可以通过三种方式传递:空气中的放电、直接接触电感耦合。在实际应用中,ESD对硅芯片、存储器、晶体管等电子元件的损害是非常严重的,这些元件的特性结构容易受到ESD的影响。 为了防止ESD对电子元件电路的损坏,需要在设计中采用一些专门的技术,比如在元器件电路板上增加ESD保护电路、在设备外壳上增加处理工艺等。对于集成电路芯片而言,可以采用对基底指的进行控制,以及在芯片电路设计过程中合理选择元器件适当布局等。 总之,ESD保护是电子元器件电路设计中非常重要的一环,需要采用针对性的技术来减缓防止ESD对设备的影响,从而保证设备的长期稳定性可靠性。 ### 回答3: ESD全程为静电放电,是由于静电在两者之间产生的高电压放电引起的电感电容的相互作用。在现代电子系统中,由于设备的电路越来越小,因此更容易受到静电干扰,人们不得不在设计中考虑如何避免或降低这种静电干扰。本文将从ESD的原理出发,简要介绍如何在电路设计中考虑防止ESD干扰。 ESD的产生是由于静电的积累导致的高电压放电,因此防止ESD干扰的基本原则是减小静电的积累。在电路设计中,静电主要通过两个方面来进行干扰:一是直接放电干扰,即静电直接放电到电路中,导致电路损坏;二是间接放电干扰,即静电放电到设备的金属外壳等部位,导致电磁场干扰影响电路的正常工作。因此,在设计中,需要采用一些措施来减小这些干扰。 1. 选择合适的元器件:在元器件的选择上,要选择一些抗ESD干扰的元器件,如采用ESD保护二极管等,能够减小ESD对电路的影响。 2. 优化电路结构:在电路设计中,要优化电路结构,减少电路间的交叉干扰,避免电路产生高电位差,这样能够减少静电的积累ESD的辐射。 3. 采用ESD保护电路:在设计电路时,引入一些ESD保护电路,能够有效地减小ESD对电路的影响。例如采用Zener二极管、TVS二极管等保护电路。 在总体设计中,需要综合以上措施,采用一些适合的方案来消除ESD对电路的干扰。同时,在实际使用中,也需要对电路进行定期维护检测,保证电路的正常运行。在电子技术的快速发展中,ESD防护的问题只会越来越重要,只有对其进行深入的研究应用,才能更好地保证电子设备的稳定运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值