꾸준함이 중요한 Lion2me의 기술블로그

Flink의 데이터 처리 방식에 대하여

28 Mar 2025
DE

Flink의 데이터 유형 별 파이프라인 개발 시 유의 할 점

최근에 프로젝트 진행 중 Flink 를 사용한 데이터 파이프라인을 개발하게 되면서 공부 한 내용을 정리하기 위해 포스트를 작성합니다. 실시간 파이프라인 개발을 담당하게 되면서 기술 스택 중 Flink를 선택하게 된 이유는 “실시간/배치 파이프라인을 지원하면서 Exactly Once를 보장”하는 특징입니다.

Exactly Once에 관련한 내용 및 실시간 파이프라인을 통한 아키텍처 관련 지식은 빅데이터 아키텍처 관련 포스트를 참고하시면 좋을 것 같습니다.

Flink 란

Flink 설계에 대한 기본적인 지식을 얻는 것은 공식 문서를 참조하는 것이 좋습니다. 공식 문서에서는 우리가 Flink에 대해 어떤 지식을 습득해야 하는지를 목차로 친절히 알려주고 있습니다.

  • 스트리밍 데이터 처리 파이프라인 구현 방법
  • 연속 스트림에서 이벤트 기반 애플리케이션을 구축하는 방법
  • 이벤트 시간을 사용해 정확한 분석 결과를 일관되게 계산하는 방법
  • Flink가 상태를 관리하는 이유와 방법
  • 정확히 한 번 처리(exactly-once semantics)를 보장하는 내결함성 있는 상태 기반 스트림 처리 방식

이 포스트에서는 위의 목차 중 Flink의 연속 스트림 내에서 애플리케이션을 개발 중 발생하는 문제점에 대해서 공유드리겠습니다.

Flink의 데이터 유형에 관련 한 포스트는 이전 포스트를 참고해주시면 감사드리겠습니다.

Flink의 데이터 처리 방식

개발 과정에서 고려되는 파이프라인 유형은 크게 2가지가 있습니다. 배치 방식과 스트림 방식은 각각의 장단점을 가지고 있으며, 두 방식을 함께 적용히여 데이터의 정합성을 높이거나, 개발 과정을 단축하기 위해 하나의 방식을 사용하여 개발하는 등의 다양한 방법으로 개발합니다. 이러한 내용은 배치 처리 관련 포스트스트림 처리 관련 포스트에서 자세하게 알 수 있습니다.

Flink는 배치 파이프라인과 스트림 파이프라인을 모두 제공합니다. Flink의 공식 문서에서도 “배치와 스트림 모두 최적의 플랫폼”이라는 말할 정도로 자신감을 가지고 있습니다. 추가적으로 스트리밍 모드에서 Table API를 사용하는 경우에는 Bounded 방식을 지원 할 수 있도록 우리가 이해하고 있는 윈도우 형식의 처리 또한 지원합니다.

처리 가능한 데이터 형태

  • Batch
  • Stream
    • Bounded
    • Unbounded

Batch Pipeline

공식 문서를 기반으로 Batch 처리의 특징을 설명하면 다음의 문장으로 정리 할 수 있습니다.

입력 데이터를 모두 알 수 있는 유한한 데이터 셋으로 전통적인 배치 처리를 통해 결과를 출력 후 종료하는 파이프라인입니다. 데이터의 구조 및 내용을 모두 알 수 있기에 최적화 할 수 있는 다양한 방식을 사용 할 수 있습니다. 예를 들면, 중간 결과를 별도로 저장하여 장애 복구 및 리소스 최적화 할 수 있고 키 기반으로 맵핑하여 맵리듀스 형태로 데이터를 처리 할 수 있습니다.

Batch 처리에 대해서는 상대적으로 활용 위험성이 적은 편 입니다. 특정 데이터 플랫폼(RDB, File …)에서 읽어 온 데이터를 일회성으로 처리하는 경우는 다양한 플랫폼(Spark, MR, …)으로 처리하던 작업이라 크게 어렵지 않는 것으로 보입니다.

여기서 주의 할 부분은 사실 크게 없습니다. 단 1회성 처리이기에 Checkpoint 및 Savepoint를 통한 오프셋 등의 상태 저장도 없습니다. 일반적인 처리로 생각해야합니다.

주의 할 부분

  • 연속적인 데이터 셋에서 사용하면 정확한 정보를 얻기 어려울 수 있습니다.
    • 가장 대표적인 부분은 Kafka입니다. Kafka의 Consumer Group 등을 Kafka 내부적으로 활용해서 오프셋 관리를 하면 가능하지만 Flink 는 Kafka의 Offset을 앱 내부에서 관리 할 수 있습니다. 이러한 정보는 Flink 의 상태(State)로 관리하는데 배치 처리는 상태를 저장 하지 않습니다.

Stream 처리 방법

Flink의 Stream은 처리는 크게 두 방식으로 나누어집니다.

  • Watermark를 이용하여 특정 범위의 데이터 처리 (Bounded)
  • 개별 레코드의 실시간 처리 (Unbounded)

특정 범위의 데이터 처리

우리는 이미 일정 범위 스트림 처리에 대해서 이해하고 있습니다. 이 포스트에는 윈도우 방식에 대한 설명이 있습니다.

일반적으로 Stream 모드로 개발하면 데이터가 인입되자마자 곧바로 처리가 되는 것을 기대합니다. 사실 이러한 방식이 대표적이기에 많은 사용자는 어려움 없이 사용하지만 문제는 “사용자 로그”와 같은 연속적인 이벤트를 묶어야 하는 경우에 발생합니다.

예를 들면 사용자 A에 대한 서비스 이용 시간을 1분 내 오차로 측정 및 서비스 사용 이벤트를 종합하고 싶을 경우에는 어떻게 파이프라인을 개발 할 수 있을끼요?

다음과 같은 방법을 활용 할 수 있습니다.

  1. 이벤트 로그의 적재 및 배치 파이프라인 개발
    • 로그의 원천 수집 및 저장소에 적재 후 배치 파이프라인을 통해 종합
    • 1회 데이터 처리 부하로 인한 긴 시간의 배치 간격 발생
  2. 이벤트 로그 수집에 대한 윈도우 기반 실시간 처리 파이프라인 개발
    • 세션 윈도우를 사용한 유저 기반 이벤트 로그 종합 및 적재 혹은 업데이트 파이프라인 개발
      • 세션을 명확히 구분 할 키가 없을 경우에는 위험 할 수 있음
    • 슬라이딩(혹은 터블링 등)의 윈도우를 사용하여 기간 내 유저 기반 이벤트를 종합 및 업데이트 파이프라인 개발

배치 파이프라인을 개발하는 개발자라면 1번 방법이 주로 사용됩니다. 가장 정확하면서 개발 과정에서 간격과 정합성 측면에서는 매우 유용 할 수 있고, 실제로 많이 사용됩니다. 하지만 이벤트를 파악하고 빠른 피드백이 필요한 경우에는 1번 방법은 N분간 이벤트를 지연시키는 경우가 있기에 아쉬운 부분이 있습니다. 이러한 문제는 2번 방식을 사용하면 유연하게 처리 할 수 있습니다.

윈도우 기반 실시간 처리 파이프라인을 개발하는 방법에 대한 이해를 갖기 전 Flink에서 시간 기반의 데이터 처리에 대한 이해를 얻기 위해 공식 문서를 살펴 볼 필요가 있습니다.

Flink에서의 Stream with Bounded 데이터 처리

Flink에서의 시간 기반 데이터 처리에서 사용되는 시간은 크게 2가지 방식이 있습니다.

첫 번째는 시스템(서버) 시간을 사용하여 정확하게 처리를 시작 한 시간인 Processing Time이 있습니다. 이 시간은 시스템의 시간을 따라서 현 프로세스에서 얻는 시간으로 실제 이벤트가 발생 한 시간과는 다른 시간을 가질 수 있습니다.

두 번째는 데이터(주로 데이터 내 timestamp)의 시간에 의존하는 방식입니다. 예를 들면 데이터를 소싱하는 Kafka의 메시지가 발행 된 Timestamp의 경우에는 Flink 시점에서 보았을 때 원천의 발생 시간으로 이벤트가 발생 한 시간으로 추정 할 수 있습니다. 이러한 Event Time이 있습니다.

이러한 시간을 기반으로 데이터를 묶어서 윈도우 동작을 만들 때는 Watermark라고 부르는 시간 기준 데이터가 필요합니다. 이 Watermark 정보를 사용해서 N 시간까지의 처리가 발생 할 때는 이전 데이터는 Watermark의 N 시간 이전의 데이터는 모두 입력이 완료 되었음을 확신해야합니다. 하지만 스트림의 특성 상 N 시간이 지난 후에도 N 시간에 가깝게 발생한 모든 데이터가 입력되었음을 확신 할 수는 없습니다. 이 경우를 위한 처리 지연 옵션등을 제공(참고)하고 있습니다.

여기서 Watermark에 대해서 간단한 내용을 더 알아두면 좋을 것 같습니다. datastream, table api 방식에 따라 다르지만, 기본적으로 시간 관련 모든 타입을 기준으로 사용 할 수는 없습니다.

Datastream에서의 Watermark

datastream에서 Watermark를 만드는 방식에 대한 문서가 존재합니다. 이 내용을 살펴보면 Watermark 기준으로 지연 데이터를 처리하기위해 대기하는 옵션을 포함해서 현재 처리중인 데이터에서 어떤 값을 Watermark 기준으로 사용 할 지를 설정하는 예제가 잘 나와있습니다. Watermark의 지정 방식이 꼭 데이터 내 특정 정보를 사용하지 않고 자체적으로 만들어서 사용 할 수 있습니다만, 추천하는 방법은 아니라는 내용도 포함되어 있습니다.

Kafka와 Kinesis의 timestamp 타입은 곧바로 적용이 가능하다고 합니다.

추가적인 의문은 파이썬으로 개발 시 **Kafka의 timestamp은 deserialization이 안되는 것으로 이해(datastream 기준)하고 있는데, Watermark로는 사용 할 수 있는 것으로 예제가 잘 나와있어서 얻을 수 있는 정보임에도 수집하지 않는 이유가 궁금합니다.

Table API에서의 Watermark

Table API에서 Watermark에 대한 내용은 해당 문서에 포함되어 있습니다.

Table API의 특성 상 SQL을 사용하여 쿼리하는 경우가 많은데, 이 경우에는 DLL을 통해 테이블 등을 정의 할 때 컬럼으로 해당 정보를 추가 할 수 있습니다.

TIMESTAMP(_LTZ) 타입을 사용해서 워터마크를 설정 할 수 있고, 이 경우에는 우리가 이해하는 이벤트 시간 기준 윈도우를 사용 할 수 있습니다.

CREATE TABLE user_actions (
  user_name STRING,
  data STRING,
  user_action_time TIMESTAMP(3),
  WATERMARK FOR user_action_time AS user_action_time - INTERVAL '5' SECOND
) WITH (...);
CREATE TABLE user_actions (
  user_name STRING,
  data STRING,
  ts BIGINT,
  time_ltz AS TO_TIMESTAMP_LTZ(ts, 3),
  WATERMARK FOR time_ltz AS time_ltz - INTERVAL '5' SECOND
) WITH (...);

여기에 옵션을 통해서 이벤트의 Watermark의 정렬 혹은 기준등을 고도화 할 수 있는 방안들이 추가 된 것으로 보입니다. 추후에 직접 활용 할 경우가 생기면 참고하면 좋을 것 같습니다.

만약 시스템 시간 기준의 윈도우 동작을 수행하고 싶다면 PROCTIME을 설정하면 윈도우는 시스템 시간으로 동작합니다.

CREATE TABLE user_actions (
  user_name STRING,
  data STRING,
  user_action_time AS PROCTIME()
) WITH (...);

실제 사용시에는 각 윈도우 방식에 따라 옵션을 추가해야 하며, 이 정보들은 공식 문서에 포함되어 있으므로 확인해서 사용하시면 될 것 같습니다.

문서에서는 추가적으로 datastream에는 타임존에 대한 정보가 없고 datastream에서 table api형태로 변경하는 과정에서 Watermark를 사용하는 방법에 대해서도 공유하고 있습니다. 이 부분도 추후에 사용 할 경우 참고하면 좋을 것 같습니다.

Flink에서의 Stream with Unbounded 데이터 처리

Flink를 Stream 모드로 실행 할 경우 기본적으로 Unbounded 데이터를 처리하고 있습니다. 즉, Watermark를 설정하고 윈도우를 설정하지않는다면 기본적으로 Unbounded 데이터를 처리하는 것을 가정합니다.

이전에 Stream 데이터 처리에 대한 공부를 하면서 Micro Batch 방식과 완전한 Streaming의 차이를 공부 한 적이 있습니다. Flink는 그 중 완전한 Streaming 데이터 처리를 수행하는 플랫폼 중 가장 유명한 플랫폼 중 하나로 이해했었습니다. 즉, Flink는 각각의 레코드에 대한 데이터를 개별로 연속적인 처리를 수행한다는 의미입니다.

100개의 레코드가 입력되었을 때 Micro Batch는 100개의 컬럼에 대한 처리를 한 후 리턴합니다. 과거의 Spark Streaming이라고 공부했었습니다. 그런데 Flink는 100개의 데이터가 입력되었을 때 각각의 입력에 대한 처리를 하여 다음 Operator로 결과를 출력합니다. 그 결과 중간 처리에 대한 병목 현상이 없이 빠르게 데이터를 얻을 수 있습니다.

이 이야기를 하는 이유는 Flink가 레코드 단위의 Operator 처리를 하기 때문에 신경써야하는 부분이 많다고 말하고 싶어서입니다. 그 중 가장 중요한 부분은 aggregation, group by(keyby) 등의 집계 및 종합하는 경우가 있습니다. datastream의 경우, 혹은 Table API를 사용하는 경우에도 이러한 종합 과정에서 연속적인 데이터가 개별로 처리됨을 인지하지 못하고 Batch 작업처럼 개발을 할 경우 중간 결과가 여럿 출력되거나(제가 그랬습니다) 원치 않는 지점에서 데이터가 증분 혹은 분산되는 경우가 발생합니다.

단일 데이터에 집계 없는 처리의 경우에는 큰 문제없이 동작 할 것으로 예상됩니다.