图解 Go 微服务中的熔断器和重试

今天我们来讨论微服务架构中的自我恢复能力 。通常情况下,服务间会通过同步或异步的方式进行通信 。我们假定把一个庞大的系统分解成一个个的小块能将各个服务解耦 。管理服务内部的通信可能有点困难了 。你可能听说过这两个著名的概念:熔断和重试 。
熔断器

图解 Go 微服务中的熔断器和重试

文章插图
 
01
想象一个简单的场景:用户发出的请求访问服务 A 随后访问另一个服务 B 。我们可以称 B 是 A 的依赖服务或下游服务 。到服务 B 的请求在到达各个实例前会先通过负载均衡器 。
后端服务发生系统错误的原因有很多,例如慢查询、network blip 和内存争用 。在这种场景下,如果返回 A 的 response 是 timeout 和 server error,我们的用户会再试一次 。在混乱的局面中我们怎样来保护下游服务呢?
图解 Go 微服务中的熔断器和重试

文章插图
 
02
熔断器可以让我们对失败率和资源有更好的控制 。熔断器的设计思路是不等待 TCP 的连接 timeout 快速且优雅地处理 error 。这种 fail fast 机制会保护下游的那一层 。这种机制最重要的部分就是立刻向调用方返回 response 。没有被 pending request 填充的线程池,没有 timeout,而且极有可能烦人的调用链中断者会更少 。此外,下游服务也有了充足的时间来恢复服务能力 。完全杜绝错误很难,但是减小失败的影响范围是有可能的 。
图解 Go 微服务中的熔断器和重试

文章插图
 
03
通过 hystrix 熔断器,我们可以采用降级方案,对上游返回降级后的结果 。例如,服务 B 可以访问一个备份服务或 cache,不再访问原来的服务 C 。引入这种降级方案需要集成测试,因为我们在 hAppy path(译注:所谓 happy path,即测试方法的默认场景,没有异常和错误信息 。具体可参见 wikipedia)可能不会遇到这种网络模式 。
状态
图解 Go 微服务中的熔断器和重试

文章插图
 
04
熔断器有三个主要的状态:
  • Closed:让所有请求都通过的默认状态 。在阈值下的请求不管成功还是失败,熔断器的状态都不会改变 。可能出现的错误是 Max Concurrency(最大并发数)和 Timeout(超时) 。
  • Open:所有的请求都会返回 Circuit Open 错误并被标记为失败 。这是一种不等待处理结束的 timeout 时间的 fail-fast 机制 。
  • Half Open:周期性地向下游服务发出请求,检查它是否已恢复 。如果下游服务已恢复,熔断器切换到 Closed 状态,否则熔断器保持 Open 状态 。
熔断器原理控制熔断的设置共有 5 个主要参数 。
// CommandConfig is used to tune circuit settings at runtimetype CommandConfig struct { Timeout                int `json:"timeout"` MaxConcurrentRequests  int `json:"max_concurrent_requests"` RequestVolumeThreshold int `json:"request_volume_threshold"` SleepWindow            int `json:"sleep_window"` ErrorPercentThreshold  int `json:"error_percent_threshold"`}查看源码
可以通过根据两个服务的 SLA(‎ Service Level Agreement,服务级别协议)来定出阈值 。如果在测试时把依赖的其他服务也涉及到了,这些值会得到很好的调整 。
一个好的熔断器的名字应该能精确指出哪个服务连接出了问题 。实际上,请求一个服务时可能会有很多个 API endpoint 。每一个 endpoint 都应该有一个对应的熔断器 。
生产上的熔断器熔断器通常被放在聚合点上 。尽管熔断器提供了一种 fail-fast 机制,但我们仍然需要确保可选的降级方案可行 。如果我们因为假定需要降级方案的场景出现的可能性很小就不去测试它,那(之前的努力)就是白费力气了 。即使在最简单的演练中,我们也要确保阈值是有意义的 。以我的个人经验,把参数配置在 log 中 print 出来对于 debug 很有帮助 。
Demo这段实例代码用的是 hystrix-go 库,hystrix Netflix 库在 Golang 的实现 。
package mainimport ( "errors" "fmt" "log" "net/http" "os" "github.com/afex/hystrix-go/hystrix")const commandName = "producer_api"func main() { hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{  Timeout:                500,  MaxConcurrentRequests:  100,  ErrorPercentThreshold:  50,  RequestVolumeThreshold: 3,  SleepWindow:            1000, }) http.HandleFunc("/", logger(handle)) log.Println("listening on :8080") http.ListenAndServe(":8080", nil)}func handle(w http.ResponseWriter, r *http.Request) { output := make(chan bool, 1) errors := hystrix.Go(commandName, func() error {  // talk to other services  err := callChargeProducerAPI()  // err := callWithRetryV1()  if err == nil {   output <- true  }  return err }, nil) select { case out := <-output:  // success  log.Printf("success %v", out) case err := <-errors:  // failure  log.Printf("failed %s", err) }}// logger is Handler wrapper function for loggingfunc logger(fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) {  log.Println(r.URL.Path, r.Method)  fn(w, r) }}func callChargeProducerAPI() error { fmt.Println(os.Getenv("SERVER_ERROR")) if os.Getenv("SERVER_ERROR") == "1" {  return errors.New("503 error") } return nil}


推荐阅读