휴가를 다녀오니 Kafka 토픽 파티션이 늘어난 건에 대하여

휴가를 다녀오니 Kafka 토픽 파티션이 늘어난 건에 대하여

제목 그대로의 일이 발생했다. 휴가 중 서비스의 다량 요청 트래픽이 몰리는 일이 발생했고, 그에 따라 늘어난 메시지 큐를 빨리 처리하기 위해서 팀에서 파티션을 늘려 조치를 했던 것이다.
하지만 문제는 파티션을 늘리게 되면서 발생했다. 기존 토픽의 파티션은 1로, 파티션이 1인 경우 싱글 스레드로 이벤트 컨슈밍 로직이 동작한다. 즉 서비스의 로직 플로우는 싱글 스레드 환경에서의 동작만을 고려해서 설계된 구조였다.
갑작스럽게 파티션이 늘어나며 멀티 스레드로 로직이 수행되다보니 기존 플로우의 순서가 보장되지 않았고, 그에 따른 여러 이슈들이 우후죽순으로 생겨나기 시작했다.

(애초에 Kafka를 순서 보장형 이벤트 스트림으로 사용하는 게 올바른 Kafka 사용 방법인지 고민해볼 필요가 있다고 생각했다.)

늘어난 파티션은 다시 줄일 수 없다.

카프카를 공부하면 알게되는 사실이 있다. 바로 파티션은 한번 늘리면 다시 줄일 수 없다는 것이다. 먼저 왜 줄일 수 없을까?에 대해 알아보자.
파티션을 줄일 수 없는 이유는 카프카를 이루는 설계요인이 복합적으로 작용한다. 가장 궁극적인 이유로는 다수의 브로커에 분배되어있는 세그먼트를 다시 재배열하는 것이 상당히 리소스가 많이 드는 작업이기 때문이다.
그래서 늘어난 파티션을 되돌리는 방법은 해당 토픽을 삭제하고 다시 생성하는 방법인데, 토픽을 삭제하기 위해서는 컨슈머를 내리고 삭제를 해야 한다. 컨슈머를 내린다는 것은 WAS를 내려야 한다는 것으로 실시간 운영 중인 WAS를 내리는 것은 리스크가 큰 작업이라고 판단해서 토픽을 삭제하는 방법은 해결 방법에서 제외했다.

여러 파티션이 존재하는 토픽에서 순서 보장하는 방법

이미 설계된 서비스의 로직을 바꾸는 일은 굉장히 리소스가 큰 작업이었기 때문에 현실적으로 불가능한 방법이었다. 따라서 어떻게든 여러 파티션에서의 순서를 보장하는 방법을 찾아야 했다.
Kafka 순서 보장에 대해서 구글링을 해보면 같은 파티션에 대해서는 순서가 보장되지만, 여러 파티션이 늘어나면 순서 보장이 힘들다는 내용이 대부분이었다.

그러다 파티션 키를 지정하는 방법에 대해서 알게 되었다. 하나의 파티션은 동일한 컨슈머 그룹 내에서 하나의 컨슈머에 의해 처리된다는 특징파티션 키로 메시지를 적재할 파티션이 정해지는 특징으로 순서를 보장해서 이벤트를 처리할 수 있다는 것이다.
처음에 파티션 키에 대해서 들었을 때는 파티션 하나 당 고정된 키 하나를 지정해서 이벤트를 적재하는 건 줄 알았다. 하지만 그게 아닌 가변 키를 지정해주는 것이고, 메시지의 순서를 유지하기 위해 동일한 키의 메시지를 같은 파티션에 적재하는 역할을 하는 것이다.
파티션 키는 키가 지정된 경우에, Kafka가 해당 키를 해싱하여 파티션을 결정한다. 이를 통해 같은 키를 가진 메시지는 항상 같은 파티션에 적재되는 것이다.

파티션 키를 사용하는 실제 예시를 보면, 특정 유저와 관련된 이벤트를 처리할 때 유저 ID 값을 파티션 키로 사용하여 유저의 이벤트를 순차적으로 처리할 수 있게 된다.
이를 활용한다면 서비스에서 사용하는 시스템 엔티티의 ID를 파티션 키로 사용한다면 같은 엔티티 단위의 이벤트는 순차적으로 처리가 되며, 현재 서비스에서 발생하는 이슈도 해결할 수 있을 것으로 기대되었다.

파티션 키는 어떻게 지정해줄까?

서비스에서는 spring-kafka 라이브러리를 사용하여 KafkaTemplate로 메시지를 Producing 하고 있다. KafkaTemplate에서 제공하는 메서드인 kafkaTemplate.send(String topic, K key, V data)를 사용하여 파티션 키를 쉽게 지정해줄 수 있다. topic은 토픽 이름, key는 파티션 키, data는 보낼 데이터가 된다.
또한 파티션 키의 타입은 기본적으로 String 타입을 사용한다.

public void sendExampleTopicWithKey(final int entityId) {
    final String topic = exampleRequestTopic.formatted(entityId);
    final String key = String.valueOf(entityId);
    kafkaTemplate.send(topic, key, new KafkaExample.Request(entityId));
}

Consuming할 때는 파티션 키를 지정해서 보내준 메시지나 키를 사용하지 않은 메시지 동일한 방법으로 Consuming 한다.

참고한 글

[Kafka] 카프카 순서 보장 구현 - 파티션키 지정
Topics, partitions and keys - stackoverflow