参考:
为什么需要熔断
微服务集群中,每个应用基本都会依赖一定数量的外部服务。但是随时都可能遇到网络连接慢、超时、依赖服务过载、服务不可用的情况,在高并发场景下如果调用方不做任何处理,继续持续请求故障服务的话很容易引起整个微服务集群雪崩。
缓存雪崩
上图的数据流程是这样的:1. 从缓存服务拿数据,如果有本地缓存,则返回结果,否则去 Redis 拿数据,如果没有则请求源服务
雪崩发生:
- Redis集群因为不可抗拒的原因挂掉了
- 大量的请求到达缓存服务,缓存服务请求 Redis,但是因为 Redis 挂掉了,缓存服务会等待、重试之类,导致延迟增加,比如从 5ms 变为 500ms
- 这个时候就会出现大量的请求hang住,占用大量的资源
- 缓存服务就会并发请求源服务,源服务则会请求MySQL,这个时候的并发量可能非常大
- MySQL会占用大量资源,导致性能急剧下降,最后甚至可能挂掉
- 然后源服务跟 MySQL 之间的延迟也会变高,然后就会有大量的请求在等待源服务返回结果(hang 住了)
- 缓存服务大量的资源都耗费在了访问 Redis、源服务无果,最后自己被拖死,无法提供服务
- 网站崩溃
集群雪崩
- 首先,服务的处理能力开始出现过载
- 然后服务由于资源耗尽而不可用
- 最后由于服务内部出现严重的过载,导致响应严重超时,服务的调用方同样会出现大量请求的积压使资源耗尽
如果在系统过载的情况下,不进行任何控制,异常情况就会急剧扩散,导致雪崩情况出现。所以,想要避免系统雪崩,要么通过快速减少系统负载,即熔断、降级、限流等快速失败和降级机制;要么通过快速增加系统的服务能力来避免雪崩的发生,即弹性扩容机制。
利用熔断避免雪崩
熔断器一般有三个状态:
-
关闭- 默认状态,请求能被到达目标服务,同时统计在窗口时间成功和失败次数,如果达到错误率阈值,则会进入断开状态
-
断开- 在该状态下,发起请求时会立即返回错误,也可以返回一个降级的结果
-
半断开- 进行断开状态会维护一个超时时间,到达超时时间开始进入
半断开状态,尝试允许一部分请求正常通过并统计成功数量,如果请求正常则认为此时目标服务已恢复进入关闭状态,否则进入断开状态。半断开状态存在的目的在于实现了自我修复,同时防止正在恢复的服务再次被大量打垮。
- 进行断开状态会维护一个超时时间,到达超时时间开始进入
自适应熔断
这里就可以直接看作者自己的分析的源码了,不献丑了 juejin.cn/post/703099…
补充一下,这里列一下demo的逻辑注释
const (
// 服务的运行时间
duration = time.Minute * 5
// 休息状态时的随机数范围
breakRange = 20
// 工作状态时的随机数范围
workRange = 50
// 请求的时间间隔,设置为毫秒级。
requestInterval = time.Millisecond
// 状态因子,用于在绘图中使状态更可见。
stateFator = float64(time.Second/requestInterval) / 2
)
type (
// 服务器结构体,包含一个状态标志。
server struct {
state int32
}
// 计量结构体,记录调用次数。
metric struct {
calls int64
}
)
func (m *metric) addCall() {
atomic.AddInt64(&m.calls, 1)
}
func (m *metric) reset() int64 {
return atomic.SwapInt64(&m.calls, 0)
}
func newServer() *server {
return &server{}
}
func (s *server) serve(m *metric) bool {
m.addCall()
return atomic.LoadInt32(&s.state) == 1
}
// 启动一个goroutine,该goroutine会定期改变服务器的状态。
func (s *server) start() {
go func() {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
var state int32
for {
var v int32
if state == 0 {
v = r.Int31n(breakRange)
} else {
v = r.Int31n(workRange)
}
time.Sleep(time.Second * time.Duration(v+1))
state ^= 1
atomic.StoreInt32(&s.state, state)
}
}()
}
// 接受一个服务器实例、断路器实例、运行时间、以及一个计量实例。
// 定期尝试执行断路器的任务,如果服务器服务可用,则执行任务;否则,返回breaker.ErrServiceUnavailable。
// 在一段时间(duration)后停止任务。
func runBreaker(s *server, br breaker.Breaker, duration time.Duration, m *metric) {
ticker := time.NewTicker(requestInterval)
defer ticker.Stop()
done := make(chan lang.PlaceholderType)
go func() {
time.Sleep(duration)
close(done)
}()
for {
select {
case <-ticker.C:
_ = br.Do(func() error {
if s.serve(m) {
return nil
} else {
return breaker.ErrServiceUnavailable
}
})
case <-done:
return
}
}
}
func main() {
srv := newServer()
srv.start()
gb := breaker.NewBreaker()
fp, err := os.Create("result.csv")
logx.Must(err)
defer fp.Close()
fmt.Fprintln(fp, "seconds,state,googleCalls,netflixCalls")
var gm, nm metric
go func() {
// 用于定期记录服务器状态和调用次数到CSV文件
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
var seconds int
for range ticker.C {
seconds++
gcalls := gm.reset()
ncalls := nm.reset()
fmt.Fprintf(fp, "%d,%.2f,%d,%d\n",
seconds, float64(atomic.LoadInt32(&srv.state))*stateFator, gcalls, ncalls)
}
}()
var waitGroup sync.WaitGroup
waitGroup.Add(1)
go func() {
// 运行断路器任务
runBreaker(srv, gb, duration, &gm)
waitGroup.Done()
}()
go func() {
// 进度条
bar := pb.New(int(duration / time.Second)).Start()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
bar.Increment()
}
bar.Finish()
}()
waitGroup.Wait()
}