본문 바로가기

빅데이터/Kafka

아파치 카프카 Exactly-once 처리의 진실과 거짓

아파치 카프카와 같은 분산 이벤트 스트리밍 플랫폼을 사용하거나 메시지 브로커를 활용하다 보면 항상 마주치는 문제는 마로 메시지 전달 시멘틱(message delivery semantic)입니다. 메시지 전달 시멘틱은 A지점에서 B지점으로 데이터를 전송할 때 어느 만큼의 신뢰도로 데이터를 전송하는지에 대한 정의입니다. 즉, 특정한 장애 상황(또는 임계치를 벗어난 상황)에서도 보증하는 데이터 전달 신뢰도라고 볼 수 있습니다.

 

메시지 전달 시멘틱은 크게 세가지로 나뉩니다. 적어도 한번(at least once), 많아도 한번(at most once), 정확히 한번(exactly once). '적어도 한번'은 데이터가 전달될 때 유실이 발생하지는 않지만 중복이 발생할 가능성이 있음을 뜻합니다. 아파치 카프카는 기본 옵션으로 사용할 경우 '적어도 한번'을 보장합니다. '많아도 한번'은 최대 한번의 메시지를 보내고 일부 데이터는 유실될 수 있음을 뜻합니다. 마지막으로 '정확히 한번'은 A에서 B로 데이터를 보낼 때 어떠한 경우에서도 유실이나 중복이 발생하지 않는 것을 뜻합니다. 

 

'정확히 한번' 처리에서 중요한 것은 '어떠한 경우'입니다. '어떠한 경우'라는 것은 다양한 상황을 뜻할 수 있는데요. 데이터 센터가 셧다운 되는 끔찍한 상황도 그 경우에 포함될 수 있고 일반적으로 자주 일어날 수 있는 네트워크 순단 현상도 포함될 수 있습니다. 즉, 엔지니어가 모르는 사이 어떠한 일이 있더라도 데이터는 단 한번만 처리되는 것을 뜻하는 것입니다.

 

그렇다면 반대로 '적어도 한번'과 '많아도 한번'은 항상 평소에도 중복/유실이 발생하는 것을 뜻하는 걸까요? 아파치 카프카를 예로 말씀드리자면 '아니다'라고 말씀드릴 수 있습니다. 아파치 카프카를 기본 옵션으로 사용할 경우 '적어도 한번'을 보장한다는 것은 '어떠한 경우'가 발생하는 경우에 대응하여 말하는 것입니다. 즉, 아파치 카프카를 사용하는 상황에서 '어떠한 경우'가 발생하지 않는다면 기본적으로는 '정확히 한번'을 수행합니다.

 

카프카에서 '어떠한 경우'가 생기는 것의 예는 네트워크상 이슈 또는 애플리케이션 이슈를 예로 들 수 있습니다. 프로듀서 입장에서 브로커에 레코드가 정상적으로 저장되더라도 ack가 유실되면 데이터가 적재되지 못했다고 생각해서 retry를 하게 되어 데이터의 중복 전달이 발생할 수 있습니다. 컨슈머 입장에서는 데이터를 안전하게 처리했음에도 불구하고 애플리케이션 장애로 인해서 commit을 못하게 되면 이전 레코드를 중복처리할 수 있는 경우가 발생할 수 있습니다.

 

이런 '어떠한 경우'가 발생하더라도 데이터 처리의 중복/유실을 방지 위해 아파치 카프카는 많은 노력을 기울여 왔습니다. 크게 두가지가 있는데요. 첫번째는 멱등성 프로듀서(idempotence producer)이고 두번째는 트랜잭션 프로듀서/컨슈머(transaction producer/consumer)입니다. 멱등성 프로듀서는 네트워크 장애가 발생하더라도 브로커에 단 하나의 레코드만 보낼 수 있도록 보장하여 '정확히 한번'을 달성하였습니다. 트랜잭션 프로듀서/컨슈머는 여러 레코드를 원자(atomic) 단위로 데이터를 처리할 수 있도록 설계되었는데, 여기에 추가적으로 컨슈머의 커밋을 트랜잭션에 포함시키는 기능(sendOffsetsToTransaction)을 가지고 있습니다. 이를 통해 A토픽->컨슈머->데이터 처리->프로듀서->B토픽 으로 이어지는 파이프라인에서 '정확히 한번'을 달성할 수 있게 되었습니다.

 

위 경우를 보면 알겠지만 프로듀서->토픽 또는 토픽->컨슈머->프로듀서->토픽 에 대해서 '정확히 한번'을 달성하였다는 사실이 있지만 우리가 흔히 사용하게 되는 토픽->컨슈머 는 '정확히 한번' 달성에 대한 이야기가 없습니다. 왜냐면 컨슈머가 레코드를 토픽에서 가져가서 처리한 뒤 중복처리를 막기 위해 오프셋 저장을 수행하는데, 오프셋 저장을 특정 처리와 함께 트랜잭션으로 묶는 것이 어렵기 때문입니다. 예를 들어 토픽의 레코드들을 데이터베이스에 데이터를 저장한다고 가정할 경우 데이터 insert와 commit offset을 하나의 트랜잭션으로 묶는 것은 불가능합니다. 서로 연동되는 서비스가 아니기 때문이죠. 그렇기 때문에 토픽->컨슈머 처리에서 '정확히 한번'을 달성하기 위해서는 멱등성(idempotence) 처리를 하도록 가이드합니다. 멱등성이라는 것은 동일한 명령이 여러번 가더라도 결과물은 동일한 것을 뜻합니다. 컨슈머가 멱등성 특징을 가지게 하기 위해서는 관련 데이터베이스에서 해당 기능을 지원해야 합니다. 예를 들어 동일한 레코드가 중복 적재될 때는 결과가 하나만 나올 수 있도록 유니크 키 같은 것을 지원한다면 이 문제가 쉽게 해소될 수 있습니다. 이를 통해 토픽->컨슈머까지 '정확히 한번' 달성을 수행할 수 있게 됩니다.

마무리

카프카 뿐만 아니라 플링크, 스파크 등 많은 스트리밍 플랫폼을 사용할 때 간과하는 것이 메시지 전달 시멘틱입니다. 또한 어떻게 동작하는지 모르는 상태에서 단순히 exactly_once=true 옵션을 적용하고 사용하는 경우가 많습니다. 비즈니스 요구사항에 따라 어떤 동작을 할지 정의하고 우리가 사용하는 플랫폼이 어떤 동작을 할지 결정하고 내부적으로 어떻게 동작하는지 명확히 파악하고 사용하는 것이 중요합니다. 그렇기 때문에 관련 옵션이나 오픈소스 도큐먼트에 적혀있는 내용이 모든 것을 커버한다고 과장해서 생각하면 추후 운영시 이슈가 발생할 확률이 커질 수 있습니다.

 

마지막으로 카프카를 사용할 때, '정확히 한번'을 달성하기 위한 방법을 알려드립니다. 크게 3단계에 대해 고려해야 하는데요. 프로듀서->토픽, 토픽->컨슈머->프로듀서->토픽, 토픽->컨슈머 입니다. '어떠한 경우'가 발생하지 않는 이상 데이터의 중복/유실이 발생하지 않겠지만 데이터 처리가 중요한 기업에서는 중복/유실이 크리티컬 할 수 있습니다. 이에 대응하여 아키텍처를 구성하기 위해서는 각각 다음과 같은 기술을 사용해야 합니다.

 

- 프로듀서->토픽 : 멱등성 프로듀서

- 토픽->컨슈머->프로듀서->토픽 : 트랜잭션 프로듀서/컨슈머

- 토픽->컨슈머 : 멱등성 처리

 

아무쪼록 카프카를 활용하면서 메시지 전달 처리를 고민하는데 많은 도움이 되었으면 좋겠습니다.

 

P.S.

- https://issues.apache.org/jira/browse/KAFKA-8587

 

[KAFKA-8587] One producer per thread for Kafka Streams EOS - ASF JIRA

Currently exactly-once producer is coupled with individual input partitions. This is not a well scaled solution for client application such as Kafka Streams, and the root cause is that EOS producer doesn't understand the topic partition move throughout con

issues.apache.org

- https://www.confluent.io/blog/simplified-robust-exactly-one-semantics-in-kafka-2-5/

 

Apache Kafka’s Exactly-Once Semantics Are Now Easier & More Robust

Kafka 2.5 brings important improvements to exactly-once semantics: enhanced producer error handling and simplified transactional APIs for more powerful, simplified production use cases.

www.confluent.io

 

반응형