Spring Cloud Gateway — Circuit Breaker, Time Limiter

nayoung
11 min readApr 4, 2024

--

MSA 환경에서 특정 서버의 장애가 다른 서버로 전파될 수 있는데 이는 CircuitBreaker를 사용해 해결할 수 있다.

위와 같이 Service-A에 대한 서킷이 open되면 Gateway에서 요청 자체를 차단해 Service-A의 부하를 줄이거나 서버 상태를 복구할 수 있는 시간을 제공할 수 있다.

트래픽이 증가하는 경우 요청을 조절하기 위해 Rate Limiter, Bulkhead, Time Limiter를 같이 사용해 서킷 상태를 조절할 수 있다.

CircuitBreaker는 Gateway뿐만 아니라 아래 코드와 같이 서비스 내 특정 기능에도 설정할 수 있다.

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;

public interface ItemServiceClient {

@Retry(name = ORDER_PROCESSING_RESULT_RETRY)
@CircuitBreaker(name = ORDER_PROCESSING_RESULT_CIRCUIT_BREAKER,
fallbackMethod = "fallback")
@GetMapping(value = "/order-processing-result/{orderEventKey}",
produces = MediaType.APPLICATION_JSON_VALUE)
OrderStatus findOrderProcessingResult(@PathVariable String orderEventKey);

CircuitBreaker Config

CircuitBreaker 관련 설정은 https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker 에서 확인하면 된다.

resilience4j:
circuitbreaker:
configs:
default:
register-health-indicator: true
allow-health-indicator-to-fail: false
sliding-window-type: COUNT_BASED
sliding-window-size: 10
minimum-number-of-calls: 10
failure-rate-threshold: 50
slow-call-rate-threshold: 50
slow-call-duration-threshold: 7s
wait-duration-in-open-state: 10s
automatic-transition-from-open-to-half-open-enabled: false
permitted-number-of-calls-in-half-open-state: 5
record-exceptions:
- java.util.concurrent.TimeoutException
- org.springframework.cloud.gateway.support.NotFoundException
- io.github.resilience4j.circuitbreaker.CallNotPermittedException
instances:
orderCircuitBreaker:
baseConfig: default
failure-rate-threshold: 30
itemCircuitBreaker:
base-config: default
failure-rate-threshold: 50

인스턴스마다 다르게 설정할 수 있으며, 다음과 같이 각 라우터 필터에 추가하면 된다.

spring:
cloud:
gateway:
routes:
- id: item-service
uri: lb://ITEM-SERVICE
predicates:
- Path=/item-api/**
filters:
- RewritePath=/item-api/?(?<segment>.*), /$\{segment}
- name: CircuitBreaker
args:
name: itemCircuitBreaker
fallbackUri: forward:/fallback/item-service

fallback 설정 및 관련 예외

요청 처리 시 Timeout이 발생하거나, 서킷 상태로 인해 요청을 처리할 수 없는 등 예외가 발생하면 설정된 fallbackUri를 호출한다.

@RequestMapping("/fallback")
public class FallbackController {

@GetMapping("/item-service")
public void itemServiceFallback(ServerWebExchange exchange) {
Throwable t = exchange.getAttribute(
ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR);
if(t instanceof NotFoundException) {...}
else if(t instanceof TimeoutException) {...}
else if(t instanceof CallNotPermittedException) {...}
}

발생할 수 있는 예외는 다음과 같다. (계속 추가할 예정…)

서비스에 대한 정보가 없는 경우 org.springframework.cloud.gateway.support.NotFoundException: 503 SERVICE_UNAVAILABLE “Unable to find instance for 서비스명”

서비스 정보는 있는데 해당 서비스가 down 상태인 경우io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /IP주소:서비스-포트-번호

서킷 상태가 open 이거나 half open이라 처리하지 못하는 경우io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker ‘itemCircuitBreaker’ is OPEN and does not permit further calls

TimeLimiter의 timeout-duration보다 요청 처리 시간이 더 소요되는 경우java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 5000ms in ‘circuitBreaker’

actuator로 상태 확인

설정한 CircuitBreaker의 상태를 보기 위해 다음과 같이 설정하고 http://localhost:{api-gateway-포트번호}/actuator/health/circuitBreakers 로 접속한다.

resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true

management:
health:
circuitbreakers:
enabled: true
endpoint:
health:
show-details: always

failureRate, slowCallRate 값이 서킷브레이커에 설정된 Threshold보다 크거나 같으면 서킷의 상태가 다음과 같이 변한다.

  • Closed: 정상 상태
  • Open: 장애로 판단해 요청 차단
  • Half-Open: 일부 요청만 처리해보고 성공하면 Closed 상태로, 그렇지 않으면 Open 상태로 변경

Timeout 설정

게이트웨이는 일정 시간 내 서비스로부터 응답받지 못하면 요청을 실패로 처리할 수 있다. 응답을 계속 기다리는 것보다는 클라이언트에게 빠른 실패를 알려주는 것이 좋다고 생각한다.

실패로 처리해도 요청 자체가 자동으로 롤백되지 않으므로 추가 구현이 필요한 것 같다.

Time Limiter

TimeLimiter로 호출에 대한 타임아웃을 설정할 수 있다.

resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s # timeout 시간 설정
cancel-running-future: false # timeout 발생 시 future 취소 (True)
circuitbreaker:
configs:
default:
slow-call-rate-threshold: 50
slow-call-duration-threshold: 5s
  • TimeLimiter의 timeout-duration: 10s
  • CircuitBreaker의 slow-call-duration-threshold: 5s
  • 요청 처리 7s 소요

위 상태로 테스트하면 timeout-duration보다 빠르므로 정상 응답하지만, slow-call-duration-threshold 보다 늦게 처리되므로 circuit breaker의 slow calls에 집계된다.

  • CircuitBreaker의 slow-call-duration-threshold: 10s
  • TimeLimiter의 timeout-duration: 5s
  • 요청 처리 7s 소요

위 상태로 테스트하면 java.util.concurrent.TimeoutException이 발생한다.

java.util.concurrent.TimeoutException: 
Did not observe any item or terminal signal within {timeout-duration}ms in 'circuitBreaker'

Http Timeout

글로벌 설정 또는 라우터마다 설정할 수 있다.

spring:
cloud:
gateway:
httpclient: // 글로벌 설정
connect-timeout: 10000
response-timeout: 10s

routes:
- id: item-service
uri: lb://ITEM-SERVICE
predicates:
- Path=/item-api/**
filters:
- RewritePath=/item-api/?(?<segment>.*), /$\{segment}
- name: CircuitBreaker
args:
name: itemCircuitBreaker
fallbackUri: forward:/fallback/item-service

metadata: // 라우터 설정
connect-timeout: 10000
response-timeout: 10000

connect-timeout은 ms 단위로 설정하나 response-timeout은 글로벌 설정은 Duration으로, 라우터별 설정은 ms 단위다. 글로벌, 라우터별 설정을 모두 작성하면 라우터별 설정의 우선순위가 더 높다.

Http Timeout 설정이 무시되는 이슈

만약 Time Limiter를 설정하지 않고 CircuitBreaker와 Http Timeout만 사용하면 Http Timeout 설정은 무시되고 1000ms가 Circuit Breaker 실패 기준이 된다.

java.util.concurrent.TimeoutException: 
Did not observe any item or terminal signal within 1000ms in 'circuitBreaker'
(and no fallback has been configured)

--

--