Architecture Improvement After Kafka Failure
Kafka를 이용하는 메시지 플랫폼에서 장애를 겪으며 아키텍처를 개선한 이야기
https://youtu.be/dG2Dr3jhSZg?si=wz9YWJ2Ny65SbsVx
배달의민족 공통시스템개발팀의 허창호, 김동환 개발자님이 발표한 **“메시지 플랫폼 아키텍처 개선 사례”**는 카프카 기반 시스템의 실전 운영과 구조적 개선 과정을 보여준 발표였습니다. 이 글에서는 해당 발표를 정리하며, 배민이 어떤 문제를 어떻게 해결했는지를 기술적 관점에서 풀어보겠습니다.
반복된 장애의 원인 분석
외부 통신사(Firebase, 알림톡 등) 문제 – 통제 불가능
내부 버그 – 발송 채널별 요구 사항 복잡성 증가
벤더사 장애 – 다중화된 벤더로 비용절감 → 그러나 장애 가능성 증가
Kafka 장애 – 발송 파이프라인 전체가 Kafka 의존
장애를 이겨내기 위한 노력 - 모니터링
발송 히스토리 테이블 : 벤더사 나 통신사 별로 발송
실패 / 성공
을 기록밴더사 트래픽 전환 기능 : 특정 밴더사에 장애가 났을때 다른 밴더사로 트래픽을 전환
옵스지니 : 크리티컬 하다고 생각되는 메트릭에 대해서 알림을 받음
아키텍처 개선
장애 현상
producer쪽에서는 InvalidProducerEpch 에러
producer 에러를 통해 장애 원인 추측
consumer쪽에서는 특정 파티션에 대한 컨슘이 중단되는 상황
특정 파티션에서 lag가 증가하면서 모니터링 알림으로 장애를 인지
원인
Kafka Exactly Once: 메세지를 단 한번만 전달할 수 있게 해주는 기능
Impotency 프로듀서
프로듀서가 메시지 & 시퀀스 넘버를 통해 메시지 중복 예방
프로듀서와 파티션 하나의 관계에서 중복을 막기 위해 지원
Transaction 기능
메시지를 한 트랜잭션에 처리하고 싶을때 사용
메시지가 각각의 토픽 파티션에 들어갈 수 있음
메시지들은 다른 스레드에서 처리될 수 있음
하나의 메시지에서 문제가 발생할 시, 다른 메시지가 중복 처리될 가능성.
Kafka Exactly Once 사용 이유
api 한 번 호출로 최대 100명에게 발송하는 기능이 존재
1번부터 발송하다가 에러가 나서 51번부터 발송하는 경우, 클라이언트쪽에서 51~100번까지 다시 발송하는 기능은 복잡도가 높음
카프카를 사용하다보면 프로듀서/브로커의 예기치 않은 셧다운으로 인해 메시지 중복이 발생할 수 있음
문제(메시지 중복 등)를 해결하기 위한 전략으로는 Kafka Exactly Once 또는 직접 아키텍처를 만들어 사용
직접 만들면 완벽한 중복 방지가 가능하지만, 인프라(Redis 등) 사용으로 인한 비용 증가, 아키텍처 복잡도 증가
Exactly Once를 사용하면 현재의 인프라를 사용하면서 비용을 감소하고, 중복 로직은 카프카에 위임하면서 관심사를 분리하고 아키텍처의 심플함이라는 이점을 얻을 수 있음
Exactly Once 제거 결정
제거 이유:
장애 원인을 파악하기 위해 부하 테스트와 가설을 설정 → 장애를 발견하기 쉽지 않음. KIP-664를 참고
Kafka Improvement Proposal (KIP)
카프카 커뮤니티에서 메이저 기능이 추가관련 논의 플랫폼
hanging transaction이 발생할 수 있고 이를 모니터링하고 조치하는 Tool을 제공한다는 내용
hanging transaction은 다양한 원인으로 발생 가능해, 사람이 판단하고 운영으로 처리해야함
따라서 현재 운영방식을 다시 한번 생각해보기로 함.
메시지는 장애가 많이 발송하는 플랫폼. 최대 과제는 플랫폼 유지 비용을 낮추는 것
Exactly Once를 계속 사용하면 플랫폼 유지 비용이 증가 → 팀에서는 허용할 수 없음
중복 제거를 위한 아키텍처
Exactly Once를 제거했을 경우 다시 중복 문제가 발생
프로듀서가 해시를 생성하고, 메시지와 해시를 큐에 넣음.
컨슈머는 해시를 캐시(redis)에서 확인하여, 중복(이미 처리됨)이면 발송하지 않음
아키텍처는 간단하지만 해시 생성 정책에 대한 고민
처음에는 프로듀서에서 수신자와 메시지 내용으로 해시 생성
동일한 수신자에게 동일한 내용은 중복되지 않도록 막을 수 있음
만약 클라이언트가 동일한 사용자에게 동일한 내용을 발송한다면 막아야 하는가? 허용해야 하는가?
메시지 카테고리에 따라 중복 허용 판단을 클라이언트에서 결정
중복에 대한 책임을 클라이언트에 넘기지만 기능이 추가될 때마다 파라미터가 추가되고 사용하기 어려워짐 → 플랫폼이 너무 복잡해지는 것이 우려되었음
메시지 내용을 바탕으로 해싱을 하게 되면, 실제로 허용된 중복인지 판단이 어려움
카프카 내부에서 발생하는 중복만 제거하기로 결정
API 리퀘스트에 대해 해시를 생성하여, 요청에 대한 중복을 막음.
API 요청이 오면 해당 API의 요청에 대해서 해시를 생성하고 해시를 캐시로 관리하면 API 요청에 대한 중복만 막을 수 있음.
캐시 용량 예측
하나의 데이터 120byte → 예측 총 용량 1.65GB → 클러스터 사이즈 4GB (널널한 기준으로 잡음)
TTL: 오프셋이 리셋되어 처음부터 메시지를 읽더라도, 중복을 막을 수 있게 TTL은 카프카 토픽 리텐션으로 잡음
그리고 롤백했습니다.
레디스 부하가 높게 나옴 (실제 예측보다 많이 나옴)
예상했던 하나의 데이터 용량보다 2.5배
실제 데이터 용량: 272byte.
레디스는 메모리를 계산할 때 데이터 셋 사이즈 + 오버헤드 사이즈의 합
데이터셋 사이즈
오버헤드 사이즈: 키를 관리하기 위한 해시 테이블이나 레플리가 정보 등
Redis 메모리 최대를 사용하면 안될까요?
key eviction 전략을 allkeys-lru 로 사용 중
모든 key들은 엑세스가 거의 없음
따라서 생성된 시간 순으로 lru가 초기화 됨(key eviction process 발생)
원인은 key eviction process
key eviction process: 메모리가 최대 메모리를 넘쳐서 사용하게 된다면, 최대 메모리 이하로 내려올때까지 키를 계속 삭제하는 과정.
이 과정이 부하를 발생시키고 → 부하는 레플리케이션으로 전이되며 → 결국 메시지 발송 지연
요구사항 있는 즐거운 세상
3배 트래픽을 받아주세요!
3배 트래픽 처리를 위한 여정
“파티션 수를 줄이고, 동시성을 확보해주세요”
카프카 사용 중이었기에 파티션을 3배로 늘리는 방법이 가장 쉬움
더 적은 수의 파티션으로 더 많은 트래픽을 받아내야 하는 상황
파티션 수 증가가 답이 아닌 이유
파티션 수 증가 시
장애 복구 타임 증가
커밋이 일어날 때 파티션들의 동등하게 커밋된 정보를 알기 위해 Latency 증가
리소스 사용량 증가
늘어난 파티션은 줄일 수 없음
효과는 불확실하고 비용과 리스크는 커지는데 비가역적
어디가 병목일까?
API 요청에 따라 Kafka를 많은 용도로 활용
카프카를 통해 API요청을 라우터로 보내어 처리
실제 푸시 발생을 담당하는 워커로 보내는 구조
아웃라이어 식별 및 제거
안드로이드 FCM 알림 Latency 이슈: 일정시간마다 수분동안 발생
15분 이상 미 응답으로 리밸런싱(Kafka) 발생
FCM Timeout 처리를 시도했으나 동일한 문제 발생
TimeLimiter를 적용
FCM 호출하여 제한 시간 초과 시 에러 발생시킴
아웃라이어는 없어지고 트래픽에 따라서 점진적으로 Latency 증가
3배의 트래픽을 처리해야 하기 때문에 트래픽에 따라 Latency가 증가하면 안됨
동시 처리량을 증가시킬 방법을 찾아야 함.
동시 처리량 증가
🧐 동시 처리량이 왜 나오지 않을까?
push worker에서 비효율적인 부분이 있다고 판단
기존 구조에서 가장 큰 부분을 변경해야 함
기존 구조 (동기적 구조)
모든 메시지는 발송 순서가 중요하지 않고 모두 독립적임
따라서 이전의 메시지의 발송 결과가 중요하지 않음
해결 방법: 비동기
Sender 스레드에 FCM 요청을 던져 놓고 바로 Kafka Commit 수행
Sender 스레드의 실패 시나리오에 대응이 필요
어느정도의 동시성을 제공해야하고 이를 효과적으로 사용할 수 있을지
비동기로 전환후 고려해야하는 점
일시적인 실패 재처리: 외부 서버가 실패했을 경우.
구글 서버도 많은 트래픽에 일시적으로 Bad Gateway, 서버 에러가 발생함
재시도하면 일반적으로 성공함. 하지만 이에 대한 처리를 고민해야 함
동기적으로 처리하면 발송한 스레드가 성공할 때까지 논다는 문제가 발생
목표는 동시 처리량 증가
실패가 발생하면 DLQ에 저장
다른 컨슈머에서 DLQ를 읽어서 다시 전송하는 형식으로 구성
일시적인 실패에 대한 처리: DLQ 도입
실패 요청을 DLQ에 던져 놓고, 다른 컨슈머가 재시도
유효 시간을 정하여 무한 재시도를 막아둠
적절한 동시성 찾기: 스레드풀 최적화
적절한 크기의 스레드 풀을 도입하여 트래픽 평탄화 필요
평탄화를 위해 스레드 풀의 성격을 더 명확히 분리하는 것이 중요
서로 다른 역할을 수행 시, 서로 간섭을 받으면 특정 스레드 풀에 대한 최적화가 어려워짐
역할에 대한 스레드 풀 분리
기존) 발송 스레드: 푸시 메타 정보 가져오기, 푸시 발송 ← 두 가지의 책임
위 두 책임을 각 다른 스레드 풀로 분리
트래픽 성격에 따른 스레드풀 분리
배달을 주문하고 받을 때까지 실시간으로 푸시가 옴
발송 스레드 풀을 하나로 관리 시, 배치성이나 광고 트래픽이 많을 때 실시간 푸시가 밀릴 수 있음
실시간성 등 성격에 따른 스레드 풀로 분리
배치 스레드 풀(광고)
실시간 스레드 풀
광고 스레드 풀
현재도 스레드 풀 분리 진행 중
불필요한 카프카 요청 제거
카프카 요청도 병목!
Kafka로의 네트워크 흐름을 줄여보자
중간 모듈(Router) 걷어내기
라우터 역할
벤더사 발송 비율 조정
발송에 필요한 메타데이터 획득
푸시가 정해져 있어 라우팅이 필요하지 않음
결국 메타데이터 획득만 하면 되기에 라우터를 걷어내기로 결정
복잡도는 올라가고 각 메시지 마다 아키텍처 통일성은 포기하지만
*더 많은 동시성을 얻게됨*
배치 리스너 적용
기존: 메시지 수 만큼 커밋 발생
변경 후: 배치로 메시지 가져와 가져온 모든 메시지에 대해 한 번의 커밋만 적용
카프카 i/o, 3배 이상의 latency 감소 확인
정리
Kafka 파티션 수: 720개 → 100개
2배 트래픽 최대 1분 이내 지연 발송까지 확인
발송하는 곳의 이슈가 있기에 3배는 만족하지 못함
실시간성이 중요한 푸시 알림을 10초 이내에 모두 발송 중
느낀점
트래픽 3배 요구 = 단순 스펙업이 아닌 구조적 개선의 기회 3배 트래픽을 받아야 한다는 요구가 단순히 Kafka 파티션을 늘리는 것이 아닌, 전체 아키텍처를 뜯어보며 병목을 하나하나 해소해 나가는 계기가 됬다는 점이 인상적이었습니다. 결국 단일 병목 제거가 아닌, 발송 방식 전환, 쓰레드 풀 분리, Kafka API 호출 수 최소화 등의 종합적인 최적화로 문제를 해결한 점을 보고 많이 배웠습니다.
Last updated