0%

服务熔断机制和golang的实现

微服务集群中,每个应用基本都会依赖一定数量的外部服务。如果依赖服务过载,服务不可用的情况,在高并发场景下如果此时调用方不做任何处理,继续持续请求故障服务的话很容易引起整个微服务集群雪崩。

所以应该采用熔断的策略,不再调用下游服务。

首先先区分下熔断、限流、降级区别

  1. 限流

    是针对服务请求数量的一种自我保护机制,当请求数量超出服务负载时,自动丢弃新的请求,是系统高可用架构的第一步。

  2. 熔断

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

  3. 降级

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

触发条件面向目标
限流上游服务请求多上游
熔断下游服务不可用下游
降级服务自身负载高自身

1. 熔断

1.1 熔断机制

img

假如此时 账户服务 过载,订单服务持续请求账户服务只能被动的等待账户服务报错或者请求超时,进而导致订单请求被大量堆积,这些无效请求依然会占用系统资源:cpu,内存,数据连接…导致订单服务整体不可用。即使账户服务恢复了订单服务也无法自我恢复。

这时如果有一个主动保护机制应对这种场景的话,订单服务至少可以保证自身的运行状态,等待账户服务恢复时订单服务也同步自我恢复,这种自我保护机制在服务治理中叫熔断机制。

img

1.2 熔断原理 (netflix hystrix)

04

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

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

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

  3. 半断开:进行断开状态会维护一个超时时间,到达超时时间开始进入 半断开 状态。

    尝试允许一部分请求正常通过并统计成功数量,如果请求正常则认为此时目标服务已恢复进入关闭 状态,否则进入断开状态。

    半断开 状态存在的目的在于实现了自我修复,同时防止正在恢复的服务再次被大量打垮。

1.3 自适应熔断(google sre)

项目中我们要使用好熔断器通常需要准备以下参数:

  1. 错误比例阈值:达到该阈值进入 断开 状态。
  2. 断开状态超时时间:超时后进入 半断开 状态。
  3. 半断开状态允许请求数量。
  4. 窗口时间大小。

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

img

其中每个变量的含义是:

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

算法解释:

  1. 正常情况下 requests=accepts,所以概率是 0。
  2. 随着正常请求数量减少,当达到 requests == K* accepts 继续请求时,概率 P 会逐渐比 0 大开始按照概率逐渐丢弃一些请求,如果故障严重则丢包会越来越多,假如窗口时间内 accepts==0 则完全熔断。
  3. 当应用逐渐恢复正常时,accepts、requests 同时都在增加,但是 K*accepts 会比 requests 增加的更快,所以概率很快就会归 0,关闭熔断。

2. golang实现

参考实现:

2.1 hystrix-go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"net/http"
"time"

"github.com/afex/hystrix-go/hystrix"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
)

func server() {
// 前 200ms 的请求都会返回 500,之后的请求都会返回 200
e := gin.Default()
start := time.Now()
e.GET("/ping", func(ctx *gin.Context) {
if time.Since(start) < 201*time.Millisecond {
ctx.String(http.StatusInternalServerError, "pong")
return
}
ctx.String(http.StatusOK, "pong")
})
e.Run(":8080")
}

func main() {
hystrix.ConfigureCommand("test", hystrix.CommandConfig{
// 执行command的超时时间,默认时间是1000毫秒
Timeout: 10,

// command的最大并发量,默认值是10
MaxConcurrentRequests: 100,

// 一个统计窗口10秒内请求数量,达到这个请求数量后才去判断是否要开启熔断,默认值是20
RequestVolumeThreshold: 10,

// 当熔断器被打开后,SleepWindow的时间就是控制过多久后去尝试服务是否可用了,默认值是5000毫秒
SleepWindow: 500,

// 错误百分比,请求数量大于等于RequestVolumeThreshold并且错误率到达这个百分比后就会启动熔断,默认值是50
ErrorPercentThreshold: 20,
})

go server()

for i := 0; i < 20; i++ {
_ = hystrix.Do("test", func() error {
resp, _ := resty.New().R().Get("http://localhost:8080/ping")
if resp.IsError() {
return fmt.Errorf("err code: %s", resp.Status())
}
return nil
}, func(err error) error {
fmt.Println("fallback err: ", err)
return err
})
time.Sleep(100 * time.Millisecond)
}
}
image-20240628211354246

hystrix-go已经可以比较好的满足我们的需求,但是存在一个问题就是一旦触发了熔断,在一段时间之类就会被一刀切的拦截请求,所以来看看 google sre 的一个实现。

2.2 go zero

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"errors"
"fmt"
"strconv"

"github.com/zeromicro/go-zero/core/breaker"
)

func main() {
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 9
func 10
func 11
func circuit breaker is open
func 13
func circuit breaker is open
func 15
func circuit breaker is open
func circuit breaker is open
func 18
func circuit breaker is open
*/

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

2.3 gin 熔断

注意熔断是上层控制的事情,gin一般是下游web服务。gin一般不会把熔断当成自己的中间件。

3. 参考资料

可以加首页作者微信,咨询相关问题!