운영과 성능 모니터링
Flink 운영은 “잡이 떠 있다”보다 “계속 따라잡고 있다”를 보는 일
Apache Flink 잡은 대부분 한 번 실행하고 끝나는 프로그램이 아니다. Kafka, CDC, 파일 스트림, 메시지 큐에서 계속 들어오는 이벤트를 받아 상태를 갱신하고 sink로 내보낸다. 그래서 운영에서 첫 번째 질문은 “프로세스가 살아 있는가?”가 아니라 입력 속도, 처리 속도, 상태 저장, 외부 sink가 함께 균형을 유지하는가다.
Flink Web UI에서 job이 RUNNING으로 보이더라도 Kafka lag가 계속 늘거나, checkpoint가 timeout으로 실패하거나, sink가 느려져 upstream operator에 back pressure가 전파되면 이미 서비스 품질은 무너지고 있을 수 있다. 반대로 일시적인 lag spike나 checkpoint duration 증가는 traffic peak 뒤에 회복된다면 장애가 아니라 용량 계획의 신호일 수 있다.
이 장에서는 Flink 운영을 네 가지 축으로 정리한다.
- throughput과 lag: 잡이 입력을 따라잡는가
- back pressure와 busy/idle time: 병목이 어디에서 시작되는가
- checkpoint와 state: 복구 가능한 상태를 안정적으로 남기는가
- 리소스와 배포 설정: 확장·장애·업그레이드에 견딜 준비가 되어 있는가
운영 대시보드의 최소 구조
Flink 대시보드는 예쁘게 많은 그래프를 붙이는 것보다 “문제가 생겼을 때 어디부터 볼지”가 분명해야 한다. 처음에는 아래처럼 파이프라인 단계별로 지표를 묶는 편이 좋다.
이 그림의 핵심은 지표를 계층으로 나눠 보는 것이다. Source lag가 늘고 sink throughput이 줄면 sink나 downstream 저장소부터 의심한다. Operator busy time이 높고 back pressure가 낮다면 해당 operator의 CPU, state access, serialization 비용을 본다. Checkpoint duration이 늘면서 alignment time도 같이 늘면 단순 storage 문제가 아니라 upstream/downstream 병목이 barrier 전파를 늦추는 상황일 수 있다.
Throughput과 lag: 잡이 따라잡고 있는지 보기
가장 먼저 보는 지표는 record가 흐르는지다. Flink는 operator와 task 수준에서 numRecordsIn, numRecordsOut, numRecordsInPerSecond, numRecordsOutPerSecond 계열 지표를 노출한다. Job 전체 합계만 보면 병목이 가려질 수 있으므로 operator별, subtask별로 나눠 봐야 한다.
예를 들어 16개 subtask 중 15개는 정상인데 1개 subtask만 numRecordsOutPerSecond가 거의 0이라면 전체 평균은 그럭저럭 괜찮아 보일 수 있다. 하지만 실제로는 특정 key에 데이터가 몰렸거나, 특정 partition의 외부 호출이 느려졌거나, 한 TaskManager의 리소스가 부족한 상태일 수 있다.
Kafka나 Kinesis 같은 source를 쓴다면 consumer lag는 end-to-end 건강 상태를 보여준다. Lag는 “아직 처리하지 못한 입력이 얼마나 쌓였는가”다. 짧은 peak 뒤에 줄어드는 lag는 정상적인 탄력성 범위일 수 있지만, 같은 traffic 수준에서 lag가 계속 우상향하면 잡이 입력 생산 속도를 따라잡지 못한다는 뜻이다.
운영에서 자주 쓰는 판단은 다음과 같다.
| 관찰 | 해석 | 다음 확인 |
|---|---|---|
| source input rate가 0이고 lag도 없음 | 입력이 실제로 없는 상태일 수 있음 | upstream 생산 상태, watermark idle 처리 |
| source input rate는 높은데 sink output이 낮음 | 중간 operator 또는 sink 병목 가능성 | back pressure, busy time, sink latency |
| lag가 계속 증가 | 처리 용량이 입력보다 작음 | parallelism, key skew, external sink throughput |
| 특정 subtask만 lag/throughput 이상 | partition 또는 key skew 가능성 | key 분포, source partition 할당, hot key |
알림은 절대값 하나로만 걸기 어렵다. 서비스마다 정상 lag 범위가 다르기 때문이다. “lag가 10만 건을 넘으면 장애”보다 “15분 동안 계속 증가하고, 동시에 output rate가 input rate보다 낮다”처럼 추세와 관계를 같이 보는 알림이 운영 소음이 적다.
Back pressure: 느린 downstream이 upstream을 막는 신호
Flink에서 back pressure는 downstream operator가 충분히 빨리 소비하지 못해 upstream operator가 output buffer를 얻지 못하는 상태다. 데이터는 source에서 sink 방향으로 흐르지만, back pressure는 반대 방향으로 전파된다. 그래서 source 쪽에서 back pressure가 보인다고 해서 source가 원인이라는 뜻은 아니다. 오히려 sink나 downstream 저장소가 느려져 source까지 막고 있을 수 있다.
Flink Web UI는 back pressure 상태를 OK, LOW, HIGH로 보여준다. 공식 문서 기준으로 대략 0~10%는 OK, 10~50%는 LOW, 50~100%는 HIGH로 해석한다. 더 중요한 것은 세 지표를 함께 보는 것이다.
backPressuredTimeMsPerSecond: 1초 중 back pressure로 막힌 시간busyTimeMsPerSecond: 실제 작업으로 바빴던 시간idleTimeMsPerSecond: 처리할 입력이 없어 기다린 시간
세 값은 대략 1000ms 주변으로 합쳐진다. 이 조합을 보면 병목 성격이 달라진다.
| 상태 | 가능한 의미 | 운영 대응 |
|---|---|---|
| busy 높음, back pressure 낮음 | 해당 operator가 계산으로 바쁨 | CPU, serialization, state backend, parallelism 확인 |
| back pressure 높음 | downstream이 막힘 | downstream operator, sink, network buffer, 외부 저장소 확인 |
| idle 높음, lag 낮음 | 입력이 적거나 source가 idle | 정상일 수 있음. watermark idle 설정 확인 |
| idle 높음, lag 높음 | source fetch 또는 partition 할당 문제 가능 | connector, consumer group, partition별 lag 확인 |
Back pressure를 볼 때 흔한 실수는 “HIGH가 뜬 operator가 원인”이라고 바로 단정하는 것이다. Back pressure는 전파되므로 원인은 더 downstream에 있을 수 있다. Web UI의 JobGraph에서 sink 방향으로 따라가며 처음으로 busy가 높거나 output이 줄어드는 지점을 찾는 편이 안전하다.
Checkpoint 지표: 장애 복구 능력을 보는 운영 지표
Checkpoint는 Flink 운영에서 생명줄이다. Job이 살아 있어도 checkpoint가 계속 실패한다면 장애가 났을 때 안전하게 돌아올 수 없다. 따라서 checkpoint는 “있으면 좋은 지표”가 아니라 production readiness의 핵심이다.
우선 다음 지표를 본다.
| 지표 | 의미 | 위험 신호 |
|---|---|---|
| last checkpoint duration | 최근 checkpoint 완료 시간 | interval보다 길어져 checkpoint가 계속 이어짐 |
| checkpoint size / state size | 저장해야 하는 상태 크기 | 지속 증가, 예상보다 큰 RocksDB state |
| number of failed checkpoints | 실패한 checkpoint 수 | 간헐적 실패를 넘어 rate가 증가 |
| checkpoint alignment time | barrier alignment 시간 | back pressure나 input skew로 barrier 도착 지연 |
| checkpoint start delay | checkpoint 시작 지연 | barrier 전파 또는 task scheduling 문제 |
Checkpoint duration이 늘었다고 바로 storage만 의심하면 안 된다. Flink 공식 문서는 back pressure 상황에서 checkpoint 시간이 barrier 전파와 alignment time에 의해 지배될 수 있다고 설명한다. 즉, checkpoint timeout은 object storage I/O 문제일 수도 있지만, sink 병목 때문에 barrier가 downstream으로 못 내려가는 문제일 수도 있다.
대응은 원인별로 다르다.
- State가 커져서 snapshot I/O가 느리다면 RocksDB, incremental checkpoint, checkpoint storage 처리량을 확인한다.
- Back pressure 때문에 alignment가 길다면 병목 operator나 sink를 먼저 줄인다.
- In-flight buffer가 너무 많다면 buffer debloating을 검토한다.
- Back pressure가 심해 aligned checkpoint가 계속 실패한다면 unaligned checkpoint를 검토한다.
Unaligned checkpoint는 buffer에 쌓인 in-flight data를 checkpoint state에 포함시켜 barrier가 buffer를 앞질러 가게 한다. 그래서 back pressure 때문에 barrier alignment가 길어지는 상황에는 도움이 될 수 있다. 다만 추가 데이터를 checkpoint storage에 써야 하므로, 이미 state storage I/O가 병목인 상황에서는 오히려 부담이 될 수 있다.
State와 memory: JVM heap만 보면 부족하다
Flink job이 상태를 많이 쓰는지, 어떤 state backend를 쓰는지에 따라 메모리 병목의 모양이 달라진다. HashMapStateBackend는 working state를 JVM heap에 두므로 접근은 빠르지만 heap과 GC 영향을 많이 받는다. EmbeddedRocksDBStateBackend는 local disk와 native memory를 사용하므로 큰 state에 적합하지만 serialization, disk I/O, RocksDB native memory 튜닝이 중요해진다.
특히 Kubernetes나 YARN 같은 container 환경에서는 taskmanager.memory.process.size처럼 전체 JVM process 기준의 메모리 제한을 이해해야 한다. Flink나 사용자 코드가 container limit 밖의 unmanaged off-heap memory를 많이 쓰면 orchestrator가 TaskManager를 kill할 수 있다. RocksDB를 쓸 때는 Flink managed memory로 RocksDB native memory를 관리하는 기본 모델을 유지하는 편이 안전하다.
운영에서 보는 증상은 다음처럼 연결된다.
| 증상 | 의심 지점 | 확인할 것 |
|---|---|---|
| GC pause 증가 | heap state, user object churn | heap 사용량, GC log, object allocation |
| TaskManager OOMKilled | process memory 초과, native/off-heap 과다 | container limit, RocksDB memory, network memory |
| checkpoint size 급증 | state 증가, TTL 부재, key cardinality 증가 | state size by operator, retention/TTL 정책 |
| RocksDB latency 증가 | local disk I/O 또는 compaction 부담 | disk throughput, compaction, managed memory |
State는 비즈니스 로직의 결과물이기도 하다. Window가 닫히지 않거나 TTL이 없거나 key cardinality가 예상보다 커지면 state가 계속 자란다. 그래서 성능 튜닝은 단순히 리소스를 늘리는 일이 아니라 “이 상태가 언제 사라져야 하는가”를 데이터 모델 관점에서 다시 확인하는 작업이기도 하다.
Parallelism, max parallelism, rescale 준비
Flink job을 확장할 때는 parallelism만 보면 부족하다. parallelism은 현재 몇 개의 parallel subtask로 실행할지이고, max parallelism은 stateful operator가 나중에 어디까지 rescale될 수 있는지를 결정한다. 공식 production readiness checklist에서 강조하듯이 stateful operator의 max parallelism은 job이 시작된 뒤 state를 버리지 않고 바꾸기 어렵다.
따라서 production job은 처음부터 다음을 명시하는 편이 좋다.
env.setParallelism(16);
env.setMaxParallelism(256);
events
.keyBy(Event::userId)
.process(new EnrichmentFunction())
.uid("enrichment-v1")
.name("enrichment");여기서 uid도 중요하다. Savepoint restore와 upgrade는 operator state를 operator ID에 매핑한다. 자동 생성 UID는 JobGraph 변경에 취약하므로 stateful operator에는 stable UID를 붙이는 것이 안전하다. 운영 관점에서는 parallelism, max parallelism, UID가 모두 “나중에 장애 없이 바꿀 수 있는가”를 좌우한다.
알림은 원인보다 증상 조합으로 설계하기
좋은 알림은 “지금 당장 사람이 봐야 하는가”를 판단하게 해준다. Flink 지표는 많기 때문에 모든 metric에 threshold를 걸면 알림 피로만 커진다. 처음에는 다음 조합을 추천한다.
| 알림 | 조건 예시 | 의미 |
|---|---|---|
| Processing falling behind | lag가 N분 연속 증가 AND output rate < input rate | 처리 용량 부족 또는 downstream 병목 |
| Checkpoint unhealthy | failed checkpoint rate 증가 OR duration이 timeout에 근접 | 복구 안정성 저하 |
| Sustained back pressure | backPressuredTimeMsPerSecond가 N분 이상 높음 | downstream 병목 지속 |
| No output while input exists | input rate > 0 AND output rate = 0 | logic, sink, serialization, external dependency 문제 |
| TaskManager instability | restart/downtime/OOMKilled 발생 | 리소스 또는 배포 안정성 문제 |
절대값은 환경마다 다르다. 중요한 것은 baseline이다. 평소 traffic에서 checkpoint duration이 20초였는데 배포 뒤 2분으로 늘었다면 timeout까지 여유가 있어도 조사해야 한다. 반대로 peak 시간에 lag가 잠깐 5분치 쌓였다가 10분 안에 해소되는 것이 정상 패턴이라면 즉시 장애로 볼 필요는 없다.
병목 조사 순서
장애 대응 때는 지표를 많이 보는 것보다 순서가 중요하다. 다음 순서로 보면 원인을 좁히기 쉽다.
- 입력이 늘었는가? Source throughput과 upstream 생산량을 확인한다.
- 잡이 따라잡는가? Consumer lag와 output throughput 추세를 본다.
- 어디가 막혔는가? Web UI에서 back pressure, busy, idle을 operator/subtask별로 본다.
- Checkpoint는 안전한가? Duration, failed count, alignment time, state size를 본다.
- 리소스 한계인가? CPU, heap, off-heap, managed memory, disk I/O, network를 본다.
- 데이터 분포 문제인가? Hot key, partition skew, 특정 tenant/user 쏠림을 확인한다.
- 외부 시스템 문제인가? Sink DB, object storage, schema registry, HTTP dependency latency를 본다.
이 순서를 지키면 “TaskManager CPU가 높다” 같은 관찰을 바로 원인으로 착각하는 일을 줄일 수 있다. CPU가 높은 이유가 hot key 때문인지, RocksDB compaction 때문인지, JSON serialization 때문인지, sink retry loop 때문인지는 다음 지표와 로그를 함께 봐야 한다.
배포 전 production readiness 체크리스트
새 Flink job을 production에 올리기 전에는 최소한 다음 항목을 확인한다.
| 항목 | 확인 질문 |
|---|---|
| checkpoint | interval, timeout, min pause, storage 위치가 운영 SLA에 맞는가 |
| state backend | state 크기와 latency 요구에 맞는 backend를 골랐는가 |
| max parallelism | 향후 rescale 범위를 고려해 명시했는가 |
| operator UID | stateful operator에 stable UID를 붙였는가 |
| HA | JobManager HA와 checkpoint storage가 단일 장애점이 아닌가 |
| metrics | Prometheus/Grafana 또는 운영 환경의 metric reporter가 연결됐는가 |
| alert | lag, checkpoint 실패, back pressure, downtime 알림이 있는가 |
| sink semantics | 중복·재시도·transaction/upsert 의미가 문서화됐는가 |
| restore drill | savepoint 또는 checkpoint restore를 실제로 시험했는가 |
| resource limit | container memory, RocksDB managed memory, disk I/O 여유를 확인했는가 |
이 체크리스트에서 restore drill은 특히 중요하다. 백업과 checkpoint는 “존재한다”가 아니라 “복구해 봤다”가 기준이다. Savepoint에서 새 버전을 띄우고 첫 checkpoint가 성공하는지, sink가 중복을 만들지 않는지, operator UID 변경으로 restore가 실패하지 않는지를 실제 절차로 검증해야 한다.
정리
Flink 운영은 네 가지 질문으로 압축할 수 있다.
- 입력을 따라잡고 있는가: throughput, lag, watermark delay
- 병목이 어디인가: back pressure, busy/idle time, subtask skew
- 장애가 나도 복구 가능한가: checkpoint duration, failed checkpoints, state size, alignment time
- 나중에 안전하게 바꿀 수 있는가: max parallelism, stable UID, savepoint restore, memory/resource 설정
좋은 운영 체계는 모든 metric을 보는 것이 아니라, 장애 시나리오별로 지표를 연결해 보는 것이다. Lag가 늘면 output과 back pressure를 보고, checkpoint가 느려지면 state I/O와 alignment를 나눠 보고, TaskManager가 죽으면 JVM heap과 off-heap/native memory를 함께 본다.
Flink는 강력한 스트리밍 엔진이지만, stateful streaming의 운영 난이도는 낮지 않다. Production job에서는 성능 튜닝보다 먼저 복구 가능성, 관측성, rescale 가능성을 설계해야 한다. 그래야 traffic이 늘거나 downstream이 흔들리거나 배포가 실패했을 때 “다시 시작해 보자”가 아니라 “어느 지점에서 안전하게 복구할지”를 말할 수 있다.