前言
分布式系统中经常会出现因为某个服务不可用导致整个系统变的不可用,这种情况称为服务雪崩。
服务雪崩
上图中, A为服务提供者, B为A的服务调用者, C和D是B的服务调用者. 当A的不可用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了
造成服务雪崩的原因
我们把服务分为服务提供者和服务调用者,造成服务雪崩的流程如下
1、服务提供者不可用
2、服务调用者重试请求服务提供者(调用放大)
3、服务调用者不可用
4、系统雪崩
服务不可用的原因:
1、硬件故障
2、缓存击穿
3、程序bug(死循环、没出口的递归等导致cpu打满)
4、请求量太大
硬件故障可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问.
缓存击穿一般发生在缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用.
在秒杀和大促开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用.
而形成 重试加大流量 的原因有:
- 用户重试
- 代码逻辑重试
在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单.
服务调用端的会存在大量服务异常后的重试逻辑.
这些重试都会进一步加大请求流量.
最后, 服务调用者不可用 产生的主要原因是:
- 同步等待造成的资源耗尽
当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.
Hystrix预防服务雪崩
Hystrix的设计原则包括:
- 资源隔离
- 熔断器
- 命令模式
资源隔离
在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如:
商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务. 如图所示:
调用三个依赖服务会共享商品详情服务的线程池. 如果其中的商品评论服务不可用, 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩. 如图所示:
Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩.
如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用.
熔断器模式
熔断器模式定义了熔断器开关相互转换的逻辑:
服务的健康状况 = 请求失败数 / 请求总数.
熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.
- 当熔断器开关关闭时, 请求被允许通过熔断器. 如果当前健康状况高于设定阈值, 开关继续保持关闭. 如果当前健康状况低于设定阈值, 开关则切换为打开状态.
- 当熔断器开关打开时, 请求被禁止通过.
- 当熔断器开关处于打开状态, 经过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只允许一个请求通过. 当该请求调用成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止通过.
熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.
命令模式
Hystrix使用命令模式(来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback).
在使用了Command模式构建了服务对象之后, 服务便拥有了熔断器和线程池的功能.
Hystrix的内部处理逻辑
下图为Hystrix服务调用的内部逻辑:
- 构建Hystrix的Command对象, 调用执行方法.
- Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法.
- 若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若超过线程池已满, 则执行降级服务getFallback方法.
- 若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法.
- 若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.
- 若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.
- 若服务执行成功, 返回正常结果.
- 若服务降级方法getFallback执行成功, 则返回降级结果.
- 若服务降级方法getFallback执行失败, 则抛出异常.
hystrix go
git地址:github.com/afex/hystrix-go/hystrix
使用
hystrix-go 的使用非常简单,你可以调用它的 Go 或者 Do 方法,只是 Go 方法是异步的方式。而 Do 方法是同步方式。我们从一个简单的例子开启。
_ = hystrix.Do("command", func() error {
// talk to other services
_, err := http.Get("https://www.baidu.com/")
if err != nil {
fmt.Println("get error:%v",err)
return err
}
return nil
}, func(err error) error {
fmt.Printf("handle error:%v\n", err)
return nil
})
Do 函数需要三个参数,第一个参数 commmand 名称,你可以把每个名称当成一个独立当服务,第二个参数是处理正常的逻辑,比如 http 调用服务,返回参数是 err。如果处理 | 调用失败,那么就执行第三个参数逻辑, 我们称为保底操作。由于服务错误率过高导致熔断器开启,那么之后的请求也直接回调此函数。
既然熔断器是按照配置的规则而进行是否开启的操作,那么我们当然可以设置我们想要的值。
hystrix.ConfigureCommand("command", hystrix.CommandConfig{
Timeout: 3000,
MaxConcurrentRequests: 10,
SleepWindow: 5000,
RequestVolumeThreshold: 10,
ErrorPercentThreshold: 30,
})
_ = hystrix.Do("wuqq", func() error {
// talk to other services
_, err := http.Get("https://www.baidu.com/")
if err != nil {
fmt.Println("get error:%v",err)
return err
}
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 并且错误率到达这个百分比后就会启动熔断
当然你不设置的话,那么自动走的默认值。
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{}
)
测试代码
func TestHystrixRequest(t *testing.T) {
hystrix.ConfigureCommand("mycommand", hystrix.CommandConfig{
Timeout: 1000, // 超时时间1秒
MaxConcurrentRequests: 20, // 最大并发数量20
SleepWindow: 1000, // 窗口时间1秒,熔断开启1秒后尝试重试
RequestVolumeThreshold: 5, // 10秒钟请求数量超过5次,启动熔断器判断
ErrorPercentThreshold: 50, // 请求数超过5并且错误率达到百分之50,开启熔断
})
wg := new(sync.WaitGroup)
// 模拟并发10次请求,5次返回err,导致熔断器开启
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
_ = hystrix.Do("mycommand", func() error {
_, err := http.Get("https://baidu.com")
if err != nil {
fmt.Printf("请求失败, err:%s\n", err.Error())
return err
}
if i % 2 == 0 {
return errors.New("测试错误!")
}
fmt.Println("success!")
return nil
}, func(err error) error {
fmt.Printf("handle error:%v\n", err)
return nil
})
}(i)
}
wg.Wait()
fmt.Println("----------------")
// 继续模拟10次请求,熔断器应该为开启状态
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
_ = hystrix.Do("mycommand", func() error {
_, err := http.Get("https://baidu.com")
if err != nil {
fmt.Printf("请求失败, err:%s\n", err.Error())
return err
}
fmt.Println("success!")
return nil
}, func(err error) error {
fmt.Printf("handle error:%v\n", err)
return nil
})
}(i)
}
wg.Wait()
fmt.Println("----------------")
// 睡眠1秒,转换为半开状态,并发请求10次,应该会有一个goroutine真正去请求,返回成功,其它请求直接走fallback逻辑
time.Sleep(1 * time.Second)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
_ = hystrix.Do("mycommand", func() error {
_, err := http.Get("https://baidu.com")
if err != nil {
fmt.Printf("请求失败, err:%s\n", err.Error())
return err
}
fmt.Println("success!")
return nil
}, func(err error) error {
fmt.Printf("handle error:%v\n", err)
return nil
})
}(i)
}
wg.Wait()
fmt.Println("----------------")
// 熔断器已经由半开转为关闭状态,请求应该全部成功
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
_ = hystrix.Do("mycommand", func() error {
_, err := http.Get("https://baidu.com")
if err != nil {
fmt.Printf("请求失败, err:%s\n", err.Error())
return err
}
fmt.Println("success!")
return nil
}, func(err error) error {
fmt.Printf("handle error:%v\n", err)
return nil
})
}(i)
}
wg.Wait()
}
执行结果:
=== RUN TestHystrixRequest
success!
handle error:测试错误!
success!
handle error:测试错误!
success!
success!
handle error:测试错误!
handle error:测试错误!
success!
handle error:测试错误!
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
success!
success!
success!
success!
success!
success!
success!
success!
success!
success!
success!