Consumer 재시도시 Exponential Backoff를 적용하면, 장애가 터졌을 때 서버와 DB에 과부하를 방지할 수 있습니다.
사용 이유
장애 발생시, 재시도 폭탄(연속 실패)를 막을 수 있습니다.
예시로, DB가 죽거나 네트워크 끊김으로 인해 장애가 터졌다고 가정을 합니다.
이런 상황에 재시도 간격(1초)이 고정이 되는 경우, 아래와 같은 시나리오가 계속 반복됩니다.
1초마다 계속 재시도 → 1초 후 실패
해당 시나리오가 장애가 발생한 시스템에 수천, 수만 개의 재시도 요청이 쏟아지게 됩니다.
서버나 DB가 다시 살아나려고 해도 과부하 때문에 다시 살아나지 못할 수 있습니다.
Exponential Backoff를 사용하는 경우, 초반에는 빠르게 시도하지만, 실패가 반복될수록 간격이 커져서 서버가 버틸 시간을 주게됩니다.
장애 복구 시점을 기다리면서, 무작정 재시도를 하는 것을 막을 수 있습니다.
DB나 외부 시스템 복구 대기 시간을 벌어줍니다.
DB 장애, 네트워크 장애는 몇 초~수십 초면 복구될 수 있습니다.
그런데 매초 재시도하면 복구되는 타이밍을 못 맞춰서 서비스 전반이 죽을 수도 있습니다.
느리게 재시도하면 자연스럽게 복구 타이밍을 기다릴 수 있습니다.
트래픽 폭주를 막고, 정상 요청과 장애 요청을 분리할 수 있습니다.
장애 상태에서 정상 요청은 정상적으로, 실패한 요청만 느리게 재시도할 수 있습니다.
즉, 장애가 전체 시스템을 마비시키지 않게 해줍니다.
단점
Expotential BackOff를 그냥 사용하게 되는 경우(간격을 2배라고 가정),
1초 후 재시도 → 실패 2초후 재시도 → 실패 4초후 재시도 → 실패 8초후 재시도
이렇게 모든 클라이언트가 거의 같은 타이밍에 재시도를 합니다.
이는 곧 똑같은 순간에 수천 개의 요청이 다시 몰리게 되는 상황이 발생하고, 서버가 다시 죽어버릴 수 있는 문제가 발생합니다.
즉, 재시도 폭탄에 서버에 장애가 발생하게 됩니다.
단점 해결 방안 Jitter
이 문제를 해결하기 위해선, Jitter를 사용하면 됩니다.
Jitter는 재시도 타이밍에 약간의 랜덤성을 섞는 것입니다.
즉, 똑같이 4초 기다리는 게 아니라, 어떤 애는 3.8초 뒤, 어떤 애는 4.2초 뒤, 어떤 애는 4.5초 뒤, 어떤 애는 3.5초 뒤 재시도 로직을 실행하는 것입니다.
이렇게 살짝 랜덤하게 재시도하도록 만들어줍니다.
결과적으로 재시도 요청이 분산돼서 재시도 요청 부하가 몰리는 것을 막을 수 있게 됩니다.
Jitter 적용 방법
Jitter를 적용하는 방법으로는 Full Jitter와 Equal Jitter방식이 있습니다.
Full Jitter는 0초부터 현재 대기 시간 사이에서 랜덤 선택을 하는 방식입니다.
Equal Jitter는 (현재 대기 시간 / 2 ) ~ 현재 대기 시간 사이 랜덤 선택을 하는 방식입니다.
Full Jitter vs Equal Jitter
Full Jitter와 Equal Jitter 중에서 어떤 방식을 사용하는 것이 좋을까요?
실제로, AWS 공식 아키텍처 가이드에서도, 다양한 실험을 통해 Full Jitter가 가장 서버 부하를 낮추고, 완료 시간도 빠른 결과를 가져온다는 것을 확인할 수 있습니다.
아래 그래프는 AWS 공식 아키텍처 가이드에서 가져온 자료입니다.
그래프에서 볼 수 있듯이 EqualJitter는 FullJitter보다 처리 시간이 오래 걸림을 확인할 수 있습니다.
클라이언트가 많아질수록 차이는 더 커집니다. 둘다 재시도 폭탄을 막아주지만, FullJitter가 훨씬 효율적입니다.
반면, Equal Jitter는 약간 더 안정적으로 보이지만, 결국 Full Jitter보다 더 많은 재시도 작업량과 시간을 소모하게 됩니다.
따라서, Full Jitter를 사용하는 것이 더 추천되는 방법입니다.
참조
차이가 발생하는 이유
EqualJitter는 기본 대기 시간 / 2 ~ 기본 대기 시간” 사이에서 랜덤을 주기 때문에, 항상 최소한 절반 정도는 기다립니다. 그래서 평균 재시도 시간이 길어집니다.
FullJitter는 0 ~ 기본 대기 시간”에서 완전히 자유롭게 랜덤이기 때문에, 빠른 애들은 훨씬 더 빨리 재시도할 수 있어서 더 빠르게 완료됩니다.
즉, EqualJitter는 무조건 절반은 기다린 뒤 랜덤하게 재시도하지만, FullJitter는 0부터 자유롭게 랜덤하게 재시도할 수 있어 전체 완료 시간이 더 빠릅니다.
결론
Exponential Backoff는 재시도 폭탄을 막아주지만, Jitter를 추가해야 트래픽을 제대로 분산시킬 수 있습니다. 특히 Full Jitter는 Equal Jitter보다 더 빠르고 효율적으로 재시도를 분산시키기 때문에, Exponential Backoff와 함께 Full Jitter를 적용하는 것이 가장 효과적인 장애 복구 전략이므로 해당 전략을 선택했습니다.
코드로 적용한 부분
package com.sangyunpark.product.config;
import java.util.Random;
import org.springframework.util.backoff.BackOff;
import org.springframework.util.backoff.BackOffExecution;
public class ExponentialBackOffWithJitter implements BackOff {
private final double multiplier;
private final long maxElapsedTime;
private final int maxAttempts;
private long currentInterval;
private int attemptCount;
private long elapsedTime;
private final Random random = new Random();
public ExponentialBackOffWithJitter(long initialInterval, double multiplier, long maxElapsedTime, int maxAttempts) {
this.maxAttempts= maxAttempts;
this.multiplier = multiplier;
this.maxElapsedTime = maxElapsedTime;
this.currentInterval = initialInterval;
this.elapsedTime = 0;
}
@Override
public BackOffExecution start() {
return () -> {
if (elapsedTime >= maxElapsedTime || attemptCount >= maxAttempts) {
return BackOffExecution.STOP;
}
long next = currentInterval;
long jittered = (next > 0) ? random.nextLong(next + 1) : 0;
currentInterval = Math.min((long) (currentInterval * multiplier), Long.MAX_VALUE / 2);
elapsedTime += jittered;
attemptCount++;
return jittered;
};
}
}
package com.sangyunpark.product.config;
import com.sangyunpark.product.application.event.StockDeductedEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.web.servlet.View;
@Slf4j
@Configuration
public class KafkaConsumerConfig {
private final String DLT = ".DLT";
private final int MAX_RETRY_COUNT = 5;
@Bean
public ConcurrentKafkaListenerContainerFactory<String, StockDeductedEvent> kafkaListenerContainerFactory(
ConsumerFactory<String, StockDeductedEvent> consumerFactory,
DefaultErrorHandler errorHandler
) {
ConcurrentKafkaListenerContainerFactory<String, StockDeductedEvent> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
factory.setCommonErrorHandler(errorHandler);
return factory;
}
@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<String, StockDeductedEvent> kafkaTemplate, View error) {
DeadLetterPublishingRecoverer recover = new DeadLetterPublishingRecoverer(kafkaTemplate,
(record, ex) -> new org.apache.kafka.common.TopicPartition(record.topic() + DLT, record.partition()));
ExponentialBackOffWithJitter backOff = new ExponentialBackOffWithJitter(1000L, 2.0, 16000L, MAX_RETRY_COUNT);
DefaultErrorHandler errorHandler = new DefaultErrorHandler(recover, backOff);
errorHandler.setRetryListeners((record, ex, deliveryAttempt) -> {
log.warn("카프카 컨슈머 재시도 횟수 {} 기록 내용: {}", deliveryAttempt, record);
});
return errorHandler;
}
}
면접 예상 질문
왜 Exponential Backoff만 적용하면 충분하지 않은가요?
Exponential Backoff만 적용하면 재시도 간격은 점점 길어지지만, 여전히 같은 타이밍에 여러 요청이 몰릴 수 있습니다.
특히 수천, 수만 개의 클라이언트가 동시에 재시도할 경우, 간격이 길어져도 한순간에 몰리는 트래픽 폭탄이 발생할 수 있습니다.
Full Jitter를 함께 적용해 재시도 타이밍 자체를 랜덤하게 분산시켜야 서버나 외부 시스템의 부하를 근본적으로 줄일 수 있습니다.
Equal Jitter와 Full Jitter의 차이는 무엇인가요? 왜 Full Jitter를 더 추천하나요?
Equal Jitter는 항상 기본 대기 시간의 절반 이상을 기다린 후 랜덤 대기를 추가하는 방식입니다. 그래서 평균 대기 시간이 길어지고, 빠른 복구가 어려울 수 있습니다. 반면 Full Jitter는 0부터 최대 대기 시간 사이를 완전히 자유롭게 랜덤하게 선택합니다. 덕분에 빠른 재시도도 가능하고, 전체 완료 시간도 짧아집니다.
AWS 아키텍처 가이드에서도 실험 결과, Full Jitter가 Equal Jitter보다 재시도 분산 효과가 크고, 처리 속도도 빠르다고 권장합니다.
Full Jitter 적용 시 이론적인 리스크는 없나요?
이론적으로 Full Jitter는 재시도 간격이 0에 가까운 값이 나올 수도 있어서, 초반에 재시도 요청이 집중될 가능성이 아주 조금은 존재합니다. 그러나 전체 요청량이 많을 경우에는 오히려 이 랜덤성 덕분에 요청이 자연스럽게 분산되고, 평균적으로 시스템 부하를 훨씬 안정적으로 제어할 수 있습니다.
Exponential Backoff를 적용할 때 주의해야 할 점은 무엇인가요?
첫 번째는 최대 대기 시간을 설정해야 한다는 점입니다. 그렇지 않으면 간격이 무한히 커질 수 있습니다.
두 번째는 최대 재시도 횟수를 설정하거나, Dead Letter Topic 같은 실패 처리 방식을 함께 마련해야 합니다.
세 번째는 Jitter를 꼭 섞어야 한다는 것입니다. 단순히 Backoff만 쓰면 여전히 재시도 타이밍이 몰릴 수 있습니다.
Kafka Consumer 재처리 시 Exponential Backoff + Full Jitter를 적용하면 어떤 효과를 기대할 수 있나요?
장애 상황에서도 재시도 요청이 빠르게 터지지 않고, 서버가 복구될 때까지 부드럽게 대기하며 재시도할 수 있습니다.
특히 Full Jitter 덕분에 같은 파티션 안에서도 재시도 시점이 분산되어, 재시도 요청 폭탄으로 인해 서버나 데이터베이스가 추가로 마비되는 상황을 방지할 수 있습니다. 결국 전체 시스템 복구 속도를 높이고, 장애 확산을 막을 수 있습니다.
Retry + Backoff 설정을 너무 보수적으로 하면 어떤 문제가 발생할 수 있나요?
Retry 간격이나 최대 재시도 횟수를 너무 짧게 잡으면, 장애 발생 시 오히려 빠르게 재시도가 몰려서 장애가 악화될 수 있습니다.
반대로 간격이나 재시도 횟수를 너무 길게 잡으면, 복구 가능한 오류에 대한 복구 시간이 너무 길어져 사용자 경험이 나빠질 수 있습니다. 따라서 시스템 부하와 복구 기대시간 사이에서 균형 잡힌 Backoff 전략을 설정하는 것이 중요합니다.
'Project' 카테고리의 다른 글
[s-market] Kafka Consumer Redis 복구방식 개선 (0) | 2025.04.29 |
---|---|
[s-market] Redis 이벤트성 상품 재고 차감 전략 (0) | 2025.04.29 |
[s-market] 상품과 재고처리 시나리오 계획 (0) | 2025.04.28 |
[s-market] 프로젝트 구조 (0) | 2025.03.31 |
[s-market] ERD 설계 개선하기 (0) | 2025.03.25 |