티스토리 뷰

컨슈머를 4개 두었지만, 다른 세 파티션의 lag가 0이 된 뒤에도 한 파티션의 lag는 남아 있었습니다. 같은 주문 데이터 1만 건을 보내도 메시지 키에 따라 이런 차이가 나타났습니다.

주문이 확정되면 이벤트를 발행하고, 컨슈머가 이를 읽어 구매 활동을 저장하도록 구현했습니다. 이때 메시지 키로는 orderId를 사용했습니다.

같은 인기 상품을 주문해도 주문 ID는 각각 다릅니다. 반면 상품 ID를 키로 사용하면 같은 상품의 주문은 같은 키를 갖게 됩니다. 주문 ID와 상품 ID를 키로 사용했을 때 파티션 분포에는 어떤 차이가 생길까요?

이를 확인하기 위해 같은 실험 데이터를 orderId와 productId를 키로 사용해 각각 발행하고, 로컬 Kafka에서 파티션 분포와 전체 소비 완료 시간을 비교했습니다.

왜 주문 ID와 상품 ID를 비교했을까요?

현재 주문 확정 이벤트는 주문 단위입니다. 주문 ID와 상품 목록을 담은 메시지 한 건을 발행하고, 주문 ID를 키로 사용합니다.

kafka.send(topic, Long.toString(event.orderId()), event);

인기 상품 A를 주문한 두 주문의 ID가 101과 102라면 메시지 키도 각각 101과 102입니다. 같은 상품을 주문했어도 키는 다릅니다.

 

반면 상품별 재고 변화를 상품마다 별도 메시지로 발행한다면 어떨까요? 이때 상품 ID를 키로 쓰면 상품 A에 관한 메시지는 같은 키를 갖게 됩니다. 이는 현재 주문 이벤트와 다른 가상의 설계입니다.

두 경우 모두 같은 인기 상품에 대한 메시지지만, 주문을 기준으로 묶을지, 상품을 기준으로 묶을지에 따라 분포가 달라질 수 있습니다.

메시지 키는 어디에 쓰일까요?

Kafka에서 메시지를 보내는 쪽을 프로듀서, 읽는 쪽을 컨슈머라고 부릅니다. 프로듀서가 메시지를 발행하는 토픽은 하나 이상의 파티션으로 나뉩니다. 메시지는 파티션 안에 순서대로 기록됩니다.

프로듀서는 메시지와 함께 보낸 키로 파티션을 선택할 수 있습니다. 이번 실험의 기본 분배 방식에서는 파티션을 직접 지정하지 않고 파티션 수와 설정이 유지되면, 같은 바이트로 표현된 키는 같은 파티션으로 갑니다. 서로 다른 키도 같은 파티션에 배정될 수 있습니다.

 

상품 ID를 키로 쓴다면 같은 상품에 관한 메시지가 한 파티션에 모입니다. 그 상품의 주문이 유독 많아지면 어떻게 될까요? 직접 확인해봤습니다.

실험 환경과 확인할 것

같은 데이터를 두 가지 키로 보내며 다음을 비교했습니다.

  1. 발행 분포: 키에 따라 메시지가 파티션별로 몇 건 기록되는지 확인합니다.
  2. 전체 소비 완료 시간: 메시지를 미리 쌓아 둔 뒤 파티션별 소비 진행 상황과 전체 메시지의 처리·커밋이 끝날 때까지의 시간을 확인합니다.
  • 로컬 Kafka 3.8.1, 단일 브로커, 토픽당 파티션 4개, 복제 계수 1
  • 주문 1만 건 중 인기 상품 7의 주문이 9천 건이며, 주문 ID는 모두 다름
  • 키별로 별도 토픽에 같은 데이터를 같은 순서로 발행
  • 소비 실험은 토픽별 별도 소비자 그룹과 컨슈머 4개를 두고 차례로 실행
  • 실제 DB 호출 대신 메시지당 3ms 지연, 최대 50건 처리 후 수동 커밋

주문 ID와 상품 ID를 하나씩 담은 별도 실험용 메시지를 사용했습니다. 실제 재고를 변경하거나 현재 프로젝트의 주문 이벤트를 상품별로 나눈 실험은 아닙니다. 발행 분포는 키별 세 번, 소비 처리는 키별 두 번 실행했습니다.

실험 1. 메시지는 어느 파티션에 기록될까요?

먼저 상품 ID를 키로 사용해 메시지를 발행하고, Kafka UI에서 파티션별 기록 건수를 확인했습니다. 아래는 같은 조건으로 다시 실행한 화면입니다. P3의 Message Count가 다른 파티션보다 훨씬 큽니다.

(화면에는 쓰기 준비를 확인하는 메시지가 파티션마다 한 건씩 포함되어 있습니다. 따라서 화면의 건수는 실험 데이터보다 각 파티션에서 한 건씩 많습니다)

 

그렇다면 주문 ID를 키로 사용했을 때와는 얼마나 차이가 날까요? 두 키의 실험 결과를 비교하기 쉽게 정리했습니다.

실제 Kafka 실행 결과에서 실험 데이터만 정리한 화면입니다.

orderId를 키로 쓴 경우 메시지는 네 파티션에 각각 약 24~25%씩 기록됐습니다. 반면 productId를 쓴 경우 전체의 93.1%인 9,310건이 P3에 모였습니다.

인기 상품 7의 메시지가 모두 P3에 배정됐고, 다른 상품의 메시지도 일부 더해진 결과입니다. 같은 입력과 순서로 세 번 실행했을 때 분포는 같았습니다. 다만 상품 7이 항상 P3에 가거나 다른 데이터에서도 같은 비율이 나온다는 뜻은 아닙니다.

기록 건수는 파티션의 끝 오프셋 차이와 발행 성공 건수를 대조했습니다. 오프셋은 파티션에서 메시지가 기록된 위치를 나타내는 번호입니다.

실험 1에서 확인한 점

같은 인기 상품 주문이라도 어떤 키를 사용하느냐에 따라 파티션 분포가 달라졌습니다.


그렇다면 P3에 쌓인 메시지는 누가 읽을까요?

P3에 몰린 메시지를 다른 컨슈머가 나눠 읽을 수 있을까요?

컨슈머 여러 개가 하나의 소비자 그룹으로 동작하면 그룹 안에서 파티션을 나누어 맡습니다. 안정적으로 할당된 상태에서 한 파티션은 같은 그룹의 한 컨슈머가 읽습니다. 한 컨슈머가 여러 파티션을 맡을 수도 있습니다.

 

파티션과 컨슈머가 각각 네 개인 아래 예시에서는 컨슈머 하나가 파티션 하나씩을 맡습니다. P3에 메시지가 몰려도 같은 그룹의 다른 컨슈머가 P3을 동시에 나누어 읽지는 않습니다.

파티션 할당을 설명하는 예시입니다. 컨슈머 처리 속도를 측정한 결과는 아닙니다.

한 파티션에 메시지가 많이 기록돼도 담당 컨슈머가 충분히 빠르면 금방 처리를 마칠 수 있습니다. 그래서 두 키의 분포 차이가 전체 소비 완료 시간에도 이어지는지 확인했습니다.

실험 2. 전체 소비 완료 시간도 달라질까요?

같은 데이터를 키별로 별도 토픽에 다시 발행했습니다. 각 토픽은 컨슈머 네 개가 읽도록 했습니다. 메시지를 먼저 전부 기록한 뒤 컨슈머 할당이 안정되면 소비를 시작했습니다. 소비 중 파티션 담당 컨슈머는 바뀌지 않았고 발행·처리·커밋 건수도 대조했습니다.

이 글에서 전체 소비 완료 시간은 컨슈머 할당이 안정된 뒤 소비를 시작한 시점부터, 실험 데이터 1만 건의 처리와 마지막 오프셋 커밋이 모두 끝날 때까지로 정했습니다.

컨슈머는 가져온 메시지를 최대 50건씩 처리한 뒤 다음에 읽을 위치를 기록했습니다. 이 작업을 오프셋 커밋이라고 합니다.

 

진행 상황을 보기 위해 consumer lag를 확인했습니다. lag는 파티션의 끝 오프셋과 컨슈머 그룹이 커밋한 다음 읽을 위치의 차이입니다. 커밋 전에 이미 처리한 메시지도 포함될 수 있으므로 실제 미완료 건수나 지연 시간과 항상 같지는 않습니다.

 

상품 ID를 키로 쓴 두 번째 실행 중 Kafka UI를 확인했습니다. 아래 화면에서 P0부터 P2까지의 lag는 0이고, P3의 lag는 7,160입니다. P3의 끝 오프셋 9,311과 커밋된 위치 2,151의 차이입니다.

두 번째 소비 실험을 진행하던 중 캡처한 화면입니다. 표시된 오프셋에는 파티션별 준비 메시지가 한 건씩 포함됩니다.

 

전체 소비 완료 시간은 주문 ID 조건에서 약 9.6~9.7초, 상품 ID 조건에서 약 34.9~35.1초였습니다. 각 조건을 두 번 실행한 결과입니다.

 

실험 2에서 확인한 점

두 조건의 건당 가상 지연은 3ms로 같았습니다.
상품 ID 조건에서는 다른 파티션의 처리가 끝난 뒤에도 P3 담당 컨슈머에 일이 남아, 전체 소비 완료 시간이 길어졌습니다.

이는 미리 쌓인 메시지에 같은 가상 지연을 적용한 로컬 실험 결과입니다. 이 수치는 메시지 한 건의 처리 속도를 측정한 값이 아닙니다.

핫 파티션은 언제 문제가 될까요?

특정 파티션에 메시지나 처리 부하가 집중되는 상황을 핫 파티션이라고 부릅니다. 이번 실험에서는 인기 상품의 메시지가 모인 P3에 처리가 오래 남았습니다.

이번에는 메시지를 미리 쌓아두고 소비했지만, 서비스에서는 소비 중에도 새 메시지가 계속 들어올 수 있습니다. 예를 들어 담당 컨슈머가 초당 300건을 처리하는데 해당 파티션에 초당 500건이 계속 들어온다고 가정하면, 처리하지 못한 메시지는 초당 200건씩 늘어납니다.

같은 소비자 그룹에서 한 파티션은 한 컨슈머에 할당됩니다. 다른 컨슈머에 여유가 있어도 해당 파티션을 동시에 나누어 읽지는 않습니다. 따라서 컨슈머 수만 늘리는 것으로 이 쏠림을 해소하기는 어렵습니다.

운영에서는 파티션별 lag가 회복되는지와 실제 업무 반영이 얼마나 늦어지는지를 함께 확인해야 합니다. 전체 lag만 보면 어느 파티션에서 밀리는지 놓칠 수 있습니다. 담당 컨슈머가 충분히 따라잡는다면 쏠림을 감수할 수도 있습니다. 반면 지연이 계속 늘어난다면 DB 호출이나 외부 API 등 처리 과정의 병목을 살펴보고, 필요한 순서를 유지하면서 부하를 나눌 방법을 검토해야 합니다.

이번 실험에서 직접 확인한 범위는 미리 쌓아둔 메시지의 분포와 전체 소비 완료 시간입니다. 지속적인 유입이나 개선 방법의 효과는 측정하지 않았습니다.

쏠림이 브로커에도 영향을 주는지는 프로듀서 응답 지연·오류와 브로커의 CPU·네트워크·디스크 사용량을 따로 확인해야 합니다. 이번 실험만으로 브로커의 한계를 판단할 수는 없습니다.

파티션을 늘리면 해결될까요?

파티션이 늘면 서로 다른 키를 더 많은 파티션에 분산할 여지는 생깁니다. 하지만 이번 실험의 기본 키 기반 분배에서는 파티션 수가 고정된 한 시점에 같은 키는 한 파티션으로 갑니다. 상품 7의 메시지가 추가된 파티션들로 자동 분산되지는 않습니다.

 

또 파티션 수가 바뀌면 같은 키가 이전과 다른 파티션에 배정될 수 있습니다. 각 파티션의 기록 순서는 별개이므로 증설 전후에도 그 키의 순서가 그대로 이어진다고 가정해서는 안 됩니다.

키를 정할 때 순서도 함께 생각해야 했습니다

처음에는 Kafka의 순서 보장을 토픽에 들어온 모든 주문이 하나의 줄에 쌓이는 것으로 막연하게 생각했습니다.

 

Kafka의 순서는 파티션 단위입니다.

서로 다른 파티션에 기록된 메시지 사이에는 전체 순서가 정해지지 않습니다.


예를 들어 같은 주문의 ‘주문 확정’과 ‘주문 취소’를 같은 토픽에 같은 주문 ID로 발행해 보겠습니다. 파티션 수와 키 분배 방식이 유지되면 두 메시지는 같은 파티션에 기록되고, 컨슈머는 그곳에 기록된 순서대로 읽을 수 있습니다. 서로 다른 키라도 같은 파티션에 기록되면 그 파티션의 순서를 따릅니다.

 

다른 주문은 다른 파티션에 갈 수 있으므로 같은 상품의 주문 간 순서까지 보장되지는 않습니다. 같은 키라도 다른 토픽 사이의 순서는 보장되지 않습니다.

Kafka에 기록된 순서가 실제 사건 발생 순서나 DB 반영 순서를 자동으로 보장하는 것은 아닙니다. 컨슈머에서 병렬 처리하면 완료 순서가 달라질 수도 있습니다.

이번 실험은 키별 분포와 전체 소비 완료 시간을 비교한 것으로, 같은 주문의 여러 이벤트가 순서대로 처리되는지는 직접 검증하지 않았습니다.

그래서 어떤 키를 선택해야 할까요?

키를 선택할 때는 먼저 순서를 유지해야 하는 대상을 정해야 합니다. 그리고 실제 요청이 어떤 키에 얼마나 몰릴 수 있는지, 그 부하를 담당 컨슈머가 필요한 시간 안에 처리할 수 있는지도 함께 확인해야 합니다.

이번 입력에서 orderId를 썼을 때 인기 상품의 주문도 네 파티션에 비교적 고르게 나뉘었습니다.

productId를 썼을 때는 한 파티션에 모였고, 그 파티션의 처리가 더 오래 남았습니다.

 

현재 프로젝트의 주문 확정 이벤트는 주문 단위이므로 orderId를 사용합니다. 같은 주문의 이벤트를 같은 토픽에 같은 키로 발행하면 파티션의 기록 순서대로 읽을 수 있습니다.

상품별 재고 변화의 순서가 필요하다면 상품마다 별도 메시지를 발행하고 productId를 키로 쓰는 구조를 고려할 수 있지만, 인기 상품에 일이 쏠릴 가능성도 살펴봐야 합니다.

상품 전체의 순서가 필요하지 않다면 상품 ID에 구분값을 붙여 여러 키로 나눌 수 있습니다. 다만 메시지가 여러 파티션에 기록되므로 상품 전체의 순서는 유지할 수 없습니다. 반대로 orderId로 메시지를 분산해도 여러 주문이 같은 재고를 동시에 변경할 수 있으므로, 재고를 변경하는 쪽에서 동시성을 설계해야 할 거 같습니다.

(실제 동시성 문제는 확인하지 못했습니다)

 

이번 실험을 통해 키를 선택할 때는 순서를 유지할 단위와 함께, 실제 요청이 어떤 키에 얼마나 집중될지도 고려해야 한다는 점을 배웠습니다. 이번 입력에서 orderId는 고르게 분산됐지만, 데이터와 요청이 몰리는 양상이 바뀌면 결과도 달라질 수 있습니다. 그래서 키의 의미가 이벤트에 맞는지와 더불어, 예상되는 쏠림을 담당 컨슈머가 감당할 수 있는지까지 확인해야겠다고 생각했습니다.

 


 

필요한 순서를 지키면서 쏠림을 어떻게 해결할 수 있을까요? 이 내용은 다음 글에서 이어서 다뤄보겠습니다... !

참고 자료

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/11   »
일 월 화 수 목 금 토
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함