Apache Flink 아키텍처와 핵심 개념
왜 Flink인가 — 스트림 처리의 계보
실시간 데이터 처리 요구는 세 갈래에서 발전해왔다. 첫째, Kafka Streams처럼 라이브러리 형태로 JVM 프로세스 안에 내장되는 방식. 둘째, Spark Structured Streaming처럼 마이크로배치로 스트리밍을 흉내 내는 방식. 셋째, Apache Flink처럼 처음부터 무한한 스트림(unbounded stream)을 일급 시민으로 설계한 방식.
Flink의 철학은 하나다. 배치는 유한한 스트림의 특수한 경우다. Flink는 레코드를 묶어서 처리하는 마이크로배치 없이 이벤트가 들어오는 즉시 처리한다. 이 덕분에 Spark Streaming(수백 밀리초 ~ 수초 지연)에 비해 수십 밀리초 수준의 end-to-end latency를 달성할 수 있다.
Apache Flink는 2014년 독일 베를린 공대(TU Berlin) 연구에서 출발해 같은 해 Apache Software Foundation에 합류했다. 현재 2025년 3월 릴리스된 Flink 2.0이 최신 안정 버전이다.
전체 아키텍처 조감
Flink 클러스터는 세 가지 요소로 구성된다.
DataStream / Table API JobGraph 생성
논리적 실행 계획
Job 수신·저장 JobMaster
ExecutionGraph·스케줄링 ResourceManager
슬롯 요청·할당
Source 슬롯 1
Map 슬롯 2
KeyedAgg
Source 슬롯 1
Map 슬롯 2
Sink
S3 / HDFS / 로컬 고가용성 스토리지
ZooKeeper / K8s
Client는 사용자 코드에서 DAG(JobGraph)를 빌드해 JobManager에 제출하고 연결을 끊는다. 이후 실행은 클러스터가 독립적으로 진행한다.
JobManager는 단일 마스터다. 내부에 세 역할이 있다. Dispatcher는 REST API 및 CLI로 Job을 수신한다. JobMaster는 논리적 JobGraph를 물리적 ExecutionGraph로 변환하고 스케줄링·체크포인트를 조율한다. ResourceManager는 TaskManager로부터 슬롯을 요청·반환하며 자원을 추적한다.
TaskManager는 실제 태스크를 실행하는 워커다. 한 TaskManager는 설정된 수만큼의 Task Slot을 갖는다. 슬롯은 TaskManager 내 CPU와 메모리의 고정 비중이다(실제 CPU 격리는 아님). 한 슬롯 안에 여러 오퍼레이터가 체인(operator chain) 으로 이어질 수 있어 네트워크 직렬화 없이 함수 호출처럼 데이터를 전달한다.
병렬도(Parallelism)와 슬롯
Job의 병렬도(parallelism)는 각 오퍼레이터가 몇 개의 병렬 서브태스크로 실행될지를 결정한다. 예컨대 병렬도 4인 keyBy().sum() 파이프라인은 sum 오퍼레이터가 4개 서브태스크로 쪼개져 서로 다른 슬롯에서 돌아간다.
필요한 슬롯 수 = 파이프라인에서 가장 높은 오퍼레이터 병렬도. Flink는 같은 Job의 서로 다른 오퍼레이터 서브태스크를 하나의 슬롯(SlotSharingGroup)에 함께 넣을 수 있다. 이 슬롯 공유 덕분에 슬롯 수 = 최대 병렬도만 있으면 충분하다.
시간 의미론(Time Semantics)
Flink는 세 가지 시간 개념을 구분한다.
| 시간 | 정의 | 사용 시점 |
|---|---|---|
| 이벤트 시간(Event Time) | 이벤트가 실제 발생한 시간. 레코드 안에 포함. | 정확한 결과가 중요할 때 |
| 처리 시간(Processing Time) | Flink 오퍼레이터가 레코드를 처리하는 벽시계 시간 | 낮은 지연이 우선일 때 |
| 수집 시간(Ingestion Time) | 소스 오퍼레이터가 레코드를 받은 시간 | 중간 절충안 |
현업에서는 이벤트 시간을 기준으로 윈도 집계를 하는 경우가 대부분이다. 단, 이벤트가 네트워크 지연·재전송으로 순서가 뒤바뀌어 올 수 있다. Flink는 이 늦게 도착하는 이벤트(out-of-order events) 를 Watermark로 처리한다.
Watermark: 시간의 진행을 추적
Watermark는 스트림 안에 주기적으로 삽입되는 특수 레코드다. Watermark(t)의 의미는 "이 이후로는 이벤트 시간이 t보다 이른 레코드가 더 이상 오지 않는다"는 보증이다.
Watermark가 윈도 종료 시각을 넘어서야 Flink는 해당 윈도를 닫고 결과를 내보낸다. allowedLateness나 sideOutput으로 정해진 지연 이후에 도착한 레코드도 처리할 수 있다.
이벤트 스트림: e1(t=10) e3(t=14) e2(t=11) e4(t=16) W(13) e5(t=18) W(15) ...
↑ 늦게 도착
Watermark W(13): t≤13인 레코드는 더 없다 → [0,10), [10,13) 윈도 닫힘윈도(Window)
시간 기반 집계는 윈도 단위로 이뤄진다. Flink의 주요 윈도 유형:
- 텀블링 윈도(Tumbling): 고정 크기, 서로 겹치지 않는다. 예) 5분마다 집계.
- 슬라이딩 윈도(Sliding): 고정 크기, 일정 간격으로 이동. 겹침이 생긴다. 예) 10분 크기, 1분 슬라이드.
- 세션 윈도(Session): 비활성 갭(gap)을 기준으로 그룹화. 사용자 세션 분석에 적합.
- 글로벌 윈도(Global): 명시적으로 트리거를 지정해야 한다.
상태(State) 관리
Flink의 차별점은 관리형 상태(managed state) 다. 오퍼레이터가 직접 맵이나 변수를 들고 다니는 대신, Flink 런타임이 상태를 소유하고 체크포인트·복구·리스케일 시 이동시켜 준다.
Keyed State는 keyBy() 이후 오퍼레이터에서 사용한다. 키별로 독립된 상태를 갖는다(ValueState, ListState, MapState, ReducingState 등). Operator State는 특정 키와 무관한 상태로 소스·싱크에 주로 쓰인다(Kafka 오프셋 등).
상태 백엔드
상태를 어디에 저장하는지를 상태 백엔드(State Backend)가 결정한다.
| 백엔드 | 상태 저장 위치 | 특징 |
|---|---|---|
| HashMapStateBackend | JVM 힙 메모리 | 빠름, 상태 크기 제한 |
| EmbeddedRocksDBStateBackend | 로컬 디스크 (RocksDB) | 대용량 상태, 증분 체크포인트 지원 |
Flink 2.0부터는 ForSt (Flink over Remote Storage)라는 새로운 상태 백엔드가 도입됐다. RocksDB 대신 S3/HDFS 같은 원격 분산 파일 시스템을 1차 저장소로 쓰고, 로컬 디스크는 캐시로 활용한다. 체크포인트 시간을 최대 94% 단축하고, 장애 복구를 최대 49배 빠르게 한다는 VLDB 2025 결과가 보고됐다.
체크포인트와 Exactly-Once 의미론
Flink는 Chandy-Lamport 분산 스냅샷 알고리즘을 기반으로 체크포인트를 구현한다. 다음 순서로 진행된다.
- JobManager가 체크포인트 N 시작을 결정하고 소스 오퍼레이터에 배리어(barrier) 를 삽입하도록 지시한다.
- 소스는 현재 위치(Kafka 오프셋 등)를 기록하고 데이터 스트림 안에 배리어를 흘려보낸다.
- 각 오퍼레이터는 모든 상류 채널에서 배리어 N을 받으면 자신의 상태를 스냅샷하고 배리어를 하류로 전달한다.
- 싱크 오퍼레이터까지 배리어가 도달하면 JobManager에 완료를 보고한다. 이 시점이 체크포인트 완료다.
- 장애 시 마지막 완료된 체크포인트로 전체 잡을 재시작한다.
소스가 오프셋 재설정(Kafka 오프셋 seek)을 지원하고, 싱크가 트랜잭션 커밋을 지원하면 Exactly-Once 종단 보장이 된다. 체크포인트 완료 후 배리어 이후 데이터를 재처리하더라도 싱크가 이미 커밋한 데이터를 중복 기록하지 않기 때문이다.
세이브포인트(Savepoint) 는 수동으로 트리거하는 체크포인트의 상위 개념이다. 잡 버전 업그레이드, 클러스터 이전, A/B 롤백 목적으로 사용한다. 세이브포인트는 자동으로 삭제되지 않는다.
DataStream API와 Table API / Flink SQL
Flink는 두 계층의 API를 제공한다.
DataStream API는 저수준 Java/Scala/Python API로, 상태·타이머·사이드 출력·비동기 I/O 같은 세밀한 제어가 필요할 때 쓴다.
DataStream<String> stream = env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "Kafka");
stream
.keyBy(Event::getUserId)
.window(TumblingEventTimeWindows.of(Duration.ofMinutes(5)))
.sum("amount")
.print();Table API / Flink SQL은 관계형 추상으로 배치·스트리밍을 같은 SQL로 표현한다. 내부적으로 Calcite 기반 옵티마이저가 최적 실행 계획을 생성한다. 최근 Flink 2.0에서는 SQL 호환성과 성능이 크게 향상됐다.
Kafka와 함께 쓰기
Flink는 Kafka를 소스·싱크로 모두 지원하며, Kafka 커넥터는 Exactly-Once를 위한 2PC(2-Phase Commit) 를 구현한다. Flink가 체크포인트를 완료할 때 Kafka 트랜잭션을 함께 커밋해 메시지가 정확히 한 번만 Kafka에 기록되도록 보장한다.
References
- https://flink.apache.org/2025/03/24/apache-flink-2.0.0-a-new-era-of-real-time-data-processing/
- https://nightlies.apache.org/flink/flink-docs-stable/docs/concepts/flink-architecture/
- https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/state_backends/
- https://risingwave.com/blog/unlocking-apache-flink-architecture-unique-powerful-insights/
- https://www.vldb.org/pvldb/vol18/p4846-mei.pdf
- https://risingwave.com/blog/stream-processing-with-apache-flink-2024s-essential-guide/
- https://medium.com/@akash.d.goel/apache-flink-series-part-6-4ef9ad38e051