1. 알고자 하는 것
- FeignClient에서의 read timeout
- FeignClient 이슈 분석
- FeignClient에서 read timeout 발생 시 주의사항
2. 알게된 것
배경
- 회사에서 MSA 전환을 준비하기 이전 단계로, 일부 이관되어 멀티모듈로 구성되어있는 프로젝트에 대해 FeignClient로 모듈 간 통신을 하며 발생했던 이슈이다.
- 특정 조건에 해당하는 n건의 데이터에 대해 일괄적으로 배치 처리를 수행하는 Job이 있다.
- 해당 Job은 1시간 간격으로 수행되며, 한 번 수행 시 평균 약 5-10건의 데이터를 대상으로 수행된다.
- Job 내에서 다른 모듈의 API를 FeignClient를 통해 호출하여 추출된 데이터에 대한 비즈니스 로직을 처리한다.
- 반환값은 따로 없으며, 데이터에 대한 update 동작만을 수행하는 Job이다.
- Job이 돌기 전, 데이터 부정합 건에 대해 일괄적으로 CS팀에서 데이터 보정을 수행하였다.
- 이로 인해 갑작스럽게 많은 건의 데이터가 배치 조건에 포함되었고, 데이터 보정 후 수행된 Job에 77건의 데이터에 대한 처리가 진행되었다.
- 이 때, FeignClient의 read timeout이 5초로 설정되어 있었고, 평소 약 5-10건의 데이터를 대상으로 수행하여 5초 내로 Job이 완료되었던 것과 달리, 77건의 데이터에 대해 일괄적으로 FeignClient를 통해 API를 호출하여 처리하다보니 read timeout이 발생했다.
- read timeout으로 인해 모니터링 로그에는 RetryableException: Read timed out 에러가 남았다.
- 하지만, 77건의 데이터에 대해서 정상적으로 처리가 완료되었다.
FeignClient에서의 read timeout
- FeignClient에서 read timeout 값은 다음과 같은 의미를 가진다.
- n초 내에 API 호출에 대한 응답이 오지 않으면 Retry 정책에 따라 Retry를 수행하고, Retry 횟수를 초과하면 RetryableException을 발생시킨다.
FeignClient 이슈 분석
- 이 때 중요한 점은, read timeout 발생 시 Transaction처럼 기존 API 호출 동작을 rollback 후 Retry 하는 등의 동작을 수행하는 것이 아니다.
- 단지, 설정한 read timeout 시간보다 오래걸렸다 = 서버 네트워크에 일시적인 문제가 있을 가능성이 있다라고 생각하고 다시 API를 호출하는 것이다.
- 따라서, read timeout이 발생했다고 하더라도 기존에 호출했던 API의 동작은 중지되지 않고 별개로 수행되는 것이다.
- 애초에 다른 서버(모듈)의 API에 대한 동작을 호출한 쪽에서 제어한다는 것이 불가능하다는 점을 생각하면 이해가 편하다.
- 이러한 read timeout의 동작에 의해 위 배경에서 77건의 데이터에 대해 read timeout이 발생했지만 호출한 API는 수행되어 정상적으로 처리가 완료된 것이다.
- 단지 read timeout을 77건의 데이터가 모두 처리될만큼 충분한 시간으로 설정하지 않았기에 호출한 쪽에서 RetryableException만 throw 된 것이다.
- 또한, 프로젝트의 FeignClient Retry 정책이 Retryer.NEVER_RETRY로 설정되어 있어 read timeout 발생 후 retry를 수행하지 않아 중복호출 역시 발생하지 않았다.
- Retry 정책을 NEVER_RETRY로 설정한 근거는 다음과 같다.
- 조회성 API가 아닌 데이터에 대한 update를 수행하는 API이므로 호출한 쪽에서 RetryableException으로 인해 응답을 받지 못하더라도 API의 동작만 모두 수행되면 update 동작은 모두 진행되므로 데이터의 부정합이 발생하지 않는다.
- 1시간 단위로 수행되는 배치에서 사용되므로, 일시적인 서버 네트워크 장애로 인해 오류가 발생하더라도 1시간 뒤 배치가 재실행되므로 자체적인 Retry가 수행된다. (데이터 처리 누락 X)
- Retry로 인해 update 동작이 중복호출되어 데이터 부정합이 발생할 수 있다.
FeignClient에서 read timeout 발생 시 주의사항
- 위 배경과 같이 FeignClient를 통해 호출한 API가 단순히 update와 같은 데이터 처리만을 수행하는 API라면 read timeout이 발생해도 API 서버에 문제가 없다면 정상적으로 처리된다.
- 그러나, 호출한 API에 대해 응답을 받아야 하고, 응답값을 통해 무언가를 한다면 read timeout 시 이후 동작이 올바르게 수행되지 않는다.
- API가 정상적으로 결과를 반환했다 하더라도, 호출한 쪽에서는 read timeout으로 인해 RetryableException이 발생하여 이후 동작이 처리되지 않기 때문이다.
- 다음과 같이 10초의 수행시간을 가지는 API가 존재한다고 하자.
- 하나는 반환값이 없이 단순히 데이터를 처리하는 API이며, 다른 하나는 응답값을 반환하는 API이다.
@Service
public class InternalService {
public void delayedLogicWithoutReturn() throws InterruptedException {
int a = 100;
Thread.sleep(10000L);
for (int i = 0; i < 100; i++) {
a++;
}
System.out.println("result a : " + a);
}
public String delayedLogicWithReturn() throws InterruptedException {
int a = 100;
Thread.sleep(10000L);
for (int i = 0; i < 100; i++) {
a++;
}
return "result a : " + a;
}
}
@RequiredArgsConstructor
@RequestMapping("/api/internal")
@RestController
public class InternalController {
private final InternalService internalService;
@GetMapping("/delayed")
public void delayedWithoutReturn() throws InterruptedException {
internalService.delayedLogicWithoutReturn();
}
@GetMapping("/delayed/return")
public String delayedWithReturn() throws InterruptedException {
return internalService.delayedLogicWithReturn();
}
}
- FeignClient를 통해 해당 API를 호출하는 쪽에서의 read timeout 값은 5초로 설정했다.
- retry 정책은 NEVER_RETRY로 설정했다. (Retry 수행 X, read timeout 발생 시 바로 RetryableException throw)
public class FeignConfig {
@Bean
public Request.Options requestOptions() {
long connectionTimeout = 10;
long readTimeout = 5; // read - timeout 5초로 설정
return new Request.Options(connectionTimeout, TimeUnit.SECONDS, readTimeout, TimeUnit.SECONDS, false);
}
@Bean
public Retryer retryer() {
return Retryer.NEVER_RETRY;
}
}
- 단순한 데이터 처리, 반환값을 주는 두 API에 대해 read timeout이 발생하도록 설정하고 FeignClient를 통해 API를 호출한다.
@RequestMapping("/api/external")
@RequiredArgsConstructor
@RestController
public class ExternalController {
private final ApiClient apiClient;
@GetMapping("/delayed")
public String delayedWithoutReturn() throws InterruptedException {
apiClient.delayedWithoutReturn();
return "Success!";
}
@GetMapping("/delayed/return")
public String delayedWithReturn() throws InterruptedException {
return apiClient.delayedWithReturn();
}
}
- 반환값이 없이 단순히 데이터가 처리되는데에 10초가 소요되는 API의 경우, 호출한 쪽에서는 RetryableException이 발생했지만 API는 10초 뒤 정상적으로 데이터 처리를 수행했다.
- 반면, 데이터를 반환하는 조회성 API의 경우, 호출한 쪽에서 RetryableException이 발생해 API에 대한 응답값을 받지 못하고 이후 동작이 수행되지 않는다.
- 따라서, 조회성 API를 FeignClient로 호출할 때에는 read timeout 발생 시 응답값을 받지 못하여 이후 동작을 수행하지 못한다.
- 이 때에는 read timeout 값과 Retry 정책을 프로젝트의 상황에 맞춰 적절히 설정하는 것이 필요하다.
- read timeout 값과 Retry 정책이 필요한 이유이기도 하다.
3. 정리
- FeignClient의 read timeout에 의한 Retry는 단순히 API를 다시 호출하는 것이다.
- update성 API에 대해서는 Retry로 인한 중복호출과 같은 문제를 주의해야 한다.
- 반대로 조회성 API에 대해서는 read timeout으로 인한 데이터 응답 실패와 같은 문제를 주의해야한다.
- 이렇듯, API의 용도와 프로젝트의 조건에 따라 적절하게 read timeout 시간과 Retry 정책을 조절해야 한다.
Source Code
https://github.com/HunSeongPark/blog-study/tree/main/feign-client-retry
'Programming' 카테고리의 다른 글
[AWS, Certified Developer Associate] EC2 - 정의, 인스턴스 유형, 보안그룹 (0) | 2024.04.28 |
---|---|
[AWS, Certified Developer Associate] IAM (0) | 2024.04.28 |
[시스템 설계: 한번에 인터뷰 합격하기] 샤딩(Sharding) & RDB vs NoSQL (1) | 2024.03.23 |
[시스템 설계: 한번에 인터뷰 합격하기] 데이터베이스의 장애 극복 전략 (0) | 2024.03.18 |
[시스템 설계: 한번에 인터뷰 합격하기] 수평 스케일링 vs 수직 스케일링 (0) | 2024.02.05 |