Scaling a stateful exactly-once Flink job to 300M+ RPM
Author: Mohamed Moataz El Zein | Source: mohamedelzein.hashnode.dev | Published: 2026-08-09
한 줄 요약
“문제 발생 → 해결하는 config 발견 → 활성화 → 그 config에서 바로 다음 문제 발생”이라는 Flink 특유의 패턴을 겪으며 얻은 13가지 실전 튜닝 — 문서화되지 않은 것들이 가장 아팠다.
핵심 주장/내용
- ①
TransactionNamingStrategy.POOLING— 문서화되지 않은 config이 4시간짜리 재시작 루프를 끝냈다: EXACTLY_ONCE KafkaSink에서 극단적 처리량 + 낮은 체크포인트 간격이라는 특정 조건이 겹치면 Flink가 무한 재시작 루프에 빠진다. 프로덕션 데이터 POC에서 재시작 루프 해소에 4시간 이상 걸렸다- 원인: Flink가 Kafka 트랜잭션을 남용해
<transactionalIdPrefix>-<subtask id>-<checkpoint id>형식의 transactional ID를 수십만 개 만든다. 시작 시 Flink가 Kafka가epoch 0(= 새 ID)을 반환할 때까지 모든 transactional ID를 순회하고, 잡이 돌면서 더 많은 ID를 만들어 검색 공간이 더 커진다. 매 시작마다 Kafka를 수십만 번 때리다 결국 타임아웃 → 죽음 → 재시작 → 반복. FLIP-511이 이 fix를 구현했고 “expert” opt-in config으로 명시되어 있다
- 원인: Flink가 Kafka 트랜잭션을 남용해
- ② V2 Async State API는 RocksDB에서 함정: Async API는 현재 사실상 ForSt 백엔드에서만 “기능적” 이다(문서 마지막 줄에 있었다). 게다가 TTL이 async API에서 조용히 동작하지 않고(
AsyncKeyedStateBackendAdaptor가 TTL을 drop), canonical savepoint도 동작하지 않는다(kubernetes.operator.savepoint.format.type을NATIVE로 설정해야 savepoint가 작동). 가능하면 RocksDB와 async API를 피하라 - ③ 모든 operator에 UID를 설정하라 — 기사의 단 하나의 조언을 꼽자면 이것: 상태 이슈를 피하고 state migration을 훨씬 쉽고 단순하게 만든다.
pipeline.auto-generate-uids를false로 설정해 강제하고 복원 시 state mapping 이슈를 피하라 - ④ rescaling 재시작 후 빠른 복원을 위한 Ingest-db: 수백 GB~TB 상태에서 재시작 후 복원은 악몽이다.
execution.state-recovery.from-local: true, rescaling 재시작에는state.backend.rocksdb.use-ingest-db-restore-mode(상태 복구 중 여러 DB를 “clipping”)- 그런데 복원이 빨라진 대신 재시작 후 첫 체크포인트가 거대해진다 — Ingest-db가 빠른 복원을 위해 서로 다른 체크포인트의 raw 미병합 SST 파일을 RocksDB에 파이프하므로 미병합·미최적화 SST가 많아지고 그것을 빠르게 compact하려는 압력이 Task Manager와 IO에 걸린다.
state.backend.rocksdb.incremental-restore-async-compact-after-rescale와state.backend.rocksdb.rescaling.use-delete-files-in-range를 켜서 복원 직후 빠르고 효율적인 정리와 디스크 회수를 보장하고 첫 체크포인트의 거의 무제한적 증가를 방지
- 그런데 복원이 빨라진 대신 재시작 후 첫 체크포인트가 거대해진다 — Ingest-db가 빠른 복원을 위해 서로 다른 체크포인트의 raw 미병합 SST 파일을 RocksDB에 파이프하므로 미병합·미최적화 SST가 많아지고 그것을 빠르게 compact하려는 압력이 Task Manager와 IO에 걸린다.
- ⑤ LEVELED보다 UNIVERSAL compaction을 시도하라: 수백만 read·write, 더 나쁘게는 수백만 delete가 있는 RocksDB 튜닝은 큰 고통이었다. delete를 해도 증분 체크포인트 크기가 계속 커지고 compaction이 뒤처지고 처리량이 곤두박질칠 정도였다. Universal compaction + 큰 write buffer + 공격적 compaction 설정이 최선이었다.
writebuffer.size,writebuffer.number-to-merge,writebuffer.count,memory.write-buffer-ratio를 늘리고 백그라운드 잡·스레드 수도 늘리면 compaction이 완화된다 — tombstone은 여전히 SST에 써야 하지만 디스크로 flush되는 파일이 더 compact하고 덜 자주 flush된다. RocksDB 튜닝은 잡의 read/write/delete 비율에 크게 의존하므로 구체 숫자는 제시하지 않음 - ⑥ K8s 오토스케일링으로 스파이크를 흡수하되
job.autoscaler.memory.tuning.enabled는 켜지 마라: 저자를 태운 정확한 순서 — 일부 테넌트에서 거대한 스파이크(100M+ RPM)가 오고 상대적으로 조용한 기간이 뒤따른다. 그 조용한 기간에 memory tuning이 켜져 있으면 Flink가maximize-managed-memory때문에 네트워크 버퍼 메모리를 빼서 RocksDB 메모리로 옮긴다. 여기까진 괜찮다. 그런데 다시 거대한 스파이크가 오면 모든 task manager가 OOM에 빠지기 시작하고 오토스케일러는 아무것도 모른 채 자기 결정을 되돌릴 시간이 없어 수동 개입을 강제한다taskmanager.load-balance.mode가MIN_RESOURCES가 아니면 오토스케일러가 유휴 Task Manager를 릴리스하지 못해 오토스케일러의 목적을 무색하게 만들 수 있다 (문서화되어 있지 않고 저자의 경험)
- ⑦ 큰 상태 잡은 savepoint보다
upgradeMode: last-state를 선호하라: 특히 오토스케일링을 쓰고 약간의 thrashing이 있다면. 오토스케일링으로 15~20분마다 재시작한다면 오토스케일러가 결정할 때마다 거대한 savepoint를 뜨고 싶지 않다. 다만 Flink 버전이나 잡을 업그레이드할 계획이면upgradeMode: savepoint로 전환하는 게 안전하다 - ⑧ S3 rate limit에 걸리기 시작하면 S3 entropy 사용:
s3.entropy.key: _entropy_,s3.entropy.length(기본 4). 저자는 S3에 폴더가 너무 많이 생겨 관리가 어려워져 길이를 2로 줄임 - ⑨ 높은 Max Parallelism으로 key group 수를 미래 대비하라: max key group 수가 나중에 스케일링할 때 병목이 될 수 있고 operator가 기본 max parallelism을 넘으면 state migration이 필요할 수 있다. 고도 합성수(highly composite number)로 설정해 고른 부하 분산을 보장하되 task manager 메모리 사용이 늘 수 있음을 인지. 저자는 4096을 썼지만 5040이 고도 합성수라 아마 더 낫다
- ⑩ backpressure가 체크포인트 완료를 막을 정도면 unaligned checkpoint를 시도하라: 다만 많은 사람이 실제 근본 원인을 고치는 대신 증상을 숨기는 시도로 본다는 점을 인지하고 진짜 병목을 고치는 동안의 진통제로 취급하라
- ⑪ 노드 로컬 SSD를 쓰고 EC2 노드 네트워크나 MSK 브로커 EBS에 throttle되고 있지 않은지 확인하라: 상태 접근이 잡을 심각하게 늦추고 이미 상태 접근을 최적화·최소화했다면 네트워크 연결 EBS가 아니라 EC2 노드에 물리적으로 붙은 SSD Instance Store를 써라. 빠르지만 모든 Flink 잡이 즉각적 처리량 개선을 보지는 않으므로 상태 접근이 정말 생각만큼 큰 문제인지 프로파일링해야 한다
- ⑫ Kafka producer/consumer config를 고처리량용으로 튜닝:
max.poll.records,fetch.max.wait.ms,fetch.min.bytes,max.partition.fetch.bytes,fetch.max.bytes,default.api.timeout.ms,request.timeout.ms,linger.ms,batch.size,buffer.memory,delivery.timeout.ms - ⑬ S3 상태 다운로드 속도를 위해 s5cmd: 상태 다운로드·복원 속도를 유의미하게 개선. 저자는 S3 entropy와 함께 작동시키는 데는 실패
주요 수치 / 사실
- 트래픽 스파이크가 분당 수억 요청, 수백 TB 데이터 인제스트를 넘었고 요구사항은 Flink 확장, Kafka와의 exactly-once 시맨틱, 최대한 실시간에 가깝고 매우 잦은 체크포인트
- 저자의 총평: “문제가 있고, 그것을 해결하는 config를 찾고, 활성화하고, 그 새로 적용한 config에서 바로 또 다른 문제가 튀어나오는 패턴이 Flink에서 불행히도 꽤 흔하다”
관련 위키
- Real-Time Stream Processing
- Database Concurrency Control
- Distributed Systems Reliability
- Idempotency
- Object Storage Evolution
- Data Engineering FinOps
Source: 원문 보기