1. How Flink Is Built

JobManager와 TaskManager, operator와 subtask, 그리고 slot

  • Haram Lee
  • 2026-09-28
  • studies / Topics / Flink

장애가 나도 데이터를 잃거나 두 번 처리하지 않으려면 Flink에서 무엇을 알아야 할까?

  • Flink는 어떤 프로세스들로 이루어져 있고, 내가 쓴 코드는 어떻게 실행되나?
  • 연산자가 기억하는 상태는 어디에 있고, 병렬도를 바꾸면 어떻게 옮겨지나?
  • 흐르는 스트림의 “한 시점"을 어떻게 멈추지 않고 저장하나?
  • Kafka를 읽고 쓸 때 exactly-once는 어디까지 보장되나?
  • 운영에서는 재시작, HA, 배포를 어떻게 다루나?

이 시리즈는 Flink를 운영하는 쪽에서 알아야 할 개념을 차례로 정리한다. 설정 key와 기본값은 Flink 2.2 공식 문서를 기준으로 했다. 논문 자체의 절별 해설은 Flink 논문 노트에 따로 정리했고, 여기서는 그 위에서 실제로 job을 돌릴 때 필요한 개념에 집중한다.

Flink는 끝나지 않는 데이터 흐름을, 상태를 들고 있는 연산자들의 파이프라인으로 계속 처리하는 엔진이다.

Spark가 큰 데이터를 조각내서 한 번에 처리하는 쪽에서 출발했다면, Flink는 반대로 스트림에서 출발했다. 배치는 끝이 있는 스트림, 즉 스트림의 특수한 경우로 본다. 이것이 2015년 논문의 핵심 주장이다.

Flink가 스트림 처리에서 강한 이유는 세 가지로 정리할 수 있다.

  1. 레코드가 들어오는 즉시 흐른다. micro-batch로 모았다가 처리하지 않고, 연산자 사이에서 레코드를 파이프라인으로 계속 넘긴다. 그래서 지연이 짧다.
  2. 상태를 엔진이 관리한다. 사용자가 집계 중간값이나 window 내용을 외부 DB에 따로 저장할 필요가 없다. 엔진이 상태를 나눠 들고, 스냅샷을 찍고, 복구하고, 재분배한다.
  3. 처리를 멈추지 않고 일관된 스냅샷을 찍는다. 데이터 흐름 속에 barrier라는 표시를 끼워 넣어 전체 파이프라인의 상태를 저장한다. 장애가 나도 상태를 exactly-once로 복구한다.

2번은 2장에서, 3번은 3장과 4장에서 자세히 본다.

1.2 A Short History

Flink는 2009년 무렵 베를린 공대 등의 연구 프로젝트 Stratosphere에서 시작했다. 원래는 MapReduce보다 표현력 있는 배치 처리 엔진이었다. 2014년에 Apache 인큐베이터에 들어가며 이름을 Flink로 바꿨고, 같은 해 최상위 프로젝트가 됐다. 이 시기에 엔진의 핵심을 파이프라인으로 흐르는 streaming dataflow로 정리하고, 배치는 그 위에 얹었다.

2015년 첫 논문은 DataSet(배치)과 DataStream(스트림) API가 같은 런타임 위에 있다는 설계와, checkpoint 알고리즘 ABS를 소개했다. 2017년 두 번째 논문은 큰 상태를 다루는 방법, 즉 key group, 비동기와 증분 스냅샷, savepoint를 정리했다. 이후 1.x에서 Table API와 SQL이 강화됐고, 1.11에서 unaligned checkpoint가 들어왔다. 2.0(2025)에서는 오래된 DataSet API가 빠지고 DataStream, Table API, SQL로 정리됐으며, 상태를 원격 저장소에 두는 ForSt state backend가 들어왔다.

1.3 The Cluster: JobManager and TaskManagers

Flink cluster는 조율하는 JobManager 하나와 실제로 일하는 TaskManager 여러 개로 이루어진다.

flowchart TD
  client["Client"] -->|"JobGraph 제출"| jm
  subgraph jm["JobManager"]
    disp["Dispatcher<br/>REST, Web UI"]
    rm["ResourceManager<br/>slot 확보와 배정"]
    jmaster["JobMaster<br/>job 하나의 실행 관리"]
  end
  jm -->|"task 배포, checkpoint 트리거"| tm1["TaskManager 1<br/>slot · slot"]
  jm --> tm2["TaskManager 2<br/>slot · slot"]
  jm --> tm3["TaskManager 3<br/>slot · slot"]
  tm1 <-->|"데이터 직접 교환"| tm2
  tm2 <--> tm3

Client는 사용자 프로그램을 dataflow graph로 바꿔 JobManager에 제출한다. 이때 연산자 사이를 오가는 데이터 타입을 보고 serializer도 만든다. 제출하고 나면 client는 없어도 된다. Spark driver처럼 실행 내내 붙어 있지 않는다. Application 모드에서는 main()이 client가 아니라 cluster 안에서 실행된다.

JobManager는 분산 실행을 조율한다. 공식 문서는 이를 세 부분으로 나눈다.

  • Dispatcher는 job 제출을 REST로 받고 Web UI를 띄운다. job마다 JobMaster를 하나 만든다.
  • ResourceManager는 task slot을 관리한다. YARN이나 Kubernetes 위에서 돌 때는 필요한 만큼 TaskManager를 새로 띄워 달라고 요청한다.
  • JobMaster는 job 하나의 실행을 책임진다. task를 배치하고, checkpoint를 시작하고, 장애가 나면 복구를 지휘한다.

JobManager는 기본적으로 하나뿐이라 단일 장애 지점이 된다. 이 약점을 보완하는 HA 구성은 5장에서 다룬다.

TaskManager는 실제 처리를 맡는다. 연산자를 실행하고, network buffer pool을 두고, 다른 TaskManager와 직접 연결해 스트림을 주고받는다. 데이터는 JobManager를 거치지 않는다.

1.4 Task Slots

task slot은 스케줄링의 최소 단위다. TaskManager는 managed memory를 slot 수만큼 고정 크기로 나눠 준다. CPU는 나누지 않는다. 그래서 slot은 CPU 코어를 할당한 것이라기보다 메모리를 나눈 단위에 가깝다. TaskManager 하나를 slot 몇 개로 나눌지는 taskmanager.numberOfTaskSlots로 정한다.

같은 job이라면 서로 다른 연산자의 subtask가 한 slot에 같이 들어갈 수 있다. 이를 slot sharing이라고 한다. 그래서 job에 필요한 slot 수는 연산자 수의 합이 아니라 가장 큰 병렬도와 같다. 가벼운 source와 무거운 window 연산자가 slot을 하나씩 따로 차지하면 자원이 낭비된다. 반면 한 slot을 공유하면 무거운 일이 TaskManager 사이에 고르게 퍼진다.

1.5 Operators and Subtasks

Flink 프로그램은 상태를 가진 연산자(operator)들이 스트림으로 연결된 방향 그래프다. map, keyBy, window를 호출할 때마다 이 그래프에 노드가 더해진다. operator는 논리적인 처리 노드이고, 실제로 실행되는 것은 그 병렬 인스턴스인 subtask다. 병렬도가 3이면 operator 하나가 subtask 3개가 되고, 각 subtask가 스트림의 한 조각(stream partition)을 맡는다.

flowchart LR
  subgraph logical["논리 그래프"]
    s["source"] --> m["map"] --> w["window"] --> k["sink"]
  end
flowchart LR
  s1["source[1]"] --> m1["map[1]"]
  s2["source[2]"] --> m2["map[2]"]
  s3["source[3]"] --> m3["map[3]"]
  m1 --> w1["window[1]"]
  m1 --> w2["window[2]"]
  m2 --> w1
  m2 --> w2
  m3 --> w1
  m3 --> w2
  w1 --> k1["sink[1]"]
  w2 --> k2["sink[2]"]

두 그림 중 아래쪽이 실행 그래프다. operator 사이의 연결 방식은 두 가지로 나뉜다. source → map처럼 1:1로 이어지면(forward) 순서가 유지된다. keyBy처럼 key로 다시 나누면 모든 subtask가 다음 operator의 모든 subtask로 레코드를 보낸다. 이렇게 재분배를 거치면 subtask 한 쌍 안에서만 순서가 유지되고 전체 순서는 보장되지 않는다.

1:1로 이어지고 병렬도가 같은 operator들은 한 thread의 task 하나로 합친다. 이를 chaining이라고 한다. thread 사이에 레코드를 넘기고 직렬화하는 비용이 사라져, 처리량이 늘고 지연이 줄어든다. Spark에서 shuffle 경계 안의 연산이 한 stage로 pipelining되는 것과 비슷하다.

1.6 Records Flow in Buffers

레코드는 하나씩 네트워크로 나가지 않는다. output buffer에 모았다가, buffer가 차거나 timeout이 되면 보낸다. 이 timeout이 지연과 처리량 사이의 균형을 정한다. 2015년 논문 실험에서는 timeout을 짧게 두면 p99 지연 20ms로 초당 150만 건을 처리했고, 50ms로 늘리면 초당 8천만 건을 처리했다. 이 값을 조절하는 설정은 5장에서 본다.

flowchart LR
  a["operator A"] -->|record| buf["output buffer"]
  buf -->|"가득 차거나<br/>timeout이 지나면 전송"| b["operator B"]

이 buffer 구조에서 backpressure가 자연스럽게 생긴다. 받는 쪽 buffer가 차면 보내는 쪽은 빈 buffer를 얻지 못해 기다린다. 이 대기가 source까지 거슬러 올라가서, 느린 operator가 전체 속도를 늦춘다. 별도의 속도 조절 장치가 없어도 파이프라인 구조 자체에서 생기는 성질이다. 이 backpressure는 3장에서 checkpoint를 느리게 만드는 원인으로 다시 등장한다.

배치처럼 입력에 끝이 있으면 중간 결과를 blocking 방식으로 교환할 수도 있다. 이 방식에서는 앞 operator가 결과를 다 만들 때까지 모아 뒀다가 넘긴다. 스트림에서는 pipelined 방식이 기본이다.

1.7 Time, Watermarks, and Windows

스트림 처리에는 시간이 두 가지 있다. processing time은 처리하는 머신의 시계 기준 시각이다. 간단하고 지연이 짧지만, 같은 데이터를 다시 돌리면 결과가 달라질 수 있다. event time은 이벤트가 실제로 일어난 시각이고 레코드 안에 들어 있다. 순서가 뒤섞여 도착해도 결과가 결정적이다.

event time 기준으로 10:00~10:05 window를 닫으려면 10:05 이전 이벤트가 모두 도착했다는 것을 알아야 한다. 이 신호가 watermark다. Watermark(t)는 이 스트림의 event time이 t에 도달했으니 이제 t 이하 이벤트는 오지 않는다고 보겠다는 선언이다.

  • watermark도 barrier처럼 스트림 안에 끼어서 흐른다.
  • source의 각 subtask가 watermark를 만든다.
  • 입력이 여러 개인 operator는 모든 입력의 watermark 중 최솟값을 자기 event time으로 삼는다. 한 입력이라도 늦으면 전체가 기다린다. partition 하나에 데이터가 들어오지 않으면 window가 닫히지 않는 흔한 문제가 여기서 생긴다. 이 문제는 idle source 설정으로 푼다.
  • watermark 뒤에 도착한 이벤트는 late 이벤트다. allowed lateness로 받아 줄지, 버릴지, side output으로 뺄지 정한다.

window는 끝없는 스트림을 유한한 묶음으로 자르는 방법이다. 겹치지 않는 고정 구간(tumbling), 겹치는 고정 구간(sliding), 활동 간격 기준(session)이 대표적이다. window에 담긴 내용은 keyed state로 저장되므로 checkpoint로 함께 복구된다.

관점SparkFlink
기본 모델배치가 기본이고, 스트림은 micro-batch로 처리한다스트림이 기본이고, 배치는 끝이 있는 스트림으로 본다
조율자driver. 실행 내내 떠 있으면서 계획을 세우고 결과를 모은다JobManager. 조율만 하고, client는 제출한 뒤 빠진다
작업 실행executor. 코어 수만큼 task를 동시에 실행한다TaskManager. slot마다 task를 실행한다
연산 묶기shuffle 경계로 stage를 자르고 stage 안을 pipelining한다1:1로 이어진 operator를 chaining하고, operator 사이로 레코드가 계속 흐른다
장애 복구lineage를 따라 사라진 partition만 다시 계산한다마지막 checkpoint의 상태를 복원하고 source를 되감는다
상태streaming state storemanaged state, state backend, key group
Discussion