resilience4j 사용해서 HTTP API 연동하기

created Oct 30, 2022 | updated Oct 30, 2022

개발을 하다보면 외부(External; Remote) 시스템의 HTTP API를 호출하는 경우가 생기곤 한다. spring boot에서 사용할 수 있는 다양한 REST(HTTP) Client가 존재하는데, 오늘은 resilience4j-feign을 사용하는 내용을 작성했다. 또한 외부 시스템의 문제(장애)가 내 시스템으로 연쇄 전파되지 않도록 Circuit Breaker와 Rate Limiter로 장애 허용 설정도 함께 한다.

개요


목표

  • 외부 시스템의 HTTP API를 호출하기
  • 외부 시스템을 호출 개수 제한하기
  • 외부 시스템에 문제가 생겼을 때 차단하기
  • HTTP Client Connection Pool 적용하기

구성요소

  • springboot2
  • resilience4j-feign
  • resilience4j RateLimiter, CircuitBreaker
  • Okhttp3

짚고 갈 개념들


Resilience4j

  • 경량(lightweight)의 장애 허용(내결함성; fault tolerance) 라이브러리이다.
  • Netflix Hystrix에서 영감을 받았고 java8과 함수형 프로그래밍을 위해 설계되었다.
  • Vavr(java8+를 위한 함수형 라이브러리)만 사용하고 다른 외부 라이브러리 의존성은 없다.
  • 고수준의 기능들(데코레이터들)을 제공하고 필요한 기능만을 선택해서 사용할 수 있다.

Feign and OpenFeign

  • Java HTTP Client binder; REST client builder; Declarative Web Service Client
  • java http clients 작성을 쉽게 만들어주는 것으로 interface를 생성하고 annotation을 추가하는 방식으로 사용한다.
  • Netflix OSS(Open Source Service) 중 하나 였고, 현재는 OpenFeign 프로젝트로 완전히 이관되었다.
  • Feign과 OpenFeign은 Spring Cloud에서 사용할 수 있다.
    • Netflix OSS 컴포넌트들은 Spring Cloud Netflix 프로젝트로 통합되어 제공된다.
    • OpenFeign은 Spring Cloud OpenFeign 프로젝트로 통합되어 제공된다.
  • 다른 REST Client로는 Retrofit, RestTemplate, WebClient 등이 있다.

Resilience4j Feign

  • Resilience4j의 애드온 모듈 중 하나이다.
  • HystrixFeign과 유사하고 "fault tolerance patterns"을 위한 resilience4j의 기능을 데코레이션해서 사용할 수 있다.
    • 현재 데코레이션 가능한 기능 : CircuitBreaker, RateLimiter, Fallback

Okhttp

  • HTTP Client이고 Square에서 개발된 오픈소스 라이브러리이다.
  • HTTP/2 지원(enables SPDY)하고 Connection Pooling, GZIP, 응답 캐싱 등을 제공한다. 또한 일반적인 네트워크의 연결 문제를 복구할 수도 있고, 최신 TLS를 지원한다.
  • 요청과 응답 API는 fluent builder와 immutability로 설계 되었고, 동기식과 비동기식을 지원한다.
  • Okio 의존성만 있고, 라이브러리가 가볍다.
  • 다른 HTTP Client로는 JDK HttpClient(Java11 JEP 321, legacy HttpUrlConnection 교체), Apache HttpClient, Netty 등이 있다.

주요 설정


의존성 설정

build.gradle
ext {
    resilience4jVersion = '1.7.1'
    openfeignVersion = '11.10'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation "io.github.resilience4j:resilience4j-spring-boot2:${resilience4jVersion}"
    implementation "io.github.resilience4j:resilience4j-feign:${resilience4jVersion}"
    implementation "io.github.openfeign:feign-okhttp:${openfeignVersion}"
    implementation "io.github.openfeign:feign-jackson:${openfeignVersion}"
    ...
}

Okhttp3 Connection Pool 적용

@Configuration
public class HttpClientConfig {
  @Bean
  public OkHttpClient okHttpClient() {
    return new OkHttpClient.Builder()
        .connectionPool(new ConnectionPool(10, 60, TimeUnit.SECONDS))
        .build();
  }
}

REST API 작성

  • 외부 시스템은 httpbin을 사용한다.
public interface HttpBinAPI {
  @RequestLine("GET /get")
  GetResponse get();

  @RequestLine("POST /delay/{delay}")
  GetResponse delay(@Param("delay") Integer delay);

  @RequestLine("PUT /status/{codes}")
  String status(@Param("codes") Integer codes);
}

REST Client와 Fault Tolerance 설정

  • 설정 파일 작성 (circuit breaker와 rate limiter)
resilience4j:
  circuitbreaker:
    instances:
      httpbin:
        sliding-window-type: COUNT_BASED # COUNT_BASED(default) or TIME_BASED
        sliding-window-size: 100 # calls or seconds
        minimum-number-of-calls: 100
        failure-rate-threshold: 50
        wait-duration-in-open-state: 60s # open -> half-open
        permitted-number-of-calls-in-half-open-state: 10
        max-wait-duration-in-half-open-state: 0s # half-open -> open
        slow-call-rate-threshold: 100
        slow-call-duration-threshold: 60s
        event-consumer-buffer-size: 10
  ratelimiter:
    instances:
      httpbin:
        timeout-duration: 5s
        limit-refresh-period: 500ns
        limit-for-period: 50
        event-consumer-buffer-size: 10
  • 설정 내용 적용
    • YAML의 설정 정보를 사용하기 위해서는 CircuitBreakerRegistry와 RateLimiterRegistry를 사용한다.
    • rate limiter, circuit breaker 기능은 FeignDecorators 생성 시 선언된 순서로 적용된다.
private static final String HTTP_BIN_NAME = "httpbin";
private static final String HTTP_BIN_URL = "https://httpbin.org:80/";

@Bean
public HttpBinAPI app2AppAPIClient() {
  CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(HTTP_BIN_NAME);  circuitBreaker.getEventPublisher()
      .onReset(event -> log.warn("CircuitBreaker > onReset > {}", event))
      .onError(event -> log.warn("CircuitBreaker > onError > {}", event))
      .onFailureRateExceeded(event -> log.warn("CircuitBreaker > onSlowCallRateExceeded > {}", event))
      .onSlowCallRateExceeded(event -> log.warn("CircuitBreaker > onSlowCallRateExceeded > {}", event))
      .onStateTransition(event -> log.warn("CircuitBreaker > onStateTransition > {}", event));

  RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter(HTTP_BIN_NAME);  rateLimiter.getEventPublisher()
      .onFailure(event -> log.warn("RateLimiter > onFailure > {}", event));

  FeignDecorators feignDecorators = FeignDecorators.builder()
      .withCircuitBreaker(circuitBreaker)      .withRateLimiter(rateLimiter)      .build();

  return Resilience4jFeign.builder(feignDecorators)
      .client(new feign.okhttp.OkHttpClient(okHttpClient)) // HTTP Client 주입
      .decoder(new JacksonDecoder(objectMapper)) // Json 설정 주입
      .target(HttpBinAPI.class, HTTP_BIN_URL);
}

@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
  return builder -> {
    builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    builder.featuresToEnable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT);
    builder.serializationInclusion(JsonInclude.Include.NON_NULL);
    builder.serializationInclusion(JsonInclude.Include.NON_EMPTY);
    builder.propertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE);
  };
}

테스트


조건과 설정

  • CircuitBreaker와 RateLimiter 적용 테스트를 해보자.
  • 테스트를 위해 조금은 극단적으로 시나리오를 잡았고, 각 조건에 맞춰 설정을 수정하자.
  • 조건 #1 : 외부 시스템에 오류가 발생한 경우 요청을 중지한다.
    • 10건의 요청을 기준으로 실패 비율이 50%로가 넘는 경우 circuit breaker를 오픈(OPEN)한다.
    • 하프 오픈(HALF-OPEN)전 10초를 대기한다. 하프 오픈 후에는 최대 5건의 요청만 허용한다.
    • circuit breaker를 닫기 전에 1초 대기한다.
    • 외부 시스템의 응답이 느린 경우 오류로 판단한다. 지연으로 인지하는 응답 시간은 3초로 한다.
    # CircuitBreaker 설정
    sliding-window-size: 10
    minimum-number-of-calls: 5
    failure-rate-threshold: 50
    wait-duration-in-open-state: 10s
    permitted-number-of-calls-in-half-open-state: 5
    max-wait-duration-in-half-open-state: 1s
    slow-call-rate-threshold: 80
    slow-call-duration-threshold: 3s
  • 조건 #2 : 외부 시스템에 초당 요청할 수 있는 개수는 정해져 있다.
    • 제한된 수는 1초당 20건이다.
    # RateLimiter 설정
    limit-refresh-period: 1s
    limit-for-period: 20
    timeout-duration: 10ms

테스트 API 작성

@RestController
@RequestMapping("/v1/test")
@RequiredArgsConstructor
public class RestTestController {
  private final HttpBinAPI httpBinAPI;

  @RequestMapping("/get")
  public String get() {
    httpBinAPI.get();
    return "success\n";
  }

  @RequestMapping("/delay")
  public String delay() {
    httpBinAPI.delay(4);
    return "delay\n";
  }

  @RequestMapping("/error")
  public String status() {
    httpBinAPI.status(500);
    return "error\n";
  }
}

테스트하기

  • httpbin 구동하기
docker pull kennethreitz/httpbin
docker run -p 80:80 kennethreitz/httpbin
  • 서버 실행하기
git clone https://github.com/oflouis/resilience4j-feign.git
cd resilience4j-feign
./gradlew clean bootRun 
  • 조건 #1 : 외부 시스템 오류 테스트
# 서킷 브레이커 열기 위한 호출
for i in $(seq 1 4); do curl "http://localhost:8080/v1/test/error"; curl "http://localhost:8080/v1/test/error"; curl "http://localhost:8080/v1/test/get"; done

# 주요 로그 확인
## 오류 발생 시 실패로 기록
CircuitBreaker 'httpbin' recorded an exception as failure:
## 오류률 초과 시 서킷 브레이커 오픈 ( 상태 전환 : CLOSED to OPEN )
Event FAILURE_RATE_EXCEEDED published: CircuitBreaker 'httpbin' exceeded failure rate threshold. Current failure rate: 80.0
Event STATE_TRANSITION published: CircuitBreaker 'httpbin' changed state from CLOSED to OPEN
## 서킷 브레이커 오픈된 경우 추가 요청 허용하지 않음 
Event NOT_PERMITTED published: CircuitBreaker 'httpbin' recorded a call which was not permitted.
CircuitBreaker 'httpbin' is OPEN and does not permit further calls


# 서킷 브레이커 닫힘 확인을 위한 호출. 서킷 브레이커가 열린 후 10초 후에 실행
for i in $(seq 1 10); do curl "http://localhost:8080/v1/test/get"; done

# 주요 로그 확인
## 오픈 유지 시간 초과하고 서킷 브레이커 하프 오픈 ( 상태 전환 : OPEN to HALF_OPEN )
Event STATE_TRANSITION published: CircuitBreaker 'httpbin' changed state from OPEN to HALF_OPEN
## 성공으로 기록되는 요청들
CircuitBreaker 'httpbin' succeeded:
Event SUCCESS published: CircuitBreaker 'httpbin' recorded a successful call. Elapsed time: 36 ms
## 외부 시스템 정상으로 판단하고 서킷 브레이커 닫힘 ( 상태 전환 : HALF_OPEN to CLOSED )
Event STATE_TRANSITION published: CircuitBreaker 'httpbin' changed state from HALF_OPEN to CLOSED
  • 조건 #1 : 외부 시스템 지연 오류 테스트
# 지연 발생으로 서킷 브레이커 오픈하기 위한 호출
for i in $(seq 1 10); do curl "http://localhost:8080/v1/test/delay"; done

# 주요 로그 확인
## 지연은 있지만 성공으로 기록
CircuitBreaker 'httpbin' succeeded:
Event SUCCESS published: CircuitBreaker 'httpbin' recorded a successful call. Elapsed time: 5054 ms
## 여러 차례의 지연으로 지연률 초과. 서킷 브레이커 오픈 ( 상태 전환 : CLOSED to OPEN )
Event SLOW_CALL_RATE_EXCEEDED published: CircuitBreaker 'httpbin' exceeded slow call rate threshold. Current slow call rate: 80.0
Event STATE_TRANSITION published: CircuitBreaker 'httpbin' changed state from CLOSED to OPEN
## 서킷 브레이커 오픈된 경우 추가 요청 허용하지 않음
Event NOT_PERMITTED published: CircuitBreaker 'httpbin' recorded a call which was not permitted.
  • 조건 #2 : 외부 시스템 호출 초과 제한 테스트
# 허용 개수 초과 후 추가 요청이 제한되는지 확인하기 위한 호출
for i in $(seq 1 30); do curl "http://localhost:8080/v1/test/get"; done

# 주요 로그 확인
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is io.github.resilience4j.ratelimiter.RequestNotPermitted: RateLimiter 'httpbin' does not permit further calls] with root cause

io.github.resilience4j.ratelimiter.RequestNotPermitted: RateLimiter 'httpbin' does not permit further calls
	...

전체 코드 확인하기


참고


※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

유리아쥬 제모스 스틱 레브르 립밤 4g x 10개, 12개, 무향솔가 어드밴스드 칼슘 컴플렉스 타블렛, 120개입, 1개커세어 코리아 정품 DARK CORE PRO 무선 충전 RGB 게이밍 마우스 / 다용도 에코백 사은품 증정, 혼합색상, RGP0076