Home 채팅 시스템 roomId 기반 파티셔닝
Post
Cancel

채팅 시스템 roomId 기반 파티셔닝

Kafka는 기본적으로 병렬성과 확장성을 제공한다. 하지만 파티션을 어떻게 나누냐에 따라 메시지의 순서 보장, 병렬 처리 방식이 완전하게 달라지게 된다.

채팅 시스템 기준에서는 roomId라는 파티셔닝의 기준이 존재한다. 꼭 그래야만 하는 것은 아니지만 순서 보장의 측면에서 유의미하다.

기존 구현에서는 파티션 키 없이 메시지를 전송했다. 트래픽이 증가하거나 컨슈머 수가 늘어날 경우 순서 보장에 문제가 생길 수 있다.

Kafka의 Key 기반 파티셔닝을 적용해 같은 roomId는 항상 같은 파티션으로 이동하는지, 여러 Consumer가 병렬로 처리하는지 확인해보고자 한다.

파티셔닝

컨슈머를 3개로 잡고, 메시지를 roomId 1번방에 빠르게 수천건을 보내보았다.

정상적이라면 JMeter Message 1부터 순서대로 처리가 되어야하는데, 로그를 보면 3353, 4348 등 순서가 뒤죽박죽이다.

즉, 순서가 꼬인 것을 볼 수 있다. 파티션도 0과 2로 분산되어 있는 것을 볼 수 있다.

실제 트래픽이 몰리는 경우 지금 구현 상태에서는 순서 보장과 파티셔닝이 모두 깨지는 것이다.

roomId를 Key로 명시하지 않으면 메시지가 Round Robin 방식으로 여러 파티션으로 분산되고, 그에 따라 브로드캐스트 순서가 어긋날 수 있는 것이다.

개선

간단하게 개선할 수 있다. 메시지를 전송할 때 roomId를 Key로 명시해 같은 채팅방 메시지는 항상 동일 파티션으로 들어가게 만들면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
public void sendMessage(String topic, ChatMessageDto message) {
    String key = message.getRoomId().toString();

    kafkaTemplate.send(topic, key, message)
            .whenComplete((result, ex) -> {
                if (ex != null) {
                    log.error("topic {}에 메시지 전달 실패: {}", topic, ex.getMessage(), ex);
                } else {
                    log.info("topic {}에 메시지 전달 성공: {}", topic, message);
                }
            });
}

파티션 = 0으로 고정되는 것을 볼 수 있다. 하지만 메시지 번호는 여전히 순서가 꼬여있다.

여러가지를 테스트해본 결과 문제는 JMeter 테스트 구조에 있었다.

users > 1로 설정된 JMeter는 여러 쓰레드가 동시에 메시지를 전송해 메시지 번호 순서대로 전송되지 않아 Kafka 수신 순서도 꼬여 보인 것이다. 따라서 저 로그로 Kafka의 처리 순서를 판단하기에는 무리가 있다.

개선 2

그래서 JMeter 테스트를 users를 1로 제한하고 메시지 수를 5000개로 늘려보았다.

순서가 꼬이지 않는 모습을 볼 수 있다.

같은 roomId가 하나의 파티션에만 저장되므로 해당 채팅방은 병렬 처리가 되지 않는다. 물론 채팅방끼리는 파티션이 분산되기 때문에 전체적으로는 병렬성을 확보할 수 있다.

roomId를 Kafka 전송 Key로 명시해준 결과 메시지가 항상 동일 파티션으로 전달되며 순서가 안정적으로 유지될 수 있었다.

결과적으로 Kafka의 파티셔닝 전략은 순서냐 성능이냐에 따라 조정해야 한다

파티셔닝 전략

현재는 단순하게 채팅방 Id를 파티션으로 고정시켰는데 파티션은 무한히 늘릴 수 없다. 파티션은 단순한 개념이 아닌 리소스 단위이다.

파티션이 많아지면 브로커에 부하가 증가한다. 카프카는 파티션마다 별도 파일 시스템 구조, 인덱스, 로그를 관리하는데 파티션 수가 많아질수록 디스크 I/O 등의 비용이 급증한다.

또, 컨슈머가 장애로 재시작되거나 그룹이 변경되면 전체 리밸런싱 작업이 일어나는데 파티션이 수천개라면 리밸런싱 자체에 시간이 매우 오래걸릴 수 있다.

그래서 보통 해시 파티셔닝이 적용된다.

내 코드를 예시로 들면 roomId를 정수 해시로 변환하고, 전체 파티션 수로 나눈 나머지를 사용하는 방식이다. 이렇게 되면 순서 보장과 병렬 처리 모두 일정 수준은 확보가 가능하다.

KafkaTemplate은 이 부분을 자동으로 처리해주기 때문에 위 코드처럼 roomId를 Key로 주는 것만으로 해시 파티셔닝을 사용하는 것이다.

정리

단순 전송 방식의 차이라고 볼 수 있지만, 실제 서비스에서 순서 보장은 사용자 경험에 큰 영향을 미칠 수 있다.

도메인에 따라 파티셔닝 전략을 성능과 순서 트레이드 오프를 생각해서 정해야된다는 것을 알 수 있었다.

This post is licensed under CC BY 4.0 by the author.

GC는 단순한 자동 관리 도구가 아니다. (Java GC)

Kafka at-least-once 메시지 보장 중복 처리