Apache Kafka에서 메세지 전달 보장 수준 설정하기
메세징 시스템을 다룬 글을 읽을 때 “At least once”, “At most once”, “Exactly once” 와 같은 표현들을 읽은 적이 있을 것이다. 메세지가 전달되는 방식을 설명하는 용어들인데, 각각이 어떤 의미인지 그리고 카프카에서는 각각의 방식을 어떻게 지원하는지 살펴보자.
메세지 전달 보장 수준 (Messaging Semantic)
메세지 전달 보장 수준에는 보통 다음과 같은 3가지 수준이 존재한다.
- At most once: 메시지가 최대 한 번 전달됨을 보장 (누락 O)
- At least once: 메시지가 최소 한 번 전달됨을 보장 (누락 X, 중복 O)
- Exactly once: 메시지가 정확히 한 번 전달됨을 보장 (누락 X, 중복 X)
위에서 아래로 갈수록 데이터 무결성(data integrity)는 높아지지만, 트랜잭션 관리나 중복 감지 등의 비용이 들기 때문에 성능이 떨어질 수 있다. 그렇기 때문에 무조건 높은 수준을 사용해야하는 것이 아니라 애플리케이션의 특성에 따라서 적절한 전달 보장 수준을 골라야한다.
예를 들면, At most once는 누락되어도 괜찮은 로그 수집이나 IoT 애플리케이션에서 센서 데이터를 수집할 때 사용될 수 있다. At least once는 메시지 유실이 발생하지 않아야 하는 분석용 데이터나 스트리밍 이벤트와 같은 경우에 적합하다. Exactly once의 경우에는 정확히 한번만 전달되어야하는 금융 데이터, 과금에 사용하는 액세스 로그 등을 전달하는데 사용될 수 있다.
카프카의 메세지 전달 보장 수준
원래 링크드인에서 카프카를 만들 때는 링크드인 웹사이트에서 생성되는 로그를 활용해 웹 사이트 활동을 추적하기 위한 용도로 카프카를 사용했다. 그래서 처리량이 높고, 데이터의 중복이 조금 있더라도 메세지를 잃지 않는 At least once 메세징 시스템을 구축하는 것이 중요했다.
그러다가, 카프카가 오픈소스가 되고 활용처가 많아지면서 더 높은 메세지 전달 보장 수준을 지원할 필요성이 생겼다. 그래서 현재의 카프카는 세 가지 전달 보장 수준을 모두 설정해줄 수 있다.
전달 보장 수준을 설정한다는 말은 producer, broker, consumer 가 모두 해당 방식으로 메세지를 전달하기로 합의되었다는 의미이다. 메세지를 가지고 있는 브로커가 한 개라도 살아있다면 설정한 수준이 보장된다.
그럼 이제 본격적으로 각각의 설정 방법을 알아보자. 프로듀서 측면과 컨슈머 측면에서 어떻게 전송 보장을 하는지 각각 살펴봐야한다.
No guarantee
Kafka 2.8 이하를 기준으로 한 설명이다. 아래 전달 보장 수준들에서 하나씩 설정들을 추가해나가는 것을 보여주기 위해 해당 버전을 기준으로 설명했다. 3.0 부터는
acks=all
,enable.idempotence=true
가 디폴트 설정이다.
따로 아무 설정도 하지 않고 카프카를 이용했을 때는 메세지가 유실될 수도 있고 중복 메세지가 발생(혹은 중복 처리가 발생)할 수도 있다. 왜 그런 현상이 나타나는지 먼저 프로듀서 측면에서 살펴보자.
카프카 프로듀서의 기본 acks 설정은 1이다. 그래서, 메세지가 유실되는 경우는 없다고 생각할 수 있지만 acks=1인 경우에도 메세지 유실은 발생할 수 있다. 예를 들어, 파티션 리더가 프로듀서의 메세지를 받고 acks 응답을 준 후에 follower로 복제하기 전에 다운될 수 있다. 그러면 해당 메세지는 acks 응답을 주었음에도 유실된다.
중복 메세지가 발생하는지 여부는 프로듀서의 retry 횟수 설정에 따라 다르다. retry를 하도록 설정되어있으면 중복 메세지가 발생할 수 있다. 특히 기본 설정에서는 멱등성 기능이 비활성화되어 있어서, 동일한 메시지가 여러 번 기록될 수 있다.
다음으로 컨슈머 측면을 살펴보자.
컨슈머는 오프셋 커밋 방식이 기본적으로 enable.auto.commit=true
로 설정되어 있기 때문에 주기적으로 오프셋이 자동으로 커밋된다. 그래서, 예를 들어 자동 커밋 설정이 5초인데, 컨슈머가 메세지를 처리하는 과정이 5초를 초과하면 메세지 처리가 완료되기 전에 오프셋 커밋이 일어나게 된다. 만약, 실제로 커밋 이후 해당 메세지 처리 과정에서 에러가 발생했으면 유실이 일어나게 된다.
또한, 컨슈머가 메세지를 처리한 후 커밋하기 전에 장애가 발생하여 다운되면, 컨슈머가 재시작될 때 이전 오프셋부터 메세지를 다시 읽기 때문에 중복 처리가 될 수 있다.
At most once
그러면 어떻게 설정을 바꾸면 메세지가 0번 혹은 1번만 처리되도록 보장해줄 수 있을까?
프로듀서 측면에서는 메세지 중복 발행만 안하면 된다. 메세지를 한 번만 보내고 브로커가 메세지를 잘 받았는지 안받았는지는 신경쓰지 않아도 된다. 그래서 retries=0
으로 설정해주고, acks=all
이 아닌 경우에는 At most once라고 볼 수 있다.
컨슈머 측면에서는 위에 설명한 이유로 자동 커밋이 활성화되어 있으면 중복 처리를 막기는 어렵다. 중복 처리를 반드시 막아야하는 상황이라면 수동 커밋(enable.auto.commit=false
)으로 메세지를 처리하기 전에 오프셋을 커밋하는 방법이 있다. 이렇게 하면 컨슈머가 재시작되어도 이미 해당 오프셋은 커밋되었기 때문에 누락은 발생해도 재처리는 일어나지 않는다.
At least once
이번에는 가장 자주 쓰이는 설정인 At least once를 살펴보자.
먼저, 프로듀서 측면에서는 메세지가 누락되면 안된다. 그렇기 때문에 acks=all
로 모든 Follower들에게 복제가 잘 이루어졌음을 보장해야한다. 프로듀서는 메세지가 커밋되었다는 응답(ack)을 받지 못하면 메세지를 재전송한다. 그렇기 때문에 메세지는 한 번 이상 보내질 수 있다.
컨슈머 측면을 보면, 자동 커밋인 경우에는 누락이나 중복 처리가 발생할 수 있다. 그래서 At most once 의 경우와 유사하게 수동 커밋으로 바꾸되, 메세지를 처리한 후에 오프셋을 커밋하는 방법이 있다. 이렇게 하면 컨슈머가 오프셋을 업데이트하지 못하고 죽은 경우에는 다른 컨슈머 프로세스가 이전 오프셋부터 읽기 때문에 해당 메세지가 다시 처리될 수 있다. 중복처리가 되긴 하지만 유실이 되는 경우는 없기 때문에 At least once라고 볼 수 있다.
Exactly once
마지막으로 가장 높은 수준인 Exactly Once를 보장하기 위한 방법을 알아보자.
프로듀서는 At least once와 마찬가지로 acks=all
설정을 해주어 누락이 없게 해주어야한다. 그리고, 메세지의 중복도 없어야하기 때문에 enable.idempotence=true
로 설정하여 멱등성을 보장해야한다. 멱등성 설정을 해주게 되면 프로듀서가 브로커에게 ProducerId와 시퀀스 번호를 전달하게 되고 브로커가 이를 바탕으로 중복을 제거하게 된다.
또한, 트랜잭션 처리가 되어야하기 때문에 transactional.id
를 지정해줘야한다. 이를 지정하면 프로듀서가 여러 파티션에 프로듀스 할 때 Exactly Once 하도록 보장한다. 트랜잭션 처리가 왜 되어야하는지는 아래 컨슈머 측면에서 더 알아보자.
컨슈머 측면은 조금 복잡하다. 수동 커밋을 사용해도 유실만 막거나 중복처리만 막을 수 있지 무조건 1번만 처리되도록 보장하기는 어렵다.
그래서 트랜잭션 설정이 들어가게 된 것이다. 프로듀스를 하고 트랜잭션이 커밋되는 것까지 완료되는 것을 하나의 트랜잭션으로 보고 원자적인 처리를 해줘야하는 것이다. 그래서 수동 커밋을 하고 isolation.level=read_committed
로 설정해줘야한다. read_committed
이어야지 트랜잭션이 커밋된 후에만 메세지가 컨슈머에게 노출이 되어서 중복 메세지나 트랜잭션이 abort 된 메세지를 읽지 않게 된다.
트랜잭션에 대한 더 자세한 내용은 해당 Confluent 영상을 참고하면 좋다.
정리
프로듀서 측 설정
보장 수준 | 설정 | 설명 |
---|---|---|
At most once | - acks=0 - retries=0 | 메시지의 중복 발행을 방지하며, 브로커가 메시지를 수신했는지 확인하지 않음. |
At least once | - acks=all | 메시지 누락을 방지하며, 메시지를 한 번 이상 전송할 수 있음. |
Exactly once | - acks=all - enable.idempotence=true - transactional.id | 멱등성 보장 및 트랜잭션을 사용하여 메시지의 중복 발행과 누락을 방지함. |
컨슈머 측 설정
보장 수준 | 설정 | 설명 |
---|---|---|
At most once | - enable.auto.commit=false - 메세지를 처리하기 전에 오프셋을 커밋함. | 메시지를 처리하지 못할 경우, 해당 메시지가 누락될 수 있음. |
At least once | - enable.auto.commit=false - 메시지를 처리한 후 오프셋을 커밋함. | 오프셋을 업데이트하지 못한 경우, 메시지 중복 처리가 발생할 수 있음. |
Exactly once | - enable.auto.commit=false - isolation.level=read_committed - 트랜잭션 커밋된 메시지만 읽음. | 메시지의 중복 처리와 유실을 방지하며, 커밋된 메시지만 소비함. |
참고
「실전 아파치 카프카」
Confluent Kafka Document: Message Delivery Guarantees
Kafka Document
Exactly-Once Semantics Are Possible: Here’s How Kafka Does It
Processing guarantees in Kafka