Home Flink의 장애 대응 및 외부 시스템과 연동 시 고려할 점
Post
Cancel

Flink의 장애 대응 및 외부 시스템과 연동 시 고려할 점

글을 작성하게 된 계기


Flink의 장애 대응 방식과 외부 커넥터와 연결할 때, 어떤 점을 주의해야 할 지 정리하기 위해 글을 작성하게 되었습니다.





1. Flink의 장애 대응


Flink는 장애가 발생하더라도 데이터 유실이나 중복 없이, 애플리케이션을 복구하고 이어서 실행 하며, 이를 위해 Flink는 내부 상태, 데이터 소스(Source), 데이터 싱크(Sink) 세 가지를 관리합니다.

  • 내부 상태
  • 데이터 소스
  • 데이터 싱크



1-1. 내부 상태

Flink 애플리케이션은 계산 도중 필요한 임시 데이터를 메모리나 로컬 디스크에 저장합니다. 예를 들어, 집계 연산을 수행할 때 지금까지 누적된 합계나 카운트를 상태로 유지합니다. 이러한 내부 상태는 Flink가 주기적으로 수행하는 체크포인트(checkpoint) 과정에서 외부 스토리지(예: DFS, S3)에 안전하게 저장됩니다. 장애 발생 시, Flink는 마지막으로 성공한 체크포인트 상태를 복원하여 애플리케이션을 정확히 복구할 수 있습니다.

1
2
3
[메모리/디스크] --(주기적 저장)--> [외부 저장소 (DFS, S3)]
           ↑                              ↓
     (임시 상태 유지)             (장애 발생 시 복구)

메모리만 쓰지 않고, 디스크에도 임시 저장하여 OutOfMemory 방지




1-2. 데이터 소스

Flink가 Kafka, 파일 시스템 등 외부 시스템으로부터 데이터를 읽어올 때, 장애 복구를 위해 어디까지 읽었는지를 정확히 관리해야 합니다. 이를 위해 Flink는 offset, file position 등 소스의 읽기 위치를 체크포인트와 함께 저장합니다. 장애가 발생하더라도 복구 후에는 저장된 위치부터 다시 읽기 시작하여 데이터 손실이나 중복 없이 처리가 이어질 수 있습니다.

1
2
3
4
5
[Kafka (Offset: 100)] --(읽기)--> [Flink Source] --(주기적 저장)--> [체크포인트 저장소]
                                       ↓
                                (장애 발생 시 복원)[Kafka (Offset: 100)부터 재시작]




이때 소스가 이전 위치로 돌아가서 읽을 수 있어야(Resettable) 정확한 복구가 가능합니다. 즉, 장애 이후에도 과거의 특정 위치로 되돌아가 다시 읽을 수 있어야 합니다. Kafka나 파일 시스템 같은 경우 이런 되돌리기(seek)가 가능하지만, 소켓 통신 처럼 읽은 순간 데이터가 사라지는 경우 는 되돌릴 수 없습니다.

소켓 통신은 데이터를 읽는 순간 바로 사라지기 때문에, Flink가 소켓을 직접 읽을 경우 Checkpoint/복구를 통한 Exactly-Once 처리는 불가능합니다. 대신, 현실적으로는 At-Least-Once ( 최소 1번 이상 처리) 보장을 목표로 설계합니다. 이를 해결하기 위해 사용하는 방법은 크게 두 가지입니다.

  1. 소켓 앞단에 버퍼링 시스템 추가
  2. Flink가 소켓 데이터를 직접 버퍼링




첫째, 소켓 앞단에 버퍼링 시스템을 추가하는 방식입니다. 소켓에서 수신한 데이터를 Kafka나 파일 시스템에 저장하고, Flink는 이 저장된 데이터를 읽습니다. Kafka나 파일 시스템은 데이터를 저장하고 위치( seek)를 관리할 수 있기 때문에, 장애 복구 시 정확히 이전 위치부터 다시 읽어올 수 있습니다. 이 방법을 사용하면 Flink는 정상적으로 Exactly-Once 처리가 가능합니다.

1
[ 소켓 통신 수신 ] --> [ Kafka 또는 파일 시스템 저장 ] --> [ Flink가 Kafka/File 읽음 (seek/reset 가능) ] --> [ Exactly-Once 보장 ]




둘째, Flink가 소켓 데이터를 직접 버퍼링하는 방식입니다. 소켓에서 읽은 데이터를 Flink의 State(예: RocksDB)에 임시로 저장하고, 장애 발생 시 이 State를 복구하여 재처리를 시도합니다. 하지만 이 방법은 읽는 순간 데이터가 소멸하는 특성상 일부 데이터를 영구히 잃어버릴 수 있어 At-Least-Once 보장만 가능하고, Exactly-Once는 보장할 수 없습니다. 또한 구현도 복잡하고 안정성도 떨어집니다.

1
[ 소켓 통신 수신 ] --> [ Flink State (RocksDB 등)에 임시 저장 ] --> [ 장애 시 State 복구 시도 ] --> [ At-Least-Once 보장 ]




1-3. 데이터 싱크

Flink는 처리한 결과를 외부 시스템에 기록합니다. 장애가 발생하면, 처리 중이던 결과가 중복 저장되거나 일부 데이터가 유실될 위험이 있습니다. 이를 막기 위해 Flink는 싱크 커넥터들이 체크포인트와 연동되도록 설계되어야 합니다. 싱크는 Idempotent 방식(덮어쓰기)이나 Transactional 방식(체크포인트 이후 커밋)을 사용하여, 장애 복구 후에도 정확히 한 번( Exactly-Once) 결과가 저장되도록 관리합니다.

1
2
3
4
5
[Flink Operator] --(데이터 출력)--> [외부 시스템]
         ↓                               ↓
   (체크포인트 요청)             (임시 저장 또는 중복 가능)
         ↓                               ↓
  (체크포인트 성공 확인) --> (최종 Commit or 덮어쓰기)







2. 정확한 데이터 처리를 위한 저장 전략


장애가 발생하더라도 데이터의 중복이나 유실 없이 정확히 한 번만 결과를 저장하기 위해서는 특별한 저장 전략이 필요합니다. Flink는 이를 위해 Idempotent WriteTransactional Write 두 가지 방법을 제공합니다.
각 방식은 데이터 일관성과 복구 과정에서의 특성에 따라 적절히 선택하여 사용할 수 있습니다.

  1. Idempotent Write
  2. Transactional Write




2-1. Idempotent Write

Idempotent Writes는 같은 데이터를 여러 번 써도 최종 결과가 변하지 않는 저장 방식입니다. 주로 “덮어쓰기” 방식을 사용합니다. 예를 들어, 이벤트 ID를 Key로 삼아 테이블에 기록할 때, 동일한 ID의 데이터가 여러 번 저장 요청되더라도 테이블의 값은 마지막 데이터로 덮어쓰기만 되기 때문에 중복 저장이나 데이터 오류가 발생하지 않습니다. Flink처럼 장애 복구가 발생해도 데이터 중복 삽입 없이 결과 일관성을 유지할 수 있습니다. 단, 장애 복구 직후에도 결과는 일관되지만, 복구 중에는 일시적으로 중복이 보일 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
[ 이벤트ID: 1001, 데이터: A ] --\
[ 이벤트ID: 1001, 데이터: A ] ----> 테이블(결과)
[ 이벤트ID: 1001, 데이터: A ] --/

테이블 상태:
+---------+---------+
| 이벤트ID | 데이터  |
+---------+---------+
| 1001    | A       |
+---------+---------+

=> 여러 번 써도 항상 최종 결과는 동일




Idempotent 방식은 특히 대량 데이터 처리나 실시간 스트림 시스템에서 빠른 복구와 낮은 오버헤드를 제공할 수 있습니다. 왜냐하면 추가적인 트랜잭션 관리나 상태 동기화 없이, 단순히 “덮어쓰기”만으로 결과 일관성을 유지할 수 있기 때문입니다. 다만 복구 타이밍에 따라 일시적으로 같은 데이터가 중복 삽입되어 “임시적으로 잘못된 상태” 가 보일 수 있다는 한계가 있습니다. 그래서 결과의 “최종 일관성(eventual consistency)”은 만족하지만, 즉시 일관성(strong consistency) 은 보장하지 못합니다. 또한 Idempotent Write를 제대로 적용하려면, 모든 기록 작업이 키 기반(Primary Key 기반)으로 수행되어야 합니다. 키 없이 순수 Append-Only 방식으로 데이터를 저장하는 경우에는 이 방법을 적용할 수 없습니다.

Eventual consistency is a consistency model used in distributed computing to achieve high availability. Put simply: if no new updates are made to a given data item, eventually all accesses to that item will return the last updated value.




2-2. Transactional Writes

Transactional Writes는 데이터 저장을 트랜잭션 단위로 묶어 처리합니다. 즉, 데이터 쓰기를 반복하는 것이 아닌, 체크포인트 또는 커밋 시점까지 모든 작업을 임시로 보관하다가, 체크포인트가 성공하면 한 번에 커밋합니다. 체크포인트가 실패하면 저장 자체를 취소하여, 복구 후에도 중간 데이터 노출 없이 완벽한 Exactly-Once 처리가 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
처리 흐름:
[ 이벤트 처리 시작 ]
     |
     v
[ 임시 저장 (Commit Pending) ]
     |
     v
[ 체크포인트 완료 ]
     |
     v
[ 트랜잭션 커밋 (최종 저장) ]

장애 발생 시:
- 체크포인트 완료 전이라면 => 롤백 (데이터 저장 안 됨)
- 체크포인트 완료 후라면 => 정상 커밋 (데이터 저장 완료)




이때, 복구 시에도 중간 단계의 결과가 보이지 않으므로, 항상 데이터 일관성과 정확성을 보장할 수 있습니다. 임시 저장된 데이터(Commit Pending 상태)는 외부 시스템에는 절대 반영되지 않습니다.

  • Flink 내부에서는 현재 진행 중이던 트랜잭션 핸들(Transaction handle)을 폐기하고,
  • 외부 시스템(JDBC, Kafka 등)에서는 커밋되지 않은 데이터는 무효로 간주합니다.
  • 따라서 복구 과정에서는 이 임시 데이터가 존재하지 않았던 것처럼 무시되고, Flink는 마지막 성공한 체크포인트 상태로 돌아가서 다시 이벤트를 읽어 처음부터 안전하게 재처리합니다.




Transactional Writes는 엄격한 Exactly-Once 보장을 목표로 합니다. 장애가 발생해도 중간에 반영된 결과가 전혀 남지 않기 때문에, 사용자나 다른 시스템 입장에서 항상 일관된 데이터 상태를 볼 수 있습니다. 이를 위해 일반적으로 두 가지 메커니즘을 사용합니다:

  • Pre-Commit/Commit: 임시 저장소에 먼저 저장한 후, 체크포인트가 성공해야만 Commit을 수행합니다.
  • Two-Phase Commit(2PC): JDBC, Kafka Transactional Sink 등 일부 Sink 는 별도의 2단계 커밋 프로토콜을 사용해서 Commit/Abort를 엄격하게 관리합니다.







3. 커스텀 Source/Sink를 구현할 때 고려사항


Flink는 Kafka, 파일 시스템, JDBC 등 다양한 커넥터를 기본 제공하지만, 특정 요구사항에 맞춰 개발자가 직접 Source 또는 Sink를 구현해야 하는 경우도 많습니다. 이때에도 Flink의 장애 복구 모델을 정확히 이해하고 구현해야, 시스템 전체의 Exactly-Once 보장을 깰 위험 없이 동작할 수 있습니다.

  • Resettable Source Functions: 소스 함수는 장애 복구를 위해 읽기 위치를 Reset할 수 있어야 합니다.
  • Timestamps and Watermarks: 이벤트 타임 처리(Event Time Processing)를 위해, 소스 함수는 이벤트에 타임스탬프를 할당하고 워터마크를 생성할 수 있어야 합니다.



이를 흐름으로 보면 다음과 같습니다.

[ 데이터 읽기 ] –> [ 타임스탬프 할당 ] –> [ 워터마크 생성 ] –> [ 이벤트 처리 흐름 진입 ]




Sink를 구현할 때 고려해야 할 점은 다음과 같습니다.

  • Idempotent Sink Connectors: 동일한 데이터가 여러 번 저장 요청되더라도, 결과가 중복되지 않고 일관되게 유지되어야 합니다.
  • Transactional Sink Connectors: 데이터를 임시 저장하고, 체크포인트 완료 이후 커밋하여 장애 복구 시에도 중간 데이터가 노출되지 않게 해야 합니다.



이를 흐름으로 보면 다음과 같습니다.

1
2
[ 데이터 임시 저장 ] --> [ 체크포인트 완료 시점 ] --> [ 트랜잭션 커밋 ](장애 발생)[ 트랜잭션 Abort, 데이터 폐기 ]







4. 정리


Flink는 장애가 발생하더라도 데이터 유실이나 중복 없이 복구를 보장하기 위해 내부 상태, 데이터 소스, 데이터 싱크를 정교하게 관리합니다. 이를 위해 소스는 읽기 위치를 복구할 수 있어야 하고, 싱크는 중복 없이 저장하거나 트랜잭션 단위로 커밋해야 합니다. 특히 소켓 통신처럼 Reset이 불가능한 소스는 별도로 버퍼링하여 처리해야 하며, 개발자가 직접 Source나 Sink를 구현할 때에도 이러한 복구 메커니즘을 이해하고 설계해야 전체 시스템의 Exactly-Once 일관성을 유지할 수 있습니다.


This post is licensed under CC BY 4.0 by the author.