장애 대응 패턴: broker failure, bad deploy, schema break, downstream outage
스트리밍 장애는 종류마다 원인과 완화 경로가 다르다
스트리밍 플랫폼 장애를 하나의 범주로 묶으면 대응이 늘 맞지 않는다. "Kafka broker 한 대가 죽었다"와 "신규 버전 배포 후 consumer가 멈췄다"와 "producer가 schema를 바꿨더니 downstream이 깨졌다"와 "sink DB가 내려갔다"는 네 가지 상황은 겉으로 모두 "consumer lag가 증가함"으로 나타날 수 있지만, 원인이 다르고 완화 순서가 다르다.
운영 경험이 쌓이면 이 네 유형을 빠르게 구별하는 감각이 생긴다. 운영 경험 없이도 이 감각을 갖추려면 각 유형이 어떤 신호를 남기는지, 어떤 순서로 조사해야 하는지를 미리 정리해 두는 것이 가장 빠르다.
1. Broker failure: ISR이 곧 내구성이다
Kafka에서 broker 한 대가 내려가면 그 broker가 leader를 맡고 있던 partition에서 write와 read가 잠시 멈춘다. Kafka 문서는 ISR(In-Sync Replicas)에 포함된 replica만이 새 leader 후보가 될 수 있다고 명시한다. clean leader election은 ISR 안에서 새 leader를 선출하므로 데이터 유실 없이 복구된다.
문제는 두 가지 조건에서 발생한다.
첫째, 모든 ISR replica가 죽은 경우. unclean.leader.election.enable이 false(기본값)이면 해당 partition은 ISR이 다시 만들어질 때까지 unavailable 상태가 된다. true로 설정하면 out-of-sync replica가 leader가 되어 가용성은 회복되지만 뒤처진 구간의 데이터가 유실될 수 있다.
둘째, 정상 복구라도 leader election 시간이 길어지는 경우. KRaft 모드에서 controller는 ZooKeeper 없이 메타데이터를 직접 관리하지만, controller 자체가 영향을 받으면 선출 지연이 생긴다. Confluent 문서는 broker 설정 중 leader.imbalance.check.interval.seconds를 통해 leader balance를 주기적으로 복구할 수 있다고 설명한다.
1.1 운영 조사 순서
broker failure를 의심할 때 확인 순서는 다음과 같다.
- partition 상태를 본다.
kafka-topics.sh --describe에서Under-replicated Partitions개수를 확인한다. ISR 목록에서 특정 broker id가 빠졌다면 해당 broker가 문제 원인이다. - controller log를 본다. leader election 이벤트, replica assignment 변경,
ERROR수준 로그를 확인한다. - consumer error를 본다. producer가
LEADER_NOT_AVAILABLE또는NOT_LEADER_OR_FOLLOWER를 받고 있다면 leader election이 아직 완료되지 않았거나 broker가 여전히 unavailable하다. min.insync.replicas설정을 본다. producer가acks=all을 쓸 때 ISR이min.insync.replicas보다 작아지면NotEnoughReplicasException이 발생한다.
1.2 consumer rebalance 연쇄
broker failure는 producer 측 장애뿐 아니라 consumer 측 rebalance도 유발할 수 있다. broker가 내려가면 해당 broker에 연결한 consumer의 heartbeat가 끊기고, group coordinator가 rebalance를 트리거한다. rebalance는 consumer group의 "stop-the-world" 이벤트이므로, broker 복구 시간과 rebalance 시간이 겹치면 lag가 이중으로 쌓인다.
완화책은 두 가지다.
session.timeout.ms와heartbeat.interval.ms를 늘려 일시적인 broker 재시작에도 consumer가 rebalance를 트리거하지 않도록 한다. 단, 이 값이 너무 크면 실제 장애 consumer를 늦게 감지한다.- Kafka 2.4 이상에서 도입된 incremental cooperative rebalancing을 쓰면 rebalance 중에도 할당을 유지하지 않은 partition만 일시 중단하므로 stop-the-world 구간을 최소화할 수 있다.
2. Bad deploy: 배포가 장애 원인이라는 판단을 빠르게 내려야 한다
스트리밍 job 배포 후 가장 위험한 창은 처음 5분이다. 새 코드가 올라간 뒤 exception 급증, consumer lag 폭등, checkpoint 실패가 이 구간에 집중된다. 문제는 이 신호가 단순한 "초기화 지연"인지 실제 버그인지를 빠르게 구별해야 한다는 것이다.
2.1 Flink job 배포와 savepoint
Flink에서 stateful job을 새 버전으로 교체할 때 가장 안전한 방법은 savepoint를 찍고 신규 버전을 그 savepoint에서 시작하는 것이다. Flink Kubernetes Operator 문서는 FlinkDeployment의 upgradeMode 필드로 이 동작을 제어할 수 있다고 설명한다.
savepoint: 업그레이드 전 savepoint를 찍고, 신규 버전은 그 savepoint에서 시작한다.last-state: 마지막 성공한 checkpoint에서 시작한다. 더 빠르지만 checkpoint 이후의 in-flight record는 재처리된다.stateless: state 없이 새로 시작한다. stateless job이거나 state 재사용이 불가능할 때만 쓴다.
배포 후 신규 버전에서 exception이 나거나 lag가 회복되지 않으면 rollback을 결정하는 기준을 미리 정해 두어야 한다. 예를 들어 "배포 후 3분 내에 exception rate가 0으로 수렴하지 않으면 이전 savepoint로 롤백한다"는 명시적 기준이 없으면, 담당자마다 판단 시점이 달라진다.
배포 runbook 예시
1. savepoint 생성: trigger savepoint → savepoint ID 기록
2. 신규 버전 배포 (savepoint 지정)
3. 배포 후 3분간 모니터링:
- exception rate: 0이면 정상
- lag trend: 감소하거나 유지이면 정상
- checkpoint: 1회 이상 성공이면 정상
4. 기준 미달 → 이전 버전 + 저장한 savepoint ID로 재배포
5. rollback 후 원인 분석 전까지 재배포 금지2.2 Consumer 쪽 bad deploy
Flink가 아닌 consumer 어플리케이션(Spring Kafka, Go consumer 등) 배포의 경우, savepoint가 없다. 대신 consumer는 committed offset을 기준으로 재시작한다. 위험한 패턴은 두 가지다.
- 재처리 중 멱등성 위반. consumer가 재시작 구간의 record를 다시 처리할 때, downstream DB에 중복 insert가 발생하거나 집계가 틀어진다. 이는 bad deploy 자체보다 설계 결함이지만, 배포 시점에 증상이 드러난다.
- offset 범위가 바뀐 경우. 신규 버전이 오래된 offset을 못 읽거나, 이전에 처리하지 않던 partition을 처음 읽게 되는 경우 초기 lag가 크게 표시된다. 이것은 실제 backlog가 있는 경우와 구별해야 한다.
3. Schema break: 가장 위험한 장애는 에러가 없는 장애다
schema break에는 두 가지 종류가 있다. 하나는 눈에 띄는 장애고, 다른 하나는 눈에 띄지 않는 장애다.
눈에 띄는 schema break: producer가 필드를 삭제하거나 타입을 바꾸면, consumer의 deserialization이 실패하면서 exception이 발생하고 consumer lag가 증가한다. 이 경우는 error log를 보면 원인이 분명하다.
눈에 띄지 않는 schema break: Avro나 Protobuf를 쓸 때 producer가 새 필드를 추가하거나 optional 필드를 제거하면, consumer는 에러 없이 record를 처리하지만 해당 필드의 값이 null이나 기본값으로 채워진다. consumer lag는 0이고 throughput도 정상이지만, 집계 결과나 downstream 데이터에 조용히 오류가 누적된다.
Streamkap의 자료는 이런 silent failure가 특히 위험한 이유를 설명한다. Kafka는 메시지 레벨 에러를 consumer에게 전파하지 않기 때문에, consumer가 record를 "성공적으로" 처리했다고 commit하더라도 내용은 손상돼 있을 수 있다.
3.1 Schema Registry가 방어선이다
Confluent Schema Registry는 schema 변경 전에 호환성 검사를 수행한다. 기본 호환성 레벨은 BACKWARD로, 새 schema로 쓴 데이터를 이전 schema의 consumer가 읽을 수 있는지를 확인한다.
| 호환성 레벨 | 의미 | 일반적 사용 |
|---|---|---|
| BACKWARD | 새 schema → 이전 consumer가 읽을 수 있음 | 기본값. 필드 추가(기본값 있음) 허용 |
| FORWARD | 이전 schema → 새 consumer가 읽을 수 있음 | producer rollback 시나리오 대비 |
| FULL | 양방향 호환 | 엄격한 운영 환경 |
| NONE | 호환성 검사 없음 | 개발 환경에서만 권장 |
Schema Registry가 있어도 다음 실수가 자주 발생한다.
- topic에 Schema Registry를 적용하지 않고 raw JSON을 사용하는 경우.
NONE레벨로 설정해 두고 변경하지 않은 경우.- Schema Registry를 통하지 않고 직접 Kafka에 메시지를 쓰는 스크립트나 임시 도구가 있는 경우.
3.2 Schema break 장애 대응
schema break가 발생했을 때 순서는 다음과 같다.
- consumer error log에서 deserialization 예외 종류를 확인한다.
SchemaParseException,AvroRuntimeException, JSON parse 오류가 있으면 schema 불일치다. - Schema Registry에서 해당 topic의 최신 schema와 이전 schema를 비교한다. 어떤 필드가 바뀌었는지, 언제 변경됐는지 확인한다.
- producer를 이전 schema 버전으로 rollback한다. Schema Registry는 schema id를 메시지 header에 포함하므로 rollback 후 새로 produce된 메시지는 이전 schema로 읽힌다.
- DLQ에 쌓인 broken record를 처리한다. 재처리가 가능하면 origin에서 replay한다. 불가능하면 DLQ record를 분석해 데이터 영향도를 평가한다.
- silent failure 구간의 데이터 품질을 검증한다. 에러 없이 처리됐지만 값이 손상된 구간이 있는지 downstream 데이터를 비교한다.
4. Downstream outage: 스트리밍 파이프라인이 sink를 기다린다
downstream outage는 Kafka나 Flink 자체의 문제가 아니라 외부 시스템 문제지만, 스트리밍 파이프라인에는 빠르게 영향이 전파된다. sink DB가 느려지면 → sink connector/consumer가 write를 기다리며 → backpressure가 upstream으로 전파되고 → consumer lag가 증가한다.
downstream outage의 위험한 점은 이 상황에서 단순히 "기다리면 복구된다"고 판단하다 보면 Kafka topic의 retention 시간이 지나가는 수 있다는 것이다. retention 7일 토픽에서 downstream이 3일 동안 내려가면, 그 구간의 record가 삭제될 위험이 있다.
4.1 DLQ와 retry: 완충 전략
Confluent 문서는 Kafka Connect에서 DLQ(Dead Letter Queue)를 "connector가 처리하지 못한 메시지를 별도 topic에 보관하는 메커니즘"으로 설명한다. downstream outage 상황에서 DLQ는 두 가지 역할을 한다.
- 데이터 보존: sink가 내려간 동안 write에 실패한 record를 DLQ에 쌓아두고, downstream이 복구된 뒤 재처리한다.
- poison message 격리: 특정 record 자체가 sink에서 항상 실패하는 경우(e.g. constraint violation, type error)를 정상 record와 분리해 처리 흐름을 보호한다.
DLQ를 운영할 때 자주 간과하는 점이 있다. DLQ topic 자체의 retention을 짧게 설정하면 재처리 전에 데이터가 삭제될 수 있다. 그리고 DLQ를 감시하는 alert가 없으면 DLQ가 쌓이고 있다는 사실을 모를 수 있다. "DLQ는 사후 장치"가 아니라 "장애 완충 장치"다.
retry 설정도 downstream outage에서 중요하다. downstream이 일시적으로 내려갔다가 복구된다면 retry는 효과적이지만, 오래 내려간 상태에서 지속적으로 retry하면 thread pool이 고갈되거나 consumer heap memory가 고갈될 수 있다. exponential backoff와 max retry count를 함께 설정해야 한다.
4.2 Flink sink 운영 시 circuit breaker 패턴
Flink에서 custom sink를 구현할 때, downstream 장애에 대응하는 방법은 다음 세 가지로 나뉜다.
| 전략 | 설명 | 트레이드오프 |
|---|---|---|
| 재시도 후 실패 전파 | sink write를 N회 retry 후 실패하면 job을 실패시킴. checkpoint에서 재시작 | 데이터 유실 없음, 단 job 재시작 overhead |
| DLQ로 우회 | write 실패 시 DLQ topic에 기록하고 정상 처리 계속 | lag 증가 없음, 단 DLQ 재처리 부담 |
| 가용성 우선 유지 | sink 장애 시에도 job을 계속 실행, 실패 건만 skip | downstream 복구 후 gap 발생 가능 |
어떤 전략이 맞는지는 SLO에 따라 다르다. "데이터 유실이 절대 없어야 함"이면 재시도 후 실패 전파가 맞다. "lag 증가가 없어야 함"이면 DLQ 우회가 맞다. 그 선택을 명시하지 않으면 운영자마다 다른 판단을 내린다.
5. 네 유형을 빠르게 구별하는 진단 순서
장애가 발생했을 때 "어떤 유형인가"를 판단하는 일반 순서는 다음과 같다.
job log / exception 확인
ISR / controller log 확인
sink 상태 / DLQ 확인
Schema Registry event 확인
실무에서 진단을 빠르게 하려면 다음 세 가지를 미리 준비해야 한다.
- 변경 이력 연동: 배포 이벤트, schema 변경 이벤트를 Grafana 대시보드에 annotation으로 표시해 둔다. "이 시점에 무엇이 바뀌었는가"를 log를 뒤지지 않고 바로 알 수 있어야 한다.
- 진단 query 모음: partition 상태 조회, consumer group lag 조회, Schema Registry 변경 이력 조회, sink error rate query를 runbook에 미리 적어 둔다. 장애 중에 명령어를 처음부터 구성하면 시간이 낭비된다.
- DLQ 모니터링: DLQ topic 4개(broker dead, consumer failed, schema error, sink error)를 만들고 각각의 증가 알림을 독립적으로 설정한다. DLQ record 수 증가가 장애 유형의 첫 번째 단서가 된다.
6. 장애 후: 재처리와 gap 검증
어떤 유형이든 장애가 복구된 뒤에는 두 가지를 확인해야 한다.
첫째, 재처리 범위. consumer가 lag를 소화하고 있는 중이라면 순서가 보장되는가? downstream이 멱등 write를 지원하는가? 배포 rollback 전에 produce된 record와 rollback 후 produce된 record가 섞이는 구간은 없는가?
둘째, 데이터 gap 검증. 장애 구간에 데이터가 drop됐거나 중복 처리됐을 수 있다. downstream에서 해당 시간 범위의 record count, aggregate 값, null 비율을 upstream 원천과 비교한다. 이 검증 없이 "장애 복구됨"이라고 선언하면 데이터 품질 문제가 조용히 다음 레이어로 전파된다.
재처리가 필요한 경우 chapter 5(Replay와 재처리)에서 다뤘던 패턴을 적용한다. offset reset 범위는 실제로 영향받은 구간만 최소화하고, 재처리 중 downstream에 미치는 부하를 rate limit로 제어한다.
마무리: 장애 유형을 먼저 분류하면 조치 시간이 절반으로 줄어든다
스트리밍 플랫폼에서 장애는 단일 원인으로 끝나지 않는다. broker failure가 consumer rebalance를 유발하고, bad deploy가 schema 오류처럼 보이기도 하며, downstream outage가 broker 문제로 오해되기도 한다. 그래서 처음 2분 안에 네 유형 중 어디에 해당하는지를 좁히는 것이 핵심이다.
이 판단을 위한 세 가지 준비는 미리 되어 있어야 한다. 변경 이력의 시각화, 진단 query 모음, DLQ per type 모니터링. 이 세 가지가 갖춰져 있으면 새벽 3시에 울리는 alert에도 첫 번째 조치가 10분 안에 나올 수 있다.
References
- Apache Kafka Docs, "Replication": https://kafka.apache.org/documentation/#replication
- Apache Kafka Docs, "Consumer Configs": https://kafka.apache.org/documentation/#consumerconfigs
- Confluent Documentation, "How to Survive a Kafka Outage": https://www.confluent.io/blog/how-to-survive-a-kafka-outage/
- Confluent Documentation, "Kafka Connect Deep Dive – Error Handling and Dead Letter Queues": https://www.confluent.io/blog/kafka-connect-deep-dive-error-handling-dead-letter-queues/
- Confluent Documentation, "Kafka Dead Letter Queue": https://www.confluent.io/learn/kafka-dead-letter-queue/
- Apache Flink Docs, "Task Failure Recovery": https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/task_failure_recovery/
- Apache Flink Kubernetes Operator, "Job Management": https://nightlies.apache.org/flink/flink-kubernetes-operator-docs-main/docs/custom-resource/job-management/
- Streamkap, "Schema Registry in Stream Processing": https://streamkap.com/resources-and-guides/schema-registry-stream-processing
- Medium (Umut Akbulut), "When Schema Evolution Silently Breaks Your Streaming Pipeline": https://medium.com/@umutt.akbulut/when-schema-evolution-silently-breaks-your-streaming-pipeline-2412428d0960