一、服务熔断 介绍

1.1 有什么用

服务熔断是指调用方访问服务时,通过熔断器做代理来进行访问,熔断器会持续观察服务返回的成功、失败的状态,当失败次数超过设置的阙值时,熔断器断开,请求就不能访问到下游服务了。

作用:

  1. 当所依赖的服务不稳定时,能够起到快速失败的目的

  2. 快速失败后,能够用一定的算法动态探测所依赖对象是否恢复


具体场景

如一个微服务化的产品线上,每一个服务都专注于自己的业务,并对外提供相应的服务接口,或者依赖于外部服务的某个逻辑接口,就像下面这样。

假设我们当前是 服务A,有部分逻辑依赖于 服务C服务C 又依赖于 服务E, 当前微服务之间进行 rpc 或者 http 通信,假设此时 服务C 调用 服务E 失败,比如由于网络波动导致超时或者服务E 由于过载,服务E 已经 down 掉了。

调用失败,一般会有失败重试等机制。但是再想想,假设服务E 已然不可用的情况下,此时新的调用不断产生,同时伴随着调用等待和失败重试,会导致 服务C服务E 的调用而产生大量的积压,慢慢会耗尽服务C 的资源,进而导致服务C 也 down 掉,这样恶性循环下,会影响到整个微服务体系,产生雪崩效应。

虽然导致雪崩的发生不仅仅这一种,但是我们需要采取一定的措施,来保证不让这个噩梦发生。而 hystrix-go 就很好的提供了 熔断和降级的措施。它的主要思想在于,设置一些阀值,比如最大并发数 (当并发数大于设置的并发数,拦截),错误率百分比 (请求数量大于等于设置 的阀值,并且错误率达到设置的百分比时,触发熔断) 以及熔断尝试恢复时间等 。





1.2 三种状态

熔断器实现主要是设置一个阙值,这个阙值可以是最大并发数,请求错误率百分比等,超过这个阙值就进行熔断。还会设置一个尝试恢复时间。


熔断器有3种状态:

  • CLOSED:默认状态。熔断器计算数(最大请求数、请求错误百分比等)没有达到设置的阙值,熔断器就认为被代理服务状态良好。
  • OPEN:熔断器计算数计算数(最大请求数、请求错误百分比等)已经达到阙值,熔断器就认为被代理的服务已经故障了,打开熔断器开关,请求不再代理服务,而是快速失败。
  • HALF OPEN:熔断器打开后,为了能自动恢复被代理服务的访问,会切换到半开放状态,去尝试请求被代理服务以查看服务故障是否已经恢复了。如果恢复了,会转为 CLOSED 状态;否则转到 OPEN 状态。


熔断器需要考虑的一些问题:

  • 熔断时长的设置,超过这个时长后切换到 HALF OPEN 进行重试。
  • 重试时,要注意业务是否允许这样做。
  • 不同的异常,需要定义熔断后不同处理逻辑。
  • 记录请求失败日志,供以后人工处理使用。




二、熔断框架 hystrix-go

https://github.com/afex/hystrix-go

1
go get github.com/afex/hystrix-go/hystrix

hystrix-go 是一个延迟和容错的库,作用是隔离系统调用、服务和第三方调用等,阻止级联故障,并在故障不可避免的复杂分布式系统中能够实现恢复的能力。它是基于 Netflix 的同名项目:https://github.com/Netflix/Hystrix原理文档


2.1 快速开始

hystrix-go 的使用非常简单,你可以调用它的 Go 或者 Do 方法,只是 Go 方法是异步的方式。而 Do 方法是同步方式。我们从一个简单的例子开启。

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 (
"context"
"errors"
"time"

"github.com/afex/hystrix-go/hystrix"
"github.com/gw-gong/gwkit-go/concurrency/hystrix_tools"
"github.com/gw-gong/gwkit-go/log"
common_utils "github.com/gw-gong/gwkit-go/utils/common"
)

func main() {
syncFn, err := log.InitGlobalLogger(log.NewDefaultLoggerConfig())
common_utils.ExitOnErr(context.Background(), err)
defer syncFn()

hystrix.SetLogger(&hystrix_tools.SugarLogger{})

hystrix.ConfigureCommand("my_command", hystrix.CommandConfig{
Timeout: 5000,
MaxConcurrentRequests: 10,
SleepWindow: 5000,
RequestVolumeThreshold: 10,
ErrorPercentThreshold: 30,
})

_ = hystrix.Do("my_command", func() error {
log.Info("talk to other services")
time.Sleep(1 * time.Second)
return errors.New("test error")
}, func(err error) error {
if err == hystrix.ErrTimeout {
log.Error("handle timeout error:%v", log.Err(err))
return nil
}
log.Error("handle error:%v", log.Err(err))
return nil
})
}

Do 函数需要三个参数,第一个参数 commmand 名称,用于区分不同的服务调用或操作,第二个参数是处理正常的逻辑,比如 http 调用服务,返回参数是 err。如果处理或调用失败,那么就执行第三个参数逻辑, 我们称为保底操作(fallback)。由于服务错误率过高导致熔断器开启,那么之后的请求也直接回调此函数。





2.2 设置

既然熔断器是按照配置的规则而进行是否开启的操作,那么我们当然可以设置我们想要的值。可以通过 ConfigureCommand 函数为特定的 command 设置参数。

command有以下作用:(同一个command会 共享 熔断机制)

  1. 唯一标识:用于标识和区分不同的熔断器实例
  2. 配置关联:将特定的熔断器配置与对应的 command 关联
  3. 指标收集:用于收集和报告该 command 的执行指标和健康状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"

"github.com/afex/hystrix-go/hystrix"
)

func main() {
_ = hystrix.Do("my_command", func() error {
fmt.Println("talk to other services")
return nil
}, func(err error) error {
fmt.Printf("handle error:%v\n", err)
return nil
})
}

上面配置的值含义:

Timeout: 执行 command 的超时时间(单位:毫秒)。(不会直接导致熔断,但是会增加错误)
MaxConcurrentRequests:command 的最大并发量 。(不会直接导致熔断,但是会增加错误)
SleepWindow:当熔断器被打开后,SleepWindow 的时间就是控制过多久后去尝试服务是否可用了。(和导致熔断的原因那个无关)
RequestVolumeThreshold: 一个统计窗口 10 秒内请求数量。达到这个请求数量后才去判断是否要开启熔断
ErrorPercentThreshold:错误百分比,请求数量大于等于 RequestVolumeThreshold 并且错误率到达这个百分比后就会启动熔断

未指定的话使用默认配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var (
// DefaultTimeout is how long to wait for command to complete, in milliseconds
DefaultTimeout = 1000
// DefaultMaxConcurrent is how many commands of the same type can run at the same time
DefaultMaxConcurrent = 10
// DefaultVolumeThreshold is the minimum number of requests needed before a circuit can be tripped due to health
DefaultVolumeThreshold = 20
// DefaultSleepWindow is how long, in milliseconds, to wait after a circuit opens before testing for recovery
DefaultSleepWindow = 5000
// DefaultErrorPercentThreshold causes circuits to open once the rolling measure of errors exceeds this percent of requests
DefaultErrorPercentThreshold = 50
// DefaultLogger is the default logger that will be used in the Hystrix package. By default prints nothing.
DefaultLogger = NoopLogger{}
)

设置logger要实现接口

1
2
3
type logger interface {
Printf(format string, items ...interface{})
}

设置:(全局默认值)

1
hystrix.SetLogger(logger)




2.3 fallback函数

如果fallback函数的err != nil,Do()函数的返回的err != nill

不管是什么错误,只要发生了就会经过fallback函数。

1
2
3
4
5
6
7
8
var (
// ErrMaxConcurrency occurs when too many of the same named command are executed at the same time.
ErrMaxConcurrency = CircuitError{Message: "max concurrency"}
// ErrCircuitOpen returns when an execution attempt "short circuits". This happens due to the circuit being measured as unhealthy.
ErrCircuitOpen = CircuitError{Message: "circuit open"}
// ErrTimeout occurs when the provided function takes too long to execute.
ErrTimeout = CircuitError{Message: "timeout"}
)

可以区分错误来源,上面是hystrix的错误定义





2.4 测试熔断

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
61
62
63
64
65
66
67
68
69
70
71
72
package main

import (
"context"
"errors"
"net/http"
"time"

"github.com/afex/hystrix-go/hystrix"
"github.com/gw-gong/gwkit-go/concurrency/hystrix_tools"
"github.com/gw-gong/gwkit-go/log"
common_utils "github.com/gw-gong/gwkit-go/utils/common"
)

func accessBaidu() error {
resp, err := http.Get("https://baidu.com")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("failed to access baidu.com, status: " + resp.Status)
}
log.Info("access baidu.com success")
return nil
}

func hystrixDemo() {
_ = hystrix.Do("my_command", func() error {
return accessBaidu()
}, func(err error) error {
if err == hystrix.ErrTimeout {
log.Error("handle timeout error:%v", log.Err(err))
return nil
}
log.Error("handle error:%v", log.Err(err))
return nil
})
}

func main() {
syncFn, err := log.InitGlobalLogger(log.NewDefaultLoggerConfig())
common_utils.ExitOnErr(context.Background(), err)
defer syncFn()

hystrix.SetLogger(&hystrix_tools.SugarLogger{})

hystrix.ConfigureCommand("my_command", hystrix.CommandConfig{
Timeout: 5000,
MaxConcurrentRequests: 10,
SleepWindow: 5000,
RequestVolumeThreshold: 10,
ErrorPercentThreshold: 30,
})

tasks := make(chan struct{}, 10)

go func() {
for {
select {
case <-tasks:
hystrixDemo()
}
}
}()

for {
tasks <- struct{}{}
time.Sleep(200 * time.Millisecond)
}
}

正常联网 -> 断网 -> 联网

输出:

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
61
62
63
64
65
66
67
68
69
2025-08-13T10:35:42.627+0800    INFO    demo/main.go:24 access baidu.com success
2025-08-13T10:35:42.974+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:43.091+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:43.196+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:43.370+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:43.456+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:43.538+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:43.654+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:43.747+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:43.839+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:43.922+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:44.005+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:44.094+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:44.171+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:44.373+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:44.561+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:49.673+0800 ERROR demo/main.go:33 handle timeout error:%v {"error": "hystrix: timeout"}
2025-08-13T10:35:49.680+0800 ERROR demo/main.go:36 handle error:%v {"error": "Get \"https://baidu.com\": dial tcp: lookup baidu.com: no such host"}
2025-08-13T10:35:49.682+0800 ERROR demo/main.go:36 handle error:%v {"error": "Get \"https://baidu.com\": dial tcp: lookup baidu.com: no such host"}
2025-08-13T10:35:49.684+0800 ERROR demo/main.go:36 handle error:%v {"error": "Get \"https://baidu.com\": dial tcp: lookup baidu.com: no such host"}
2025-08-13T10:35:49.686+0800 ERROR demo/main.go:36 handle error:%v {"error": "Get \"https://baidu.com\": dial tcp: lookup baidu.com: no such host"}
2025-08-13T10:35:49.687+0800 ERROR demo/main.go:36 handle error:%v {"error": "Get \"https://baidu.com\": dial tcp: lookup baidu.com: no such host"}
2025-08-13T10:35:49.689+0800 ERROR demo/main.go:36 handle error:%v {"error": "Get \"https://baidu.com\": dial tcp: lookup baidu.com: no such host"}
2025-08-13T10:35:49.689+0800 INFO hystrix_tools/logger.go:27 [hystrix] hystrix-go: opening circuit my_command
2025-08-13T10:35:49.689+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:49.689+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:49.689+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:49.689+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:49.689+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:49.877+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:50.078+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:50.280+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:50.482+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:50.687+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:50.890+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:51.093+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:51.294+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:51.495+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:51.697+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:51.898+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:52.098+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:52.299+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:52.501+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:52.702+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:52.902+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:53.103+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:53.305+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:53.509+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:53.710+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:53.912+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:54.113+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:54.316+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:54.516+0800 ERROR demo/main.go:36 handle error:%v {"error": "hystrix: circuit open"}
2025-08-13T10:35:54.716+0800 INFO hystrix_tools/logger.go:27 [hystrix] hystrix-go: allowing single test to possibly close circuit my_command
2025-08-13T10:35:55.004+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:55.005+0800 INFO hystrix_tools/logger.go:27 [hystrix] hystrix-go: closing circuit my_command
2025-08-13T10:35:55.125+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:55.205+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:55.412+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:55.609+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:55.802+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:56.018+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:56.150+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:56.342+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:56.517+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:56.643+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:56.933+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:57.148+0800 INFO demo/main.go:24 access baidu.com success
2025-08-13T10:35:57.239+0800 INFO demo/main.go:24 access baidu.com success

分析:

可以看到第23行后触发熔断,第23行往前的10s事件窗口中,也就是1-23行中,发送的错误有7个(6/22 <30%, 7/23 > 30%),所以这个点就发生了熔断。熔断后,sleep 5s,然后开始尝试,尝试成功后,关闭熔断。





2.5 异步熔断 Go()

特性 Go() 函数 Do() 函数
执行方式 异步,立即返回 同步,阻塞等待
返回值 chan error error
使用场景 需要并发处理多个请求 简单的同步调用
错误处理 通过 channel 接收 直接返回错误

可以不学,感觉没必要,异步自己处理就好了。

摘抄:

https://www.cnblogs.com/jiujuan/p/17327387.html

https://learnku.com/articles/53019