Apache Flink — 스트림을 엔진으로 돌린다 · 이벤트 시간과 워터마크 · 이론
워터마크 — 엔진이 '이제 이 창은 닫는다' 고 정하는 방법
한 줄 요약
이벤트 시간 창은 워터마크가 창의 끝을 지나야 닫힌다. Flink SQL 의 워터마크는 WATERMARK FOR ts AS ts - INTERVAL '5' SECOND 처럼 선언하고, 값은 "지금까지 본 최대 ts − 지연" 이다. 창이 닫힌 뒤 도착한 행은 조용히 버려진다. 지연은 정확도와 지연 시간을 맞바꾸는 손잡이다.
왜 이게 필요했나
앞 모듈의 GROUP BY user_id 는 결과를 계속 고쳐 쓰면 됐다. 그런데 "1분마다 센서 값을 합해 한 번만 내보내라" 는 요구는 다르다. 한 번만 내려면 그 1분이 끝났다는 것을 알아야 한다. 벽시계로 판단하면(처리 시간) 간단하지만, 공식 문서(Timely Stream Processing)가 짚듯 처리 시간은 도착 속도·장애·재처리에 흔들려 결정적이지 않다. 어제 자료를 다시 돌리면 결과가 달라진다.
그래서 레코드에 적힌 이벤트 시간으로 창을 나눈다. 문제는 이벤트가 순서대로 오지 않는다는 것이다. 12:00:59 의 센서 값이 12:01:10 의 값보다 늦게 도착할 수 있다. 12:00 창을 언제 닫아야 하나? 영원히 기다릴 수는 없다. 워터마크는 이 질문에 대한 약속이다 — Watermark(t) 는 "이제부터 ts ≤ t 인 이벤트는 더 오지 않는다고 보겠다" 는 선언이다.
어떻게 동작하나
선언. 문서(CREATE Statements)에 따르면 WATERMARK 절은 TIMESTAMP(3) 열 하나를 이벤트 시간 속성으로 만든다. DESCRIBE 로 보면 그 열의 타입에 *ROWTIME* 이 붙는다. 흔한 전략은 셋이다.
| 식 | 뜻 |
| --- | --- |
| ts | 엄격히 오름차순 — 본 최대 ts 가 곧 워터마크 |
| ts - INTERVAL '0.001' SECOND | 오름차순 — 최대 ts 와 같은 시각의 행은 늦지 않다 |
| ts - INTERVAL '5' SECOND | 순서가 뒤섞인 입력 — 5초까지 기다린다 |
언제 나가나. 식은 레코드마다 평가되지만, 문서는 워터마크가 pipeline.auto-watermark-interval(기본 200ms) 주기로 나간다고 적는다. 그런데 워터마크를 붙이는 연산자의 소스를 보면 한 가지가 더 있다 — 새 워터마크가 마지막으로 내보낸 값보다 간격보다 더 앞서면 주기를 기다리지 않고 그 레코드에서 바로 내보낸다. 그리고 순서가 중요하다. 레코드를 먼저 아래로 흘려보낸 뒤에 워터마크를 올린다. 그래서 어떤 행이 보는 워터마크는 "그 앞 행들까지의 최대 ts − 지연" 이다. 초 단위 타임스탬프라면 전진 폭이 늘 200ms 보다 커서 레코드마다 워터마크가 나가고, 결과가 실행 속도와 상관없이 정해진다. 실습 채점기가 도착 순서만으로 결과를 재현할 수 있는 이유다.
창이 닫히는 조건. 창 집계는 window_end ≤ 워터마크 가 되는 순간 그 창의 결과를 한 번 내보내고 상태를 비운다. 그 뒤에 그 창에 들어갈 행이 오면 받을 자리가 없어 버린다. 오류도 경고도 없다. 끝이 있는 파일을 다 읽으면 엔진이 최대 워터마크를 보내 남은 창을 모두 닫는다.
행이 늦은 것과 창이 닫힌 것은 다르다. CURRENT_WATERMARK(ts) 는 그 행이 지나는 연산자의 현재 워터마크를 돌려준다(문서의 Built-in Functions, 아직 없으면 NULL). ts <= CURRENT_WATERMARK(ts) 인 행은 워터마크 기준으로 늦은 행이다. 하지만 그 행의 창이 아직 열려 있을 수 있다 — 12:00:58 의 행이 워터마크 12:00:59 뒤에 와도 12:00 창의 끝(12:01:00)은 아직 워터마크 오른쪽이다. 그래서 문서의 거르기 식(CURRENT_WATERMARK(ts) IS NULL OR ts > CURRENT_WATERMARK(ts))으로 창 앞에서 걸러 내면, 창에 맡길 때보다 더 많은 행을 버린다. 실습 자료로 재면 5초 지연에서 창이 버리는 행은 15건인데, 워터마크 기준으로 늦은 행은 98건이다.
병렬일 때. 여러 입력을 받는 연산자의 이벤트 시간은 입력들의 워터마크 중 최솟값이다(문서의 Watermarks in Parallel Streams). 한 파티션만 조용해도 전체 워터마크가 멈춘다. 이것을 풀려고 table.exec.source.idle-timeout 으로 조용한 원천을 잠시 빼는 설정이 있다. 이 실습은 병렬도 1 이라 이 효과는 보지 않는다.
현장에서 만나는 모습
"집계 숫자가 원본보다 조금 적다" 는 신고의 상당수가 늦은 행이다. 버려진 행은 로그에도 안 남으니, 같은 자료를 배치로 한 번 돌려 차이를 재는 것이 첫 진단이다. 차이가 지연을 늘리면 줄어드는지 보면 원인이 가려진다. 지연을 늘리면 대신 창 결과가 그만큼 늦게 나온다 — 대시보드가 5초 늦어도 되는지, 30초까지 괜찮은지는 사업이 정할 문제다.
두 번째는 "결과가 아예 안 나온다" 다. 워터마크가 전진하지 않으면 창이 영영 닫히지 않는다. 파티션 하나가 비어 있거나, 원천의 ts 가 NULL 이거나, 테스트 자료의 시각이 한 점에 몰려 있는 경우다. 워터마크 간격을 아주 크게 잡아도 비슷한 일이 생긴다 — 실습에서 간격을 1시간으로 두면 첫 워터마크 뒤로 전진이 멈추고, 끝이 있는 파일이라 마지막에 한꺼번에 닫혀 버린 행이 하나도 없는 결과가 나온다. 무한 스트림이었다면 창이 한 시간 동안 닫히지 않았을 것이다.
세 번째는 늦은 행을 버리지 말고 따로 모으라는 요구다. SQL 에서는 CURRENT_WATERMARK 로 늦은 행을 표시해 다른 싱크로 보내는 것이 흔한 방법이다. 다만 위에서 본 대로 "워터마크보다 이른 행" 과 "창이 이미 닫혀 버려질 행" 은 다른 집합이다. 어느 쪽을 모을지 먼저 정해야 한다.
다음 실습에서 할 것
센서 이벤트 600건(파일 순서가 도착 순서)을 5초 지연 워터마크로 선언하고 DESCRIBE 로 확인한다. 1분 TUMBLE 집계를 배치로 돌려 기준선을 만들고, 지연 5초·0초·30초 스트리밍 결과와 비교해 버려진 행을 센다. CURRENT_WATERMARK 로 늦은 행을 뽑고, 그 행들을 창 앞에서 거른 결과가 어떻게 다른지 보고, 워터마크 간격을 1시간으로 바꿔 전진이 멈추면 무엇이 달라지는지 확인한 뒤 숫자를 보고서로 정리한다.