>백엔드 개발 >Golang >Go의 회로 차단기: 계단식 오류 중지

Go의 회로 차단기: 계단식 오류 중지

WBOY
WBOY원래의
2024-07-17 17:41:111009검색

Circuit Breakers in Go: Stop Cascading Failures

회로 차단기

회로 차단기는 오류를 감지하고 오류가 지속적으로 반복되는 것을 방지하는 방식으로 이러한 오류를 처리하는 논리를 캡슐화합니다. 예를 들어, 외부 서비스, 데이터베이스 또는 일시적으로 오류가 발생할 수 있는 시스템의 모든 부분에 대한 네트워크 호출을 처리할 때 유용합니다. 회로 차단기를 사용하면 계단식 장애를 방지하고, 일시적인 오류를 관리하며, 시스템 고장 중에도 안정적이고 응답성이 뛰어난 시스템을 유지할 수 있습니다.

계단식 오류

계단식 오류는 시스템 한 부분의 오류가 다른 부분의 오류를 유발하여 광범위한 중단을 초래할 때 발생합니다. 분산 시스템의 마이크로서비스가 응답하지 않아 종속 서비스가 시간 초과되어 결국 실패하는 경우를 예로 들 수 있습니다. 애플리케이션의 규모에 따라 이러한 오류의 영향은 치명적일 수 있으며 이로 인해 성능이 저하되고 심지어 사용자 경험에도 영향을 미칠 수 있습니다.

회로 차단기 패턴

회로 차단기 자체는 기술/패턴이며 작동하는 세 가지 상태가 있습니다. 이에 대해 설명하겠습니다.

  1. 닫힌 상태: 닫힌 상태에서 회로 차단기는 모든 요청이 정상적으로 대상 서비스로 전달되도록 허용합니다. 요청이 성공하면 회로는 닫힌 상태로 유지됩니다. 그러나 특정 오류 임계값에 도달하면 회로는 개방 상태로 전환됩니다. 사용자가 문제 없이 로그인하고 데이터에 액세스할 수 있는 완전한 운영 서비스라고 생각하십시오. 모든 일이 순조롭게 진행되고 있습니다.

Circuit Breakers in Go: Stop Cascading Failures

2. 열린 상태 : 열린 상태에서 회로 차단기는 대상 서비스에 접속을 시도하지 않고 들어오는 모든 요청을 즉시 실패합니다. 실패한 서비스의 추가 과부하를 방지하고 복구할 시간을 주기 위해 상태로 들어갑니다. 미리 정의된 시간 초과 후 회로 차단기는 반개방 상태로 이동합니다. 관련된 예는 다음과 같습니다. 온라인 상점에서 모든 구매 시도가 실패하는 갑작스러운 문제가 발생했다고 상상해 보세요. 시스템 과부하를 피하기 위해 매장에서는 신규 구매 요청 접수를 일시적으로 중단합니다.

Circuit Breakers in Go: Stop Cascading Failures

3. 반 개방 상태 : 반 개방 상태에서 회로 차단기는 (구성 가능한) 제한된 수의 테스트 요청이 대상 서비스를 통과하도록 허용합니다. 그리고 이러한 요청이 성공하면 회로는 다시 닫힌 상태로 전환됩니다. 실패하면 회로는 열린 상태로 돌아갑니다. 위의 열린 상태에서 제공한 온라인 상점의 예에서 문제가 해결되었는지 확인하기 위해 온라인 상점에서 몇 번의 구매 시도를 허용하기 시작합니다. 몇 번의 시도가 성공하면 매장은 서비스를 완전히 재개하여 새로운 구매 요청을 수락합니다.

이 다이어그램은 회로 차단기가 서비스 B에 대한 요청이 성공했는지 확인한 후 실패/중단되는 경우를 보여줍니다.

Circuit Breakers in Go: Stop Cascading Failures

다음 다이어그램은 서비스 B에 대한 테스트 요청이 성공하고 회선이 닫히고 모든 추가 통화가 다시 서비스 B로 라우팅되는 경우를 보여줍니다.

Circuit Breakers in Go: Stop Cascading Failures

참고 : 회로 차단기의 주요 구성에는 실패 임계값(회로를 여는 데 필요한 실패 횟수), 열린 상태의 시간 제한, 반 열린 상태에서의 테스트 요청 수가 포함됩니다. 상태입니다.

Go에서 회로 차단기 구현

이 글을 따라가려면 Go에 대한 사전 지식이 필요하다는 점을 언급하는 것이 중요합니다.

다른 소프트웨어 엔지니어링 패턴과 마찬가지로 회로 차단기도 다양한 언어로 구현될 수 있습니다. 그러나 이 기사에서는 Golang에서의 구현에 중점을 둘 것입니다. goresilience, go-resilience, gobreaker 등 이 목적으로 사용할 수 있는 여러 라이브러리가 있지만 특히 gobreaker 라이브러리 사용에 집중하겠습니다.

Pro Tip : gobreaker 패키지의 내부 구현은 여기에서 확인하세요.

외부 API에 대한 호출을 처리하기 위해 회로 차단기가 구현된 간단한 Golang 애플리케이션을 고려해 보겠습니다. 이 기본 예는 회로 차단기 기술을 사용하여 외부 API 호출을 래핑하는 방법을 보여줍니다.

몇 가지 중요한 사항을 살펴보겠습니다.

  1. gobreaker.NewCircuitBreaker 함수는 사용자 정의 설정으로 회로 차단기를 초기화합니다
  2. cb.Execute 메소드는 HTTP 요청을 래핑하여 자동으로 회선 상태를 관리합니다.
  3. MaximumRequests는 상태가 Half Open일 때 통과할 수 있는 최대 요청 수
  4. 간격은 차단기가 내부 카운트를 클리어하기 위해 닫힌 상태의 순환 주기입니다
  5. 타임아웃은 열린 상태에서 반 열린 상태로 전환되기까지의 시간입니다.
  6. ReadyToTrip은 닫힌 상태에서 요청이 실패할 때마다 카운트 복사본과 함께 호출됩니다. ReadyToTrip이 true를 반환하면 회로 차단기가 열린 상태로 전환됩니다. 여기서는 요청이 3회 이상 연속으로 실패하면 true를 반환합니다.
  7. OnStateChange는 회로 차단기의 상태가 변경될 때마다 호출됩니다. 일반적으로 여기에서 상태 변경에 대한 측정항목을 수집하고 원하는 측정항목 수집기에 보고하려고 합니다.

회로 차단기 구현을 확인하기 위해 몇 가지 단위 테스트를 작성해 보겠습니다. 이해해야 할 가장 중요한 단위 테스트만 설명하겠습니다. 전체 코드는 여기에서 확인하실 수 있습니다.

  1. 연속 실패한 요청을 시뮬레이션하고 회로 차단기가 열린 상태로 전환되는지 확인하는 테스트를 작성하겠습니다. 기본적으로 3번의 실패 후 네 번째 실패가 발생하면 조건이 counts.ConsecutiveFailures >이므로 회로 차단기가 작동(열림)할 것으로 예상합니다. 3 . 테스트 내용은 다음과 같습니다.
 t.Run("FailedRequests", func(t *testing.T) {
         // Override callExternalAPI to simulate failure
         callExternalAPI = func() (int, error) {
             return 0, errors.New("simulated failure")
         }

         for i := 0; i < 4; i++ {
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err == nil {
                 t.Fatalf("expected error, got none")
             }
         }

         if cb.State() != gobreaker.StateOpen {
             t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
         }
     })
  1. open > - 열림 > 폐쇄 상태입니다. 하지만 먼저 개방 회로를 시뮬레이션하고 타임아웃을 호출하겠습니다. 시간 초과 후 회로가 반개방으로 전환되도록 성공 요청을 하나 이상 수행해야 합니다. 반개방 상태 이후에는 회로가 다시 완전히 닫히도록 또 다른 성공 요청을 해야 합니다. 어떠한 이유로든 해당 케이스에 성공 요청 기록이 없는 경우 다시 오픈 상태로 돌아갑니다. 테스트 방법은 다음과 같습니다.
     //Simulates the circuit breaker being open, 
     //wait for the defined timeout, 
     //then check if it closes again after a successful request.
         t.Run("RetryAfterTimeout", func(t *testing.T) {
             // Simulate circuit breaker opening
             callExternalAPI = func() (int, error) {
                 return 0, errors.New("simulated failure")
             }
    
             for i := 0; i < 4; i++ {
                 _, err := cb.Execute(func() (interface{}, error) {
                     return callExternalAPI()
                 })
                 if err == nil {
                     t.Fatalf("expected error, got none")
                 }
             }
    
             if cb.State() != gobreaker.StateOpen {
                 t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
             }
    
             // Wait for timeout duration
             time.Sleep(settings.Timeout + 1*time.Second)
    
             //We expect that after the timeout period, 
             //the circuit breaker should transition to the half-open state. 
    
             // Restore original callExternalAPI to simulate success
             callExternalAPI = func() (int, error) {
                 resp, err := http.Get(server.URL)
                 if err != nil {
                     return 0, err
                 }
                 defer resp.Body.Close()
                 return resp.StatusCode, nil
             }
    
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err != nil {
                 t.Fatalf("expected no error, got %v", err)
             }
    
             if cb.State() != gobreaker.StateHalfOpen {
                 t.Fatalf("expected circuit breaker to be half-open, got %v", cb.State())
             }
    
             //After verifying the half-open state, another successful request is simulated to ensure the circuit breaker transitions back to the closed state.
             for i := 0; i < int(settings.MaxRequests); i++ {
                 _, err = cb.Execute(func() (interface{}, error) {
                     return callExternalAPI()
                 })
                 if err != nil {
                     t.Fatalf("expected no error, got %v", err)
                 }
             }
    
             if cb.State() != gobreaker.StateClosed {
                 t.Fatalf("expected circuit breaker to be closed, got %v", cb.State())
             }
         })
    
    1. 요청이 2번 연속 실패하면 트리거되는 ReadyToTrip 조건을 테스트해 보겠습니다. 연속적인 실패를 추적하는 변수가 있습니다. ReadyToTrip 콜백은 2번의 실패(counts.ConsecutiveFailures > 2) 후에 회로 차단기가 작동하는지 확인하도록 업데이트됩니다. 오류를 시뮬레이션하고 횟수를 확인하며 지정된 오류 횟수 후에 회로 차단기가 개방 상태로 전환되는 테스트를 작성합니다.
       t.Run("ReadyToTrip", func(t *testing.T) {
               failures := 0
               settings.ReadyToTrip = func(counts gobreaker.Counts) bool {
                   failures = int(counts.ConsecutiveFailures)
                   return counts.ConsecutiveFailures > 2 // Trip after 2 failures
               }
      
               cb = gobreaker.NewCircuitBreaker(settings)
      
               // Simulate failures
               callExternalAPI = func() (int, error) {
                   return 0, errors.New("simulated failure")
               }
               for i := 0; i < 3; i++ {
                   _, err := cb.Execute(func() (interface{}, error) {
                       return callExternalAPI()
                   })
                   if err == nil {
                       t.Fatalf("expected error, got none")
                   }
               }
      
               if failures != 3 {
                   t.Fatalf("expected 3 consecutive failures, got %d", failures)
               }
               if cb.State() != gobreaker.StateOpen {
                   t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
               }
           })
      

      고급 전략

      회로 차단기 구현에 지수 백오프 전략을 추가하면 한 단계 더 나아갈 수 있습니다. 이 기사에서는 지수 백오프 전략의 예를 보여줌으로써 간단하고 간결하게 설명하겠습니다. 그러나 부하 차단, 격벽, 폴백 메커니즘, 컨텍스트 및 취소와 같이 언급할 가치가 있는 회로 차단기에 대한 다른 고급 전략도 있습니다. 이러한 전략은 기본적으로 회로 차단기의 견고성과 기능을 향상시킵니다. 다음은 지수 백오프 전략을 사용하는 예입니다.

      지수 백오프

      지수 백오프를 사용한 회로 차단기

      몇 가지 사항을 명확하게 짚어보겠습니다.

      사용자 정의 백오프 기능: exponentialBackoff 기능은 지터를 사용하여 지수 백오프 전략을 구현합니다. 기본적으로 시도 횟수를 기준으로 백오프 시간을 계산하므로 재시도를 시도할 때마다 지연이 기하급수적으로 증가합니다.

      재시도 처리: /api 핸들러에서 볼 수 있듯이 이제 로직에는 지정된 시도 횟수(시도 := 5)까지 외부 API 호출을 시도하는 루프가 포함됩니다. 시도가 실패할 때마다 재시도하기 전에 exponentialBackoff 함수에 의해 결정된 기간 동안 기다립니다.

      회로 차단기 실행: 회로 차단기는 루프 내에서 사용됩니다. 외부 API 호출이 성공하면( err == nil) 루프가 중단되고 성공적인 결과가 반환됩니다. 모든 시도가 실패하면 HTTP 503(서비스를 사용할 수 없음) 오류가 반환됩니다.

      회로 차단기 구현에 사용자 지정 백오프 전략을 통합하는 것은 실제로 일시적인 오류를 보다 원활하게 처리하는 것을 목표로 합니다. 재시도 간 지연 시간이 늘어나면 실패한 서비스에 대한 로드를 줄여 복구할 시간을 확보하는 데 도움이 됩니다. 위 코드에서 알 수 있듯이 외부 API를 호출할 때 재시도 사이에 지연을 추가하기 위해 exponentialBackoff 함수가 도입되었습니다.

      또한 측정항목과 로깅을 통합하여 실시간 모니터링 및 경고를 위한 Prometheus와 같은 도구를 사용하여 회로 차단기 상태 변경을 모니터링할 수 있습니다. 간단한 예는 다음과 같습니다.

      고급 전략으로 서킷 브레이커 패턴 구현

      보시겠지만 이제 우리는 다음을 수행했습니다.

      1. L16-21에서는 요청 수와 상태(성공, 실패, 회로 차단기 상태 변경)를 추적하기 위해 프로메테우스 카운터 벡터를 정의합니다.
      2. L25-26에서 정의된 측정항목은 init 함수에서 Prometheus에 등록됩니다.

      프로 팁 : Go의 init 함수는 메인 함수나 패키지의 다른 코드가 실행되기 전에 패키지의 상태를 초기화하는 데 사용됩니다. 이 경우 init 함수는 Prometheus에 requestCount 지표를 등록합니다. 이는 본질적으로 Prometheus가 이 측정항목을 인식하고 애플리케이션이 실행되기 시작하자마자 데이터 수집을 시작할 수 있도록 보장합니다.

      1. 우리는 고장 카운터를 늘리고 회로 트립 시기를 결정하는 ReadyToTrip 기능을 포함하여 맞춤 설정으로 회로 차단기를 만듭니다.

      2. 상태 변경을 기록하고 해당 프로메테우스 측정항목을 증가시키는 OnStateChange

      3. /metrics 엔드포인트에서 Prometheus 측정항목을 노출합니다

      마무리

      이 기사를 마무리하면서, 탄력적이고 안정적인 시스템을 구축하는 데 회로 차단기가 어떻게 큰 역할을 하는지 확인하셨기를 바랍니다. 연속적인 오류를 사전에 방지함으로써 마이크로서비스와 분산 시스템의 안정성을 강화하고 역경 속에서도 원활한 사용자 경험을 보장합니다.

      확장성을 위해 설계된 모든 시스템에는 장애를 적절하게 처리하고 신속하게 복구하기 위한 전략이 통합되어야 한다는 점을 명심하세요 —  Oluwafemi , 2024

      원본은 https://oluwafemiakinde.dev 2024년 6월 7일에 게시되었습니다.

      위 내용은 Go의 회로 차단기: 계단식 오류 중지의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
이전 기사:파이프라인 개념다음 기사:파이프라인 개념