Foundry
분산 시스템
중급
핵심

타임아웃과 재시도, 서킷 브레이커

한 서비스의 지연이 전체로 번지지 않게

타임아웃과 재시도, 서킷 브레이커

장애는 이렇게 번진다

결제 API 지연 3s → 요청 스레드 점유 → 스레드 풀 고갈
                  → 무관한 상품 조회까지 응답 불가 (연쇄 장애)
  • 원인은 대개 완전한 다운이 아니라 느려짐이다. 느린 응답이 자원을 잠식한다

3층 방어

장치역할기준
타임아웃무한 대기 차단하류 p99보다 약간 크게, 상류 타임아웃보다는 짧게
재시도일시 오류 흡수멱등 요청만, 지수 백오프와 지터, 1~2회
서킷 브레이커실패가 지속되면 호출 자체를 차단실패율 임계 초과 시 open, 일정 시간 후 half-open으로 탐색

실무 포인트

  • 타임아웃을 설정하지 않은 HTTP 클라이언트가 가장 흔한 사고 원인이다. 연결과 읽기 모두 지정한다
  • 계층마다 재시도를 걸면 재시도 폭풍이 된다. 2단계씩 2회면 하류에는 8배가 도달한다
  • open 상태에서 무엇을 반환할지(캐시된 값, 부분 응답, 명시적 오류) 미리 정한다
  • 자원 격리(벌크헤드)로 결제 호출용 커넥션 풀과 조회용 풀을 분리한다
면접에서 이렇게 나옵니다

Q.외부 API 하나가 느려졌을 때 전체 서비스가 멈추는 이유는?

느린 호출이 스레드나 커넥션을 붙잡고 있어 다른 요청이 쓸 자원이 사라지기 때문입니다.

순서벌어지는 일
1외부 호출이 5초씩 걸린다
2그 호출을 하는 요청이 스레드를 5초간 점유한다
3스레드풀이 그 요청들로 가득 찬다
4외부 호출과 무관한 요청도 스레드를 못 받는다
5전체 서비스가 응답하지 않는다

4번이 핵심입니다. 문제는 외부 API 하나인데 증상은 전체 장애로 나타납니다. 그래서 원인을 찾기 어렵습니다.

막는 방법내용
타임아웃5초가 아니라 상위 목표 안으로 짧게
격리외부 호출용 자원을 따로 둔다. 그것만 마른다
차단기실패가 지속되면 즉시 실패로 응답한다
대체 응답캐시된 값이나 축소된 응답을 준다
비동기화즉시 필요 없는 호출은 큐로 뺀다

두 번째가 근본입니다. 자원을 나눠 두면 그 부분만 고장 나고 나머지는 계속 돕니다.

흔한 실수: 타임아웃만 줄여 해결하려는 것. 짧게 해도 그 시간 동안 자원을 점유하는 것은 같아서, 호출량이 많으면 여전히 전체가 막힙니다.

Q.서킷 브레이커의 상태 전이를 설명해주세요

상태행동다음으로 넘어가는 조건
닫힘정상적으로 호출한다실패율이 임계를 넘으면 열림으로
열림호출하지 않고 즉시 실패한다정해진 시간이 지나면 반열림으로
반열림소수의 호출만 시험해 본다성공하면 닫힘, 실패하면 다시 열림

열림 상태의 의미가 중요합니다. 실패할 것이 뻔한 호출을 하지 않는 것입니다. 그러면 우리 자원을 점유하지 않고, 상대에게 부하를 더 얹지도 않습니다.

반열림이 필요한 이유는 회복을 감지하기 위해서입니다. 열림을 영구히 유지하면 상대가 복구돼도 모릅니다. 소수만 보내 확인하고, 괜찮으면 전체를 재개합니다.

설정에서 정할 것들입니다.

항목판단
실패율 임계낮으면 자주 끊긴다. 보통 50% 안팎
최소 호출 수호출이 적을 때 우연한 실패로 열리지 않게
열림 유지 시간상대의 회복 시간을 고려한다
무엇을 실패로 볼지타임아웃과 5xx 는 실패, 4xx 는 아니다

흔한 실수: 마지막 항목을 구분하지 않는 것. 잘못된 요청(4xx)을 실패로 세면, 우리 버그 때문에 정상인 상대를 차단하게 됩니다.

Q.벌크헤드(격리)는 무엇을 막아주나요?

자원을 칸으로 나눠 한 칸이 마르더라도 다른 칸은 계속 돌게 하는 것입니다. 배의 격벽에서 온 이름입니다.

나누는 대상
스레드풀외부 호출용과 내부 처리용을 분리
커넥션 풀조회용과 쓰기용, 또는 기능별
프로세스나 인스턴스중요 API 와 배치를 다른 서버로
우선순위별로 다른 큐

없을 때와 있을 때가 이렇게 갈립니다.

구성외부 API 가 느려지면
공용 풀 하나그 호출이 풀을 채워 전체 서비스가 멈춘다
기능별 분리그 기능만 실패하고 나머지는 정상

차단기와 함께 쓰면 더 낫습니다. 격리가 피해 범위를 가두고, 차단기가 그 안에서도 빨리 포기하게 만듭니다.

대가는 자원 효율입니다. 나눠 두면 한쪽이 남고 다른 쪽이 부족한 상황이 생깁니다. 전체 활용도를 조금 내주고 장애 격리를 얻는 거래입니다.

흔한 실수: 격리를 인스턴스 분리로만 생각하는 것. 한 프로세스 안에서 스레드풀을 나누는 것만으로도 큰 효과가 있고, 훨씬 값쌉니다.

Q.부하가 지속될 때 요청을 거부하는 것이 왜 필요한가요?

받을 수 없는 요청을 받으면 아무도 응답받지 못합니다. 일부를 거절하는 것이 전체를 지키는 방법입니다.

거절하지 않으면결과
큐에 계속 쌓인다대기가 길어져 이미 타임아웃된 요청을 처리한다
메모리가 는다대기 중인 요청 객체가 쌓인다
지연이 무한히 커진다모든 사용자가 실패한다

핵심은 이미 포기한 요청을 처리하는 낭비입니다. 클라이언트가 3초에 타임아웃하는데 큐 대기가 10초면, 처리한 결과를 받을 사람이 없습니다. 그런데도 자원은 씁니다.

방법내용
큐 길이 제한넘으면 즉시 429 로 응답한다
대기 시간 기준 폐기큐에서 오래된 요청은 버린다
우선순위결제는 받고 추천은 거절한다
동시 처리 수 제한처리 중인 요청 수에 상한을 둔다
축소 응답무거운 부분을 빼고 핵심만 준다

빨리 실패하는 것이 사용자에게도 낫습니다. 10초 기다려 실패하는 것보다 즉시 실패해 재시도 안내를 받는 편이 낫습니다.

흔한 실수: 큐를 무한으로 두는 것. 요청을 잃지 않으려는 의도지만, 결과적으로 모든 요청을 잃습니다.

먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.

읽었으면 문제로 확인해보세요

분산 시스템 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.