카프카의 설정은 진짜일까..?! - Acks 편

반응형
 

EDA 패턴을 적용해보자.(feat.kafka)

이전 포스트에서 이어져서 진행이 되어집니다. 선착순 쿠폰 발급기 개발당신의 동시성 테스트가 원하는 결과가 나오지 않는 이유TL;DR: 낙관락과 비관락을 고르는 기준에 대해 설명합니다.배경

b-programmer.tistory.com

마지막 글에 간단하게 outbox패턴을 얘기하면서 카프카의 설정을 살짝 언급한 적이 있습니다. 사실 outbox패턴이 없어도 카프카는 메시지를 다시 읽을 수 있었습니다. 바로 @RetryalbeTopic이라는 어노테이션을 이용하면 producer에서 발송된 메시지를 다시 읽을 수 있게 유도할 수 있었습니다. 그렇다면, 이러한 설정들이 실제로 어떻게 동작을 할까요? 마지막에도 언급했듯이 제 목표는 native 하게 kafka를 사용하는 것이 목표입니다. 그 이후에는 CDC까지 사용해서 outbox를 풀링 하는 방식이 아닌 DB내부의 로그를 읽는 방식으로 변경할 예정입니다. 한번 해봅시다.

시작은 간단하게 producer의 설정들부터 들어가 봅시다.

ack를 간단하게 설명하면, 어떻게 요청하는 방법에 대해 정의를 하고 있습니다.

acsks=0:
- 가장 빠르게 전송할 수 있는 방식이지만, 유실 위험이 가장 큽니다. 
- 지연 시간은 줄일 수 있지만, 메시지가 broker에 실제로 저장되었는지는 보장하지 않습니다.

acsks=1:
- 리더 broker가 메시지를 받으면 성공으로 판단합니다.
- follower replica에 반영될 때까지는 기다리지 않습니다.
- 따라서 리더까지 전달되었다는 보장은 있지만, 리더 장애가 발생하면 유실 가능성이 있습니다.

acks=all(-1):
- 리더와 ISR(in-sync replicas)에 기록되어야 성공으로 판단합니다.
- 가장 안전한 방식이지만, 상대적으로 지연 시간이 길어질 수 있습니다. 

broker가 여러 대가 있어야 의미 있는 테스트를 할 수 있는 걸까요? 사실은 그렇지 않습니다. acks=0을 다시 읽어보면
메시지가 "broker에 저장되었는지는 보장하지 않는다"라고 하였습니다. 그렇다는 이야기는 broker한대로도 충분히 테스트가 가능할거 같습니다. 만약, 제가 원하는 테스트라고 되었다는 가정하에 말하자면, 아무래도 유실이 있을 거라 예상합니다.
하지만, 일반적인 상황에서는 아무런 변화가 없을겁니다. broker에 강제적으로 장애를 심어줘야 하지 않을까 싶습니다.

먼저  broker를 한개만 두고 실습해 보죠.
다음과 같은 시나리오로 테스트를 수행했습니다. 각 설정별로 총 50만 건의 메시지를 전송했으며, 전송은 5만 건 단위의 배치 방식으로 진행했습니다. 전체 전송량의 50%인 25만 건을 보낸 시점 이후 장애를 주입했고, 다음 배치를 전송하는 도중 브로커를 3초간 중지한 뒤 재시작했습니다. 전송 실패가 발생한 배치는 브로커 복구 후 재시도하도록 구성했습니다.

이번 테스트를 통해 acks=0과 acks=1의 정합성 차이를 직접적으로 판단하기는 어려웠습니다.
두 설정 모두 장애 구간에서 약 5만 건의 전송 실패가 발생했으며, 이후 재시도 과정에서 중복 메시지가 관찰되었습니다. 특히 acks=1의 경우, acks=0 대비 약 2~3배 더 많은 중복 메시지가 발생했습니다.
다만 이 결과만으로 acks=1이 acks=0보다 정합성을 더 잘 보장한다고 단정하기는 어렵습니다. 단일 브로커 환경에서는 복제가 존재하지 않기 때문에, acks 설정에 따른 내구성(정합성) 차이가 실제로 드러나지 않기 때문입니다.

이번 테스트를 통해 확인할 수 있었던 것은 acks 설정에 따른 정합성 차이 자체보다는, 장애 이후 재시도 과정에서 중복 메시지가 발생하는 양상이었습니다. 따라서 본 결과는 정합성에 대한 직접적인 검증이라기보다는, 단일 브로커 환경에서 acks 설정에 따라 중복 및 재시도 양상이 어떻게 나타나는지를 보여주는 참고 수준의 결과로 보는 것이 적절합니다.

계속 안정성을 언급해왔었는데 acks=1이 중복값이 많다는건 어떤 안전성을 말하고 있는걸까요?
여기서 말하고 있는 안전성이란, 유실 가능성을 말하고 있습니다.
중복 메시지의 증가는 동일한 데이터를 여러 번 처리해야 함을 의미하며, 네트워크 전송, 디스크 I/O, 컨슈머 처리량 측면에서 추가적인 비용을 발생시킬 수 있습니다. 따라서 재시도로 인해 중복 메시지가 증가하는 상황은 시스템 전체 관점에서 성능 저하 요인으로 작용할 수 있습니다. 사실 이것은 kafka의 의도적인 트레이드 오프라고 볼 수 있습니다.
중복 메시지를 늘려 유실 가능성이 줄게 된다면, 최종적으로는 최소 한 번 이상은 반드시 처리가 되어집니다. 다른 말로는 at-least-once라고 합니다.

그렇다면, acks=0에서 중복데이터가 발생한것은 무엇때문일까요?

acks=0에서 중복 데이터가 발생한 이유는, 장애가 발생한 시점에 일부 메시지가 이미 브로커에 전달되었을 가능성이 있기 때문입니다.

그림을 그려봅시다.

producer가 broker쪽으로 메시지를 보냅니다. 여기에 만약 acks=0인 상태라면

broker에 확인하지 않고 지나갑니다. producer가 해당 메시지가 실제로 broker에 정상 반영되었는지 확인하지 못한 채 다음 전송으로 넘어간다는 뜻입니다.
만약 broker가 중지된 상태라면, producer broker로 메시지를 정상적으로 전달하지 못할 수 있습니다. 이후 broker 다시 활성화되더라도, producer 입장에서는 broker 장애 시점에 해당 메시지가 실제로 전송되었는지, 또는 전송되지 않았는지를 정확히 알 수 없습니다.

이처럼 저장 여부가 불확실한 상태에서는, 유실을 방지하기 위해 동일한 메시지 또는 동일한 배치를 다시 전송하게 됩니다.
그 결과, 장애 직전에 이미 일부 저장된 메시지까지 함께 재전송되면서 중복 메시지가 발생할 수 있습니다.

이렇게 하면 단일 브로커에서의 acks동작 결과에 대해 이해를 해보았습니다. 

하지만 Kafka는 브로커를 단일로만 구성하는 시스템이 아닙니다. 필요에 따라 여러 대의 브로커로 클러스터를 구성할 수 있으며, 이 환경에서 acks=1과 acks=all의 차이가 보다 분명하게 드러납니다. 단일 브로커 환경에서는 acks=1acks=all 모두 사실상 리더 브로커 1대의 응답만 기준이 되기 때문에, 두 설정의 차이를 충분히 확인하기 어렵습니다. 반면 다중 브로커 환경에서는 리더 외에 복제본(replica)이 함께 존재하므로, acks=1은 리더 브로커의 저장만 확인한 뒤 응답하지만, acks=all은 in-sync replica(ISR)까지 반영된 이후 응답하게 됩니다.

따라서 다중 브로커 환경에서는 acks=1과 acks=all이 단순한 응답 속도 차이를 넘어, 장애 상황에서 데이터 유실 가능성과 정합성 보장 수준에 어떤 차이를 만드는지를 보다 명확하게 이해할 수 있습니다.

이제 acks=1 vs acks=all을 비교해보자

두 개 모두 replica라는 용어가 등장합니다. replica는 메인 브로커가 아닌 서브 브로커라고 생각하시면 됩니다.
그렇다면, 어떤 점이 다를까요? 저는 이번 테스트를 위해 kafka를 2대를 더 설치하기로 하였습니다.

대략 요런 느낌이 되는거죠. 
테스트는 2가지로 진행이 되어질 예정입니다.

정상적으로 동작하였을때, 어떤 방법이 더 빠른지, 안정적인지 점검해봅시다.

메시지는 5000건으로 진행하였습니다.

5회 평균에서도 acks=0이 가장 빠르고, acks=all이 가장 느렸다. 하지만 내구성 차이는 장애 상황에서 따로 확인해야 한다.

이번 결과는 acks=0이 Throughput이 높다는 의미라기보다는,
브로커의 응답을 기다리지 않는 방식으로 동작하기 때문에 producer 관점에서의 latency가 낮게 나타난 것으로 해석할 수 있습니다.

패킷 수를 비교해보면 acks=0은 약 7,000개 수준인 반면, acks=1 및 acks=all은 약 16,000개로 약 2.5배 더 많은 패킷이 발생했습니다.

이는 acks=1과 acks=all이 브로커의 응답을 포함한 request-response 구조로 동작하기 때문에,
왕복 통신 및 추가적인 네트워크 비용이 발생한 결과로 볼 수 있습니다. 즉, acks 설정에 따른 차이는 단순한 속도 차이가 아니라,
응답 대기 여부에 따른 latency와 네트워크 비용의 트레이드오프로 해석하는 것이 적절합니다.

그렇다면, 장애 상황에서는 어떻게 변화할까요?

레플리카 broker을 사용하는 acks는 1과 all입니다. 그렇다는건 레플리카를 사용하지 않는 acks=0은 어떻게 동작이 되어질까요?
한번 확인해봅시다.

테스트는 leader가 죽었을때와 follow가 죽었을때로 비교가 되어집니다.

리더가 죽게되어지면 wireshark를 통해 다음과 같이 보여집니다.

이렇게 발생하는 이유는 producer입장에서는 기존 리더가 죽었는지 모르는 상황이라는 뜻입니다. 그래서 기존리더에게 요청을 계속 보내는 것을 확인 할 수 있습니다. 메시지는 3만건씩을 발송하였습니다.

leader가 죽으면 acks=0은 빠르지만 큰 유실이 생기고, acks=1은 거의 유지되며, acks=all은 느리지만 이번 5회에서는 전량 보존됐다

놀랍게도 acks=0은 11,800건이 유실이 되었으며, 약 1/3정도가 유실이 되었지만 latency는 여전히 가장 빠른것을 확인 할 수 있었습니다.
acks=1도 약간의 유실이 있긴하지만, 나름 괜찮은 대안이 된다고 여겨집니다.

그렇다면 replica에 장애가 발생한다면 어떻게 되어질까요?

요약: follower 1대가 죽는 조건에서는 acks=0, 1, all 모두 전량 적재됐다. 차이는 주로 속도에서만 나타났다

리더와 달리 follow에 장애가 발생했을때는 유실이 되어지는것이 존재하지 않았습니다. 하지만 평균 실행 속도가 1초가량 늘었다는것을 확인할 수 있었습니다.
이걸로 kafka의 성능은 leader의 장애 여부에 따라 차이가 존재한다는것을 확인할 수 있었습니다.

마무리

원래 producer쪽 전체를 전부 작성하고 싶었지만 ack하나만 적었을 뿐인데도 생각보다 길게 적게 되었네요. 정상 상태에서는 acks=0가 가장 빠르지만, leader 장애까지 고려하면 acks=all이 가장 안전하였습니다. 또한, acks=1은 성능과 내구성 사이의 중간 지점이지만, leader 장애에서 완전 무손실을 보장하지는 못한다는것을 확인하였습니다.

참고로 테스트는 kcat과 tcpdump를 이용해 ai와 함께 테스트 하였습니다.

반응형

댓글

Designed by JB FACTORY