모든 시스템, 특히 분산 시스템은 실패를 겪기 마련이다. 이러한 실패를 처리할 수 있는 애플리케이션을 설계하는 것은 소프트웨어 개발자의 중요한 과업이다. 그러나 회복 가능한 시스템을 설계할 때, 대부분의 소프트웨어 엔지니어는 주로 인프라스트럭처나 주요 서비스의 일부가 완전히 작동하지 않을 상황만을 고려한다. 이들은 핵심 서버의 클러스터링, 서비스 로드 밸런싱, 인프라스트럭처를 여러 위치로 분산하는 등의 기술을 활용하여 애플리케이션 각 계층에 중복성을 확보하는 데 초점을 맞춘다.
이 접근법은 시스템 구성 요소가 완전히, 그리고 종종 심각하게 손상되는 상황을 상정하지만, 이는 회복력 있는 시스템 구축에서 일부 문제만 다루는 것에 불과하다. 서비스가 완전히 중단되면 이를 감지하고 애플리케이션이 우회하도록 처리하기는 비교적 쉽다. 그러나 서비스가 느려지는 상황에서는 성능 저하를 감지하고 이를 우회하는 것이 훨씬 더 어려워진다.
- 서비스 성능 저하는 간혈적으로 시작되어 확산될 수 있다: 서비스 저하는 사소한 지점에서 갑작스럽게 시작될 수 있다. 애플리케이션 컨테이너의 스레드 풀이 빠르게 소진되고 시스템이 완전히 붕괴되기 전까지, 초기 실패의 징후는 소수의 사용자가 문제를 제기하는 수준에서 드러날 수 있다.
- 원격 서비스 호출은 대개 동기식이며 장기간 수행되는 호출을 중단하지 않는다: 대체로 애플리케이션 개발자는 작업을 수행하기 위해 서비스를 호출하고 결과를 기다린다. 하지만 호출자 측에서 서비스 호출이 장시간 대기 상태(hanging)에 빠지지 않도록 하는 타임아웃 개념은 없는 경우가 많다.
- 대개 원격 자원의 부분적인 저하가 아닌 완전한 실패를 처리하도록 애플리케이션을 설계한다: 서비스가 완전히 실패하지 않는 한, 애플리케이션은 종종 불량한 서비스를 계속 호출하며 빠르게 실패하지 못하는 경우가 많다. 이때 호출하는 애플리케이션이나 서비스는 성능이 저하될 수 있지만, 자원 고갈로 인해 고장이 날 가능성이 더 크다. 자원 고갈(resource exhaustion)은 스레드 풀이나 데이터베이스 커넥션과 같은 제한된 자원이 과도하게 사용되어, 호출 클라이언트가 자원이 다시 가용해질 때까지 대기해야 하는 상황을 의미한다.
성능이 나쁜 원격 서비스가 야기하는 문제를 간과할 수 없는 이유는 이를 탐지하기 어려울 뿐만 아니라, 전체 애플리케이션 생태계에 연쇄적인 영향을 미칠 수 있기 때문이다. 보호 장치가 없다면 불량한 서비스 하나가 빠르게 여러 애플리케이션을 다운시킬 수 있다. 특히 클라우드 기반이면서 마이크로서비스 아키텍처를 사용하는 애플리케이션은 이런 유형의 장애에 취약하다. 이는 사용자 트랜잭션을 완료하는 데 연관된 다양한 인프라와 여러 세분화된 서비스들이 상호작용하기 때문이다.
▶ 7.1 클라이언트 측 회복성이란?
클라이언트 측 회복성 소프트웨어 패턴은 에러나 성능 저하로 원격 자원이 실패할 때, 원격 자원의 클라이언트가 고장 나지 않도록 보호하는 데 중점을 둔다. 이러한 패턴을 사용하면 클라이언트가 빠르게 실패하고, 데이터베이스 커넥션이나 스레드 풀과 같은 중요한 자원을 낭비하는 것을 방지할 수 있다. 또한 성능이 저하된 원격 서비스 문제의 영향을 소비자에게 상향 (upstream)으로 확산되는 것을 막을 수 있다.
- 서비스 클라이언트는 서비스 디스커버리에서 조회한 마이크로서비스의 앤드포인트를 캐싱한다.
- 회로 차단기 패턴은 서비스 클라이언트가 고장난 서비스를 계속해서 호출하지 못하게 한다.
- 호출이 실패하면 폴백은 실행 가능한 대안이 있는지 확인한다.
- 벌크 헤드는 서비스 클라이언트에서 서로 다른 호출을 격리하여 성능 낮은 서비스가 클라이언트의 모든 자원을 사용하지 못하게 한다.
이 네가지 클라이언트 회복성 패턴은 서비스 소비자 사이에서 보호대 역할을 한다.
▷ 7.1.1 클라이언트 측 로드 밸런싱
클라이언트 측 로드 밸런싱은 클라이언트가 서비스 디스커버리 에이전트 (넷플릭스 유레카)에서 서비스의 모든 인스턴스를 검색한 뒤, 해당 서비스 인스턴스의 물리적 위치를 캐싱하는 과정을 포함한다.
서비스 소비자가 서비스 인스턴스를 호출할 때, 클라이언트 측 로드 밸런싱은 관리 중인 서비스 위치 폴에서 적절한 위치를 반환한다. 클라이언트 측 로드 밸런서는 서비스 클라이언트와 서비스 소비자 사이에 위치하여, 서비스 인스턴스가 에러를 발생시키거나 정상적으로 동작하지 않는지 탐지할 수 있다. 클라이언트 측 로드 밸런서가 문제를 감지하면, 가용 서비스 폴에서 문제가 발생한 서버 인스턴스를 제거하여 더 이상 해당 서비스 인스턴스로 호출되지 않도록 한다.
이것은 스프링 클라우드 로드 밸런서 라이브러리가 추가 구성 없이 제공하는 제품 기본 기능이다.
▷ 7.1.2 회로 차단기
회로 차단기 패턴은 전기 회로의 차단기를 모델링한 것이다. 전기 시스템에서 회로 차단기는 전선을 통해 과전류가 흐르는지 탐지한다. 만약 회로 차단기가 문제를 감지하면, 나머지 전기 시스템의 연결을 차단하여 하부 구성 요소가 손상되지 않도록 보호한다.
소프트웨어 회로 차단기(circuit breaker)는 원격 서비스 호출을 모니터링한다. 호출이 너무 오래 걸리면 차단기가 개입하여 호출을 종료한다. 회로 차단기 패턴은 원격 자원에 대한 모든 호출을 감시하고, 호출이 일정 횟수 이상 실패하면 회로 차단기가 열려(팝), 빠르게 실패하며 고장난 원격 자원에 대한 추가 호출을 방지한다.
▷ 7.1.3 폴백 처리
폴백 패턴 (fallback)을 사용하면 원격 서비스 호출이 실패할 때 예외를 발생시키지 않고, 서비스 소비자가 대체 코드 경로를 실행하여 다른 방식으로 작업을 수행할 수 있다. 이 과정에는 보통 다른 데이터 소스에서 데이터를 찾거나, 향후 처리를 위해 사용자 요청을 큐에 입력하는 작업이 포함된다. 사용자에게는 호출에 문제가 있다는 예외를 표시하지 않지만, 나중에 요청을 다시 시도해야 한다는 메시지를 전달할 수 있다.
예를 들어, 사용자 행동 양식을 모니터링하고 구매 희망 항목을 추천하는 기능을 제공하는 전자 상거래 사이트가 있다고 가정해보자. 일반적으로 마이크로서비스를 호출하여 사용자 행동을 분석하고 특정 사용자에게 맞춤화된 추천 목록을 반환한다. 그러나 기호 설정 서비스가 실패할 경우, 폴백 패턴을 사용하여 모든 사용자의 구매 정보를 기반으로 더 일반화된 기호 목록을 검색할 수 있다. 이 데이터는 완전히 다른 서비스나 데이터 소스에서 추출될 수 있다.
▷ 7.1.4 벌크헤드
벌크헤드(bulkhead) 패턴은 선박 건조에서 유래한 개념이다. 배는 격벽으로 나누어져 있으며, 각 구획은 수밀 처리가 되어 있다. 선체에 구멍이 뚫리더라도 물은 격벽에 의해 해당 구획에만 제한되어, 배 전체가 침수되는 것을 방지한다.
여러 원격 자원과 상호 작용하는 서비스에도 동일한 개념을 적용할 수 있다. 벌크헤드 패턴을 사용하면 원격 자원에 대한 호출을 자원별로 분리된 스레드 풀로 관리함으로써, 하나의 느린 원격 자원 호출로 인한 문제가 전체 애플리케이션 다운을 초래할 위험을 줄일 수 있다. 스레드 풀은 서비스의 벌크헤드 역할을 하며, 각 원격 자원에 대해 별도의 스레드 풀을 할당한다. 만약 한 서비스가 느리게 응답한다면, 해당 서비스의 호출 그룹에 대한 스레드 풀만 포화되어 요청 처리가 중단된다. 스레드 풀을 분리하여 할당하면 다른 서비스는 영향을 받지 않으므로, 병목 현상을 효과적으로 피할 수 있다.
▶ 7.2 클라이언트 회복성이 중요한 이유
회로 차단기가 차단되면 라이언싱 서비스는 조직 서비스를 호출하지 않게 되어, 조직 서비스는 회복할 기회를 갖게 된다. 이를 통해 조직 서비스는 여유를 얻고, 서비스 저하가 발생했을 때 연쇄 장애를 방지하는 데 도움이 된다.
회로 차단기는 때때로 저하된 서비스에 대한 호출을 허용하는데, 이 호출들이 연속적으로 필요한 만큼 성공하면 회로 차단기가 자동으로 재설정된다.
- 빠른 실패 (fail fast): 원격 서비스가 성능 저하를 겪으면 애플리케이션은 빠르게 실패하여, 전체 애플리케이션이 다운되는 것을 초래할 수 있는 자원 고갈 문제를 예방한다. 대부분의 장애 상황에서는 전체 시스템이 다운되는 것보다 일부 서비스만 다운되는 것이 더 바람직하다.
- 원만한 실패 (fail gracefully): 타임아웃과 빠른 실패를 사용하는 회로 차단기 패턴은 실패를 원만하게 처리하거나 사용자 의도를 충족할 수 있는 대체 매커니즘을 제공한다. 예를 들어, 사용자가 한 데이터 소스에서 데이터를 검색하려 할 때, 해당 데이터 소스가 성능 저하를 겪고 있다면, 다른 위치에서 해당 데이터를 검색할 수 있다.
- 원활한 회복 (recover seamlessly): 회로 차단기 패턴은 중재 역할을 하므로, 회로 차단기는 요청 중인 자원이 다시 온라인 상태인지 확인하고, 사람의 개입 없이 자원에 대한 재접근을 허용하도록 주기적으로 점검한다.
수백 개의 서비스를 가진 대규모 클라우드 기반 애플리케이션에서 원활하게 회복하는 것은 매우 중요하다. 이는 서비스를 복구하는 데 필요한 시간을 크게 줄일 수 있기 때문이다. 또한 회로 차단기가 서비스 복원에 직접 개입함으로써(실패한 서비스를 재시작하면서) 피로한 운영자나 애플리케이션 엔지니어가 더 많은 문제를 일으킬 위험을 크게 감소시킨다. 회로 차단기는 자동화된 회복 메커니즘을 제공하여 시스템의 안정성을 높이는 데 중요한 역할을 한다.
Resilience4j 이전에는 마이크로서비스에서 회복성 패턴을 구현할 수 있는 가장 일반적인 자바 라이브러리 중 하나로 히스트릭스(Hystrix)가 사용되었다. 그러나 현재 히스트릭스는 유지 보수 단계로 전환되어 더 이상 새로운 기능이 추가되지 않는다. 히스트릭스의 대체 라이브러리로 권장되는 것 중 하나가 바로 Resilience4j이다. Resilience4j는 회로 차단기, 재시도, 속도 제한, 폴백, 벌크헤드 등의 다양한 회복성 패턴을 지원하며, 특히 가벼운 의존성과 높은 성능을 제공하여 마이크로서비스 환경에서 널리 사용되고 있다.
▶ 7.3 Resilience4j 구현
Resilience4j는 히스트릭스에서 영감을 받은 내결함성 라이브러리로, 네트워크 문제나 여러 서비스 고장으로 발생하는 결함 내성을 높이기 위해 다양한 패턴을 제공한다.
- 회로 차단기 (circuit breaker): 요청받은 서비스가 실패할 때 요청을 중단한다.
- 재시도 (retry): 서비스가 일시적으로 실패할 때 재시도한다.
- 벌크헤드 (bulkhead): 과부하를 피하고자 동시 호출하는 서비스 요청 수를 제한한다.
- 속도 제한 (rate limit): 서비스가 한 번에 수신하는 호출 수를 제한한다.
- 폴백 (fallback): 실패하는 요청에 대해 대체 경로를 설정한다.
Resilience4j를 사용하면 메서드에 여러 어노테이션을 정의하여 동일한 메서드 호출에 여러 패턴을 적용할 수 있다. 예를 들어, 벌크헤드와 회로 차단기 패턴을 사용하여 나가는 호출 수를 제한하려면 메서드에 @CircuitBreaker와 @Bulkhead 어노테이션을 정의할 수 있다. Resilience4j의 재시도 순서에서 주목할 점은 다음과 같다
Retry(CircuitBreaker(RateLimiter(TimeLimiter(Bulkhead(Function)))))
호출의 마지막에 Retry(재시도)가 적용된다. (필요한 경우) 패턴을 결합할 때는 이 순서를 기억해야 하며, 각 패턴은 개별적으로도 사용할 수 있다.
회로 차단기, 재시도, 속도 제한, 폴백, 벌크헤드 패턴을 구현하려면 스레드와 스레드 관리에 대한 깊은 지식이 필요하다. 이러한 패턴을 높은 품질로 구현하려면 많은 작업이 필요하지만, 다행히 스프링 부트와 Resilience4j 라이브러리를 사용하면 여러 마이크로서비스 아키텍처에서 검증된 도구를 제공받을 수 있다.
▶ 7.4 스프링 클라우드와 Resilience4j를 사용하는 라이선싱 서비스 설정
※ 예제 및 예제 설명 생략
▶ 7.5 회로 차단기 구현
회로 차단기를 이해하려면 전기 시스템과 비교하는 것이 유용하다. 전기 시스템에서 회로 차단기가 문제를 감지하면 시스템의 나머지 부분과의 연결을 끊어 다른 구성 요소의 추가 손상을 방지한다. 소프트웨어 코드 아키텍처에서도 이와 비슷한 방식으로, 문제가 발생한 부분의 연결을 차단하여 시스템 전체의 장애를 예방하는 역할을 한다.
코드에서 회로 차단기가 추구하는 것은 원격 호출을 모니터링하고 서비스를 장시간 기다리지 않도록 하는 것이다. 이때 회로 차단기는 연결을 종료하고, 실패가 많거나 오작동이 발생한 호출을 모니터링한다. 그런 다음 이 패턴은 빠른 실패를 구현하여, 실패한 원격 서비스에 추가 요청을 보내는 것을 방지한다. Resilience4j의 회로 차단기에는 세 가지 일반 상태를 가진 유한 상태 기계가 구현되어 있다.
Resilience4j 회로 차단기는 닫힌 상태에서 시작하여 클라이언트의 요청을 기다린다. 닫힌 상태에서는 링 비트 버퍼(ring bit buffer)를 사용해 요청의 성과와 실패 상태를 저장한다. 요청이 성공하면 회로 차단기는 링 비트 버퍼에 0비트를 저장하고, 호출된 서비스에서 응답을 받지 못하면 1비트를 저장한다.
실패율을 계산하려면 링을 모두 채워야 한다. 모두 채우지 않으면 모든 호출이 실패하더라도 회로 차단기는 열린 상태로 변경되지 않는다. 회로 차단기는 고장률이 임계 값 (구성 설정 가능한)을 초과할 때만 열린다.
회로 차단기가 열린 상태에서는 설정된 시간 동안 모든 호출이 거부되며, 회로 차단기는 CallNotPermittedException 예외를 발생시킨다. 설정된 시간이 만료되면 회로 차단기는 반열린 상태로 전환되며, 서비스가 여전히 사용 불가한지 확인하기 위해 일부 요청을 허용한다.
반열린 상태에서 회로 차단기는 설정 가능한 다른 링 비트 버퍼를 사용하여 실패율을 평가한다. 만약 실패율이 설정된 임계치보다 높으면 회로 차단기는 다시 열린 상태로 변경된다. 실패율이 임계치보다 작거나 같으면 회로 차단기는 닫힌 상태로 돌아간다. 열린 상태에서는 회로 차단기가 모든 요청을 거부하고, 닫힌 상태에서는 요청을 수락한다.
Resilience4j 회로 차단기 패턴에서 추가 상태를 정의할 수 있다. 해당 상태를 벗어난 방법은 회로 차단기를 재설정하거나 상태 전환을 트리거 하는 것이다.
- 비활성 상태 (DISABLED): 항상 액세스 허용
- 강제 열린 상태 (FORCED_OPEN): 항상 액세스 거부
Resilience4j 구현 방법은 두 가지다. 첫째, Resilience4j는 회로 차단기를 사용하여 라이선스 및 조직 서비스의 데이터베이스에 대한 모든 호출을 래핑(wrapping)한다. 둘째, Resilience4j를 사용하여 두 서비스 간 호출을 래핑할 수 있다. 두 호출 범주는 다르지만, Resilience4j를 사용하면 이러한 호출이 완전히 동일한 방식으로 처리된다는 것을 알 수 있다.
동기식 회로 차단기로 라이선싱 데이터베이스에서 라이선싱 서비스의 데이터 검색 호출을 래핑하는 방법을 논의해보자. 이 경우, 동기식 호출을 사용하여 라이선싱 서비스는 데이터를 검색한다. 라이선싱 서비스는 SQL 문이 완료될 때까지 대기하거나, 회로 차단기가 타임아웃될 때까지 기다린다. 이 방식에서는 호출이 완료되거나 타임아웃이 발생할 때까지 호출을 기다리게 되며, 만약 회로 차단기가 열린 상태라면 요청을 거부하거나 빠르게 실패하게 된다.
Resilience4j와 스프링 클라우드는 @CircuitBreaker 어노테이션을 사용하여 Resilience4j 회로 차단기가 관리하는 자바 클래스 메서드를 표시한다. 스프링 프레임워크가 이 어노테이션을 만나면 동적으로 프록시를 생성하여 해당 메서드를 래핑하고, 원격 호출을 처리할 때만 별도로 설정된 스레드 풀을 사용하여 해당 메서드에 대한 모든 호출을 관리한다. 이를 통해 회로 차단기는 설정된 조건에 따라 원격 호출의 상태를 모니터링하고, 필요시 요청을 빠르게 실패시키거나 대체 경로로 우회하도록 한다.
※ 예제 및 예제 설명 생략
@CircuitBreaker 어노테이션을 사용하면 해당 메서드가 호출될 때마다 해당 호출은 Resilience4j 회로 차단기로 래핑된다. 회로 차단기는 실패한 모든 해당 메서드에 대한 메서드 호출 시도를 가로챈다.
※ 예제 및 예제 설명 생략
▷ 7.5.1 조직 서비스에 회로 차단기 추가
메서드 레벨의 어노테이션으로 회로 차단기 기능을 호출에 삽입할 경우의 장점은 데이터베이스를 액세스하든, 마이크로서비스를 호출하든 상관없이 동일한 어노테이션을 사용할 수 있다는 것이다. 예를 들어, 회로 차단기로 조직 서비스에 대한 호출을 래핑하고 싶다면, RestTemplate 호출 부분을 메서드로 분리하고 @CircuitBreaker 어노테이션을 추가하면 된다. 이렇게 하면 해당 메서드가 호출될 때 회로 차단기가 적용되어, 서비스 호출 실패 시 자동으로 처리할 수 있는 기능을 제공하게 된다.
회로 차단기의 기본값을 확인하려면 포스트맨에서 다음 URI를 선택한다: http://localhost:<service_port>/actuator/health. 기본적으로 스프링 부트 액추에이터는 상태 정보(health) 서비스를 통해 회로 차단기의 구성 정보를 노출한다. 이를 통해 시스템의 건강 상태와 회로 차단기 관련 설정을 확인할 수 있다.
▷ 7.5.2 회로 차단기 사용자 정의
Resilience4j는 애플리케이션의 프로퍼티를 통해 회로 차단기의 동작을 사용자 지정할 수 있게 해 준다. 원하는 만큼 인스턴스를 구성할 수 있고 각 인스턴스별로 다른 구성을 설정할 수 있다.
◎ 공통 설정 요소
1. registerHealthIndicator: true
- 서킷 브레이커의 상태를 Spring Boot Actuator의 /health 엔드포인트에 노출할지 여부를 설정합니다. true로 설정하면 서킷 브레이커의 상태(CLOSED, OPEN, HALF-OPEN)를 health 엔드포인트에서 확인할 수 있다.
2. recordExceptions
- 서킷 브레이커가 실패로 간주할 예외들을 정의한다. 이 예외들이 발생하면 서킷 브레이커는 실패로 처리하고, 필요한 경우 서킷을 열거나 상태를 변경한다.
- org.springframework.web.client.HttpServerErrorException: HTTP 서버 오류 발생 시 실패로 간주
- java.io.IOException: 입출력 오류
- java.util.concurrent.TimeoutException: 타임아웃 예외
- org.springframework.web.client.ResourceAccessException: 리소스 접근 예외
◎ licenseService 설정
resilience4j.circuitbreaker:
instances:
licenseService:
registerHealthIndicator: true # 상태 정보 엔드포인트에 대한 구성 정보 노출 여부
ringBufferSizeInClosedState: 5 # => deprecated
sliding-window-size: 100 # 링 버퍼의 닫힌 상태 크기
ringBufferSizeInHalfOpenState: 3 => deprecated
permitted-number-of-calls-in-half-open-state: 10
waitDurationInOpenState: 10s # 링 버퍼의 반열린 상태 크기
failureRateThreshold: 50 # 열린 상태의 대기 시간
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
- java.util.concurrent.TimeoutException
- org.springframework.web.client.ResourceAccessException
1. ringBufferSizeInClosedState: 5 (deprecated)
- 서킷 브레이커의 CLOSED 상태에서 요청을 기록하는 링 버퍼의 크기 (현재 deprecated되어, sliding-window-size로 대체되었다.)
- 링 버퍼는 요청 및 응답을 추적하는 데 사용. 기본 값은 100이다.
2. sliding-window-size: 100
- CLOSED 상태에서 서킷 브레이커가 성공 및 실패를 판단하는 데 사용할 링 버퍼의 크기
- 값을 100으로 설정하면, 서킷 브레이커는 마지막 100개의 요청을 기록하여 성공률을 계산하고, 실패율을 기준으로 서킷을 열지 결정한다.
3. ringBufferSizeInHalfOpenState: 3 (deprecated)
- 서킷 브레이커가 HALF-OPEN 상태에서 사용할 링 버퍼의 크기. 이 항목은 deprecated되어, 더 이상 사용되지 않는다. 대신, permitted-number-of-calls-in-half-open-state를 사용한다. 기본 값은 10이다.
4. permitted-number-of-calls-in-half-open-state: 10
- HALF-OPEN 상태에서 서킷 브레이커가 허용하는 최대 요청 수. 이 요청들이 성공하면 서킷 브레이커는 CLOSED 상태로 전환된다.
- 값이 10이면 최대 10번의 요청을 허용하고, 이 요청들이 모두 성공적으로 처리되면 서킷 브레이커가 닫히게 된다.
5. waitDurationInOpenState: 10s
- 서킷 브레이커가 OPEN 상태일 때, 다시 HALF-OPEN 상태로 전환되기까지 대기하는 시간을 설정한다. 이 시간 동안 서킷 브레이커는 외부 서비스에 요청을 보내지 않는다. 기본 값은 60,000ms다.
- 값을 10s로 설정하면 서킷 브레이커는 10초 동안 OPEN 상태를 유지한 후 반열림 상태로 전환된다.
6. failureRateThreshold: 50
- 서킷 브레이커가 OPEN 상태로 전환되기 위한 실패율 임계값. 기본 값은 50이다.
- 값을 50으로 설정하면, 전체 요청 중 50% 이상이 실패하면 서킷 브레이커가 OPEN 상태로 전환된다.
◎ organizationService 설정
organizationService:
registerHealthIndicator: true
ringBufferSizeInClosedState: 6
sliding-window-size: 6
ringBufferSizeInHalfOpenState: 4
permitted-number-of-calls-in-half-open-state: 4
waitDurationInOpenState: 20s
failureRateThreshold: 60 # 실패율 임계치 ( % )
1. ringBufferSizeInClosedState: 6
- organizationService에 대한 CLOSED 상태에서 링 버퍼 크기 설정입니다.
- 값을 6으로 설정하면, 요청 6개를 추적하여 성공/실패를 판단한다.
2. sliding-window-size: 6
- organizationService의 CLOSED 상태에서 서킷 브레이커가 사용할 링 버퍼의 크기
- 값을 6으로 설정하면, 마지막 6개의 요청을 사용해 성공률을 계산하고, 실패율을 추적한다.
3. ringBufferSizeInHalfOpenState: 4
- HALF-OPEN 상태에서 사용할 링 버퍼의 크기. 서킷 브레이커가 반열림 상태에서 테스트할 최대 요청 수를 나타낸다다.
4. permitted-number-of-calls-in-half-open-state: 4
- HALF-OPEN 상태에서 서킷 브레이커가 허용하는 요청 수.
- 값을 4 설정하면 반열림 상태에서 최대 4개의 요청을 허용하고, 이 요청들이 성공적으로 처리되면 서킷 브레이커가 CLOSED 상태로 전환됩니다.
5. waitDurationInOpenState: 20s
- 서킷 브레이커가 OPEN 상태에서 대기할 시간을 설정
- 값을 20s로 설장하면, 20초가 지난 후 서킷 브레이커가 반열림 상태로 전환된다.
6. ailureRateThreshold: 60
- organizationService에 대해 서킷 브레이커가 OPEN 상태로 전환되기 위한 실패율 임계값을 설정
- 값을 60으로 설정하면, 전체 요청의 60% 이상이 실패하면 서킷 브레이커가 OPEN 상태로 전환된다.
◎ Rate Limiter 설정 (resilience4j.ratelimiter)
Rate Limiter는 특정 시간 내에 허용되는 요청 수를 제한하는 기능을 제공한다. 과도한 요청을 제어하여 시스템 과부하를 방지하고, 외부 서비스에 대한 요청을 제한할 수 있다.
resilience4j.ratelimiter:
instances:
licenseService:
limitForPeriod: 5
limitRefreshPeriod: 5000
timeoutDuration: 1000ms
1. limitForPeriod: 5
- 주어진 시간 동안 허용되는 최대 요청 수
- 값을 5로 설정하면 5초 동안 5번의 요청만 허용된다.
2. limitRefreshPeriod: 5000
- 제한이 갱신되는 주기. 기서 5000은 5초를 의미한다. 즉, 5초마다 5개의 요청이 새로 허용된다.
- 이 값을 적절히 설정하면 주어진 시간 동안 특정 수의 요청만 처리하고, 그 이후의 요청은 대기하거나 거부된다.
3. timeoutDuration: 1000ms
- 요청이 제한을 초과했을 때, 대기 시간으로 설정되는 시간
- 값을 1000ms로 설정하면, 제한된 요청 수를 초과하시 요청은 1초 동안 대기하고, 그 후에 요청이 다시 진행된다.
▶ 7.6 폴백 처리
회로 차단기 패턴의 장점 중 하나는 이 패턴이 중개자의 역할을 하며, 원격 자원과 이를 소비하는 클라이언트 사이에 위치한다는 점이다. 이를 통해 서비스 실패를 가로채고, 문제 상황에 따라 다른 대안을 실행할 수 있게 한다. 이러한 특성은 시스템의 안정성을 높이고, 소비자가 실패의 영향을 직접적으로 받지 않도록 보호하는 데 기여한다.
Resilience4j에서는 이러한 대안을 폴백(Fallback) 전략이라고 하며, 이를 통해 서비스 호출이 실패했을 때 대체 동작을 정의할 수 있다. Resilience4j는 폴백 전략을 간단하고 직관적으로 구현할 수 있는 기능을 제공하여, 애플리케이션이 서비스 장애 상황에서도 안정적으로 동작하도록 돕는다.
※ 예제 및 예제 설명 생략
Resilience4j에서 폴백 전략을 구현하려면 두 가지 작업이 필요하다. 첫 번째 작업은 Resilience4j에서 제공하는 @CircuitBreaker 또는 다른 관련 어노테이션에 fallbackMethod 속성을 추가하는 것이다. 이 속성은 호출이 실패하거나 Resilience4j가 호출을 중단했을 때 대신 실행할 메서드의 이름을 지정해야 한다. 이를 통해 장애 상황에서 기본 동작 대신 폴백 메서드가 실행되도록 설정할 수 있다.
두 번째 작업은 폴백 메서드를 정의하는 것이다. 이 메서드는 @CircuitBreaker가 적용된 원래 메서드와 동일한 클래스에 위치해야 하며, 원래 메서드와 동일한 매개변수 형식을 가져야 한다. 동일한 서식을 유지해야만, 원래 메서드 호출 시 전달된 모든 매개변수를 폴백 메서드로 그대로 전달할 수 있다. 폴백 메서드는 장애 상황에서 실행될 대체 로직을 구현하여, 서비스 중단에도 애플리케이션이 안정적으로 동작하도록 돕는다.
폴백 전략의 구현 여부를 결정할땐 몇 가지를 염두에 두어야 한다.
- 폴백은 자원이 타임아웃되거나 실패했을 때의 동작을 정의하는 지침을 제공한다. 폴백을 활용하여 타임아웃 예외를 처리하고 에러를 기록할 수 있다. 하지만 폴백을 설정하지 않거나 아무 작업도 수행하지 않는다면, 서비스 호출을 처리하기 위해 일반적인 try ... catch 블록을 사용해야 한다. 이는 예외를 포착하고, 필요한 경우 로딩 로직이나 대체 동작을 try ... catch 블록 내에 추가하여 문제 상황을 직접적으로 다루는 방식이다.
- 폴백 함수에서 수행할 작업은 신중히 계획해야 한다. 특히 폴백 서비스가 또 다른 분산 서비스를 호출하는 경우, 해당 호출 역시 실패할 가능성을 염두에 두어야 한다. 이를 처리하기 위해 폴백 서비스도 @CircuitBreaker로 래핑하여 보호해야 할 수 있다. 1차 폴백에서 실패를 처리하는 중에 동일한 유형의 실패가 2차 폴백에서도 발생할 수 있음을 고려하여, 방어적으로 코딩하는 것이 중요하다. 이는 실패 상황을 예측하고 중복되는 오류를 방지하며, 연속적인 장애가 애플리케이션 전반에 영향을 미치지 않도록 시스템의 내구성을 높이는 데 기여한다.
폴백 메서드는 다른 데이터 소스에서 데이터를 읽어오는 것도 가능하다.
▶ 7.7 벌크헤드 패턴 구현
마이크로서비스 기반 애플리케이션에서 특정 작업을 수행하려면 종종 여러 마이크로서비스를 호출해야 한다. 이때 벌크헤드 패턴을 적용하지 않으면, 모든 호출이 동일한 자바 컨테이너 스레드를 공유하게 된다. 특히 대규모 요청이 발생하면, 하나의 서비스 성능 문제로 인해 컨테이너의 모든 스레드가 포화 상태에 도달할 수 있다. 새로운 요청은 대기 상태로 밀려나게 되고, 이는 결국 자바 컨테이너가 응답하지 못하는 상황으로 이어진다. 벌크헤드 패턴은 각 서비스에 전용 스레드 풀을 할당해 이러한 문제를 방지하고, 하나의 서비스 성능 저하가 다른 서비스나 전체 시스템에 영향을 미치지 않도록 한다.
벌크헤드 패턴은 원격 자원 호출을 격리하여, 하나의 서비스에서 발생한 실패가 다른 서비스에 영향을 미치지 않도록 한다. Resilience4j는 벌크헤드 패턴을 두 가지 구현 방식으로 제공하여, 각 자원에 대한 동시 실행 수를 제한할 수 있다.
- 세마포어 벌크헤드 (semaphore bulkhead): 세마포어 격리 방식으로 서비스에 대한 동시 요청 수를 제한한다. 한계에 도달하면 요청을 거부한다.
- 스레드 폴 벌크헤드 (thread pool bulkhead): 제한된 큐와 고정 스레드 폴을 사용한다 이 방식은 폴고 큐가 다 찬 경우에만 요청을 거부한다.
이 모델은 애플리케이션에서 액세스하는 원격 자원의 수가 적고 각 서비스에 대한 호출량이 고르게 분산될 때 효과적이다. 그러나 일부 서비스에 대한 호출량이 다른 서비스보다 현저히 많거나 해당 서비스의 처리가 오래 걸리는 경우 문제가 발생한다. 이 경우 한 서비스가 기본 스레드 풀의 모든 스레드를 점유하게 되어, 결국 모든 스레드를 소진하고 다른 서비스에 대한 요청을 처리할 수 없게 된다.
Resilience4j에서 벌크헤드 패턴을 구현하려면 @CircuitBreaker와 이 패턴을 결합하는 구성을 추가해야 한다.
- 특정 메서드 호출을 위한 별도 스레드 폴 설정
- bootstrap.yml 파일에 벌크헤드 구성 정보 생성
- 세마포어 방식에서 maxConcurrentCalls와 maxWithDuration 프로퍼티 설정
- 스레드 폴 방식에서 maxThreadPoolSize, coreThreadPoolSize, queueCapacity, keepAliveDuration 설정
※ 예제 및 예제 설명 생략
Resilience4j를 사용하면 애플리케이션 프로퍼티를 통해 벌크헤드 동작을 맞춤 설정할 수 있다. 다른 회로 차단기와 마찬가지로 인스턴스를 원하는 만큼 생성할 수 있으며, 각 인스턴스에 서로 다른 구성을 설정할 수 있다.
◎ Thread Pool Bulkhead 설정 (resilience4j.thread-pool-bulkhead)
resilience4j:
thread-pool-bulkhead:
instances:
bulkheadLicenseService:
max-thread-pool-size: 20
core-thread-pool-size: 10
queue-capacity: 10
1. max-thread-pool-size: 20
- 해당 서비스가 사용할 수 있는 최대 스레드 수를 설정. 이 값을 설정하면, 서비스가 과도하게 스레드를 사용하지 않도록 제한할 수 있다
- 값이 20으로 설정되어 있으면 최대 20개의 스레드가 동시에 작업을 처리할 수 있다.
2. core-thread-pool-size: 10
- 기본 스레드 풀 크기. 이 값은 스레드 풀의 핵심 크기이며, 사용량에 따라 스레드 수가 조정될 수 있다.
- 값을 10으로 설정하면 최소 10개의 스레드가 항상 유지된다.
3. queue-capacity: 10
- 대기 큐의 크기를 설정.
- 만약 큐가 가득 차면, 추가 요청은 거부되거나 다른 방식으로 처리되어야 한다.
- 값을 10으로 설정하면 대기 큐에 최대 10개의 요청을 넣을 수 있다.
서비스가 부하를 받기 전까지 성능 특성을 알기 어려운 경우가 많다. 이러한 상황에서 스레드 풀의 프로퍼티를 조정해야 하는 주요 지표는 서비스가 정상적인 환경에서도 호출이 타임아웃을 겪는 경우이다. 이는 원격 자원의 성능이나 부하가 특정 지점에서 급격하게 저하될 수 있다는 신호일 수 있으며, 이때 스레드 풀 크기를 늘리거나 요청 처리 방식을 조정하는 등의 대응이 필요하다.
@Bulkhead 어노테이션은 벌크헤드 패턴을 적용하기 위한 설정을 나타낸다. 이 어노테이션을 메서드나 클래스에 추가하면, 해당 메서드가 실행될 때 각 서비스 호출에 대해 동시 실행을 제한하는 벌크헤드 패턴이 활성화된다. 기본적으로, Resilience4j는 애플리케이션 프로퍼티에 추가 설정이 없는 경우, 벌크헤드 타입에 대한 기본값을 사용한다. 이러한 기본값은 설정된 동시 실행 수를 제한하고, 호출이 지나치게 많은 경우 자원 소진을 방지하는 역할을 한다.
▶ 7.8 재시도 패턴 구현
재시도 패턴은 서비스 호출이 실패할 경우, 실패한 요청을 자동으로 일정 횟수만큼 다시 시도하여 일시적인 장애를 극복하는 방법을 제공한다. 이 패턴의 핵심은 네트워크 장애나 서비스의 일시적인 문제로 실패가 발생했을 때, 재시도를 통해 성공적인 응답을 받을 수 있다는 점이다. 재시도 패턴을 적용하려면, 서비스 인스턴스에 대해 재시도 횟수와 재시도 간격을 설정해야 한다.
Resilience4j에서는 재시도 패턴을 @Retry 어노테이션을 사용하여 적용할 수 있다. 기본적으로 실패한 요청에 대해 지정된 횟수만큼 재시도를 하고, 각 재시도 간에 대기 시간을 설정하여 서비스를 안정적으로 복구할 수 있게 한다.
◎ Retry 설정 (resilience4j.retry)
Retry는 요청이 실패했을 때 재시도하는 메커니즘을 제공한다. 네트워크 문제나 일시적인 장애 등으로 실패한 요청을 자동으로 다시 시도하여 성공할 가능성을 높다.
resilience4j.retry:
instances:
retryLicenseService:
max-attempts: 5
waitDuration: 10000
retry-exceptions:
- java.util.concurrent.TimeoutException
1. max-attempts: 5
- 요청이 실패했을 때 재시도할 최대 횟수. 5로 설정하면 최대 5번까지 재시도된다.
- 기본적으로 첫 번째 시도 후 실패한 경우, 설정된 횟수까지 요청을 다시 시도하게 된다.
2. waitDuration: 10000
- 재시도 간의 대기 시간을 설정. 10000은 10초로 설정되어 있다.
- 요청이 실패하고 재시도하기 전, 10초 동안 대기하는 시간을 설정한다.
3. retry-exceptions
- 어떤 예외가 발생했을 때 재시도할지를 지정한다. 위 설정에서는 TimeoutException이 발생할 때만 재시도하도록 설정되어 있다.
- 이 설정을 통해 특정 예외에 대해서만 재시도하고, 나머지 예외는 재시도 없이 실패로 처리할 수 있다.
※ 예제 및 예제 설명 생략
▶ 7.9 속도 제한기 패턴 구현
재시도 패턴은 서비스 과부하를 방지하고, 일시적인 장애가 발생해도 서비스를 안정적으로 유지하는 데 중요한 역할을 한다. 고가용성과 안정성을 위한 API 구축에 필수적인 기술이다.
Resilience4j는 속도 제한기 패턴을 구현하기 위해 두 가지 주요 구현체를 제공한다: AtomicRateLimiter와 SemaphoreBasedRateLimiter이다. 기본 구현체는 AtomicRateLimiter로, 이는 내부적으로 원자적 연산을 사용해 속도 제한을 처리한다.
SemaphoreBasedRateLimiter는 java.util.concurrent.Semaphore를 사용하여 현재 스레드 허용(permission) 수를 관리하는 가장 단순한 구현체이다. 각 사용자 스레드는 semaphore.tryAcquire() 메서드를 호출하여 사용 가능한 리소스를 확보하며, 새로운 limitRefreshPeriod가 시작될 때 semaphore.release()를 통해 내부 스레드에서 호출을 트리거하여 리소스를 재공유한다.
AtomicRateLimiter는 SemaphoreBasedRateLimiter와 달리 사용자 스레드가 직접 허용 로직을 실행하므로 스레드 관리가 필요 없다. AtomicRateLimiter는 시작부터 나노초 단위의 사이클로 분할되며, 각 사이클 시간이 갱신 기간(나노초)이다. 매 사이클의 시작 지점에서 가용한 허용(활성화된 허용) 수를 설정하여 사이클 시간을 제한하고, 이를 통해 사용자 스레드가 허용된 속도에 맞춰 작업을 수행할 수 있도록 한다.
- ActiveCycle: 마지막 호출에서 사용된 사이클 번호
- ActivePermissions: 마지막 호출 후 가용한 허용 수
- NanoToWait: 마지막 호출 후 허용을 기다릴 나노초 수
이 패턴에 대해 Resilience4j 선언을 고려할 수 있다.
- 사이클은 동일한 시간 단위다.
- AtomicRateLimiter는 가용한 허용 수가 부족할 경우, 현재 허용 수를 줄이고 여유가 생길 때까지 대기할 시간을 계산하여 호출을 예약한다. 이 예약 기능은 일정 기간 동안 허용되는 호출 수(limitForPeriod)를 정의할 수 있도록 해준다. 허용이 갱신되는 빈도(limitRefreshPeriod)와 스레드가 대기할 수 있는 시간(timeoutDuration)을 기반으로 허용 예약이 이루어진다. 이를 통해 요청이 과도하게 몰리는 상황을 방지하고 안정성을 유지할 수 있다.
이 패턴을 위해서는 타임아웃 시간, 갱신 제한 기간, 기간 동안 제한 수를 지정해야 한다.
※ 예제 및 예제 설명 생략
벌크헤드 패턴과 속도 제한기 패턴의 주요 차이점은 벌크헤드 패턴이 동시 호출 수를 제한하는 데 중점을 두는 반면, 속도 제한기 패턴은 주어진 시간 동안 총 호출 수를 제한하는 데 초점을 맞춘다는 것이다. 예를 들어, 벌크헤드 패턴은 동시에 허용할 수 있는 호출 수를 X개로 제한하는 반면, 속도 제한기는 Y초마다 최대 X번의 호출을 허용하는 방식으로 작동한다.
동시 횟수를 차단하고 싶다면 벌크헤드가 최선이지만, 특정 기간의 총 호출 수를 제한하려면 속도 제한기가 더 낫다. 두 시나리오 모두 검토하고 있다면 이 둘을 결합할 수 있다.
▶ 7.10 ThreadLocal과 Resilience4j
ThreadLocal을 사용하면 스레드마다 독립적인 값을 제공할 수 있다. 이는 각 스레드에서만 읽고 쓸 수 있는 변수를 생성하여 다른 스레드와 공유되지 않도록 한다. 자바에서 스레드를 안전하게 만들기 위해서는 일반적으로 동기화(synchronization)를 사용하는데, 동기화를 피하고 싶다면 ThreadLocal을 활용하여 각 스레드가 독립적인 상태를 유지하도록 할 수 있다. Resilience4j에서 이와 같은 방식으로 ThreadLocal 변수를 정의하면, 동일한 스레드 내에서 Resilience4j 어노테이션을 사용하는 메서드에 전파되어 스레드마다 상태를 관리할 수 있다.
REST 기반 환경에서 상관관계 ID (correlation ID)나 인증 토큰을 전달할 때, ThreadLocal을 사용하면 각 스레드에서만 값을 저장하고 이를 Resilience4j와 같은 라이브러리에서 서비스 호출 시 자동으로 전파할 수 있다. 상관관계 ID는 트랜잭션 내 여러 서비스 호출을 추적할 수 있는 고유한 식별자로, 이를 활용하면 요청 흐름을 추적하고 문제 발생 시 효율적으로 대응할 수 있다.
스프링 Filter 클래스를 사용하면 모든 REST 서비스 호출을 가로채서 유입된 HTTP 요청에서 컨텍스트 정보를 추출할 수 있다. 이 정보를 UserContext 객체에 저장한 후, 서비스 호출 내에서 ThreadLocal을 사용하여 필요할 때 언제든지 해당 정보를 읽을 수 있다. 이를 통해 서비스 호출 전반에 걸쳐 상관관계 ID나 인증 토큰 같은 중요한 정보를 전파하고 추적할 수 있다.
※ 예제 및 예제 설명 생략
직접 ThreadLocal를 사용할 때는 주의해야 한다. ThreadLocal을 잘못 개발하면 애플리케이션을 실행할 때 메모리 누수를 초래할 수 있기 때문이다.
호출이 회복성으로 보호된 메서드에 도달하면, ThreadLocal에 저장된 상관관계 ID를 추출할 수 있다. 이 방식으로, Resilience4j 어노테이션을 사용하는 메서드에서도 부모 스레드의 값을 사용할 수 있다. 이를 통해, 원격 호출을 포함한 모든 서비스 호출에서 상관관계 ID를 유지하고 추적할 수 있다.
Resilience4j는 애플리케이션에서 회복성 패턴을 구현하는 데 탁월한 선택이다. 히스트릭스가 유지보수 모드로 전환되면서, Resilience4j는 자바 에코시스템에서 회복성을 위한 최고의 선택으로 자리 잡았다. 다양한 회복성 패턴을 제공하고, 설정이 간단하며, 마이크로서비스 아키텍처와 잘 맞아떨어진다.
'스프링 마이크로서비스' 카테고리의 다른 글
8장. 스프링 클라우드 게이트웨이를 이용한 서비스 라우팅 (2) | 2024.12.17 |
---|---|
6장. 서비스 디스커버리 (2) | 2024.12.12 |
5장. 스프링 클라우드 컨피그 서버로 구성 관리 (0) | 2024.12.12 |
4장. 도커 (0) | 2024.12.11 |
3장. 스프링 부트로 마이크로서비스 구축하기 (1) | 2024.12.10 |