首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

保姆级教程,golang熔断实践

当前处于「随机更」状态

何时恢复「周更」未知……

我的第「231」篇原创敬上

大家好,我是Z哥。

不得不说,我现在已经从「周更」变成「随机更」了,我自己都不知道哪天能更新,工作实在太忙了。

好了快速进入正题,最近团队里的一个重点工作是增加系统的稳定性和可用性,因此避不开的话题就是熔断、降级、限流。

这三个概念我在之前写的分布式系统系列中也有提及,有兴趣的可以在文末移步到之前的文章中阅读。

不过今天我们主要聊的是,在 golang 项目中如何落地「熔断」。

熔断是一种通用能力,可以在服务端做也可以在客户端做。我们的项目中大多数都基于 go-zero 框架实现,而使用 go-zero 框架实现的项目自带服务端熔断能力,所以本文的目的是阐述如何在客户端侧实现熔断机制。

由于 go-zero 内置熔断器能力,因此我们优先想到的是能否直接使用 go-zero 框架内的熔断器组件,如果可以满足需求的话,也避免了增加额外的外部依赖。

扒开 go-zero 的源码就能找到它的熔断器使用,以下是在使用 go-zero 构建 http 的服务端时,其通过 AOP 的方式利用 Handler 来注入熔断器的代码。这部分代码现在不用深究,等看完本篇文章,你再回头来看很容易知道它写的是什么意思。

通过以上代码继续深入源码,我们发现了go-zero框架中的熔断器模块(https://github.com/zeromicro/go-zero/tree/master/core/breaker)其底层使用了 google 的熔断器思路来实现。

可能说起熔断器,很多人脑子里第一印象是 netflix的 hystrix 。但是我认为 google 的思路更棒一些,两者的区别从效果来说就是 google 的方案自适应能力更强。因为 hystrix 中使用三种状态来控制,当状态为 open 期间,所有请求都会直接被拦截,相对更粗暴一些。为了便于理解两者的不同,我用“水管”来比喻画了一张图。

这里就不展开了,hystrix 的熔断器思路在我之前写的文章《分布式系统关注点(8)——如何在到处是“雷”的系统中「明哲保身」?这是第一招》中也有提到。如果对 google 的熔断器思路感兴趣的话,可以看这篇文章:https://sre.google/sre-book/handling-overload/

好了,那么 go-zero 中的熔断器该怎么使用呢?

首先,使用它的途径有三种方式,分别是:

01  持有熔断器实例 + 负责管理实例的生命周期

02  持有熔断器实例 + 不管理实例的生命周期

将管理每个熔断器实例的职责交由框架内的「池」来实现。

03不持有熔断器实例

直接使用非实例的 Do() 函数,需要定义一个标识 name,这个 name 就是熔断器的唯一标识。

以上示例代码中的 Do() 函数中的 func() error,就是需要在熔断器的保护下执行的具体代码。

运行以下代码,就可以看到熔断器生效的效果:

输出:

以上的输出内容不是固定的,每次运行的结果都不同(为什么不同后面会提到原因)。其中“func circuit breaker is open”表示 Do()函数中的 func() error 直接被熔断器拦截了,没有实际执行。

上面是最基本的使用方式,除此之外,go-zero 封装的 breaker 还提供以下几个能力:

熔断器外的代码实现熔断

主动让熔断器失效

自定义计数规则

触发熔断时的回调函数

接下来我们来一个个说下。

01 熔断器外的代码实现熔断

前面的三种使用方式中,Do() 函数的作用是将需要执行的代码放到熔断器内执行,而有时候我们可能不便将代码放到熔断器内,但是也想实现熔断的能力可以吗?当然可以。

breaker 对象暴露了一个Allow() (Promise, error) 函数,返回一个 Promise 对象。

可以通过直接操作 promise.Accept() 实现前面示例代码中 Do()函数中的 func()执行后返回的 err == nil 的效果

也可以通过 promise.Reject(reason string) 实现 err != nil 的效果。

前提是,你得使用前两种持有 breaker 实例的方式。go-zero 实现服务端熔断的 BreakerHandler 就是利用这个机制来实现的,根据返回的 HttpCode 决定请求算成功还是失败(前面贴的第一段代码中的 17~21 行)。

02 主动让熔断器失效

如果你使用熔断器的方式是前面提到的方式二和方式三,那么可以通过调用下面的函数,将「池」中的 breaker 实例移除。这样的话,下次申请获取相同 name 的熔断器时会重新实例化一个新的 breaker,因此间接达到了清空计数器数字的效果。

在前面熔断器生效的代码基础上,增加三行代码,就能看到不会再出现“func circuit breaker is open”了。

03  自定义计数规则

在讲自定义计数规则之前先得了解一下 googleBreaker 的实现原理。googleBreaker 的底层实现基于一个「客户端请求拒绝概率」的公式:

其中每个变量的含义是:

requests:发起请求的总数

accepts:后端接受的请求数

K:一般建议该值在1.1~2之间。数字越小触发熔断的概率越高,反之则越低。如果K=2,意味着我们认为每接受 10 个请求,后端正常情况下最多只会拒绝 5 个请求,如果发现拒绝了6个,就触发熔断。

在 go-zero 提供的 breaker 实现中,基于上面的公式增加了两处微调。

第一处是,为了避免极端情况下发起第一次请求就出现失败而导致触发熔断,在 go-zero 的代码中针对上面公式中的「分子」增加了一个 protection 常量,该值固定为 5,因此分子部分实际在代码中是 requests - protection - K * accepts。

第二处是,当公式计算的结果 >0 时,不会直接触发熔断,而是会与一个半开半闭区间 [0.0,1.0) 的伪随机数对比,如果大于这个伪随机数则该次请求触发熔断。

针对上面公式的中,涉及到的计数的变量是 requests 和 accepts。默认的计数规则是:如果 func() 执行返回的 err == nil,则 requests+1,accepts+1;否则 requests +1,accepts 不变。

有时候,有些 error 我们可能不希望将其视作「不可用」的信号,因此,我们可以通过使用以下函数代替 Do(req func() error) error

该函数多了一个 Acceptable 对象,该对象是一个函数,用于判断 error 是否是可忽略的:

返回 true 表示忽略,效果等价于 func() 执行返回的 err == nil 的情况

返回 false 则等价于 func() 执行返回的 err != nil 的情况。

你可以试试运行以下代码:

你看不到表示触发熔断的“circuit breaker is open”字眼,都是“acceptable”。

04 触发熔断时的回调函数

当某次 func() 的执行被熔断器拦截时,允许触发回调(callback)函数,以便外部调用方感知到这个事件,并基于此做一些其它的事情。比如使用降级方案来代替原 func() 的实现。

要使用该能力,需要调用以下函数代替 Do() 函数:

该函数多了一个 fallback 的 func()。当某次请求由于触发熔断器导致被拦截时会被触发。触发方式是 sync 的,且 fallback 函数中返回的 err 即为调用方接收到 DoWithFallback 函数的返回值。直接上源码可能更好理解:

其实还有一个函数

从名字也能看出来,它同时支持上面提到的 03 和 04 能力。

到此为止,相信你应该会用这个熔断器了。

可能有些想更进一步的小伙伴会问,熔断器的触发策略除了计数规则之外,其它的规则可以自定义吗?

很遗憾,目前框架没有暴露相关的参数出来,都是在代码中固定写死的常量。除了前面提到的 protection ,还有 3 个常量与熔断器的触发策略相关。

K 的含义前面有提到过,主要讲一下 window 和 buckets 变量的作用。

googleBreaker 的底层使用了滑动窗口算法,这两个变量是用来定义滑动窗口的:

含义是,将滑动窗口分为 40 个区间,每个区间对 250ms 内的请求进行计数。

好了,总结一下。

今天呢,Z 哥带你深入剖析了一下 go-zero 框架中的熔断器,以及教你如何使用它。

首先,使用熔断器的方式有三种:

持有熔断器实例 + 负责管理实例的生命周期。

持有熔断器实例 + 不管理实例的生命周期。

不持有熔断器实例。

其次,熔断器总共提供 6 种能力:

最基础的,在熔断器的保护下执行代码:Do(req func() error) error

熔断器外的代码实现熔断:Allow() (Promise, error) + promise.Accept() / promise.Reject(reason string)

让「池」里的熔断器失效:breaker.NoBreakerFor(name string)

自定义计数规则:DoWithAcceptable(req func() error, acceptable Acceptable) error

触发熔断时的回调函数:DoWithFallback(req func() error, fallback func(err error) error) error

自定义计数规则+触发熔断时的回调函数:DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error

这篇文章比较干,如果你之前对熔断器了解不多的话,可能需要多花点时间仔细阅读几遍,消化一下。

希望对你有所帮助。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20230505A039SN00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券