假设我们要在 API 每次出错时都中断连接链。但何时才能将其恢复到“关闭”状态呢?为此,我们需要引入一个额外的状态——“半开放”。它的目的是发送一些请求来检查 API 是否正常运行。因此,它充当了从“开放”状态到“关闭”状态路径上的一个中间层。
所以逻辑很简单:一旦出现任何错误,我们就切换到“打开”状态,并阻止 API 请求。当超过一定时间后,我们切换到“半打开”状态,并发送请求以检查一切是否正常。如果出现错误,我们就切换回“打开”状态;如果成功,则切换回“关闭”状态。
为了提高效率,我们还需要添加两样东西:
错误策略——忽略一些预期的错误;
阈值或制动策略——用来描述打破链条的规则。
综上所述,让我们来看一下活动图:
动手实践
API
开始之前,我们先创建一个测试环境。我们的 Go 应用需要监听两个 HTTP 端点。一个用作短信网关 API 的模拟接口。另一个端点会切换服务器的状态,从“正常”到“故障”,然后再切换回来。我们把这个功能放在一个单独的文件中实现。
// server.gopackagemainimport("log""net/http""os")// ExampleServer is a test server to check the "CircuitBreaker" patterntypeExampleServerstruct{addrstringlogger*log.LoggerisEnabledbool}// NewExampleServer creates the instance of our serverfuncNewExampleServer(addrstring)*ExampleServer{return&ExampleServer{addr:addr,logger:log.New(os.Stdout,"Server\t",log.LstdFlags),isEnabled:true,}}// ListenAndServe starts listening on the address provided // on creating the instance.func(s*ExampleServer)ListenAndServe()error{// The main endpoint we will request tohttp.HandleFunc("/",func(whttp.ResponseWriter,r*http.Request){ifs.isEnabled{s.logger.Println("responded with OK")w.WriteHeader(http.StatusOK)}else{s.logger.Println("responded with Error")w.WriteHeader(http.StatusInternalServerError)}})// Toggle endpoint to switch on and off responses from the main onehttp.HandleFunc("/toggle",func(whttp.ResponseWriter,r*http.Request){s.isEnabled=!s.isEnableds.logger.Println("toggled. Is enabled:",s.isEnabled)w.WriteHeader(http.StatusOK)})returnhttp.ListenAndServe(s.addr,nil)}
// client.gopackagemainimport("errors""net/http")typeNotificationClientinterface{Send()error// We ignore all the arguments to simplify the demo}typeSmsClientstruct{baseUrlstring}funcNewSmsClient(baseUrlstring)*SmsClient{return&SmsClient{baseUrl:baseUrl,}}func(s*SmsClient)Send()error{url:=s.baseUrl+"/"resp,err:=http.Get(url)iferr!=nil{returnerr}deferresp.Body.Close()ifresp.StatusCode<200||resp.StatusCode>=300{returnerrors.New("bad response")}returnnil}
客户端已准备就绪。我们只需要初始化文件中的所有内容即可使其正常工作main.go。
// main.gopackagemainimport("log""os""time")funcmain(){logger:=log.New(os.Stdout,"Main\t",log.LstdFlags)server:=NewExampleServer(":8080")gofunc(){_=server.ListenAndServe()}()client:=NewSmsClient("http://127.0.0.1:8080")for{err:=client.Send()time.Sleep(1*time.Second)iferr!=nil{logger.Println("caught an error",err)}}}
// circuit_breaker.gopackagemainimport("log""os""time""github.com/sony/gobreaker")typeClientCircuitBreakerProxystruct{clientNotificationClientlogger*log.Loggergb*gobreaker.CircuitBreaker// downloaded lib structure}// shouldBeSwitchedToOpen checks if the circuit breaker should// switch to the Open statefuncshouldBeSwitchedToOpen(countsgobreaker.Counts)bool{failureRatio:=float64(counts.TotalFailures)/float64(counts.Requests)returncounts.Requests>=3&&failureRatio>=0.6}funcNewClientCircuitBreakerProxy(clientNotificationClient)*ClientCircuitBreakerProxy{logger:=log.New(os.Stdout,"CB\t",log.LstdFlags)// We need circuit breaker configurationcfg:=gobreaker.Settings{// When to flush counters int the Closed stateInterval:5*time.Second,// Time to switch from Open to Half-openTimeout:7*time.Second,// Function with check when to switch from Closed to OpenReadyToTrip:shouldBeSwitchedToOpen,OnStateChange:func(_string,fromgobreaker.State,togobreaker.State){// Handler for every state change. We'll use for debugging purposelogger.Println("state changed from",from.String(),"to",to.String())},}return&ClientCircuitBreakerProxy{client:client,logger:logger,gb:gobreaker.NewCircuitBreaker(cfg),}}func(c*ClientCircuitBreakerProxy)Send()error{// We call the Execute method and wrap our client's call_,err:=c.gb.Execute(func()(interface{},error){err:=c.client.Send()returnnil,err})returnerr}
// main.gopackagemain// ...funcmain(){// ...varclientNotificationClient// Create a common Clientclient=NewSmsClient("http://127.0.0.1:8080")// And then wrap itclient=NewClientCircuitBreakerProxy(client)for{err:=client.Send()time.Sleep(1*time.Second)iferr!=nil{logger.Println("caught an error",err)}}}