[Dica rápida] Implementando resiliência em projeto Golang
Veja nesse post rápido como configurar o Circuit Breaker no seu projeto Golang
Dando continuidade ao meu post anterior: Introdução ao Circuit Breaker, onde abordei um dos conceitos mais utilizados quando falamos sobre resiliência e tolerância a falhas em sistemas distribuídos.
Hoje vamos explorar a implementação prática dele em um projeto Golang.
Caso tenha interesse em clonar a versão final do código que será apresentado neste artigo, segue o seu link no meu Github: go-circuitbreaker
Bom, avançando para parte do código, eu criei uma arquitetura simples com os seguintes arquivos:
├── config
│ └── config.go
├── handler
│ └── handler.go
├── main.go
├── request
│ └── investment.go
└── request.http
Vamos conhecer cada um destes arquivos. Iniciando pelo arquivo: config/config.go
ele é responsável por configurar o Circuit Breaker, definindo:
package config
import (
"time"
"github.com/sony/gobreaker"
)
func NewCircuitBreaker() *gobreaker.CircuitBreaker {
settings := gobreaker.Settings{
Name: "InvestmentAPI",
Timeout: 5 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 3 && failureRatio >= 0.2
},
}
return gobreaker.NewCircuitBreaker(settings)
}
Analisando este trecho de código nós temos:
- Name: o nome do circuito
- Timeout: Define quanto tempo o Circuit Breaker permanecerá aberto antes de permitir novas requisições para testar se o serviço se recuperou
- ReadyToTrip: Este é um callback (função) que define a condição para que o Circuit Breaker entre no estado aberto (ou seja, para que ele “dispare”). O ReadyToTrip é chamado sempre que uma requisição falha ou é bem-sucedida. O retorno true fará com que o Circuit Breaker se abra, enquanto o retorno de false o mantém fechado.
Aqui, a lógica é a seguinte:
counts.TotalFailures
: O número total de requisições que falharam;counts.Requests
: O número total de requisições feitas (bem-sucedidas ou falhas).
A função calcula a proporção de falhas:
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
Isso converte os valores inteiros de falhas e requisições para float64
para obter a razão entre o número de falhas e o número total de requisições.
Se o número de requisições for maior ou igual a 3 e a proporção de falhas for maior ou igual a 20% (0.2), o Circuit Breaker será disparado (abrirá):
return counts.Requests >= 3 && failureRatio >= 0.2
Emrequest/investment.go
nós temos a implementação das requisições HTTP que o cliente faz para o servidor. Essa função está diretamente integrada ao Circuit Breaker, garantindo que o sistema não tente acessar o servidor de investimentos repetidamente quando ele está fora do ar ou com problemas.
O Circuit Breaker abre o circuito após um número de falhas consecutivas e fecha após um tempo de recuperação.
No trecho de código a seguir eu adicionei um case para que possamos ter um print na console com os status relacionados ao nosso circuito.
package request
import (
"fmt"
"io"
"net/http"
"github.com/sony/gobreaker"
)
type InvestmentRequest struct {
cb *gobreaker.CircuitBreaker
client http.Client
url string
}
func NewInvestmentRequest(cb *gobreaker.CircuitBreaker) *InvestmentRequest {
return &InvestmentRequest{
cb: cb,
client: http.Client{},
url: "http://localhost:8081/api/v1/investment",
}
}
func (r *InvestmentRequest) GetInvestmentData() ([]byte, error) {
switch r.cb.State() {
case gobreaker.StateClosed:
fmt.Println("Circuit Breaker Status: FECHADO")
case gobreaker.StateOpen:
fmt.Println("Circuit Breaker Status: ABERTO")
case gobreaker.StateHalfOpen:
fmt.Println("Circuit Breaker Status: SEMI-ABERTO")
}
body, err := r.cb.Execute(func() (interface{}, error) {
req, err := http.NewRequest(http.MethodGet, r.url, nil)
if err != nil {
return nil, err
}
resp, err := r.client.Do(req)
if err != nil {
fmt.Println("Erro ao chamar a API de investimentos:", err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Erro na resposta da API de investimentos: %v\n", resp.Status)
return nil, fmt.Errorf("erro na resposta: %v", resp.Status)
}
return io.ReadAll(resp.Body)
})
if err != nil {
fmt.Printf("Erro ao buscar dados: %v\n", err)
return nil, err
}
return body.([]byte), nil
}
Avançando para o arquivo:handler/handler.go,
nele nós temos a lógica que lida com as requisições HTTP.
Esse arquivo é responsável por lidar com as respostas e erros vindos do servidor de investimentos. Caso a requisição falhe, o erro é tratado e o Circuit Breaker entra em ação para impedir que novas requisições sobrecarreguem o sistema.
package handler
import (
"encoding/json"
"fmt"
"go-circuitbreaker/request"
"net/http"
)
type Handler struct {
investmentRequest *request.InvestmentRequest
}
func NewHandler(investmentRequest *request.InvestmentRequest) *Handler {
return &Handler{
investmentRequest: investmentRequest,
}
}
func (h *Handler) GetInvestmentData(w http.ResponseWriter, r *http.Request) {
data, err := h.investmentRequest.GetInvestmentData()
if err != nil {
http.Error(w, fmt.Sprintf("Erro: %v", err), http.StatusInternalServerError)
return
}
response := map[string]interface{}{
"status": "success",
"data": string(data),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Avançando para o nosso arquivo: main.go,
aqui como já sabemos é o ponto de entrada da aplicação Golang.
Neste arquivo nós estamos iniciando o servidor de investimentos e configuramos as rotas HTTP que serão consumidas pelo cliente. A função principal também é responsável por configurar o Circuit Breaker e chamar o serviço de investimentos periodicamente, aplicando o Circuit Breaker para proteger o sistema contra falhas repetitivas.
package main
import (
"fmt"
"go-circuitbreaker/config"
"go-circuitbreaker/handler"
"go-circuitbreaker/request"
"net/http"
)
func main() {
circuitBreaker := config.NewCircuitBreaker()
investmentRequest := request.NewInvestmentRequest(circuitBreaker)
investmentHandler := handler.NewHandler(investmentRequest)
http.HandleFunc("/api/v1/investment", investmentHandler.GetInvestmentData)
fmt.Println("Investment API running on port 8080")
http.ListenAndServe(":8080", nil)
}
Por fim, eu criei um arquivo chamado request.http
: que nos permite testar as requisições diretamente usando ferramentas como o REST Client no VS Code.
GET http://localhost:8080/api/v1/investment
Accept: application/json
Agora para simular um microserviço de investimentos que simula algumas falhas entre algumas requisições, eu criei o projeto go-invent-example e publiquei ele no server render.com gratuito.
Caso tenha interesse em saber como publicar o seu projeto no Render, eu recomendo a leitura do seguinte artigo onde demonstro esse passo a passo: [Dica rápida] Publicando projeto Golang em server grátis
Agora para testar o nosso passo a passo, eu atualizarei o arquivo investment.go com a nova url publicado do meu projeto:
func NewInvestmentRequest(cb *gobreaker.CircuitBreaker) *InvestmentRequest {
return &InvestmentRequest{
cb: cb,
client: http.Client{},
url: "https://go-invest-example.onrender.com/api/v1/investment",
}
}
Executando ele para que possamos ver o circuito funcionando, nós temos o seguinte resultado na nossa console:
Bom, com isso finalizo mais este artigo, espero que tenham gostado e até o próximo pessoal.