Apache Flink의 스트림 처리는 병렬도(parallelism)와 window 설정에 따라 여러 Task Slot에서 동시에 수행된다. 각 태스크는 독립적으로 실행되며, 처리 중간 결과는 다른 태스크로 전달된다. 이로 인해 태스크 간 데이터 전송 메커니즘은 Flink 실행 모델의 핵심 구성 요소가 된다.
Flink는 태스크 간 데이터를 객체나 JSON 형태로 직접 전달하지 않는다. 자바 객체를 바이트 배열로 직렬화한 뒤 네트워크 버퍼(Network Buffer)에 적재하여 전송한다.
생산자 태스크의 전송 속도가 소비자 태스크의 처리 속도를 초과하면 네트워크 버퍼에 데이터가 지속적으로 누적된다. 이러한 상태가 장시간 유지되면 전체 파이프라인 지연이 증가하고, 심한 경우 메모리 고갈(OOM)로 이어질 수 있다.
Flink는 이러한 문제를 방지하기 위해 크레딧 기반 플로우 컨트롤을 사용하며, 동시에 네트워크 버퍼 풀을 동적으로 관리하여 메모리 사용 효율과 처리 안정성을 함께 확보한다.
태스크 간 데이터는 셔플(shuffle) 전략에 따라 하위 태스크로 재분배된다. 셔플 전략은 각 레코드가 어느 태스크로 전달될지를 결정하는 라우팅 규칙이다.
| 전략 | 분배 기준 | 특징 | 사용 사례 |
|---|---|---|---|
| Hash Partitioning | key 기반 해시 | 동일 key는 항상 동일 태스크로 전달됨 | 사용자별 집계, 상태 기반 처리 |
| Broadcast | 전체 태스크 | 모든 레코드를 모든 태스크에 전달 | 작은 참조 데이터 공유 |
| Rebalance | 라운드 로빈 | 키 무시, 균등 분배 | 데이터 쏠림(Skew) 완화 |
| Rescale | TaskManager 로컬 | 같은 노드 내 태스크로만 분배 | 네트워크 비용 최소화 |
해시 파티셔닝은 다음 수식으로 동작한다.
targetTask = hash(key) % numberOfTasks
예를 들어 userId가 100이고 태스크 수가 4개라면 다음과 같이 계산된다.
hash(100) % 4 = 2
이 경우 해당 레코드는 항상 task 2로 전달된다. 이 구조를 통해 동일 사용자의 데이터는 항상 동일 태스크에서 처리되며, 안정적인 상태 관리와 정확한 집계가 가능해진다.
셔플 전략이 데이터의 전달 대상을 결정한다면, 데이터 전송 방식은 언제 어떻게 전달할지를 결정한다. Flink는 다음 두 가지 실행 방식을 제공한다.
| 방식 | 실행 구조 | 중간 데이터 저장 | 지연 시간 | 리소스 사용 |
|---|---|---|---|---|
| 파이프라인 방식 | 상·하위 태스크 동시 실행 | 메모리 | 매우 낮음 | 높음 |
| 블로킹 디스크 방식 | 상위 종료 후 하위 실행 | 로컬 디스크 | 높음 | 낮음 |
파이프라인 방식은 스트리밍 처리의 기본 전략이다. 레코드가 생성되는 즉시 하위 태스크로 전달되며, 전체 파이프라인이 동시에 실행된다. 이 방식은 지연 시간이 매우 짧지만, 동시에 실행되는 태스크 수가 많아 메모리와 네트워크 사용량이 증가한다.
블로킹 디스크 방식은 배치 처리에 적합하다. 상위 태스크가 모든 데이터를 처리하고 종료한 뒤 하위 태스크가 시작되며, 중간 결과는 로컬 디스크에 저장된다. 슬롯 재사용이 가능하고 메모리 사용량이 적지만, 디스크 I/O 비용과 전체 지연 시간이 증가한다.
송신 태스크가 초당 100,000개의 레코드를 전송하지만, 수신 태스크가 초당 50,000개만 처리할 수 있다면 처리되지 못한 데이터가 계속 쌓이게 된다. 이런 상태를 일반적으로 consumer lag이라고 한다.
기존의 백프레셔 방식에서는 수신 측이 포화 상태가 되면 송신 측에 전송 중단을 요청한다. 하지만 이 방식에는 다음과 같은 한계가 있다.
Flink는 이러한 문제를 피하기 위해 크레딧 기반 제어 방식을 사용한다.
수신 측은 현재 자신이 처리할 수 있는 만큼의 네트워크 버퍼 수를 크레딧(credit)으로 환산하여 송신 측에 전달한다. 송신 측은 이 크레딧이 남아 있을 때만 데이터를 전송할 수 있으며, 버퍼 하나를 보낼 때마다 크레딧을 하나씩 차감한다.
Flink는 태스크 간 데이터를 전송할 때 TaskManager 단위의 네트워크 버퍼 풀(Network Buffer Pool)을 사용한다. 이 버퍼 풀은 TaskManager가 시작될 때 한 번 생성되며, 실행 중에는 크기가 변경되지 않는다.
각 네트워크 버퍼는 고정 크기(기본 32KB)로 할당된다. 데이터 전송이 끝나면 버퍼를 해제하지 않고 다시 재사용한다. 이 방식으로 메모리 할당과 해제 비용을 줄이고, 불필요한 GC 발생도 최소화한다.
예를 들어 소스 태스크와 맵 태스크의 병렬도가 각각 10이라면, 두 연산자 사이에는 10 × 10 = 100개의 네트워크 채널이 만들어진다. 각 채널은 독립적인 데이터 전송 경로로 동작한다.
네트워크 버퍼 풀이 1GB인 경우, 기본 버퍼 크기 32KB 기준으로 전체 버퍼 수는 약 32,000개가 된다.
각 채널에는 최소 2개의 버퍼가 항상 할당된다. 나머지 버퍼는 공유 풀(shared pool)에 두고, 트래픽이 많은 채널에 우선적으로 추가 할당한다. 반대로 사용량이 적은 채널은 최소 버퍼만 유지한다.
이 구조 덕분에 Flink는 다음과 같은 특성을 갖는다.
버퍼가 부족하면 데이터 전송이 지연되면서 백프레셔가 발생한다. 반대로 버퍼 풀이 지나치게 크면 사용되지 않는 메모리가 늘어나 시스템 전체의 메모리 효율이 떨어진다.