Micro Service/mSVC개발

[SC13] Spring Cloud Circuit Breaker & Resilience4J 란 ?

Happy@Cloud 2021. 2. 14. 10:11

1. Spring Cloud Circuit Breaker와 Resilience4J 이해

1) WHY ?

Spring Cloud Hystrix편에서 얘기했듯이 Circuit Breaker가 필요한 이유는,
누전차단기가 전기사고가 발생하기 전에 전기를 미리 차단하는것과 동일하게,
문제가 있는 마이크로서비스로의 트래픽을 차단하여 전체서비스가 느려지거나 중단되는것을 미리 방지하기 위해서 입니다.

2) HOW ?

Spring Cloud Circuit Breaker는 Netflix Hystrix, Resilience4J, Alibaba Sential, Spring Retry와 같은
Circuit Breaker제품들을 사용하기 위해 표준 인터페이스를 제공하는 추상화(또는 Facade) 라이브러리입니다.
Spring Cloud 커뮤니티에서는 EOS(End Of Service: 더 이상 업그레이드와 지원 없음)된 Hystrix의 대안으로 Resilience4J를 권고하고 있습니다.
Resilience4J는 Java 전용으로 개발된 경량화된 Fault Tolerance(장애감내) 제품입니다.
Resilience4J는 아래 6가지 핵심모듈로 구성되어 있습니다.
- Circuit Breaker: Count(요청건수 기준) 또는 Time(집계시간 기준)으로 Circuit Breaker제공
- Bulkhead: 각 요청을 격리함으로써, 장애가 다른 서비스에 영향을 미치지 않게 함(bulkhead-격벽이라는 뜻)
- RateLimiter: 요청의 양을 조절하여 안정적인 서비스를 제공. 즉, 유량제어 기능임.
- Retry: 요청이 실패하였을 때, 재시도하는 기능 제공
- TimeLimiter: 응답시간이 지정된 시간을 초과하면 Timeout을 발생시켜줌
- Cache: 응답 결과를 캐싱하는 기능 제공
이 글에서는 Resilience4J의 Circuit Breaker기능만 설명합니다.

그럼 위와 같은 기능들은 어떤 원리로 제공될 수 있는걸까요 ?
Spring Cloud Hystrix편에서 Hystrix가 동작하는 원리와 동일합니다.
사실, Resilience4J는 Hystrix의 동작원리에 착안해 만든 오픈소스입니다.
요청 마이크로서비스와 제공 마이크로서비스사이에 Resilience4J를 통해 통신하는것이 핵심 원리입니다.
모든 traffic이 Resilience4J를 통하므로, Circuit breaker, 격벽, 유량제어같은것이 가능해지는겁니다.
나중에 zipkin에서 보면 아래와 같이 요청 서비스인 SCG와 제공 서비스인 consumer 사이에 있는 'SCG'가 resilience4J입니다.
(요청서비스에 적용되어 있기 때문에 이름은 요청서비스와 동일하게 나옵니다.)

 

2. Getting Started

실습은 Spring Cloud Gateway(SCG)서버를 이용하도록 하겠습니다.
아직 SCG서버를 안만들었으면, Spring Cloud Gateway편을 참조하여 먼저 만들어 주십시오.
1) Dependency 추가
scg의 pom.xml에 'sping-cloud-starter-circuitbreaker-reactor-resilience4j'를 추가합니다.
'sping-cloud-starter-circuitbreaker-resilience4j'도 있는데,
reactor를 사용하는것이 보다 Fault Tolerance(장애감내) 솔루션에
적합하기 때문에 위 라이브러리를 사용합니다.
Reactor는 반응성, 탄력성, 가용성, 비동기성을 특징으로 하는 리액티브 프로그래밍의 핵심 라이브러리입니다.
Project Reactor에 대해서는 아래 글을 참조하십시오.
brunch.co.kr/@springboot/152

2) Route설정에 Circuit Breaker 정의 및 Circuit Breaker만들기
Circuit Breaker는 어떤 설정이 필요한지 생각해 봅시다. 가장 기본적으로 '{...}'안의 설정들이 필요할겁니다.

{최소요청횟수} 이후 {timeout}시간 기준으로 {최근 통계시간}동안 또는 {최근 통계건수}로 평가했을때,
{실패율}이상이 되면 Circuit Breaker가 Open되고, {Circuit Breaker지속시간}동안 유지된다.
유지되는 동안 Backend service를 호출하지 않는다.
그 시간이 경과하면 Circuit Breaker는 Half Open상태가 되고, Backend service를 1번 호출한다.
요청이 성공하면 Circuit Breaker는 Close되고, 실패하면 다시 Open된다.

그림출처: https://resilience4j.readme.io/docs/circuitbreaker


Hystrix는 아래 예와 같이 설정합니다.
{최소요청횟수 3회} 이후 {timeout 200ms}시간 기준으로 {최근 통계시간 50초}동안 평가했을때,
{실패율 50%}이상이 되면 Circuit Breaker가 Open되고, {Circuit Breaker지속시간 5초}동안 유지된다.

hystrix.command: 
  default: 
    execution: 
      isolation: 
        thread: 
          timeoutInMiliseconds: 200 # {timeout}시간 
    metrics: 
      rollingStats: 
        timeInMilliseconds: 50000 # {최근 통계시간} 
    circuitBreaker: 
      enabled: true 
      requestVolumeThreshold: 3 # {최소요청횟수} 
      errorThresholdPercentage: 50 # {실패율} 
      sleepWindowInMiliseconds: 5000 # {Circuit Breaker 유지 시간}


Resilience4J Circuit Breaker는 yaml로는 Circuit Breaker 이름만 지정하고, 옵션은 프로그램적으로 해야 합니다.
configmng의 scg-common.yaml에 아래와 같이 routing설정을 추가합니다.
주의할건, dynamic routing설정 앞에 추가해야 한다는 겁니다. yaml에서 우선순위는 정의한 순서대로이기 때문입니다.
'mycb'라는 Circuit Breaker를 정의하였습니다.

spring.cloud.gateway: 
  routes:
  - id: consumer-circuit-breaker
    uri: lb://consumer 
    predicates: 
    - Path=/cb/**
    filters: 
    - RewritePath=/.*/(?<param>.*), /delay/${param} 
    - name: CircuitBreaker
      args: 
        name: mycb
        fallbackUri: forward:/fallback    # fallback은 Circuit 상태와 상관 없이, fail조건에 해당된 경우 호출됨 
    metadata:
      connect-timeout: 2500
      response-timeout: 2500


그리고, Resilience4jConfig.java에서 Circuit Breaker를 만들고, 'mycb'에 적용합니다.
@Configuration과 @Bean을 이용하여, 어플리케이션 구동시에 Spring Bean으로 생성합니다.
Resilience4J는 SlidingWindow라는 요청 결과 저장소를 이용합니다.
SlidingWindow의 유형은 건수 또는 시간 중 선택할 수 있습니다.
예를 들어 시간유형의 SlidingWindow를 사용하고 SlidingWindowSize를 10으로 지정했다면, 10초동안의 응답결과를 저장한다는 의미입니다.
아래 예에서는 건수 유형의 SlidingWindow를 사용했습니다.
그래서 아래 설정을 풀어쓰면, 아래와 같습니다.
"2회 이후에 최근 10건의 요청중 실패율이 60%이상이면, Circuit Breaker를 오픈하고, 10초간 유지한다."
그런데, {timeout}에 대한 설정은 어디 있을까요 ? 위 configmng의 scg-common.yaml의 metadata.response-timeout이 그것입니다.
결론적으로 Circuit Breaker에 대한 설정은 아래와 같습니다.
{최소요청횟수 2회} 이후 {timeout 1000ms}시간 기준으로 {통계건수 최근 10건}을 평가했을때,
{실패율 60%}이상이 되면 Circuit Breaker가 Open되고, {Circuit Breaker지속시간 10초}동안 유지된다.

@Configuration 
public class Resilience4jConfig { 
  private final Logger log = LoggerFactory.getLogger(getClass()); 
  @Bean 
  public Customizer<ReactiveResilience4JCircuitBreakerFactory> myCB() { 
    CircuitBreakerConfig config = CircuitBreakerConfig.custom() 
                            .slidingWindowType(SlidingWindowType.COUNT_BASED) 
                            .slidingWindowSize(10) # {통계건수} 
                            .minimumNumberOfCalls(2) # {최소요청횟수} 
                            .failureRateThreshold(60) # {실패율} 
                            .waitDurationInOpenState(Duration.ofMillis(10000)) # {Circuit Breaker유지시간} 
                            .build(); 
    return factory -> factory.configure(builder -> builder.circuitBreakerConfig(config) 
                            .build(), "mycb"); 
  } 
}

참고) 아래와 같이 적용할 Circuit Breaker 이름을 복수로 지정할 수도 있습니다.

 return factory -> factory.configure(builder -> builder.circuitBreakerConfig(config) 
                                               .build(), "mycb", "mycb2", "mycb3" );

주의) {최근통계시간 또는 최근통계건수}를 모두 채워야 실패율을 계산하는게 아닙니다. 즉, 아래 예제와 같이 계산됩니다.

요청순번 1 2 3 4 5 6 7 8 9 10
성공(S),실패(F) F S S S S F S F F S
실패율 100% 50% 1/3=33% 1/4=25% 1/5=20% 2/6=33% 2/7=29% 3/8=38% 4/9=44% 4/10=40%
Circuit Breaker Close Close Close Close Close Close Close Close Close Close

SlidingWindowSize가 10이므로, 11번째 요청때는 2번째에서 11번째 결과로 실패율을 계산합니다.

3) 배포 및 테스트
configmng를 git repository에 push합니다.
scg를 재배포합니다.
그리고, 아래와 같이 테스트 합니다.
마지막에 'pass'를 넘기면 바로 결과를 리턴하고, 그 외의 값을 넘기면 1.5초 지연하다가 결과를 리턴합니다.

curl -i http://scg.169.56.84.41.nip.io/cb/pass1

위 요청을 2번 하십시오.
{최소요청횟수}가 2이므로, 2번까지는 실패해도 Circuit Breaker는 오픈되지 않습니다.
하지만 2번째 요청 후 실패율이 100%가 되므로, Circuit Breaker가 오픈됩니다.
그래서 3번째도 동일하게 요청하면, backend service를 연결하지 않고 아래와 같은 에러를 리턴합니다.
그래서 504 Timeout이 아닌, 503 Service Unavailable로 변경됩니다.

 

3. Resilience4J Config 분리

위 Resilience4jConfig.java처럼 Circuit Breaker의 옵션을 하드코딩하면, 옵션을 바꿀때마다 재배포를 해야만 합니다.
따라서, config server로 configuration을 분리하는게 더 좋습니다.
안타까운것은 Resilience4jConfig class가 어플리케이션 구동시에 생성되는 Spring Bean이기 때문에 reload해야 하는데,
그 방법을 아직 모른다는겁니다. 그래서 config변경 후 어플리케이션을 수동으로 재구동해야 합니다.
- configmng의 scg-common.yaml에 아래 설정을 추가합니다.
옵션을 쉽게 풀이하면 아래와 같습니다.
{최소요청횟수 2회} 이후 {timeout 1000ms}시간 기준으로 {통계건수 최근 10건}을 평가했을때,
{실패율 60%}이상이 되면 Circuit Breaker가 Open되고, {Circuit Breaker지속시간 10초}동안 유지된다.


resilience4j:
  circuitbreaker:
    default:
      # 최근 10개 요청 중 응답속도가 1초 초과한 비율이 70% 이상일때 Circuit Breaker OPEN
      # 단, 5개 요청까지는 OPEN하지 않음. http timeout정책은 resilience.circuitbreaker.timeout 참조 
      # slowCallDurationThreshold값은 http timeout시간보다 작아야 제대로 동작함. 크면, timeout이 먼저 발생하므로 의미가 없음.
      slidingWindowType: COUNT_BASED           # default: COUNT_BASED 
      slidingWindowSize: 10                    # default: 100
      minimumNumberOfCalls: 5                  # default: 100
      failureRateThreshold: 50                 # default: 50
      waitDurationInOpenState: 30000           # default: 60000


- Resilience4jConfig를 수정합니다.
@RefreshScope을 추가하여, 변경된 config가 적용되는 scope임을 명시합니다.
@Value를 이용하여, 각 config변수값을 property로 받습니다.

@Configuration 
@RefreshScope 
public class Resilience4jConfig { 
  private final Logger log = LoggerFactory.getLogger(getClass()); 
  @Value("${resilience4j.circuitbreaker.custom.minimumNumberOfCalls:5}") 
  private int customMinimumNumberOfCalls; 
  @Value("${resilience4j.circuitbreaker.custom.failureRateThreshold:50}") 
  private float customFailureRateThreshold; 
  @Value("${resilience4j.circuitbreaker.custom.slidingWindowType:COUNT_BASED}") 
  private String customSlidingWindowType; 
  @Value("${resilience4j.circuitbreaker.custom.slidingWindowSize:10}") 
  private int customSlidingWindowSize; 
  @Value("${resilience4j.circuitbreaker.custom.waitDurationInOpenState:5000}") 
  private long customWaitDurationInOpenState; 
  @Value("${resilience4j.circuitbreaker.custom.slowCallDurationThreshold:3000}") 
  private long customSlowCallDurationThreshold; 
  @Value("${resilience4j.circuitbreaker.custom.slowCallRateThreshold:100}") 
  private float customSlowCallRateThreshold; 
  @Value("${resilience4j.timeout.custom:1000}") 
  private long customTimeout; 
  @Bean public Customizer<ReactiveResilience4JCircuitBreakerFactory> myCustomizer() { 
    log.info(">>>>>>>>>>>> START myCustomizer()"); 
    SlidingWindowType winType = ("COUNT_BASED".equals(this.customSlidingWindowType)?SlidingWindowType.COUNT_BASED:SlidingWindowType.TIME_BASED); 
    CircuitBreakerConfig config = CircuitBreakerConfig.custom() 
                             .slidingWindowType(winType) 
                             .slidingWindowSize(this.customSlidingWindowSize) 
                             .minimumNumberOfCalls(this.customMinimumNumberOfCalls) 
                             .failureRateThreshold(this.customFailureRateThreshold) 
                             .waitDurationInOpenState(Duration.ofMillis(this.customWaitDurationInOpenState)) 
                             .recordExceptions(java.io.IOException.class, java.util.concurrent.TimeoutException.class, org.springframework.web.server.ResponseStatusException.class) 
                             .build(); 
    return factory -> factory.configure(builder -> builder.circuitBreakerConfig(config) 
                               .build(), "mycb"); 
  } 
}

참고) recordExceptions: 실패로 간주할 Exception class 들을 지정합니다. 반대되는 ignoreExceptions도 있습니다.

configmng와 scg를 git repository에 push하고, scg를 재배포합니다.
다시 테스트하여, 적용된 옵션이 잘 적용되는지 확인합니다.
sc-common.yaml에서 옵션을 바꾸고, git repository에 push한 후, scg의 POD를 삭제하여 다시 시작합니다.
예를 들어 resilience4j.circuitbreaker.minimumNumberOfCalls의 값을 5로 늘리고, git push한 후
scg POD를 삭제합니다. 그럼 자동으로 POD가 재시작됩니다.

k delete po scg-0

다시 테스트했을때, 이번에는 5번까지는 Circuit Breaker가 오픈되지 않아 504 Gateway Timeout이 발생할겁니다.

4. Fallback 메소드 추가

Resilience4J도 Circuit Breaker정의 시 Fallback메소드를 지정할 수 있습니다.
- scg-common.yaml의 Circuit Breaker설정에 'fallbackUri'로 지정합니다.
특이하게, 'forward://'가 아니라, 'forward:/'로 시작해야 합니다.

spring.cloud.gateway: 
  routes: 
  ... 
  - id: consumer-circuit-breaker
    uri: lb://consumer 
    predicates: 
    - Path=/cb/**
    filters: 
    - RewritePath=/.*/(?<param>.*), /delay/${param} 
    - name: CircuitBreaker
      args: 
        name: mycb
        fallbackUri: forward:/fallback    # fallback은 Circuit 상태와 상관 없이, fail조건에 해당된 경우 호출됨 
    metadata:
      connect-timeout: 2500
      response-timeout: 2500

- Fallback을 처리할 class를 만듭니다.

@RestController 
public class FallbackController { 
  Logger logger = LoggerFactory.getLogger(this.getClass()); 
  @GetMapping("/fallback") 
  public Mono<String> fallback(ServerWebExchange exchange) { 
    Throwable exception = exchange.getAttribute(ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR); 
    logger.error("##### ERROR: ", exception); 
    return Mono.just("fallback-gateway"); 
  } 
}


- git push, 재배포하고 테스트 합니다.
아래와 같이 fallback method에서 지정한 응답이 리턴됩니다.


어!, Circuit Breaker가 벌써 오픈된건가? 라고 오해하실분이 있을 수 있는데요,
Fallback은 Circuit Breaker 오픈 여부와 상관 없습니다.
지정된 조건에 실패하면 무조건 호출되는겁니다.
참고) 최초에 아래와 같이 delay없는 주소('/cb/pass')로 호출했는데도, Fallback 이 수행된 이유는
최초에 라우터가 구동될 때 시간이 좀 걸려 지정된 timeout 1초를 넘었기 때문입니다.

 

5. 모니터링: 메트릭 수집 및 대시보드 만들기

지금까지는 circuit breaker가 오픈되었는지 응답 코드로 유추할 수 밖에 없었습니다.
근데, fallback이 적용된 후에는 응답 코드로도 알 수가 없습니다.
따라서, Hystrix dashboard와 같은 모니터링 툴이 resilience4j에도 필요합니다.
Resilience4J는 직접 모니터링툴을 제공하는 대신에 Micrometer(http://micrometer.io)로 metric만 제공하고,
메트릭 수집과 대시보드는 다른 3rd-Party제품을 사용하도록 하고 있습니다.
예를 들면, Prometheus로 메트릭 수집을 하고 Grafana로 대시보드를 만들라는 겁니다.
Micrometer는
각 모니터링 제품에서 제공하는 메트릭 수집 클라이언트(에이젼트)를 위한 표준 Facade(인터페이스)입니다.
Micrometer가 표준형식으로 Metric을 제공하면, 각 메트릭수집 클라이언트가 자기 format대로 metric형식을 변화하면 됩니다.
따라서 특정 벤더에 종속되지 않고, 필요에 따라 적절한 모니터링 제품으로 쉽게 바꿀 수 있습니다.

1) Resilience4J를 위한 Micrometer 라이브러리 추가
pom.xml에 'resilience4j-micrometer'를 추가합니다.
'micrometer-registry-prometheus'는 prometheus의 메트릭 수집 에이젼트를 위한 라이브러리입니다.

 <dependency> 
   <groupId>io.github.resilience4j</groupId> 
   <artifactId>resilience4j-micrometer</artifactId> 
   <!-- <version>1.7.0</version> --> 
 </dependency> 
 <dependency> 
   <groupId>io.micrometer</groupId> 
   <artifactId>micrometer-registry-prometheus</artifactId> 
   <scope>runtime</scope> 
 </dependency>

scg를 git push하고, 재배포합니다.

2) Prometheus 서버 생성
Spring Boot Actuator편에서 Prometheus서버를 생성한것과 동일한 방법으로, SCG전용 Prometheus서버를 생성합니다.
- config-scg.yaml파일 생성

serviceAccounts: 
  alertmanager: 
    create: false 
  nodeExporter: 
    create: false 
  pushgateway: 
    create: false 
  server: 
    create: true 

alertmanager: 
  enabled: false 

configmapReload: 
  prometheus: 
    enabled: false 
    
kubeStateMetrics: 
  enabled: false 
    
nodeExporter: 
  enabled: false 
  
server: 
  enabled: true 
  
  global: 
    ## Path to a configuration file on prometheus server container FS 
    configPath: /etc/config/prometheus.yml 

    ## How frequently to scrape targets by default 
    scrape_interval: 10s 
    
    ## How long until a scrape request times out 
    scrape_timeout: 10s 
    
    ## How frequently to evaluate rules 
    evaluation_interval: 10s 
  
  ingress: 
    enabled: true 
    annotations: 
      kubernetes.io/ingress.class: nginx 
    hosts: 
    - prometheus-scg.169.56.84.41.nip.io 
  
  persistentVolume: 
    enabled: true 
    accessModes: 
    - ReadWriteOnce 
    mountPath: /data 
    size: 8Gi 
    
pushgateway: 
  enabled: false 
    
## Prometheus server ConfigMap entries ## 
serverFiles: 
  prometheus.yml: 
    rule_files: 
    - /etc/config/recording_rules.yml 
    - /etc/config/alerting_rules.yml 
  
  scrape_configs: 
  - job_name: prometheus 
    static_configs: 
    - targets: 
      - localhost:9090 
  - job_name: 'scg' 
    metrics_path: '/actuator/prometheus' 
    static_configs: 
    # don't use protocol(http:// or https://). you can use service name or ingress host. 
    - targets: [ 'scg.169.56.84.41.nip.io' ] 
      labels: 
        application: scg 
        instance: 'scg'

아래 부분은 본인거에 맞게 반드시 바꿔야 합니다.

... 
hosts: 
- prometheus-scg.169.56.84.41.nip.io 
... 

static_configs: # don't use protocol(http:// or https://). you can use service name or ingress host. 
- targets: [ 'scg.169.56.84.41.nip.io' ] 
...


- helm chart로 Prometheus 서버를 배포합니다.
namespace는 본인걸로 바꿔야 합니다.

kubens hklee helm install prometheus-scg -f config-scg.yaml prometheus-community/prometheus


- prometheus-scg의 ingress host로 접근하여, SCG어플리케이션과 연결되었는지 확인합니다.



3) Grafana 대시보드 만들기
위에서 만든 prometheus-scg를 Grafana에 연결하여 대시보드를 만듭니다.
- Data source추가
'scg'라는 이름으로 만듭니다. prometheus의 주소는 full service url을 지정합니다.
prometheus-scg.{namespace}.svc.cluster.local

- Dashboard만들기

'Import via panel json'에 아래 내용을 붙여 넣습니다.
참고) Datasource를 바꾸려면 "datasource": "scg" 를 찾아 일괄 변경하면 됩니다.
docs.google.com/document/d/1pgD4rbjRB27FAFq0KretOoyS1oUiFK9vu7WQtBKYIR0/edit?usp=sharing
json에 지정된 이름을 변경하여, 이름을 지정합니다.


다시 테스트를 하면서, 대시보드를 보면 실패율과 Circuit Breaker가 오픈되었는지를 볼 수 있습니다.
위 옵션인 경우 아래와 같이 테스트하면 됩니다.
아래 수행을 2번 합니다.

curl -i http://scg.169.56.84.41.nip.io/cb/pass1

아래와 같이 실패율이 100%가 되고, 최소요청횟수 2번을 넘었으므로 Circuit Breake가 오픈됩니다.

Circuit Breaker 지속시간인 10초가 되기까지 3번 정도 계속 아래 요청을 보냅니다.

curl -i http://scg.169.56.84.41.nip.io/cb/pass

10초 동안은, 계속 OPEN상태를 유지하는걸 볼 수 있습니다.
10초를 경과 후, 지연 요청을 보냅니다.

curl -i http://scg.169.56.84.41.nip.io/cb/pass1

아래와 같이 HALF_OPEN상태가 됩니다.


이 상태에서 지연 요청을 보내면 다시 OPEN상태가 되고, 정상 요청을 보내면 CLOSED상태가 됩니다.
정상요청인 '/cb/pass'로 요청합니다.
이제 CLOSED상태로 변경되는걸 볼 수 있습니다.

 

5. TimeLimiterConfig 적용하기

지금까지는 timeout을 Resilience4J의 설정이 아닌 HTTP client의 설정을 이용했습니다.
http client의 timeout설정은 각 gateway라우트 설정 시 metadata.response-timeout으로 합니다.
이 설정이 생략되면, spring.cloud.gateway.httpclient.response-timeout설정이 적용됩니다.
Resilience4J의 timeout설정은 TimeLimiter모듈을 이용합니다.
- scg-common.yaml에 아래 설정을 추가합니다.
주의) Resilience4J의 timeout은 http client의 timeout보다는 작아야 합니다.
제대로 된 테스트를 위해 라우터의 http timeout은 2.5초로 하고, resilience4j의 timeout은 1초로 수정합니다.

... 
- id: consumer-circuit-breaker 
  ... 
  metadata: 
    connect-timeout: 2500 
    response-timeout: 2500 
    
  ... 
  
resilience4j: 
  circuitbreaker: 
    custom: 
    ... 
  timeout: 
    custom: 1000

- Resilience4J.java를 아래와 같이 수정합니다.

public class Resilience4jConfig { 
  ... 
  @Value("${resilience4j.timeout.custom:1000}") 
  private long customTimeout; 
  ... 
  @Bean 
  public Customizer<ReactiveResilience4JCircuitBreakerFactory> myCustomizer() { 
    ... 
    TimeLimiterConfig timeoutConfig = TimeLimiterConfig.custom() 
                              .timeoutDuration(Duration.ofMillis(customTimeout)) 
                              .build(); 
    return factory -> factory.configure(builder -> builder.circuitBreakerConfig(config) 
                       .timeLimiterConfig(timeoutConfig) .build(), "mycb"); 
  } 
}


- git push, scg 재배포 후 테스트 합니다.
3번 이상 지연 요청 '/cb/pass1'을 보내면, circuit breaker가 오픈되는것을 볼 수 있습니다.
http client의 timeout 2.5초를 초과하지 않았지만, Resilience4J의 timeout 1초를 초과했기 때문에 실패로 처리됩니다.


scg-common.yaml에서 resilience4j.timeout.custom을 2500으로 변경한 후,
POD 'scg-0'을 삭제해서 재시작해 보십시오.
그리고 테스트하면 이제 실패율이 안올라가는걸 볼 수 있습니다. 당연히 Circuit Breaker는 오픈되지 않습니다.

6. slowCall* 옵션 적용하기

HTTP client timeout 또는 Resilience4J Timeout만으로도 느린 서비스에 대해 Circuit Breaker를 적용할 수 있지만,
Resilience4J는 timeout은 초과하지 않았지만 느린 응답인지를 판단하는 별도의 옵션을 더 제공합니다.
보통 이 옵션을 사용하는 경우는
timeout을 초과해도 fallback메소드를 호출하지 않고, 계속 느린 경우 Circuit Breaker만 오픈하고 싶을때 입니다.

- scg-common.yaml에 아래 옵션을 추가합니다.
slowCallDurationThreshold가 응답속도가 느린지를 판단하는 기준시간입니다. timeout보다는 작아야 합니다.
따라서, resilience4j의 timeout시간은 2초로 늘려줍니다.

resilience4j: 
  circuitbreaker: 
    custom: 
      ... 
      slowCallDurationThreshold: 1000 # slowCall* 조건에 해당되도 fallback 호출 안됨 
      slowCallRateThreshold: 50 
  timeout: 
    custom: 2000

- Resilience4J를 수정합니다.

public class Resilience4jConfig { 
  ... 
  @Value("${resilience4j.circuitbreaker.custom.slowCallDurationThreshold:3000}") 
  private long customSlowCallDurationThreshold; 
  @Value("${resilience4j.circuitbreaker.custom.slowCallRateThreshold:100}") 
  private float customSlowCallRateThreshold; 
  @Bean 
  public Customizer<ReactiveResilience4JCircuitBreakerFactory> myCustomizer() { 
    ... 
    CircuitBreakerConfig config = CircuitBreakerConfig.custom() 
                    ... 
                   .slowCallDurationThreshold(Duration.ofMillis(this.customSlowCallDurationThreshold)) 
                   .slowCallRateThreshold(this.customSlowCallRateThreshold) 
                   ... 
                   ... 
  } 
}


- git push하고 scg를 재배포한 후, 테스트합니다.
이번에는 지연 요청 '/cb/pass1'을 보내도, fallback이 수행되지 않습니다.
그리고, 대시보드에서 보면 'Failure Rate'는 안 올라가는데, 'Slow Call Rate'가 올라가면서 Circuit Breaker가
오픈되는걸 확인할 수 있습니다.

 

7. Default Circuit Breaker 만들기

지금까지는 특정 라우터에만 Circuit Breaker를 적용해 봤는데,
이제는 전체 라우터에 Default Circuit Breaker를 적용해 보겠습니다.
- configmng의 scg-common.yaml에 Default Circuit Breaker필터를 설정합니다.
저는 Circuit Breaker의 이름을 defaultCB라고 했습니다. 이름은 바꾸셔도 됩니다.

---
spring.cloud.gateway: 
  ... 
  default-filters: 
    ... 
  - name: CircuitBreaker 
    args: 
      name: defaultCB 
      fallbackUri: forward:/fallback

- configmng의 scg-common.yaml에 Default Circuit Breaker의 옵션을 정의합니다.
기존에 설정한 옵션과 함께 표시하면 아래와 같습니다.
그리고 default circuit breaker설정을 쉽게 풀이하면 아래와 같습니다.
최근 10개 요청 중 응답속도가 1초 초과한 비율이 70% 이상일때 Circuit Breaker OPEN
단, 5개 요청까지는 OPEN하지 않음.

resilience4j: 
  circuitbreaker: 
    default: 
      # 최근 10개 요청 중 응답속도가 1초 초과한 비율이 70% 이상일때 Circuit Breaker OPEN 
      # 단, 5개 요청까지는 OPEN하지 않음. http timeout정책은 resilience.circuitbreaker.timeout 참조 
      # slowCallDurationThreshold값은 http timeout시간보다 작아야 제대로 동작함. 크면, timeout이 먼저 발생하므로 의미가 없음. 
      slidingWindowType: COUNT_BASED # default: COUNT_BASED 
      slidingWindowSize: 10 # default: 100 
      minimumNumberOfCalls: 5 # default: 100 
      failureRateThreshold: 50 # default: 50 
      waitDurationInOpenState: 30000 # default: 60000 
      slowCallDurationThreshold: 1000 # default: 60000 
      slowCallRateThreshold: 70 # default: 100 
      permittedNumberOfCallsInHalfOpenState: 5 #Circuit 상태가 HALF-OPEN일때 허용되는 요청 수. default: 10. 별로 안 중요 
  
    custom: 
      slidingWindowType: COUNT_BASED 
      slidingWindowSize: 10 
      minimumNumberOfCalls: 2 
      failureRateThreshold: 60 
      waitDurationInOpenState: 10000 
      slowCallDurationThreshold: 1000 # slowCall* 조건에 해당되도 fallback 호출 안됨 
      slowCallRateThreshold: 50 
      
  #== http timeout 정책 # TimeLimiterConfig 없으면, spring.cloud.gateway.routes.metadata.response-timeout 적용됨 # metadata.response-timeout 없으면, spring.cloud.gateway.http-client.response-timeout 적용됨 
  timeout: 
    default: 2500 
    custom: 2000


- Resilience4J를 수정합니다.
전체 소스는 아래와 같습니다.

https://github.com/sc-hklee/scg/blob/main/src/main/java/com/springcloud/scg/Resilience4jConfig.java


- git push하고 scg를 재배포한 후, 테스트합니다.
이제는 다른 route에도 Circuit Breaker 'defaultCB'가 적용되었습니다.
아래와 같이 Dynamic Routing이 되어 있으니, http://{SCG host}/{service id}/{uri}로 테스트 해보겠습니다.

  - id: dynamic-routing 
    uri: lb://tbd
    predicates: 
    - Path=/*/**
    filters: 
    - StripPrefix=1
    - name: UriHostPlaceholderFilter
      args: 
        order: 10001

예를 들어 아래와 같이 하면 '/cb/pass1'과 동일하게 지연 요청이 수행됩니다.

curl -i http://scg.169.56.84.41.nip.io/consumer/delay/pass1


우리가 설정한 Circuit Breaker옵션은 아래와 같으므로, 지연 요청을 5개 보내면 Circuit Breaker가 오픈될것입니다.
"최근 10개 요청 중 응답속도가 1초 초과한 비율이 70% 이상일때 Circuit Breaker OPEN
단, 5개 요청까지는 OPEN하지 않음"
Garafana 대시보드에서 확인하면 아래와 같이 나오면 됩니다.