Engineering Note
Kafka Streams 4.2 이후, 장애를 덜 아프게 만드는 두 가지 변화
Kafka Streams 운영에서 자주 만나는 나쁜 레코드와 rebalance 문제를 Kafka 4.2 이후 DLQ 지원과 Streams Rebalance Protocol 관점에서 정리합니다.
Kafka Streams 애플리케이션을 운영하다 보면 장애는 보통 아주 큰 사건으로만 오지 않습니다.
때로는 메시지 하나가 문제입니다. 역직렬화가 실패하거나, 처리 로직에서 예상하지 못한 값이 들어오거나, output topic으로 쓰는 과정에서 오류가 납니다. 메시지 하나 때문에 stream thread가 죽고, 애플리케이션이 멈추고, lag이 쌓이기 시작합니다.
또 다른 순간은 rebalance입니다. 인스턴스를 하나 더 띄우거나, 배포로 인스턴스가 교체되거나, 장애가 나서 member가 빠지면 Kafka Streams는 task와 state를 다시 배치해야 합니다. Stateless consumer group의 partition 이동보다 훨씬 민감합니다.
Kafka Streams 운영에서 자주 아픈 지점:
나쁜 레코드 하나가 전체 처리를 흔든다.
rebalance가 task와 state 이동으로 이어진다.
Apache Kafka 4.2 이후에는 이 두 지점을 덜 아프게 만들기 위한 변화가 들어왔습니다.
- Kafka Streams DLQ 지원
- Streams Rebalance Protocol
둘 다 마법처럼 장애를 없애는 기능은 아닙니다. 하지만 운영자가 직접 구현하거나 해석해야 했던 부담을 Kafka Streams의 표준 기능 쪽으로 옮긴다는 점에서 의미가 큽니다.
나쁜 레코드 하나가 왜 위험한가
Kafka Streams는 record를 계속 읽고, topology를 따라 처리하고, 필요하면 state store를 갱신하고, 결과를 output topic으로 씁니다.
문제는 이 흐름이 record 단위로 깨질 수 있다는 점입니다.
input topic
-> deserialize
-> process
-> update state store
-> produce output
이 중 어디서든 예외가 날 수 있습니다.
- input record의 key/value format이 깨져 있다.
- schema가 바뀌었는데 애플리케이션이 따라가지 못했다.
- business rule상 허용하지 않는 값이 들어왔다.
- 외부 library나 custom processor가 예외를 던졌다.
- output record 직렬화나 produce 과정에서 문제가 생겼다.
기존에도 Kafka Streams에는 exception handler가 있었습니다. 하지만 선택지는 운영자 입장에서 꽤 불편했습니다.
| 방식 | 운영상 문제 |
|---|---|
| 실패하면 앱 중단 | 메시지 하나 때문에 전체 Streams 앱이 멈출 수 있다 |
| 로그만 남기고 계속 진행 | 문제 record가 어디로 갔는지 추적하기 어렵다 |
| 직접 DLQ 구현 | handler와 producer logic을 애플리케이션마다 다시 만든다 |
여기서 DLQ가 들어옵니다.
DLQ는 실패 레코드를 격리하는 표준 경로다
Kafka Streams DLQ는 처리 중 문제가 생긴 record를 별도 Kafka topic으로 보낼 수 있는 경로를 제공합니다.
예를 들어 주문 집계 애플리케이션이 있다고 하겠습니다.
orders
-> order-aggregator
-> order-counts
이때 처리하지 못한 record를 다음 topic으로 보낼 수 있습니다.
order-aggregator-dlq
이렇게 하면 정상 record 처리는 계속 이어가고, 실패 record는 별도 topic에서 분석하거나 재처리할 수 있습니다.
하지만 여기서 중요한 선이 하나 있습니다.
DLQ = 실패 record를 격리하는 경로
DLQ != 자동 복구
DLQ topic에 record가 들어갔다고 해서 문제가 해결된 것은 아닙니다. 실패 record를 누가 보고, 어떤 기준으로 재처리하고, 다시 실패하면 어떻게 막을지까지 운영 흐름으로 설계해야 합니다.
설정은 단순하지만 의미는 조심해야 한다
Kafka Streams 설정에는 DLQ topic 이름을 지정하는 옵션이 있습니다.
errors.dead.letter.queue.topic.name=order-aggregator-dlq
공식 Kafka Streams config 문서는 이 값이 null이 아니면 기본 exception handler가 오류 발생 시 DLQ record를 만들어 지정한 topic으로 보낼 수 있다고 설명합니다. 다만 custom deserialization, production, processing exception handler를 쓰면 이 설정이 해당 handler에서는 무시될 수 있습니다. 또한 regular stream processing task에만 적용되고 global state store update에는 적용되지 않습니다.
즉 다음처럼 이해하는 것이 안전합니다.
DLQ topic 설정:
실패 record를 보낼 기본 목적지를 제공한다.
Exception handler 설정:
실패 후 계속할지, 멈출지, retry할지를 결정한다.
DLQ 설정 하나만 보고 “이제 앱은 무조건 계속 돈다”고 생각하면 위험합니다. 기본 handler는 실패를 선택할 수 있고, custom handler를 쓰는 경우에는 DLQ record 생성 자체를 직접 책임져야 할 수 있습니다.
운영에서는 DLQ topic을 자동 생성에 맡기기보다 명시적으로 만드는 편이 좋습니다.
kafka-topics.sh \
--bootstrap-server broker1:9092 \
--create \
--topic order-aggregator-dlq \
--partitions 3 \
--replication-factor 3
DLQ record에는 가능한 경우 원본 record의 raw key/value와 함께 source topic, partition, offset, exception, stacktrace 같은 metadata가 header로 포함될 수 있습니다. 이 정보가 있어야 나중에 “어떤 record가 왜 실패했는지”를 다시 따라갈 수 있습니다.
DLQ를 만들었다면 운영도 같이 만들어야 한다
DLQ는 실패 record를 잃어버리지 않게 도와줍니다. 하지만 DLQ가 쌓이는 순간부터는 또 다른 운영 대상이 생긴 것입니다.
input topic
-> Kafka Streams
-> output topic
-> DLQ topic
-> alert
-> analysis
-> replay or correction
그래서 DLQ를 쓸 때는 최소한 다음을 같이 정해야 합니다.
| 항목 | 확인할 것 |
|---|---|
| Topic 설계 | partition 수, replication factor, retention |
| Metadata | 원본 topic, partition, offset, exception 정보 보존 |
| Alert | DLQ produce count, DLQ topic lag, 처리 지연 |
| 재처리 | 원본 topic 재투입인지, 보정 topic 경유인지 |
| 반복 실패 방지 | retry count, 실패 사유, 처리 상태 기록 |
특히 “절대 누락되면 안 되는 이벤트”라면 DLQ로 보냈다는 사실만으로는 부족합니다. DLQ에 쌓인 record를 누가 언제 어떤 기준으로 처리할지까지 정해야 합니다.
Rebalance는 Streams에서 더 무겁다
두 번째 변화는 Streams Rebalance Protocol입니다.
일반 consumer group에서 rebalance는 partition ownership을 다시 나누는 일입니다. 물론 이것도 운영에는 부담이지만, Kafka Streams에서는 의미가 더 무겁습니다.
Kafka Streams는 단순히 partition을 소비하는 애플리케이션이 아닙니다.
topic partition
-> stream task
-> state store
-> changelog topic
-> standby task
-> interactive query metadata
Stateful topology에서는 task가 어느 인스턴스에 배치되는지가 중요합니다. Local RocksDB state가 있고, changelog restore가 있고, standby replica나 warm-up task 같은 개념도 따라옵니다.
예를 들어 집계 애플리케이션은 이렇게 보일 수 있습니다.
orders
-> groupByKey()
-> count()
-> RocksDB state store
-> order-counts
이 애플리케이션에서 rebalance가 발생하면 단순히 consumer가 바뀌는 것이 아닙니다. Task가 이동하고, state restore가 필요할 수 있으며, 그 동안 처리 지연이 커질 수 있습니다.
그래서 Kafka Streams rebalance는 운영자에게 더 날카롭게 보입니다.
배포 한 번 했을 뿐인데 lag이 튄다.
인스턴스 하나 늘렸을 뿐인데 state restore가 길어진다.
장애 복구 중에 task 이동이 겹쳐 처리 지연이 커진다.
Streams Rebalance Protocol은 조율 중심을 브로커로 옮긴다
기존 Kafka Streams는 consumer group rebalance 구조 위에 Streams 전용 metadata를 얹어 동작했습니다. Assignment 계산과 조율의 많은 부분이 client 쪽에 있었습니다.
Streams Rebalance Protocol은 이 방향을 바꿉니다.
기존 방식:
Streams client가 assignment 계산과 조율에 큰 책임을 가진다.
새 방식:
broker group coordinator가 streams group metadata를 관리하고
task assignment를 broker 중심으로 조율한다.
Apache Kafka 문서는 Streams Rebalance Protocol을 Kafka Streams 애플리케이션을 위한 broker-driven rebalancing system으로 설명합니다. 일반 consumer rebalance 조율을 client에서 broker로 옮긴 KIP-848의 방향을 Streams workload로 확장한 것입니다.
설정은 애플리케이션에서 다음처럼 시작합니다.
group.protocol=streams
공식 Kafka Streams config 문서에 따르면 group.protocol은 classic 또는 streams를 지원하고, 기본값은 classic입니다. streams를 지정해야 Streams Rebalance Protocol을 사용합니다.
Kafka 4.3 Streams Rebalance Protocol 문서는 이 프로토콜이 new Kafka 4.2 cluster에서는 기본적으로 enable되어 있지만, broker와 client가 모두 Kafka 4.2 이상이어야 한다고 설명합니다. 기존 cluster를 업그레이드한 경우에는 feature flag 상태도 확인해야 합니다.
bin/kafka-features.sh --bootstrap-server localhost:9092 \
upgrade --feature streams.version=1
즉 다음 두 조건을 같이 봐야 합니다.
cluster가 streams group protocol을 지원하는가?
Streams 애플리케이션이 group.protocol=streams를 쓰는가?
무엇이 좋아지나
Streams Rebalance Protocol의 방향은 명확합니다.
| 변화 | 운영상 의미 |
|---|---|
| Broker-driven coordination | client별 조율 차이를 줄이고 group coordinator가 중심이 된다 |
| Dedicated streams group | consumer group과 다른 Streams 전용 metadata와 state를 다룬다 |
| Broker-side assignment | topology와 member metadata를 broker가 관리하는 방향으로 간다 |
| Better observability | streams group 상태와 rebalance metric을 더 명확히 볼 수 있다 |
| Group-level config | 일부 Streams group 설정을 client 재배포 없이 조정할 수 있다 |
이것은 “rebalance가 사라진다”는 뜻이 아닙니다.
더 정확히는 다음에 가깝습니다.
Kafka Streams의 task/state 재배치 조율을
client 내부 구현이 아니라 broker 중심의 protocol로 끌어올린다.
이 변화는 특히 stateful application에서 중요합니다. State store와 changelog restore가 얽힌 애플리케이션일수록 rebalance가 처리 지연과 직접 연결되기 때문입니다.
하지만 아직 바로 바꿀 기능은 아니다
여기서도 선을 조심해야 합니다.
Streams Rebalance Protocol은 중요한 방향이지만, 현재 지원 범위를 확인하고 적용해야 합니다.
Kafka 4.3 문서는 아직 지원하지 않는 항목을 명시합니다.
- static membership은 지원되지 않습니다.
- 큰 topology 변경은 새 streams group을 요구할 수 있습니다.
- high availability assignor는 아직 지원되지 않고 sticky assignor만 지원됩니다.
- warmup task와 rack-aware assignment는 아직 지원되지 않습니다.
- regular expression 기반 topic subscription은 지원되지 않습니다.
- classic protocol과 streams protocol 사이의 online migration은 지원되지 않습니다.
- 일부 client-side Streams 설정은 streams protocol에서 무시되고 group-level config를 사용해야 합니다.
따라서 기존 운영 애플리케이션을 바로 바꾸는 것보다는 신규 앱이나 테스트 환경에서 먼저 확인하는 편이 좋습니다.
신규 stateless 앱:
비교적 부담이 작다.
기존 stateful 앱:
topology, state store, standby, restore 시간까지 같이 검증해야 한다.
운영 중인 critical 앱:
classic 유지 후 별도 migration test를 먼저 한다.
적용 순서는 이렇게 보는 편이 안전하다
두 기능을 같은 속도로 적용할 필요는 없습니다.
DLQ는 실패 record를 어떻게 다룰지의 문제입니다. 신규 Kafka Streams 애플리케이션이라면 비교적 먼저 검토할 가치가 큽니다. 다만 DLQ topic, alert, 재처리 절차를 같이 설계해야 합니다.
Streams Rebalance Protocol은 애플리케이션의 조율 방식 자체를 바꾸는 문제입니다. 효과가 클 수 있지만, 지원하지 않는 기능과 migration 제약도 같이 봐야 합니다.
실무 적용 순서는 다음 정도가 무난합니다.
1. DLQ topic과 실패 record 처리 정책을 먼저 설계한다.
2. DLQ produce count와 lag 기준 alert를 만든다.
3. 재처리 흐름을 수동이라도 명확히 둔다.
4. 테스트 환경에서 group.protocol=streams를 켠다.
5. Stateful topology의 rebalance 시간과 restore 시간을 비교한다.
6. 지원하지 않는 Streams 기능을 쓰는지 확인한다.
7. 신규 앱부터 운영 적용을 검토한다.
운영 관점에서 같이 봐야 할 것
이런 변화는 결국 Kafka Streams를 코드가 아니라 운영 대상(system)으로 보는 문제로 이어집니다.
DLQ topic에 record가 쌓인다는 것은 business event 처리에 실패했다는 신호입니다. Rebalance가 잦거나 오래 걸린다는 것은 Streams instance, broker group coordinator, state store, changelog topic, host resource가 함께 흔들리고 있다는 신호일 수 있습니다.
Konduo는 Kafka 같은 클러스터형 리소스를 플러그인으로 연결해 broker, topic, consumer group, metric evidence, alert response를 하나의 운영 흐름에서 확인할 수 있도록 합니다. Kafka Streams 애플리케이션도 DLQ, lag, rebalance, broker 상태, host resource를 따로 보지 않고 같은 문맥에서 읽을 때 문제의 위치가 더 분명해집니다.
정리
Kafka Streams 4.2 이후의 DLQ 지원과 Streams Rebalance Protocol은 서로 다른 문제를 다룹니다.
DLQ:
실패 record를 잃어버리지 않고 격리한다.
Streams Rebalance Protocol:
task/state 재배치 조율을 broker 중심으로 옮긴다.
DLQ는 자동 복구 장치가 아닙니다. Rebalance Protocol도 모든 migration 부담을 없애는 기능은 아닙니다.
하지만 둘 다 운영자가 직접 떠안던 부담을 Kafka Streams의 표준 기능과 protocol 쪽으로 옮기는 변화입니다. Kafka Streams 애플리케이션을 오래 운영해야 한다면, 이 두 변화는 “언젠가 볼 기능”이 아니라 지금부터 적용 기준을 정해둘 만한 운영 주제입니다.
함께 읽기 좋은 글
이 글의 관점과 이어지는 주제를 더 보려면 아래 글도 함께 읽어볼 수 있습니다.
- Kafka Streams로 EIP Aggregator 패턴 구현하기 - Stateful Kafka Streams 애플리케이션에서 task와 state가 왜 중요한지 이어서 볼 수 있습니다.
- Kafka 4.0 Consumer Rebalance: 이제 조율은 브로커가 한다 - 일반 consumer group에서 broker-driven rebalance가 어떤 의미인지 정리합니다.
- Kafka consumer lag을 처리 압력으로 읽기 - lag을 단순한 숫자가 아니라 처리 능력과 복구 시간 관점에서 읽는 방법을 다룹니다.