이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Kafka topic, partition, consumer group은 각각 어떤 설계 결정을 표현하는가?
- partition key가 순서, 병렬성, skew, 장애 범위를 어떻게 바꾸는가?
- consumer group을 서비스별로 나눌 때와 같은 group으로 묶을 때의 차이는 무엇인가?
- Spring Kafka listener concurrency를 partition 수와 어떻게 맞춰야 하는가?
개요
topic은 이벤트의 논리적 이름이다.
partition은 topic 내부의 물리적 log 조각이며, 순서와 병렬성의 단위다.
consumer group은 같은 목적의 consumer들이 partition을 나눠 읽는 실행 단위다.
Kafka 설계 리뷰에서 가장 먼저 봐야 하는 것은 topic 이름이 아니라 partition key다.
partition key가 잘못되면 순서가 깨지거나, 특정 partition에만 부하가 몰리거나, consumer를 늘려도 처리량이 늘지 않는다.
세 가지 역할
topic: order.events
partition-0: key=order-1, order-4, order-8
partition-1: key=order-2, order-5
partition-2: key=order-3, order-6, order-7
consumer group: settlement-service
consumer-a -> partition-0
consumer-b -> partition-1
consumer-c -> partition-2topic은 “무슨 event인가”를 표현한다.
partition은 “어떤 key 단위로 순서와 병렬성을 나눌 것인가”를 표현한다.
consumer group은 “누가 같은 업무 목적을 나눠 처리하는가”를 표현한다.
같은 topic을 배송 서비스와 분석 서비스가 읽는다면 서로 다른 group을 사용한다.
같은 정산 서비스를 3개 인스턴스로 띄워 처리량을 나누려면 같은 group을 사용한다.
Partition Key
partition key는 순서를 보장하고 싶은 business 단위로 고른다.
주문 상태 변경 순서가 중요하면 orderId가 자연스럽다.
사용자 활동 순서가 중요하면 userId가 자연스럽다.
가맹점 정산 순서가 중요하면 merchantId가 후보가 된다.
하지만 key 선택에는 항상 trade-off가 있다.
orderId: 주문별 순서는 안전하지만 한 사용자의 여러 주문 흐름은 흩어진다.userId: 사용자별 순서는 안전하지만 heavy user가 partition skew를 만들 수 있다.merchantId: 정산 묶음에는 좋지만 대형 가맹점 하나가 병목이 될 수 있다.- random key: 분산은 좋지만 business 순서를 설명하기 어렵다.
순서 기준이 없는 event라면 key를 무작위로 둘 수도 있다.
그러나 결제, 취소, 환불처럼 상태 전이가 있는 event는 key 기준을 반드시 문서화해야 한다.
Partition 수
partition 수는 최대 병렬성의 상한이다.
topic partition이 3개라면 같은 consumer group 안에서 동시에 일을 나눌 수 있는 consumer도 최대 3개다.
consumer를 5개 띄워도 2개는 할당받을 partition이 없다.
partition count = 3
consumer instances = 5
active consumers = 3
idle consumers = 2partition 수는 나중에 늘릴 수 있지만, 늘린 뒤에는 key 분포와 순서 기대가 바뀔 수 있다.
특히 producer가 hash partitioner를 사용한다면 partition 수 변경은 같은 key가 앞으로 다른 partition에 배정될 가능성을 만든다.
기존 key의 절대 순서 보장이 중요하면 partition 확장 전략을 더 조심해야 한다.
Consumer Group
consumer group은 offset을 공유한다.
같은 group 안의 consumer들은 partition을 나눠 맡는다.
다른 group은 같은 topic을 별도의 offset으로 읽는다.
order.events
settlement-service group -> 정산 DB 반영
notification-service group -> 알림 발송
analytics-loader group -> warehouse 적재각 group은 서로 다른 SLA와 장애 기준을 가진다.
정산 consumer lag가 커졌다고 알림 consumer가 늦는 것은 아니다.
운영 대시보드도 topic 단위가 아니라 group 단위 lag를 보여줘야 한다.
Spring Kafka Concurrency
Spring Kafka에서 concurrency는 같은 listener container 안의 consumer thread 수를 의미한다.
@KafkaListener(
topics = "order.events",
groupId = "settlement-service",
concurrency = "3"
)
public void consume(OrderPaidEvent event) {
settlementService.apply(event);
}concurrency가 partition 수보다 커도 처리량은 partition 수를 넘지 못한다.
반대로 partition 수가 12인데 concurrency가 1이면 한 인스턴스는 한 consumer로 모든 partition을 순차적으로 poll한다.
운영에서는 partition 수, 인스턴스 수, concurrency를 함께 계산한다.
effective consumers = instance count * listener concurrency
parallelism upper bound = min(partition count, effective consumers)Rebalance
consumer가 추가되거나 제거되면 group 안에서 partition 할당이 다시 이뤄진다.
이를 rebalance라고 한다.
rebalance 동안 일부 partition 처리가 잠시 멈출 수 있다.
처리 시간이 긴 listener, 너무 작은 session timeout, 잦은 배포는 rebalance 증가와 lag 증가를 함께 만든다.
그래서 consumer 코드는 하나의 record를 오래 붙잡지 않도록 설계해야 한다.
무거운 외부 API 호출이 필요하면 timeout, bulkhead, retry 분리를 고려한다.
실패 모델
- key 없이 round-robin으로 보내고 주문별 순서를 기대한다.
- partition 수보다 많은 consumer를 띄운 뒤 처리량이 늘 것으로 기대한다.
- 모든 서비스를 같은 consumer group으로 묶어 서로 메시지를 나눠 먹게 만든다.
- consumer group별 lag를 보지 않고 topic lag만 본다.
- 특정 key에 트래픽이 몰리는데 partition skew를 측정하지 않는다.
이 문제들은 API 레벨 테스트로 잘 드러나지 않는다.
event key 분포와 consumer group offset을 봐야 발견된다.
운영 지표
- records lag by group/partition: 어느 group의 어느 partition이 뒤처졌는지 본다.
- partition bytes in/out: 특정 partition에 트래픽이 몰리는지 본다.
- rebalance count: 배포나 consumer 불안정으로 할당이 자주 바뀌는지 본다.
- consumer processing time: poll 이후 업무 처리 시간이 max poll interval에 가까운지 본다.
- idle consumer count: partition보다 consumer가 많아 놀고 있는 인스턴스가 있는지 본다.
지표는 반드시 group과 partition label을 포함해야 한다.
평균 lag만 보면 hot partition 하나를 놓치기 쉽다.
개인 프로젝트 기준
작은 프로젝트에서는 topic을 많이 만들기보다 하나의 event 흐름을 제대로 설계한다.
예를 들어 order.events topic 하나에 orderId key를 사용하고, settlement-service와 notification-service group을 분리해 본다.
테스트는 다음 세 가지면 충분하다.
- 같은
orderId의OrderPaid,OrderCanceled가 같은 partition으로 들어가는지 확인한다. - consumer 인스턴스를 늘렸을 때 partition 할당이 어떻게 바뀌는지 확인한다.
- 한 group의 consumer를 중지해도 다른 group은 계속 읽는지 확인한다.
확인 질문
확인 질문
- 이 topic의 partition key는 어떤 business 순서를 보장하기 위해 선택했는가?
- 답이 “분산을 위해서”뿐이라면 순서 요구를 다시 확인해야 한다.
- consumer group은 서비스 목적별로 분리되어 있는가?
- 서로 다른 업무가 같은 group을 쓰면 메시지를 나눠 먹어 누락처럼 보일 수 있다.
- concurrency를 늘렸을 때 실제 처리량이 늘 수 있는 partition 수인가?
- partition 수가 병렬성 상한이라는 점을 함께 계산해야 한다.
참고 문서
- Apache Kafka Documentation
- Spring for Apache Kafka Reference
- Martin Kleppmann, Designing Data-Intensive Applications