Apache Flink와 Storm 스트림 처리, OS 최적화까지 연결하기

Apache Flink와 Storm의 실행 모델, 상태 관리, 체크포인트를 살피고 네트워크·메모리·CPU·I/O 최적화 관점을 정리한다.

2026-08-14 · 최초 발행 2026-01-19

끝나지 않는 이벤트를 다루는 처리 모델

실시간 데이터 처리는 계속 생성되는 데이터 스트림을 밀리초 단위 지연시간으로 처리한다. 유한한 데이터셋을 한 번에 끝내는 배치 처리와 달리, 스트림 처리 시스템은 끝없이 이어지는 이벤트를 받아 변환하고 결과를 내보낸다.

이 차이 때문에 윈도우 연산, 상태 유지, 장애 시 중복이나 손실 없이 처리하기 위한 Exactly-Once 보장이 중요해진다. MapReduce나 Spark 같은 배치 처리는 유한한 데이터셋의 일괄 처리에 적합하고, 스트림 처리는 시간 또는 이벤트 수를 기준으로 데이터를 묶어 연산하면서 집계 값과 조인 상태를 계속 유지한다.

이벤트 소스(Kafka)SourceOperatorTransform(Map, Filter)WindowedAggregationSinkOperator출력(DB, HDFS)상태 백엔드(RocksDB)체크포인트(분산 스냅샷)

파이프라인은 Source가 이벤트를 받아 Operator 체인으로 전달하고, 변환과 집계를 거쳐 Sink에 결과를 기록하는 형태다. 상태 백엔드는 집계 결과를 보관하며, 체크포인트는 장애가 났을 때 복구할 스냅샷을 남긴다.

Flink가 작업과 상태를 배치하는 방식

Flink는 DataStream API를 통해 선언적으로 스트림 처리를 구성한다. JobManager는 작업 스케줄링과 체크포인트 조정을 맡고, TaskManager는 데이터를 실제로 처리하는 워커 노드다. TaskManager 안의 Task Slot은 병렬 실행 단위이며, 여러 연산자를 하나의 Task로 묶는 Operator Chain은 실행 오버헤드를 낮춘다.

네트워크 버퍼네트워크 버퍼JobManager(마스터)TaskManager 1TaskManager 2Task Slot 1Task Slot 2Task Slot 3Task Slot 4Source Task(파티션 0)Source Task(파티션 1)Transform TaskSink Task

JobManager가 전체 작업을 조정하면 TaskManager는 Task Slot을 이용해 병렬 작업을 실행한다. 각 Source Task는 파티션을 독립적으로 처리하고, 다음 Task로 데이터가 넘어갈 때는 네트워크 버퍼가 사용된다.

상태는 키-밸류 형태로 관리할 수 있다. 소규모 상태는 힙 메모리에 저장하는 MemoryStateBackend가 대상이며, FsStateBackend는 로컬 메모리와 HDFS 체크포인트를 함께 사용한다. 대규모 상태는 디스크를 활용하는 RocksDBStateBackend에 저장한다.

체크포인트는 Chandy-Lamport 분산 스냅샷 알고리즘을 사용한다. JobManager가 Source에 Barrier를 넣으면 각 Task는 Barrier를 받는 시점에 로컬 상태를 스냅샷하고, 모든 Task의 완료가 확인되면 체크포인트를 커밋한다.

Sink TaskTransform TaskSource TaskJobManagerSink TaskTransform TaskSource TaskJobManager모든 Task 완료 시체크포인트 커밋Barrier 주입(체크포인트 시작)로컬 상태스냅샷Barrier 전달로컬 상태스냅샷Barrier 전달로컬 상태스냅샷체크포인트 완료(ACK)

Storm 토폴로지와 전달 보장

Storm은 입력을 만드는 Spout과 이벤트를 가공하는 Bolt로 토폴로지를 표현한다. Nimbus는 클러스터 마스터로서 토폴로지를 배포하고 관리한다. Supervisor는 워커 노드에서 Executor 프로세스를 실행하며, Tuple은 필드 리스트로 구성된 불변 이벤트 레코드다.

Tuple 전송Tuple 전송Nimbus(마스터)Supervisor 1Supervisor 2Worker JVM 1Worker JVM 2Worker JVM 3Executor 1(Spout)Executor 2(Bolt)Executor 3(Bolt)Executor 4(Bolt)

Nimbus가 Supervisor에 토폴로지를 배포하면 Supervisor는 Worker JVM을 시작하고, Worker는 여러 Executor 스레드에서 Spout과 Bolt를 처리한다.

메시지 전달 보장은 요구 수준에 맞춰 선택한다. At-Most-Once는 ACK 없이 최선 노력으로 전달하므로 손실될 수 있다. At-Least-Once는 ACK 기반 재전송을 보장하지만 중복 가능성이 있다. Exactly-Once는 Trident API를 통해 제공한다.

지연시간을 좌우하는 네트워크 경로

스트림 처리 노드는 대량의 작은 메시지를 주고받는다. 따라서 소켓 버퍼, TCP 버퍼 자동 조정, 연결 대기열을 다루는 net.core.rmem_max, net.core.wmem_max, net.ipv4.tcp_rmem, net.ipv4.tcp_wmem, net.core.somaxconn 설정이 통신 경로에 영향을 준다. TCP Fast Open은 3-way 핸드셰이크를 단축해 지연시간을 낮춘다.

Zero-Copy는 CPU 사이클을 아끼고 대역폭을 높이기 위한 방식이다.

DMAsendfile()DMA우회네트워크 카드(NIC)커널 버퍼소켓 버퍼NIC 전송사용자 공간복사 없음

sendfile() 시스템 콜은 커널 버퍼의 데이터를 소켓 버퍼로 직접 전송해 사용자 공간 복사를 생략한다. 이후 DMA를 이용해 CPU 개입 없이 NIC로 전송한다.

NIC 인터럽트 빈도를 줄이는 Interrupt Coalescing, 수신 패킷을 여러 CPU 코어로 분산하는 RSS(Receive Side Scaling), Nagle 알고리즘을 비활성화하는 TCP No Delay, 커널 네트워크 스택을 우회하는 XDP(eXpress Data Path)도 함께 검토할 수 있다.

JVM 메모리와 GC 정지 구간

JVM 힙 밖의 메모리를 쓰면 GC 압력을 줄일 수 있다. Flink 네트워크 버퍼는 Off-heap Direct ByteBuffer로 네트워크 I/O를 수행하고, RocksDB 블록 캐시는 Off-heap 메모리에 상태 블록을 캐싱한다. -XX:MaxDirectMemorySize는 Off-heap 한계를 설정하며, 대용량 상태는 mmap()으로 가상 메모리에 매핑할 수 있다.

낮은 지연시간이 필요한 스트림 처리에서는 GC 일시정지를 줄이는 것이 관건이다.

JVM 구조Young Generation(Eden + Survivor)Old Generation(장수 객체)Minor GC(수십 ms)Major GC(수백 ms)G1GC(점진적 수집)혼합 수집(Young + Old 일부)예측 가능한일시정지

G1GC는 힙을 리전으로 나누고 Young 및 Old Generation을 섞어 수집해 일시정지 시간을 예측 가능한 범위로 유지한다. -XX:+UseG1GC로 저지연 GC를 활성화하고, -XX:MaxGCPauseMillis=200으로 일시정지 목표를 지정할 수 있다. Young과 Old의 비율을 조정해 Major GC 빈도를 낮추고, 상태 백엔드를 RocksDB로 두어 힙 사용을 최소화하는 방식도 연결된다.

CPU와 NUMA를 작업 배치에 맞추기

지연시간 민감한 작업에는 실시간 스케줄링과 CPU 고정을 고려할 수 있다. SCHED_FIFO는 우선순위 기반의 선점형 실시간 스케줄링이고, SCHED_RR은 실시간 라운드 로빈 스케줄링이다. taskset은 프로세스를 특정 CPU 코어에 고정하며, Isolcpus는 특정 코어를 일반 스케줄링에서 격리해 스트림 처리 전용으로 사용할 수 있게 한다.

최소 원격 액세스NUMA 노드 0CPU 0-7메모리 뱅크 0NUMA 노드 1CPU 8-15메모리 뱅크 1Flink TaskManager(노드 0 전용)Kafka Consumer(노드 1 전용)

NUMA 환경에서는 프로세스를 특정 노드에 배치해 로컬 메모리 액세스를 늘리고 원격 노드 접근으로 인한 지연시간을 줄일 수 있다.

상태 저장소의 I/O 병목 줄이기

RocksDB는 Flink에서 대규모 상태를 저장할 때 사용하는 임베디드 키-밸류 스토어다. 블록 캐시는 자주 접근하는 블록을 메모리에 두고, Write Buffer 크기는 메모리 테이블 크기를 늘려 쓰기 배치를 최적화한다. Level-Based Compaction은 읽기 성능을 최적화하며, Bloom Filter는 키 존재 여부를 빠르게 판단해 디스크 I/O를 낮춘다.

RocksDB를 NVMe SSD에 배치하면 랜덤 읽기 성능을 높일 수 있다. XFS 파일시스템에서는 noatime, nodiratime 옵션으로 메타데이터 갱신을 줄이고, SSD에서는 noop 또는 none I/O 스케줄러를 사용한다. 랜덤 액세스 패턴에서는 Read-Ahead를 비활성화해 불필요한 읽기를 피한다.

처리량만 보지 않는 관측

성능 조정에는 처리량, 지연시간, 백프레셔, 체크포인트 시간을 함께 추적해야 한다.

Prometheus(메트릭 수집)처리량(레코드/초)지연시간(P50, P99)백프레셔(버퍼 사용률)체크포인트 시간Grafana대시보드알림(지연시간 초과)

Prometheus가 처리량, 지연시간, 백프레셔, 체크포인트 시간을 수집하고 Grafana가 이를 시각화한다. 지연시간이 임계값을 넘으면 알림을 발송한다.

백프레셔는 다운스트림의 처리 속도가 업스트림보다 느릴 때 나타난다. Flink Web UI의 백프레셔 지표로 버퍼 채움율을 확인하고, 처리량이 낮은 Operator를 찾아 병목을 식별한다. 병목 Operator의 Task Slot 수를 늘리거나 불필요한 상태를 제거해 상태 접근 시간을 단축하는 방식으로 대응한다.

체크포인트와 고가용성의 연결

Flink는 체크포인트와 두 단계 커밋(Two-Phase Commit)을 사용해 Exactly-Once 처리를 보장한다. Aligned Checkpoint는 모든 입력 채널의 Barrier를 정렬해 일관된 스냅샷을 만들고, Unaligned Checkpoint는 백프레셔 상황에서 Barrier를 넘어 처리해 체크포인트 지연을 막는다. Kafka와 JDBC Sink에서는 Sink Two-Phase Commit이 트랜잭션 커밋과 체크포인트를 동기화한다.

고가용성 구성에서는 Zookeeper를 통한 JobManager 자동 장애조치, 실패한 TaskManager의 자동 재시작, HDFS와 S3에 체크포인트를 외부 저장하는 방식을 사용한다. 이는 클러스터 전체 장애에 대응하기 위한 구성이다.

Flink와 Storm의 처리 모델을 이해하는 것만으로는 안정적인 스트림 워크로드를 만들기 어렵다. 네트워크 버퍼와 Zero-Copy, Off-Heap 메모리와 G1GC, CPU 격리와 NUMA 배치, RocksDB I/O를 함께 다뤄야 처리량과 지연시간을 제어할 수 있다. 체크포인트와 백프레셔를 운영 지표로 연결하면 장애 복구와 성능 병목을 같은 흐름에서 관리할 수 있다.

스트림 처리Apache FlinkApache Storm운영체제RocksDB백프레셔