Flink 실시간 스트리밍 지연 최적화 가이드: 체크포인트, RocksDB, 비동기 I/O 설정법

실시간 스트리밍 파이프라인을 운영하다 보면, 데이터가 늦게 도착하는 그 순간의 답답함은 이루 말할 수 없습니다. 제가 Apache Flink를 도입했을 때만 해도 ‘분산 처리면 충분하지’라고 생각했지만, 실제 운영 환경에서는 생각보다 훨씬 많은 병목 지점이 존재했습니다. 특히 초당 수백만 건의 이벤트가 유입되는 환경에서 Flink 실시간 스트리밍 딜레이가 수백 밀리초까지 치솟는 것을 보고, 이 문제를 해결하기 위해 몇 주간의 삽질을 시작했습니다. 이 글에서는 제가 직접 경험하고 적용한 결과, 스트리밍 딜레이를 평균 120ms → 35ms로 약 3배가량 단축한 핵심 최적화 세팅과 그 과정에서 마주친 함정들을 상세히 공유합니다. 단순히 이론이 아닌, 실제 운영 환경의 체크포인트(Checkpoint) 설정부터 메모리 튜닝까지 모두 다루므로, 같은 고민을 하고 있는 개발자라면 도움이 될 수 있습니다.

📍 이 글의 핵심 요약 (3줄 요약)
1. Flink 딜레이의 주범은 ‘체크포인트 과다 설정’과 ‘불균형한 백프레셔(Backpressure)’입니다.
2. RocksDB 상태 백엔드 튜닝 + 비동기 I/O 적용 + 체크포인트 인터벌 조정을 함께 적용해 레이턴시가 평균 120ms → 35ms로 감소했습니다.
3. 코드 한 줄 수정보다, 운영 파라미터 튜닝이 실시간 스트리밍 성능 개선에 더 큰 비중을 차지했습니다.

1. 실시간 스트리밍 딜레이의 주범: 과도한 체크포인트

많은 엔지니어가 정확성(Correctness)을 위해 Flink의 체크포인트 주기를 극단적으로 짧게 설정합니다. 제가 처음 운영했던 파이프라인은 체크포인트 인터벌이 1초였습니다. 문제는 이 체크포인트가 모든 오퍼레이터(Operator)의 상태를 스냅샷할 때까지 스트림 처리를 잠시 중단(Aligning)시킨다는 점입니다. 상태 크기가 커지면 커질수록 이 중단 시간은 기하급수적으로 늘어납니다.

실제로 제가 모니터링한 결과, 체크포인트 수행 시간(CP Duration)이 평균 800ms에 달했고, 이는 전체 스트리밍 시간의 상당 부분을 차지했습니다. 이를 해결하기 위해 체크포인트 인터벌을 1초 → 10초로 늘리고, 정확히 한 번(Exactly Once) 대신 최소 한 번(At Least Once) 모드로 전환했습니다. 지표가 중요한 실시간 대시보드에서는 최소 한 번으로도 충분한 경우가 많습니다. 그 결과, 체크포인트로 인한 스트림 중단 시간이 약 90% 감소했습니다.

단, 이 결정을 내리기 전에 아래 표를 참고하여 자신의 서비스 특성에 맞는 트레이드오프를 신중히 검토해야 합니다.

설정 모드 딜레이 영향 데이터 정합성 추천 상황
Heap 상태 백엔드 높음 (GC로 인한 지연 누적) 높음 상태 크기가 1GB 미만일 때
RocksDB (기본 튜닝 없음) 중간 (직렬화 오버헤드) 높음 대용량 상태, 장기 운영 시
RocksDB (압축 튜닝 + 블록 캐시 증가) 낮음 (최적화됨) 높음 딜레이에 민감한 실시간 처리 필수

2. 메모리 압박을 해소하는 RocksDB 상태 백엔드 튜닝

로컬 상태가 수십 GB 이상으로 커지면 필연적으로 RocksDB를 사용하게 됩니다. 하지만 기본 설정으로 RocksDB를 사용하면 LSM 트리 구조 특성상 Compaction(압축) 작업이 백그라운드에서 실행되면서 CPU와 디스크 I/O를 많이 소비합니다. 이 과정에서 실시간 스트리밍 딜레이가 주기적으로 증가하는 ‘지터(Jitter)’ 현상이 나타났습니다.

핵심 해결: 블록 캐시 크기 증가 및 압축 병렬화

저는 다음 세팅을 적용하여 RocksDB 관련 딜레이를 크게 줄일 수 있었습니다. 특히 실시간 데이터를 자주 읽는 윈도우 연산이 많은 파이프라인이라면 아래 설정은 꼭 검토해보세요.

  • state.backend.rocksdb.block.cache-size: 128MB → 512MB로 증가 (읽기 캐시 히트율 상승)
  • state.backend.rocksdb.writebuffer.size: 64MB → 128MB로 증가 (쓰기 버퍼 과부하 방지)
  • state.backend.rocksdb.thread.num: CPU 코어 수에 맞춰 4 이상으로 설정 (압축 병렬 처리)

여기에 블록 캐시를 너무 크게 잡으면 오히려 JVM 힙 메모리 압박이 생길 수 있으므로, 전체 워커 메모리 여유를 먼저 확인하는 것이 좋습니다.

또한, 네트워크 I/O 병목을 해결하기 위해 비동기 I/O (Async I/O)를 적용했습니다. 외부 데이터베이스나 Redis 조회가 필요한 경우, 전통적인 동기 방식은 조회가 끝날 때까지 전체 스트림이 블로킹됩니다. Flink의 AsyncFunction을 사용하여 DB 조회를 비동기로 처리하도록 코드를 리팩토링했고, 네트워크 왕복 시간(RTT)이 전체 레이턴시에 영향을 주는 비중을 크게 낮출 수 있었습니다. 이를 통해 전체 처리량(Throughput)은 유지하면서 평균 레이턴시는 급격히 감소했습니다.

3. 백프레셔(Backpressure)와 배치 크기의 상관관계 분석

아무리 소스(Source)에서 빠르게 데이터를 읽어도, 이후 연산 단계에서 처리가 느리면 결국 파이프라인 전체가 지연됩니다. 저는 Flink 대시보드에서 백프레셔 지수(Backpressure Ratio)가 높은 오퍼레이터를 발견했고, 원인은 다운스트림 싱크(Sink)의 배치 처리 방식에 있었습니다.

기존 코드는 HTTP 클라이언트로 1건씩 데이터를 전송했습니다. 이 부분을 100건 단위 마이크로 배치(Micro-Batch)로 묶어서 전송하도록 변경했습니다. 코드 변경은 10줄도 되지 않았지만, 결과는 놀라웠습니다. 분당 요청 수가 1/100로 줄어들면서 네트워크 커넥션 병목이 해소되었고, 배치가 쌓이는 동안의 지연(최대 5ms)마저도 전체 딜레이 관점에서는 무시할 수준이 되었습니다.

실전 삽질 포인트 (중요)
배치 크기만 무작정 키우면 부정적인 효과가 나타납니다. 예를 들어 500건으로 묶으면 배치가 꽉 차기까지 대기하는 시간이 오히려 딜레이의 주범이 됩니다. 따라서 배치 크기(건 수)와 배치 대기 시간(예: 2ms)을 함께 설정하여 어느 하나가 병목이 되지 않도록 해야 합니다. 데이터 스트림이 들쭉날쭉한 경우에는 대기 시간 기반 플러시(Flush)를 우선적으로 고려하세요.

4. 구조적 최적화 vs 파라미터 최적화 (실측 비교)

결론적으로 말하면, 애플리케이션 로직(비동기 처리, 배치 싱크)과 운영 설정(RocksDB, 체크포인트)을 함께 개선해야 최적의 결과가 나옵니다. 아래는 제가 운영하는 동일한 이벤트 로그 데이터(초당 200만 이벤트)로 테스트한 실측 결과입니다.

최적화 적용 단계 평균 딜레이 (p50) 최대 딜레이 (p99) 백프레셔 비율
초기 설정 (1초 CP, Heap 상태) 120ms 480ms 85%
RocksDB + CP 인터벌 10초 적용 78ms 220ms 45%
비동기 I/O + 마이크로 배치 싱크 35ms 78ms 5%

위 표에서 확인할 수 있듯이, 파라미터 최적화만으로도 딜레이가 절반 가까이 줄었고, 로직 최적화를 추가하니 최종적으로는 이론적 수치에 근접한 35ms를 달성했습니다. 이 과정에서 가장 중요했던 것은 각 단계별로 모니터링 메트릭을 분리해서 병목 지점을 명확히 찾아낸 것입니다.

결론: 가장 빠른 최적화는 ‘모니터링’부터 시작됩니다

Flink 실시간 스트리밍 딜레이를 줄이는 것은 마법의 코드가 아니라, 정확한 원인 분석과 적절한 운영 설정의 조합입니다. 처음부터 모든 최적화를 적용하려 하기보다는, 먼저 Flink 웹 UI에서 백프레셔와 체크포인트 지표를 확인해보세요. 그 지표가 바로 삽질의 방향을 알려주는 지도입니다.

이 글이 작은 단서가 되어, 여러분의 파이프라인에서도 실질적인 개선 효과를 경험하실 수 있기를 바랍니다. 특히 체크포인트와 RocksDB 설정 변경은 서비스 특성에 따라 최적값이 다를 수 있으므로, 워크로드가 낮은 시간대에 단계적으로 테스트해보시길 권장합니다. 그렇게 하면 큰 장애 없이 지연 시간을 확실히 줄이는 경험을 하실 수 있을 것입니다.

Similar Posts