Go에서 회로 차단기 패턴: 연쇄적 고장 방지
Go 마이크로서비스의 캐스케이딩 장애를 방지하세요.
회로 차단기(Circuit Breaker)는 고장난 의존성(dependency)을 계속 호출하는 것을 막아, 고루틴(goroutine), 소켓, 메모리를 소모하며 전체 시스템이 붕괴하는 연쇄 고장을 방지합니다.
어려운 부분은 상태 머신(state machine) 구현이 아닙니다. 회로 차단기를 어디에 배치할지, 어떤 상태를 실패로 간주할지, 타임아웃 및 재시도와 어떻게 상호작용할지, 회로가 열렸을 때 서비스는 어떤 행동을 해야 할지를 결정하는 것입니다.

Go 언어에서 회로 차단기 패턴은 외부 호출(HTTP API, 결제 게이트웨이, 검색 서비스, 이메일 제공자, LLM 게이트웨이, 내부 마이크로서비스 등) 주위에서 특히 유용합니다. 이러한 의존성들은 느려지거나 과부하가 걸리거나 부분적으로 사용 불가능해질 수 있습니다. 올바르게 사용하면 회로 차단기는 연쇄 고장을 줄여줍니다. 그러나 잘못 사용하면 또 다른 모호한 실패 모드가 됩니다.
회로 차단기는 어떤 문제를 해결합니까?
분산 시스템은 깔끔하게 실패하는 경우가 드뭅니다.
의존성이 완전히 다운된 것이 아닐 수 있습니다. 다음과 같은 상태일 수 있습니다.
- 500 오류 반환
- 429 속도 제한 응답 반환
- TCP 연결은 수락하지만 응답하지 않음
- 300밀리초 대신 30초 동안 응답
- 일부 요청만 실패
- 모든 클라이언트가 동시에 재시도하여 과부하 발생
가장 나쁜 경우는 종종 단단한 실패(hard failure)가 아닙니다. 느린 의존성입니다.
느린 호출은 고루틴, 소켓, 데이터베이스 연결, 메모리 및 워커 용량을 소모합니다. 이미 건강하지 않은 의존성을 계속 기다리면, 당신의 서비스도 건강하지 않게 될 수 있습니다.
회로 차단기는 의존성이 실패 임계값을 넘었을 때 빠르게 실패(fail fast)함으로써 이를 방지합니다.
무한정 이런 식으로 진행하는 대신:
request -> call dependency -> wait -> timeout -> retry -> wait -> fail
서비스는 결국 이런 식으로 동작합니다:
request -> circuit open -> return fallback or error immediately
이 빠른 실패는 항상 기분 좋은 것은 아니지만, 예측 가능합니다. 느린 붕괴보다 예측 가능한 실패가 운영하기 더 쉽습니다.
회로 차단기의 세 가지 상태
대부분의 회로 차단기는 세 가지 상태를 사용합니다.
닫힘(Closed)
회로는 정상 작동 중에 닫힙니다.
요청이 통과할 수 있습니다. 차단기는 성공과 실패를 기록합니다. 실패의 수나 비율이 임계값을 넘으면 차단기가 열립니다.
‘닫힘’은 “영원히 안전하다"는 의미는 아닙니다. “현재 트래픽이 허용된다"는 의미입니다.
열림(Open)
의존성이 건강하지 않다고 판단될 때 회로는 열립니다.
요청이 즉시 거부됩니다. 서비스는 대체 응답(fallback), 캐시된 응답, 성능이 저하된 응답, 또는 명확한 상류 오류를 반환해야 합니다.
‘열림’ 상태는 의존성을 고치지는 않습니다. 이는 의존성이 복구될 시간을 주며, 호출자가 자원을 낭비하는 것을 보호합니다.
반개방(Half-Open)
냉각 기간(cool-down period) 이후, 차단기는 반개방 상태로 진입합니다.
제한된 수의 시험 요청만 통과할 수 있습니다. 성공하면 차단기가 닫히고, 실패하면 다시 열립니다.
반개방 상태는 두 가지 나쁜 극단을 피하기 때문에 중요합니다.
- 의존성을 다시 시도하지 않음
- 너무 빨리 전체 트래픽을 다시 보내는 것
상태 전환은 다음과 같습니다:
회로 차단기 vs 타임아웃 vs 재시도
회로 차단기, 재시도, 타임아웃을 서로 교환 가능하다고 여기는 것은 흔한 실수입니다. 이들은 관련이 있지만 다른 문제를 해결합니다.
타임아웃(Timeout)
타임아웃은 한 작업이 실행될 수 있는 시간을 제한합니다.
Go에서는 일반적으로 만료 시간 또는 타임아웃이 설정된 context.Context를 외부 호출에 전달합니다.
타임아웃은 이 질문에 답합니다:
이 한 번의 호출을 위해 얼마나 기다릴 수 있습니까?
재시도(Retry)
재시도는 실패가 일시적일 수 있을 때 작업을 반복합니다.
재시도는 짧은 네트워크 장애, 일시적인 503 응답, 연결 재설정 및 기타 일시적인 실패에 유용합니다.
재시도는 이 질문에 답합니다:
이 호출을 다시 시도해야 합니까?
회로 차단기(Circuit Breaker)
회로 차단기는 의존성이 아마도 건강하지 않을 때 호출을 중단합니다.
이 질문에 답합니다:
지금 이 의존성을 호출해야 합니까?
속도 제한기(Rate Limiter)
속도 제한기는 시간에 따라 허용되는 트래픽 양을 제어합니다.
이 질문에 답합니다:
이 호출자가 얼마나 많은 트래픽을 보내야 합니까?
벌크헤드(Bulkhead)
벌크헤드는 자원을 격리하여 하나의 의존성이 모든 것을 소비하지 못하게 합니다.
이 질문에 답합니다:
이 의존성이 내 서비스의 얼마나 많은 부분을 손상시킬 수 있습니까?
이러한 패턴은 함께 사용될 때 가장 강력합니다. 타임아웃이 없는 회로 차단기는 약합니다. Jitter가 없는 재시도는 재시도 폭풍을 유발할 수 있습니다. 메트릭이 없는 대체 응답은 장애를 숨길 수 있습니다.
Go에서 회로 차단기를 사용해야 하는 시기
서비스가 서비스와 독립적으로 실패할 수 있는 의존성을 호출할 때 회로 차단기를 사용하십시오.
좋은 후보 사례에는 다음이 포함됩니다.
- 외부 HTTP API
- 결제 처리기
- 이메일 및 SMS 제공자
- 검색 서비스
- 추천 서비스
- LLM 추론 게이트웨이
- 내부 마이크로서비스 엔드포인트
- 타사 SaaS API
- 느리거나 과부하가 걸린 읽기 측 서비스
호출자가 우아하게 성능을 저하시킬 수 있을 때 회로 차단기는 특히 유용합니다.
예를 들어:
- 캐시된 제품 데이터 반환
- 추천 블록 건너뛰기
- 결제 제공자를 일시적으로 사용 불가능하게 표시
- 작업을 나중에 처리할 수 있도록 대기열에 추가
- 부분적 응답 반환
- 명확한 임시 오류와 함께 빠르게 실패
중요한 질문은 “이 호출이 실패할 수 있습니까?“가 아닙니다. 모든 것은 실패할 수 있습니다. 더 나은 질문은 다음과 같습니다:
이 의존성이 실패한다면, 전체 트래픽을 계속 보내야 합니까?
답이 아니라면 회로 차단기가 도움이 될 수 있습니다.
회로 차단기를 사용하지 말아야 하는 시기
패턴이 책임감 있어 보인다고 해서 모든 함수에 회로 차단기를 추가하지 마십시오.
회로 차단기는 일반적으로 다음과 같은 경우에 유용하지 않습니다.
- 로컬 인프로세스 함수 호출
- 모놀리식 아키텍처 내의 단순 CRUD
- 유효성 검사 로직
- 결정론적 비즈니스 규칙
- CPU만 사용하는 로컬 연산
- 유용한 대체 응답이 없는 코드 경로
- 멱등하지 않은 쓰기 작업
- 더 강력한 워크플로우 레이어로 이미 보호된 의존성
회로 차단기는 또한 기본적인 위생 조치를 대체하지 않습니다.
- 타임아웃 설정
- 컨텍스트 전파
- 연결 풀을 올바르게 사용
- 오류를 명시적으로 처리
- 재시도를 안전하게 만듦
- 실패율 관찰
나쁜 회로 차단기는 시스템을 추론하기 더 어렵게 만들 수 있습니다. 실제 문제를 숨기거나, 트래픽을 너무 공격적으로 거부하거나, 복구 중에 혼란스러운 동작을 생성할 수 있습니다.
약간 주관적인 규칙은 간단합니다:
회로 차단기를 모든 곳에 두지 말고, 의존성 경계에서 추가하십시오.
Go 회로 차단기 라이브러리 선택
기본 회로 차단기는 직접 구현할 수 있지만, 대부분의 프로덕션 Go 서비스는 라이브러리를 사용해야 합니다.
가장 일반적이고 간단한 선택지는 sony/gobreaker입니다.
이 라이브러리는 다음을 제공합니다:
- 닫힘, 열림, 반개방 상태
- 구성된 실패 임계값
- 구성된 열림 상태 타임아웃
- 상태 변경 콜백
- 요청 카운터
- v2의 제네릭 지원
- 작은 API 표면
더 큰 탄력성 파이프라인을 위해 재시도, 타임아웃, 대체 응답, 속도 제한, 벌크헤드 격리, 회로 차단기 등 여러 정책을 구성하는 라이브러리를 살펴볼 수도 있습니다. 이는 단일 탄력성 레이어를 작업 주위에두고 싶을 때 유용할 수 있습니다.
그러나 많은 Go 서비스에게는 gobreaker면 충분합니다.
Go 회로 차단기 패키지 비교
Go 표준 라이브러리에는 내장된 회로 차단기가 없습니다. 실제로는 작은 회로 차단기 라이브러리, 더 큰 탄력성 프레임워크, 또는 오래된 Hystrix 스타일 패키지 중 하나를 선택하게 됩니다.
대부분의 새로운 Go 서비스에서는 결정이 간단합니다:
- 작고 집중적인 회로 차단기를 원한다면
sony/gobreaker사용 - 회로 차단기를 재시도, 타임아웃, 대체 응답, 벌크헤드, 속도 제한 및 기타 탄력성 정책과 함께 구성하고 싶다면
failsafe-go사용 - 이미 레거시 코드를 사용하고 있지 않다면
hystrix-go로 새 프로젝트를 시작하지 않음
| Package | Best for | Strengths | Tradeoffs |
|---|---|---|---|
sony/gobreaker/v2 |
Simple circuit breakers around HTTP/RPC clients | Small API, generic v2 support, clear state model, easy to wrap dependency clients | Only solves circuit breaking; retries, timeouts, and fallbacks must be composed separately |
failsafe-go |
Full resilience policy composition | Retry, fallback, circuit breaker, timeout, bulkhead, rate limiter, cache, hedge, adaptive limiter, and adaptive throttler policies | More concepts to learn; heavier than needed if you only want a basic breaker |
afex/hystrix-go |
Legacy Hystrix-style systems | Familiar Hystrix concepts, command-style execution, historical usage | Older design; not the best default for new Go services |
go-kit/kit/circuitbreaker |
Go kit endpoint-based services | Fits Go kit middleware style and endpoint architecture | Mostly useful if your service already uses Go kit |
cep21/circuit |
Hystrix-like circuit breaker behavior | More featureful Hystrix-style approach | Less common as the simple default; may be more than needed for small services |
저의 기본 추천은 고의적으로 지루합니다: 회로 차단기만 필요하면 sony/gobreaker/v2로 시작하십시오. 하나의 장소에서 완전한 탄력성 정책을 표현하고 싶다면 failsafe-go를 사용하십시오.
이러한 분리는 아키텍처를 깔끔하게 유지합니다. 작은 서비스 클라이언트는 실패하는 의존성을 호출하는 것을 멈추기 위해 전체 탄력성 프레임워크가 필요하지 않습니다. 그러나 게이트웨이, 어그리게이터, API 클라이언트 SDK 또는 고트래픽 통합 레이어는 구성된 정책에서 이점을 얻을 수 있습니다.
gobreaker 설치
새로운 코드에는 v2 패키지를 사용하십시오:
go get github.com/sony/gobreaker/v2
그리고 가져오십시오:
import "github.com/sony/gobreaker/v2"
Go 기본 회로 차단기
다음은 HTTP 호출 주위의 작은 예제입니다.
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/sony/gobreaker/v2"
)
var ErrTemporaryUnavailable = errors.New("dependency temporarily unavailable")
type UserClient struct {
baseURL string
http *http.Client
cb *gobreaker.CircuitBreaker[[]byte]
}
func NewUserClient(baseURL string) *UserClient {
settings := gobreaker.Settings{
Name: "user-service",
MaxRequests: 3,
Interval: 30 * time.Second,
Timeout: 10 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
},
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
fmt.Printf("circuit breaker %s changed from %s to %s\n", name, from, to)
},
}
return &UserClient{
baseURL: baseURL,
http: &http.Client{
Timeout: 3 * time.Second,
},
cb: gobreaker.NewCircuitBreaker[[]byte](settings),
}
}
func (c *UserClient) GetUser(ctx context.Context, userID string) ([]byte, error) {
result, err := c.cb.Execute(func() ([]byte, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
c.baseURL+"/users/"+userID,
nil,
)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("user service returned %d", resp.StatusCode)
}
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("user not found")
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("user service client error: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
})
if errors.Is(err, gobreaker.ErrOpenState) {
return nil, ErrTemporaryUnavailable
}
if errors.Is(err, gobreaker.ErrTooManyRequests) {
return nil, ErrTemporaryUnavailable
}
return result, err
}
이것은 완전한 프로덕션 클라이언트는 아니지만, 형태를 보여줍니다:
- 차단기가 외부 호출을 감쌉니다
- HTTP 요청은 컨텍스트를 받습니다
- HTTP 클라이언트에는 타임아웃이 있습니다
- 서버 측 실패는 차단기 실패로 간주됩니다
- 열린 회로 오류는 애플리케이션 오류로 매핑됩니다
gobreaker 설정 구성
핵심 설정은 이해할 가치가 있습니다.
Name
Name은 차단기를 식별합니다.
안정적이고 구체적인 이름을 사용하십시오:
payment-api
search-service
llm-gateway
user-service
다음과 같은 모호한 이름은 피하십시오:
http-client
external-call
default
이 이름은 로그와 메트릭에서 필요하게 될 것입니다.
MaxRequests
MaxRequests는 차단기가 반개방 상태일 때 허용되는 요청 수를 제어합니다.
작은 숫자가 일반적으로 더 안전합니다. 반개방의 목적은 복구를 테스트하는 것이지, 전체 트래픽을 즉시 보내는 것이 아닙니다.
Interval
Interval은 차단기가 닫힌 동안 내부 카운트가 언제 지워지는지를 제어합니다.
0이면 카운트가 자동으로 지워지지 않습니다. 0이 아닌 간격은 차단기에 롤링-ish 메모리 창을 제공하지만, 전체 슬라이딩 윈도우 구현과는 동일하지 않습니다.
Timeout
Timeout은 차단기가 반개방으로 이동하기 전에 열려 있는 시간을 제어합니다.
타임아웃이 너무 짧으면 복구가 완료되지 않은 의존성을 계속 프로브하게 됩니다. 너무 길면 복구가 지연됩니다.
10~30초와 같은 보수적인 값으로 시작하고, 프로덕션 메트릭에서 조정하십시오.
ReadyToTrip
ReadyToTrip은 차단기가 언제 열려야 하는지를 결정합니다.
간단한 규칙은 연속 실패입니다:
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= 5
}
이것은 추론하기 쉽지만, 고량(high-volume) 서비스에게는 적합하지 않을 수 있습니다.
다른 옵션은 최소 요청 수 이후의 실패 비율입니다:
ReadyToTrip: func(counts gobreaker.Counts) bool {
total := counts.Requests
failures := counts.TotalFailures
if total < 20 {
return false
}
return float64(failures)/float64(total) >= 0.5
}
이것은 작은 샘플 크기 이후에 회로를 열지 않도록 합니다.
OnStateChange
OnStateChange는 로그 또는 메트릭을 방출해야 하는 곳입니다.
최소한 다음을 기록하십시오:
- 차단기 이름
- 이전 상태
- 새 상태
- 타임스탬프
프로덕션 시스템에서는 차단기 상태를 메트릭으로 노출하십시오. 로그는 디버깅에 유용하지만, 메트릭은 알림 및 대시보드에 더 좋습니다.
IsSuccessful
IsSuccessful은 어떤 오류가 실패로 간주되는지를 결정할 수 있게 해줍니다.
이것은 중요합니다.
모든 오류가 차단기를 열어야 하는 것은 아닙니다. 예를 들어, 사용자 서비스에서 온 404 Not Found는 유효한 비즈니스 결과일 수 있습니다. 400 Bad Request는 호출자의 잘못일 수 있으며, 의존성의 잘못이 아닐 수 있습니다.
503 Service Unavailable, 타임아웃, 연결 재설정 또는 429 Too Many Requests는 실제 의존성 건강 신호일 수 있습니다.
여기서 조심하십시오. 잘못된 오류를 카운팅하는 것은 노이즈가 많은 회로 차단기를 구축하는 가장 쉬운 방법 중 하나입니다.
무엇을 실패로 간주해야 합니까?
여기에서 엔지니어링 판단이 중요합니다.
일반적으로 다음을 실패로 간주합니다:
- 네트워크 타임아웃
- 연결 거부
- 연결 재설정
- HTTP 500
- HTTP 502
- HTTP 503
- HTTP 504
- 반복적인 429 응답
- 의존성에서 온 변형된 응답
- 외부 호출 중 컨텍스트 마감 시간 초과
일반적으로 다음을 의존성 실패로 간주하지 않습니다:
- 유효성 검사 오류
- 로직 직렬화 오류
- 예상되는 404 응답
- 호출자 측 인증 실패
- 비즈니스 규칙 거부
- 사용자 입력 오류
차단기는 일반적인 애플리케이션 실패가 아니라 의존성 건강을 나타내야 합니다.
회로 차단기와 context.Context
Go에서 회로 차단기는 context.Context를 대체해서는 안 됩니다.
회로 차단기는 호출을 시도할지 여부를 결정합니다. 컨텍스트는 해당 호출이 얼마나 오래 실행될 수 있는지, 그리고 호출자가 사라졌을 때 중지해야 하는지를 제어합니다.
좋은 외부 호출은 일반적으로 둘 다 가져야 합니다:
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()
data, err := client.GetUser(ctx, userID)
컨텍스트는 호출 체인을 통해 흘러야 합니다:
incoming request context
-> service method
-> client method
-> HTTP request
-> dependency
요청 범위 코드 내부에서 분리된 백그라운드 컨텍스트를 생성하지 마십시오. 사용자 요청이 취소되면, 하위 작업도 일반적으로 중지되어야 합니다.
차분한 규칙은 다음과 같습니다:
차단기는 시스템을 보호합니다. 컨텍스트는 요청을 보호합니다.
일반적으로 둘 다 필요합니다.
회로 차단기와 재시도
재시도와 회로 차단기는 함께 잘 작동할 수 있지만, 순서가 중요합니다.
가장 안전한 기본값은 다음과 같습니다:
timeout per attempt
retry with backoff and jitter
circuit breaker around the dependency call
그러나 보편적인 답은 없습니다. 무엇을 카운트하고 싶은지 생각하십시오.
각 재시도 시도가 차단기를 통과하면, 하나의 사용자 요청이 여러 실패에 기여할 수 있습니다. 이는 차단기를 더 빨리 열 수 있으며, 이는 좋거나 나쁠 수 있습니다.
차단기가 전체 재시도 작업을 감싸면, 차단기는 사용자 요청당 하나의 최종 성공 또는 실패만 봅니다. 이는 더 차분하지만, 실패한 시도의 수를 숨길 수 있습니다.
많은 애플리케이션 서비스에게는 이 형태가 합리적입니다:
user request
-> circuit breaker
-> retry policy
-> one HTTP attempt with timeout
이는 차단기가 의존성 작업이 호출자에게 궁극적으로 작동했는지 추적함을 의미합니다.
하위 수준의 클라이언트에게는 이 형태도 의미가 있을 수 있습니다:
user request
-> retry policy
-> circuit breaker
-> one HTTP attempt with timeout
이는 차단기가 각 시도를 보호함을 의미합니다.
더 중요한 규칙은 이것입니다:
맹목적으로 재시도하지 마십시오.
다음을 사용하십시오:
- 작은 최대 재시도 횟수
- 지수 백오프
- Jitter
- 시도별 타임아웃
- 전체 요청 마감 시간
- 쓰기에 대한 멱등성
- 재시도 시도에 대한 메트릭
이러한 것들이 없으면, 재시도는 작은 장애를 더 큰 장애로 바꿀 수 있습니다. 재시도 안전성에 대한 더 깊은 처리는 분산 시스템에서 실제로 작동하는 멱등성를 참조하십시오.
회로 차단기와 멱등성
회로 차단기는 종종 재시도 옆에 나타나며, 재시도는 멱등성 문제를 제기합니다.
읽기 작업의 경우, 재시도는 일반적으로 안전합니다.
쓰기 작업의 경우, 재시도는 위험할 수 있습니다.
다음 결제 호출을 고려하십시오:
POST /charge
요청이 타임아웃되면, 결제가 실패했을까요? 아마도. 성공했지만 응답이 손실되었을까요? 그것도 아마도.
멱등성 키 없이 재시도하면 두 번 청구할 수 있습니다.
쓰기 작업의 경우 다음 중 하나 이상을 사용하십시오:
- 멱등성 키
- 요청 ID
- 작업 ID
- 고유 제약 조건
- 트랜잭셔널 아웃박스
- 워크플로우 오케스트레이션
- 명시적 조정
회로 차단기는 실패하는 결제 제공자를 계속 호출하는 것을 멈출 수 있지만, 안전하지 않은 재시도를 안전하게 만들 수는 없습니다.
회로 차단기와 대체 응답(Fallback)
회로가 열려 있을 때, 서비스에는 계획이 필요합니다.
가능한 대체 전략에는 다음이 포함됩니다:
- 캐시된 데이터 반환
- 경고와 함께陈旧한 데이터 반환
- 비임계 섹션 생략
- 작업을 나중에 처리할 수 있도록 대기열에 추가
- 다른 제공자로 전환
- 임시 오류 반환
- 성능이 저하된 기능 표시
- 요청을 빠르게 실패
대체 응답은 정직해야 합니다.
예를 들어, 이 경우 일반적으로 좋습니다:
{
"status": "temporary_unavailable",
"message": "Recommendations are temporarily unavailable"
}
이 경우 위험합니다:
{
"recommendations": []
}
빈 목록은 유효한 결과처럼 보일 수 있습니다. 이는 장애를 숨기고, 사용자를 혼란스럽게 하며, 디버깅을 더 어렵게 만들 수 있습니다.
침묵하는 대체 응답은 유혹적입니다. 또한 위험합니다.
회로 차단기와 관찰 가능성(Observability)
관찰 가능성이 없는 회로 차단기는 대부분 놀라움 생성기입니다.
최소한 다음 메트릭을 추적하십시오:
- 현재 차단기 상태
- 상태 변경
- 허용된 호출
- 거부된 호출
- 성공
- 실패
- 타임아웃
- 대체 응답
- 재시도 시도
- 하위 지연 시간(latency)
- 하위 상태 코드
유용한 레이블에는 다음이 포함됩니다:
- 차단기 이름
- 의존성 이름
- 작업 이름
- 상태 클래스
- 오류 카테고리
사용자 ID, 전체 URL, 요청 ID 또는 원시 오류 메시지와 같은 고基数(high-cardinality) 레이블은 피하십시오.
대시보드에서 다음 질문에 답할 수 있어야 합니다:
- 지금 어떤 회로 차단기가 열려 있습니까?
- 얼마나 자주 열리습니까?
- 어떤 의존성이 열림을 유발했습니까?
- 사용자가 대체 응답을 보고 있습니까?
- 차단기가 열린 후 지연 시간이 개선되었습니까?
- 차단기가 열리기 전에 재시도 볼륨이 급증했습니까?
- 의존성이 복구되었습니까?
차단기를 관찰할 수 없으면 조정할 수 없습니다. 메트릭과 잘 어울리는 구조화된 로깅에 대해서는 Go의 slog를 사용한 구조화된 로깅을 참조하십시오.
프로덕션 친화적인 HTTP 클라이언트 형태
실제 서비스의 경우, 핸들러 전반에 회로 차단기 로직을 흩어지지 마십시오.
의존성 주위에 작은 클라이언트 패키지를 생성하십시오.
예제 구조:
internal/
userservice/
client.go
errors.go
metrics.go
핸들러는 gobreaker의 세부 사항을 알 필요가 없습니다. 도메인 수준의 클라이언트 메서드에 의존해야 합니다:
type UserService interface {
GetUser(ctx context.Context, userID string) (*User, error)
}
그리고 구현에는 다음을 포함할 수 있습니다:
- HTTP 요청 생성
- 컨텍스트 전파
- 차단기 실행
- 상태 코드 처리
- 응답 디코딩
- 메트릭
- 오류 매핑
이는 탄력성 정책을 의존성 경계 근처에 유지합니다. 경계에서의 오류 분류에 대한 자세한 내용은 Go 오류 처리 아키텍처: 경계와 패턴을 참조하십시오.
애플리케이션 아키텍처에서 회로 차단기의 위치
회로 차단기 패턴은 통합 경계에 속합니다.
Go 애플리케이션에서는 일반적으로 다음을 의미합니다:
가능하면 비즈니스 로직에서 차단기를 제외하십시오.
비즈니스 레이어는 다음과 같은 도메인 오류를 이해해야 합니다:
payment provider unavailable
recommendations unavailable
profile service timeout
gobreaker 상태를 이해할 필요는 없습니다.
이러한 분리는 아키텍처를 깔끔하게 유지합니다:
- 전송 관심사는 클라이언트에 남아 있습니다
- 탄력성 정책은 의존성 근처에 남아 있습니다
- 도메인 로직은 읽기 쉽게 남아 있습니다
- 핸들러는 얇게 남아 있습니다
- 테스트는 작성하기 더 쉽습니다
이 기사는 프로덕션용 앱 아키텍처 토픽의 일부입니다 — 통합 패턴의 멱등성, 아웃박스, 사가 및 오케스트레이션 가이드와 함께.
흔한 실수
실수 1: 타임아웃 없음
회로 차단기는 호출이 반환하지 않는 한 느린 호출을 마법처럼 멈추지 않습니다.
외부 작업이 영원히 멈출 수 있으면, 차단기가 실패를 충분히 빠르게 보지 못할 수 있습니다.
항상 타임아웃을 사용하십시오.
실수 2: 모든 것에 하나의 글로벌 차단기
모든 의존성에 하나의 차단기를 사용하지 마십시오.
실패하는 이메일 제공자는 결제 제공자에 대한 회로를 열어서는 안 됩니다. 느린 검색 엔드포인트는 사용자 프로필 호출을 차단해서는 안 됩니다.
실패 모드가 다를 때, 별도의 의존성 작업에 대해 별도의 차단기를 사용하십시오.
실수 3: 호출자 오류를 의존성 실패로 카운팅
서비스가 잘못된 입력을 보내고 400 Bad Request를 받으면, 이는 일반적으로 하위 장애가 아닙니다.
자신의 버그로 차단기를 훈련하지 마십시오.
실수 4: 비멱등적 쓰기 재시도
재시도는 무료가 아닙니다. 쓰기, 결제, 메시지 또는 사이드 이펙트를 중복할 수 있습니다.
재시도하기 전에 쓰기를 멱등하게 만드십시오.
실수 5: 대체 응답 뒤에 장애 숨기기
대체 응답은 우아하게 성능을 저하시켜야 하며, 현실을 위조해서는 안 됩니다.
의존성이 다운되면, 메트릭과 로그가 이를 명확하게 만들어야 합니다.
실수 6: 프로덕션 데이터 없이 튜닝
예제에서 복사한 임계값은 시작점일 뿐입니다.
다음에 기반하여 조정하십시오:
- 요청 볼륨
- 정상 오류율
- 의존성 지연 시간
- 사용자 영향
- 복구 시간
- 대체 응답 품질
실수 7: 용량 관리 대신 회로 차단기 사용
회로 차단기는 다음과 같은 대체물이 아닙니다:
- 로드 셔딩(load shedding)
- 속도 제한
- 큐 제한
- 자동 스케일링
- 데이터베이스 튜닝
- 연결 풀 제한
- 상류 할당량
이는 탄력성 전략의 일부일 뿐입니다.
실용적인 기본값
내부 HTTP 의존성을 호출하는 일반적인 Go 서비스의 경우, 합리적인 시작점은 다음과 같을 수 있습니다:
HTTP client timeout: 2 to 5 seconds
per-request context timeout: based on caller SLA
breaker failure rule: 5 consecutive failures or 50 percent failure after 20 requests
open timeout: 10 to 30 seconds
half-open requests: 1 to 5
retry count: 1 to 3 attempts
retry backoff: exponential with jitter
이것은 보편적인 값이 아닙니다. 이들은 비교적 안전한 시작점입니다.
사용자面向社会 API의 경우, 전체 지연 시간 예산을 타이트하게 유지하십시오. 백그라운드 작업의 경우, 더 긴 대기 시간을 용인할 수 있습니다. 결제 제공자의 경우, 재시도와 멱등성에 대해 훨씬 더 조심해야 합니다.
회로 차단기 체크리스트
회로 차단기를 추가하기 전에 다음 질문에 답하십시오:
- 어떤 의존성이 보호되고 있습니까?
- 어떤 작업이 보호되고 있습니까?
- 어떤 오류가 의존성 실패로 간주됩니까?
- 어떤 오류가 차단기에 의해 무시되어야 합니까?
- 각 호출에 어떤 타임아웃이 적용됩니까?
- 재시도가 허용됩니까?
- 쓰기가 멱등합니까?
- 회로가 열렸을 때 어떤 일이 발생합니까?
- 대체 응답이 있습니까?
- 대체 응답이 메트릭에서 가시적인가요?
- 회로가 계속 열리면 누구에게 알림이 가옵니까?
- 배포 후 차단기가 어떻게 조정될까요?
이러한 질문에 답할 수 없다면, 차단기를 추가하는 것은 탄력성보다 더 많은 혼란을 초래할 수 있습니다.
Go에서 회로 차단기 테스트
라이브러리의 내부 상태 머신이 아닌 동작을 테스트하십시오.
유용한 테스트에는 다음이 포함됩니다:
- 의존성 성공 및 응답 반환
- 의존성 반복 실패 및 회로 개방
- 열린 회로가 임시 오류 반환
- 클라이언트 측 유효성 검사 오류가 차단기를 트리거하지 않음
- 컨텍스트 타임아웃 존중
- 예상될 때 대체 응답 반환
- 상태 변경 시 메트릭 방출
통합 스타일 테스트를 위해 가짜 HTTP 서버를 사용하십시오:
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()
단위 테스트의 경우, 의존성을 인터페이스 뒤에 숨기고 가짜 구현을 주입하십시오.
테스트를 결정론적으로 유지하십시오. 긴 실제 지속 동안 자지 마십시오. 테스트에서 짧은 차단기 타임아웃을 구성하십시오. 가짜 시간과 격리된 버블을 사용하여 동시 Go 코드를 테스트하는 방법에 대해서는 testing/synctest를 사용한 동시 Go 코드 테스트을 참조하십시오.
자체 회로 차단기를 구축해야 합니까?
작은 회로 차단기를 구축하는 것은 좋은 학습 연습입니다. 상태 머신을 이해하는 데 도움이 됩니다.
프로덕션 코드의 경우, 요구 사항이 매우 특정하지 않는 한 유지 관리되는 라이브러리를 선호하십시오.
프로덕션 차단기는 다음을 처리해야 합니다:
- 동시성
- 상태 전환
- 카운터
- 반개방 프로브
- 콜백
- 사용자 정의 실패 분류
- 레이스 없는 동작
- 예측 가능한 오류 처리
이것은 불가능하지는 않지만,微妙하게 잘못하기 쉽습니다.
지루한 라이브러리가 일반적으로 더 나은 선택입니다.
결론
회로 차단기 패턴은 마법 같은 신뢰성 가루가 아닙니다.
Go에서 이는 작고 명시적인 탄력성 스택의 일부일 때 가장 잘 작동합니다:
context timeout
+ retry with backoff and jitter
+ circuit breaker
+ fallback
+ metrics
이 패턴은 느려지거나 부분적으로 사용 불가능해질 수 있는 원격 서비스 주위에서 특히 유용합니다.
연쇄 고장을 멈추기 위해 사용하십시오. 의존성이 명확하게 건강하지 않을 때 빠르게 실패하기 위해 사용하십시오. 과부하가 걸린 시스템에 복구할 공간을 주기 위해 사용하십시오.
그러나 타임아웃, 멱등성, 관찰 가능성 또는 깔끔한 아키텍처를 무시하는 변명으로 사용하지 마십시오.
좋은 회로 차단기는 실패를 더 명확하고 저렴하게 만듭니다. 나쁜 하나는 실패를 더 신비롭게 만듭니다.
참고 자료
- AI/ML 오케스트레이션을 위한 Go 마이크로서비스 — 회로 차단기가 적합한 더 넓은 오케스트레이션 컨텍스트
- 분산 트랜잭션의 Saga 패턴 — 회로 차단기와 짝을 이루는 분산 트랜잭션 패턴
- 분산 시스템의 멱등성 — 재시도 안전성 및 멱등 작업
- Go의 트랜잭셔널 아웃박스 패턴 — 탄력성 패턴과 함께 신뢰할 수 있는 이벤트 전달
- Go 오류 처리 아키텍처 — 의존성 경계에서의 오류 분류
- synctest를 사용한 동시 Go 코드 테스트 — 회로 차단기와 함께 비동기 동작 테스트
- Go의 slog를 사용한 구조화된 로깅 — 회로 차단기와 함께 관찰 가능성
github.com/sony/gobreaker/v2— 공식 gobreaker v2 패키지- Go 컨텍스트 취소 및 타임아웃 — 회로 차단기와 짝을 이루는 컨텍스트 패턴