LabHub
배우기 러닝패스 코스

Apache Flink — 스트림을 엔진으로 돌린다 · 체크포인트와 세이브포인트 · 이론

체크포인트와 세이브포인트 — 멈췄다 이어 도는데 왜 겹치지 않는가

LabHub 에서 이어서 보기

한 줄 요약

체크포인트는 원천이 어디까지 읽었는지 · 연산자가 무엇을 쥐고 있는지 · 싱크가 무엇을 아직 확정하지 않았는지를 한 순간으로 맞춰 찍은 사진이다. 파일 싱크는 그 사진이 완성됐다는 통지를 받고서야 파일을 확정하므로, 잡이 죽었다 살아나도 확정된 결과에는 빈틈도 중복도 없다. 세이브포인트는 같은 장치로 사람이 찍어 두는 사진이다.

개념 지도: 사람이 찍어 두는 · 어디서부터 · 상태 · 결과

왜 이게 필요했나

스트리밍 잡은 몇 주씩 돈다. 그 사이 TaskManager 가 죽고, 코드를 고쳐 다시 배포하고, 클러스터를 옮긴다. 다시 시작하는 순간 세 가지를 동시에 정해야 한다. 원천의 어디서부터 다시 읽을지, 지금까지 쌓은 합계·창 같은 상태를 어떻게 되살릴지, 이미 밖으로 내보낸 결과를 어떻게 할지.

셋을 따로 정하면 반드시 어긋난다. 처음부터 다시 읽으면 이미 쓴 결과가 두 번 나가고, 마지막으로 읽은 위치에서 이어 읽으면 상태가 비어 합계가 틀린다. 상태만 따로 저장해 두어도, 저장한 순간과 원천의 위치가 몇 건이라도 어긋나면 그만큼 두 번 세거나 빠뜨린다. 필요한 것은 "세 가지가 같은 순간이었다" 는 보증이다. Flink 는 이것을 데이터 흐름 안에 표식을 흘려보내는 방법으로 푼다.

어떻게 동작하나

위 줄은 순번 원천이 1 부터 차례로 내보내는 행이고, 사이사이에 체크포인트 배리어 n 과 n+1 이 끼어 흐른다. 아래 줄은 파일 싱크의 디렉터리다. 배리어 n 이 싱크에 닿아 체크포인트 n 이 완료되면 그때까지 쓰던 점 파일(.part-…inprogress)이 part-… 로 이름이 바뀌어 확정되고, 새 점 파일에 다음 행이 쓰인다. 오른쪽은 STOP JOB WITH SAVEPOINT 로 세이브포인트를 찍으며 멈추면 쓰던 파일까지 확정되고, 그 세이브포인트에서 되살린 새 잡은 원천의 다음 순번부터 이어 쓴다는 것을, 아래 갈래는 세이브포인트 없이 새로 시작하면 1 부터 다시 써서 겹친다는 것을 보여 준다

배리어. JobManager 의 체크포인트 코디네이터가 원천에 배리어 n 을 끼워 넣는다(공식 문서의 Stateful Stream Processing). 배리어는 레코드와 같은 길로 흘러 내려가고, 연산자는 배리어를 받는 순간 자기 상태를 떠서 저장한 뒤 배리어를 아래로 넘긴다. 원천의 "상태" 는 읽은 위치다 — 순번 원천이면 다음에 낼 번호다. 모든 태스크가 사진을 다 찍었다고 알리면 체크포인트 n 이 완료되고, 코디네이터가 완료를 다시 알린다.

파일 싱크는 이 통지를 기다린다. 싱크가 쓰는 파일은 세 단계를 거친다. 쓰는 중에는 이름이 점으로 시작하는 .part-…inprogress… 이고, 배리어를 받으면 닫혀 확정을 기다리며, 완료 통지가 오면 part-… 로 이름이 바뀌어 확정된다. 점 파일은 관례상 읽는 쪽이 건너뛰는 숨김 파일이다. 그래서 하류가 보는 결과는 늘 "어느 체크포인트까지" 의 결과이고, 체크포인트 간격이 곧 결과가 보이기까지의 지연이 된다. 이 파드에서 1초 간격으로 돌려 보면 1초마다 part 파일이 하나씩 늘어나고, 점 파일은 체크포인트 사이에 잠깐씩만 보였다 사라진다.

저장 위치. execution.checkpointing.dir 을 주면 파일 시스템에 저장된다. 문서가 적은 구조는 <dir>/<job-id>/chk-<n>/ 이고, 그 안의 _metadata 가 사진의 목차다. 기본으로는 체크포인트를 보존하지 않는다 — 새 체크포인트가 완료되면 옛것을 지우고, 잡을 취소하면 전부 지운다. 장애 복구용이지 사람이 쓰라고 남기는 것이 아니다. 남기고 싶으면 execution.checkpointing.externalized-checkpoint-retentionRETAIN_ON_CANCELLATION 으로 둔다.

세이브포인트는 같은 메커니즘으로 찍되 주인이 사람이다. <savepoint-dir>/savepoint-<잡 id 앞 6자리>-<무작위>/ 에 생기고 Flink 가 알아서 지우지 않는다. 문서가 강조하는 함정이 하나 있다. Flink 1.15 부터 멈추지 않고 찍은 중간 세이브포인트는 부작용을 커밋하지 않는다. 파일을 확정해 주는 것은 체크포인트와 STOP ... WITH SAVEPOINT 다.

SET 'execution.checkpointing.savepoint-dir' = 'file:///root/flink/checkpoint/sp';STOP JOB '<jid>' WITH SAVEPOINT;                       -- 찍고 멈춘다. 쓰던 파일까지 확정SET 'execution.state-recovery.path' = 'file:/.../savepoint-xxxxxx-yyyy';INSERT INTO sink SELECT id FROM seq;                   -- 같은 질의를 되살린다 — 다음 순번부터

되살린 잡은 세이브포인트의 원천 위치싱크 상태를 함께 받는다. 그래서 싱크 경로를 바꿔도 순번은 이어지고, 두 디렉터리를 합치면 빈틈도 중복도 없다. 보존된 체크포인트에서도 똑같이 되살릴 수 있다(문서: 체크포인트의 메타데이터 파일로 세이브포인트처럼 재개).

현장에서 만나는 모습

가장 흔한 사고는 세이브포인트 없이 재배포다. SQL 을 조금 고쳐 다시 INSERT 하면 새 잡은 빈 상태로 원천의 처음(또는 설정된 시작 위치)부터 읽는다. 순번 원천이면 1 부터 다시 쓴다. 결과 디렉터리가 같다면 이미 있는 행이 한 번 더 들어간다. 이 실습에서 그 겹침을 직접 센다.

두 번째는 취소로 멈춘 잡이다. 취소는 세이브포인트를 찍지 않으므로 마지막 체크포인트 뒤에 쓰던 점 파일이 디렉터리에 그대로 남는다. 보존한 체크포인트에서 되살리면 새 잡은 체크포인트 시점부터 다시 쓰고, 결과(점으로 시작하지 않는 파일)는 여전히 빈틈·중복이 없다. 그런데 점 파일까지 읽는 도구를 하류에 붙여 두면 그 순간 중복이 생긴다. "정확히 한 번" 은 확정된 것만 읽는다는 약속과 짝이다.

세 번째는 복원 실패다. 세이브포인트는 연산자 id 별로 상태를 담는다. 문서는 자동으로 만든 id 가 프로그램 구조에 민감하다고 경고한다 — SQL 의 모양을 크게 바꾸면 상태를 맞춰 넣을 연산자를 못 찾는다. 상태가 있는 잡을 고칠 때는 "이 변경 뒤에도 세이브포인트가 들어가는가" 를 먼저 시험한다.

재시작 전략은 1모듈에서 본 대로 체크포인트를 켜면 기본이 재시작 쪽으로 바뀐다. 이 모듈은 그 재시작이 어디로 돌아가는지를 다룬다.

다음 실습에서 할 것

1초마다 체크포인트를 찍는 순번 잡을 파일 싱크에 붙이고, 도는 동안 확정 파일과 점 파일이 함께 있는 목록을 남긴다. /checkpoints 응답에서 마지막 체크포인트의 위치를 읽고, STOP JOB ... WITH SAVEPOINT 로 멈춘 뒤 세이브포인트에서 되살려 순번이 이어지는지 본다. 이어서 세이브포인트 없이 새로 돌려 겹침을 만들고, 취소해도 남긴 체크포인트에서 이어 쓴 뒤, 모든 숫자를 디스크에서 다시 세어 보고서로 정리한다.