카프카 사용 전략 : 수신부의 재시도, 복구 정책 고민 본문
배경
나는 아래의 상황에서 카프카를 생각한다.
- 처리에 시간이 걸려서 메인 흐름 부에서 분리하고 싶을 때
- 서비스 간 안전하고 순서가 보장된 이벤트 전달이 필요할 때
물론 비동기 처리와 이벤트 전달 방법은 많지만 방법마다 다른 제약이 존재한다.
- Http : 이벤트를 전달받는 쪽의 상태에 의존적
- Redis PS : Fire and Forget. 쏘기만 하고 잘 받았는지는 검증 않음
- RabbitMQ : 여러 수신처에서 동일하게 수신하려면 여러 개 큐가 필요. 꼼꼼한 순서 보장을 위해선 단일 컨슈머 필요
카프카를 다루면서 고민해야 하는 포인트가 있다면 다음과 같다.
- 브로커 측면 : 파티션 크기, 브로커 성능과 수
- 프로듀서 측면 : 메시지 키, 배치 크기, 배치 간격, 직렬화 방법
- 컨슈머 측면 : 컨슈머 그룹, 커밋 방법, 초기 오프셋 위치, 수신 배치 크기, 멱등성, 재시도 처리와 DLT
최근 이벤트 파이프라인 개선을 맡다 보니, 부족했던 재시도 처리나 복구 정책을 다시 고민할 수 있는 시간이 많았다.
서비스나 이벤트 종류별로 달랐던 고민들을 기억하고 싶어 이를 정리하게 되었다.
이 글에서는 메시지 처리에 실패했을 때의 커밋 전략과 재시도, 복구 전략을 내 경험에 물려 소개한다.
Auto Commit
Auto commit은 끄는 것을 선호한다.
Auto commit은 미리 설정한 인터벌 시간 간격으로 그 시점까지 수신한 Kafka 마지막 오프셋을 커밋한다.
보통 카프카 수신처는 네트워크 통신 비용을 아끼기 위해 배치로 이벤트를 가져온다.
만약 1000개의 이벤트를 배치로 가져와서 600개까지 정상 처리한 상황에서 커밋이 수행된다면,
나머지 400개는 처리를 시도하지도 않고 완료 커밋되는 것이다.
만약 그 시점에 문제가 생겼다가 복구되면 400개의 유실이 생긴다.
반대로 600개까지 정상 처리한 상황에서 서비스가 갑자기 종료되면 어떨까
자동 커밋이 수행되기 이전이기에 브로커는 해당 컨슈머가 600개를 이미 처리했는지 모를 것이다.
컨슈머가 복구되면 앞서 처리한 600개를 다시 수신하게 되는 중복 처리 문제가 생긴다.
Ack Strategy
Auto commit을 끄고, 커밋 전략을 직접 선택할 수 있다.
처리 단건 별로 커밋하는 Record, 배치 단위로 커밋하는 Batch, 애플리케이션에서 직접 결정하는 Manual이 대표적이다.
Batch 모드는 수신한 배치 메시지를 모두 처리한 시점에서 마지막 오프셋을 커밋한다.
Auto commit과는 달리, 처리되지 않은 메시지를 커밋하여 유실이 발생하는 경우는 생기지 않는다.
대신 중간에 예외에 발생하면 Ack를 하지 않아, 모든 배치 메시지를 다시 수신하게 되고 중복이 발생한다.
단건으로 커밋하는 Record를 사용하면 유실과 중복 수신 문제에서 안전해 보인다.
대신 매 메시지를 처리할 때마다 ACK가 필요하기에 네트워크 비용이 든다.
안전하지만 네트워크 비용과 성능 측면에서 비효율적일 수 있다.
내 경우에는 성능이 중요하고 중복 문제에 안전한 서비스에선 Batch를,
안전이 우선되는 서비스에선 Record를 먼저 생각한다.
결국 서비스 성격에 맞는 전략 선택이 필요해 보인다.
중복 처리 문제
앞서 커밋 모드에 따라 중복 처리에 대한 대비가 필요했다.
나는 메시지 ID를 기록하고, 로직 전 처리 여부를 비교하는 방법을 사용하고 있다.
DB의 UPSERT나 키 중복 방지 쿼리를 사용할 수 있을 것 같고,
Redis 같은 공유 메모리에 처리한 메시지 ID를 기록해 두고 비교하는 것도 방법이다.
멱등성 처리가 안되어 있는 카프카 수신 서비스에 멱등성을 처리한 경험이 있다.
단순히 중복 처리에 안전하다는 것도 있지만, 배포할 때 큰 힘이 됨을 배웠다.
기존 멱등성 보장이 안되어 있을 때는 이전 버전과 새로운 버전을 동시에 띄우는 시점에 중복 문제가 발생했다.
그렇다고 중복 문제를 피하려면 이벤트 처리를 완전히 끝내고 커밋까지 마쳐야 다음 버전을 배포할 수 있었다.
멱등성 처리 이후에는 두 버전을 동시에 띄워 중단을 최소화하면서도, 중복 문제에 안전한 배포를 만들 수 있었다.
독약 문제
독약 문제(Poison Pill)는 카프카 토픽에 올라갔지만 수신처에서 항상 실패하는 메시지를 말한다.
한번 독약이 발생하면 그 메시지가 유실되지 않는 이상 해당 파티션은 다음 메시지를 받아 처리할 수 없기에 매우 주의해야 한다.
예를 들어 약속되지 않는 메시지 포맷으로 수신처에서 처리할 수 없는, 발신처의 코드 수정이 필요한 메시지가 독약이다.
적절한 예외 처리가 없다면 Ack를 만들지 못하고, 메시지 수신과 처리 실패를 무한히 반복하게 될 것이다.
내 방법은 수신처에서 최대한 넓은 범위로 예외를 잡아 대응하고, 카프카에는 정상 처리를 알리는 것이다.
수신처에서 기록이나 알림, 복구 정책을 처리하고 카프카엔 Ack를 전달한다.
메시지 자체나 수신처의 로직 문제로 커밋 실패가 되는 경우를 최소화하고, 브로커부터의 메시지 수신 성공 여부에 집중하게 된다.
특히 재시도 이후에도 정상 처리를 완료하지 못한 메시지를 어떻게 다룰 것인지가 재밌다.
서비스 성격이나 이벤트에 따라 꾸준히 재시도해야 할 때도 있고, 알람과 로그만 남기고 무시해야 하는 경우도 있을 수 있다.
다음 단락부터는 서비스 성격에 따라 전략을 달리했던 몇 가지 경험을 소개한다.
복구 전략 1. 수정과 복구
이벤트를 수신해서 필터링, DB에 넣어야 하는 서비스였다.
급한 재시도가 아닌, 유실 없이 처리되는 것이 중요했다.
반복된 재시도가 아닌 에러 원인을 확인하고 수정, 다시 처리될 수 있도록 메시지의 관리의 필요했다.
재시도까지 실패한 메시지를 담는 복구용 토픽(DLT)을 만들어 관리한다.
DLT에 메시지가 올라오면 이를 수신하는 알림 서비스가 개발자가 알 수 있도록 알림을 보낸다.
개발자는 실패 원인을 분석해서 처리 부를 수정하고, DLT에 올라온 메시지를 재처리할 수 있도록 복구한다.
DLT의 메시지를 복구하는 방법도 여러 가지다.
원본 토픽으로 Redrive 방법은 변경점 없이 쉽게 재수신을 만들 수 있지만, 다른 컨슈머들의 중복 처리 문제가 발생할 수 있다.
복구를 위한 임시 애플리케이션을 만들어 DLT 자체를 수신하고 처리를 마치는 것도 방법이다.
이런 DLT를 사용한 실패 이벤트 관리는 에러 메시지 자체를 정확하게 확인하고 간편히 복구 시도를 처리하는데 유리했다.
복구 전략 2. 재시도 반복
사용자의 특정 상태 이벤트를 수신하여, 외부 서비스에 전달해야 하는 로직이었다.
이벤트 양이 많지 않고, 안전이 중요했다.
만약 외부 서비스 장애로 요청 전달에 실패해도, 넓은 간격으로 재시도 반복이 필요했다.
이 경우에는 요청 전달에 실패한 이벤트를 DB에 쌓고, 다른 스레드에서 적재된 이벤트를 확인해서 전달 시도를 반복했다.
사전에 정의되지 않은 실패 코드인 경우에는 1번 정책과 마찬가지로 DLT로 관리했다.
복구 전략 3. 무시
알림 처리를 위해 사용되는 서비스였다.
성능이 중요했고, 재시도를 오래 끌고 가지 않아야 했다.
유실보다 뒤늦은 전달을 더 피해야 하는 케이스였다.
이 경우에는 수신처에서 빠른 재시도 이후에도 처리되지 않은 메시지는 단순 로그만 남기고 포기한다.
그런 버그가 지정 시간 안에서 여러 번 발생하면 이를 개발자가 알 수 있도록 알림을 남기는 정도였다.
정리
서비스 상황과 메시지 타입별 경험을 정리해 보았는데, 역시 정답은 없는 것 같다.
이슈
- 카프카 커밋과 실패 시 복구 전략
커밋 전략
- Auto commit : 간편하지만 중복 처리 문제와 유실 문제가 존재
- Batch 모드 : Ack를 배치 수신 단위로 처리하여 유실 문제에서 안전하지만, 중복 처리 문제 여전히 존재
- Record : 유실과 중복 처리 문제에 안전하지만, 네트워크 비용과 성능에 비효율적
두 가지 문제
- 중복 처리 문제 : DB의 중복 키 방지 혹은 Upsert, Redis를 사용한 처리 메시지 비교로 멱등성 처리
- 독약 문제 : 수신처에서 최대한 넓은 예외 처리와 커밋, Ack는 메시지 수신 성공 여부에 집중
복구 전략
- 수정과 복구 : 재시도까지 실패한 메시지를 DLT로 옮기고 수정, 재시도 반복
- 재시도 반복 : DB에 저장하고 넓은 간격으로 재시도 스케줄링
- 무시 : 단순 로그, 알림만 처리하고 무시. 오히려 늦은 재시도가 더 위험한 경우
'Architecture > Application' 카테고리의 다른 글
| 두 번의 갱신 분실 문제와 락 (12) | 2024.01.23 |
|---|---|
| RepeatableRead 에서 발생할 수 있는 동시성 문제와 락 (8) | 2024.01.10 |
| Future 를 활용한 비동기 이미지 비동기 업로드 흐름과 시연 (0) | 2023.11.28 |
| 트랜잭션 아웃 박스 패턴 : 이벤트 발행과 DB 트랜잭션 원자성 유지하기 (0) | 2023.02.18 |
| 도메인 이벤트를 이용하여 의존성 분리 연습 (4) | 2022.01.12 |