Kafka 에서 Exactly once 를 지원한다고 들었고 정확히 어떤 기능인지 찾아보았다.
- https://docs.spring.io/spring-kafka/reference/kafka/exactly-once
- https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/
- https://www.uber.com/at/en/blog/money-scale-strong-data
결론적으론 생각보다 많이 달랐다.
내가 생각하는 Exactly once 란 메세지 A 를 정확히 1번 publish -> 컨슈머가 정확히 1번 consume 어떤 메세지가 한 번 소비되는 것 인데, 그게 아닌 메세지 A offset commit + topic B 에 메세지 publish 와 같이 카프카 내부 트랜젝션을 하나로 묶는 기능이였다. 그래서 예를 들어 message A 처리 결과를 topic B, C 에 쓴다라고 했을 떄 message 를 원자단위로 잘 묶어주는 기능이다.
그래서 Exactly Once 메세지를 정확히 한 번 소비를 만족하기 위해선 최종적으로 옵션딸깍으로 되는게 아닌 아래와 같이 설계해야한다.
- idempotent 옵션(redis stream 의 경우 xadd 에 옵션이 있다.)로 메세지 중복 publishing 을 막아야하고
- consume 시 비즈니스 로직에 따라 멱등을 보장해야한다.
- 비즈니스 로직이 DB 쓰기작업의 경우 로직수행 + event-id unique key insert tx 해놓으면 이후 동일 event-id 으로 로직수행 시 key validation 에러나서 중복소비를 막아준다. 트랜젝션 롤백일어나면 뭐 없던일이 되니까 재수행하면 된다.
- 마찬가지 외부 API 호출이면 DB 에 event-id 랑 같이
SENDING,SUCCESS,FAILED,UNKNOWN로 개별 commit 해서 DB 저장. 네트워크 연결유실이 흔하니까 polling batch 해서 보정처리한다.
이후 로직 완료하고 메세지 ACK commit 하면 된다.
What Does Kafka Exactly Once Semantics Guarantee?
I heard that Kafka supports Exactly Once, so I looked into what it actually means.
- https://docs.spring.io/spring-kafka/reference/kafka/exactly-once
- https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/
- https://www.uber.com/at/en/blog/money-scale-strong-data
In the end, it was quite different from what I expected.
What I thought Exactly Once meant was publish message A exactly once -> consumer consumes it exactly once, meaning a message is consumed only once. But that is not what it means.
It is closer to tying Kafka’s internal transaction together, such as message A offset commit + publishing a message to topic B.
For example, if the processing result of message A is written to topic B and C, Kafka helps bind those messages atomically.
So to actually satisfy “consume a message exactly once”, it is not something that works by simply toggling an option. It has to be designed like this.
- Prevent duplicate publishing with an idempotent option. In Redis Stream’s case, there is an option on
XADD. - Guarantee idempotency during consume depending on the business logic.
- If the business logic is a DB write, run the logic and insert an
event-idunique key in the same transaction. Then if the sameevent-idis processed again, key validation fails and duplicate consumption is blocked. If the transaction rolls back, nothing happened, so it can be retried. - Similarly, for external API calls, store the
event-idin DB with states likeSENDING,SUCCESS,FAILED, andUNKNOWNthrough separate commits. Since network connection loss is common, correct the state later through polling batches.
- If the business logic is a DB write, run the logic and insert an
After the logic is completed, commit the message ACK.