保姆级教程,golang熔断实践
跨界架构师
共 10919字,需浏览 22分钟
· 2023-06-03
这里是Z哥的个人公众号
当前处于「随机更」状态
何时恢复「周更」未知……
我的第「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 的熔断器思路来实现。// BreakerHandler returns a break circuit middleware.
func BreakerHandler(method, path string, metrics *stat.Metrics) func(http.Handler) http.Handler {
brk := breaker.NewBreaker(breaker.WithName(strings.Join([]string{method, path}, breakerSeparator)))
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
promise, err := brk.Allow()
if err != nil {
metrics.AddDrop()
logx.Errorf("[http] dropped, %s - %s - %s",
r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent())
w.WriteHeader(http.StatusServiceUnavailable)
return
}
cw := &response.WithCodeResponseWriter{Writer: w}
defer func() {
if cw.Code < http.StatusInternalServerError {
promise.Accept()
} else {
promise.Reject(fmt.Sprintf("%d %s", cw.Code, http.StatusText(cw.Code)))
}
}()
next.ServeHTTP(cw, r)
})
}
可能说起熔断器,很多人脑子里第一印象是 netflix的 hystrix 。但是我认为 google 的思路更棒一些,两者的区别从效果来说就是 google 的方案自适应能力更强。因为 hystrix 中使用三种状态来控制,当状态为 open 期间,所有请求都会直接被拦截,相对更粗暴一些。为了便于理解两者的不同,我用“水管”来比喻画了一张图。
这里就不展开了,hystrix 的熔断器思路在我之前写的文章《 分布式系统关注点(8)——如何在到处是“雷”的系统中「明哲保身」?这是第一招 》中也有提到。如果对 google 的熔断器思路感兴趣的话,可以看这篇文章:https://sre.google/sre-book/handling-overload/
好了,那么 go-zero 中的熔断器该怎么使用呢? 首先,使用它的途径有三种方式,分别是: 01 持有熔断器实例 + 负责管理实例的生命周期
brk := breaker.NewBreaker()
brk.Do(func() error {
//do something.
return nil
})
02 持有熔断器实例 + 不管理实例的生命周期 将管理每个熔断器实例的职责交由框架内的「池」来实现。
brk := breaker.GetBreaker(“起个名字”)
brk.Do(func() error {
//do something.
return nil
})
03 不持有熔断器实例 直接使用非实例的 Do() 函数,需要定义一个标识 name,这个 name 就是熔断器的唯一标识。
breaker.Do(“起个名字”, func() error {
//do something.
return nil
})
以上示例代码中的 Do() 函数中的 func() error,就是需要在熔断器的保护下执行的具体代码。 运行以下代码,就可以看到熔断器生效的效果:
for i := 0; i < 20; i++ {
err = breaker.Do("func", func() error {
return errors.New(strconv.Itoa(i))
})
fmt.Println("func", err)
}
输出:
func 0
func 1
func 2
func 3
func 4
func 5
func 6
func 7
func circuit breaker is open
func circuit breaker is open
func circuit breaker is open
func 11
func circuit breaker is open
func circuit breaker is open
func circuit breaker is open
func 15
func 16
func 17
func 18
func circuit breaker is open
以上的输出内容不是固定的,每次运行的结果都不同(为什么不同后面会提到原因)。其中“func circuit breaker is open”表示 Do()函数中的 func() error 直接被熔断器拦截了,没有实际执行。 上面是最基本的使用方式,除此之外,go-zero 封装的 breaker 还提供以下几个能力:
- 熔断器外的代码实现熔断
- 主动让熔断器失效
- 自定义计数规则
- 触发熔断时的回调函数
接下来我们来一个个说下。
01 熔断器外的代码实现熔断 前面的三种使用方式中,Do() 函数的作用是将需要执行的代码放到熔断器内执行,而有时候我们可能不便将代码放到熔断器内,但是也想实现熔断的能力可以吗?当然可以。 breaker 对象暴露了一个Allow() (Promise, error) 函数,返回一个 Promise 对象。
Allow() (Promise, error)
- 可以通过直接操作 promise.Accept() 实现前面示例代码中 Do()函数中的 func()执行后返回的 err == nil 的效果
- 也可以通过 promise.Reject(reason string) 实现 err != nil 的效果。
前提是,你得使用前两种持有 breaker 实例的方式。 go-zero 实现服务端熔断的 BreakerHandler 就是利用这个机制来实现的,根据返回的 HttpCode 决定请求算成功还是失败(前面贴的第一段代码中的 17~21 行)。
02 主动让熔断器失效 如果你使用熔断器的方式是前面提到的方式二和方式三,那么可以通过调用下面的函数,将「池」中的 breaker 实例移除。这样的话,下次申请获取相同 name 的熔断器时会重新实例化一个新的 breaker,因此间接达到了清空计数器数字的效果。
breaker.NoBreakerFor("起个名字")
在前面熔断器生效的代码基础上,增加三行代码,就能看到不会再出现“func circuit breaker is open”了。
for i := 0; i < 20; i++ {
err = breaker.Do("func", func() error {
return errors.New(strconv.Itoa(i))
})
fmt.Println("func", err)
if i%5 == 0 {
breaker.NoBreakerFor("func")
}
}
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
DoWithAcceptable(req func() error, acceptable Acceptable) error
该函数多了一个 Acceptable 对象,该对象是一个函数,用于判断 error 是否是可忽略的:
- 返回 true 表示忽略,效果等价于 func() 执行返回的 err == nil 的情况
- 返回 false 则等价于 func() 执行返回的 err != nil 的情况。
你可以试试运行以下代码:
你看不到表示触发熔断的“circuit breaker is open”字眼,都是“acceptable”。for i := 0; i < 20; i++ {
err = breaker.DoWithAcceptable("DoWithAcceptable", func() error {
return errors.New("acceptable")
}, func(err error) bool {
return err == nil || err.Error() == "acceptable"
})
fmt.Println(err)
}
04 触发熔断时的回调函数 当某次 func() 的执行被熔断器拦截时,允许触发回调(callback)函数,以便外部调用方感知到这个事件,并基于此做一些其它的事情。比如使用降级方案来代替原 func() 的实现。 要使用该能力,需要调用以下函数代替 Do() 函数:
DoWithFallback(req func() error, fallback func(err error) error) error
该函数多了一个 fallback 的 func()。当某次请求由于触发熔断器导致被拦截时会被触发。触发方式是 sync 的,且 fallback 函数中返回的 err 即为调用方接收到 DoWithFallback 函数的返回值。直接上源码可能更好理解:
其实还有一个函数
从名字也能看出来,它同时支持上面提到的 03 和 04 能力。 到此为止,相信你应该会用这个熔断器了。DoWithFallbackAcceptable(req func() error, fallback func(err error) error,
acceptable Acceptable) error
可能有些想更进一步的小伙伴会问,熔断器的触发策略除了计数规则之外,其它的规则可以自定义吗? 很遗憾,目前框架没有暴露相关的参数出来,都是在代码中固定写死的常量。除了前面提到的 protection ,还有 3 个常量与熔断器的触发策略相关。
K 的含义前面有提到过,主要讲一下 window 和 buckets 变量的作用。 googleBreaker 的底层使用了滑动窗口算法,这两个变量是用来定义滑动窗口的:window = time.Second * 10
buckets = 40
k = 1.5
protection = 5
含义是,将滑动窗口分为 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
推荐阅读:
原创不易,如果你觉得这篇文章还不错,就「 点赞 」或者「在看」一下吧,鼓励我的创作 :)
也可以分享我的公众号名片给有需要的朋友们。
如果你有关于软件架构、分布式系统、产品、运营的困惑
可以试试点击「阅读原文」
评论
从原理到实践:掌握DPDK内存池技术
前言:本文整理下之前的学习笔记,基于DPDK17.11版本源码分析。主要分析一下内存管理部分代码。一、概述内存管理是数据面开发套件(DPDK)的一个核心部分,以此为基础,DPDK的其他部分和用户应用得以发挥其最佳性能。本系列文章将详细介绍DPDK提供的各种内存管理的功能。但在此之前,有必要先谈一谈为
开源Linux
0
Java与lua互相调用简单教程
来源:网络👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 / 赠书福利全栈前后端分离博客项目 2.0 版本完结啦, 演示链接:http://116.62.199.48/ ,新项目
小哈学Java
0
使用 GitHub Actions 构建 Golang PGO
今年 2 月,我宣布 Dolt 版本现已构建为配置文件引导优化 (pgo) 二进制文件,利用 Golang 1.20 的强大功能将 Dolt 的读取性能提高 5.3%。在我宣布这一消息之前,我们的一位常驻 Golang 专家 Zach 试验并测试了 Golang 的 pgo 功能
GoCN
0
微服务与领域驱动设计,架构实践总结
来源:知了一笑👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 / 赠书福利全栈前后端分离博客项目 2.0 版本完结啦, 演示链接:http://116.62.199.48/ ,新
小哈学Java
0
AI智能视觉检测技术在工业级测量领域的创新应用--AMB Tube-Q导管数字化测量系统
技术背景在深度学习算法出来之前,对于视觉算法来说,大致可以分为以下5个步骤:特征感知,图像预处理,特征提取,特征筛选,推理预测与识别。早期的机器学习中,占优势的统计机器学习群体中,对特征是不大关心的。深度学习是机器学习技术的一个方面,由人工神经网络提供支持。深度学习技术的工作原理是教机器通过实例学习
机器视觉
0
超赞!这个ChatGPT提问教程,PDF免费下载
你好,我是郭震AI来袭,我们该如何学习?今天先分享给大家一份超好的GPT提问指南。教程的详细介绍参考下面视频:这个PDF资料旨在教我们更好的给GPT发送指令,让GPT更准确的回答我们的提问。一共有30页,内容包括7个小章节,按照逻辑展开。分别介绍文本回答,代码辅助,结构化结果输出,非结构化结构输出,
Python与算法社区
3
侠盗猎车手免安装教程,支持安卓+PC!
前几天有粉丝朋友问我有没有侠盗猎车手罪恶都市这个游戏,今天就从解决实际问题的角度上,亲测这款游戏从安装到试玩,当然这篇文章使用的免安装版本,方便大家解压即玩!废话不多说先来看一下实测结果吧,我两台电脑都可以玩,一台是Win10,一台是Win11免安装,解完压缩就可以直接玩:非常经典的游戏开始画面:《
dotNET全栈开发
10
2024版,尚硅谷Java学科全套教程(289.1GB),含最新技术
尚硅谷 Java 学科全套视频,很早之前分享过一次,大概是 200G 左右,今天这个是《2024 版尚硅谷 Java 学科全套教程》,新增了 80 多个 GB,里面涵盖了 2023 年、2024 年最新更新的技术,基本上涵盖了 Java 所有的技术,共 289.1GB,文末附网盘地址。目录 基础部分
路人甲Java
0