스트리밍 처리
고친 사람 github-actions[bot]
스트리밍 처리는 데이터가 들어오는 대로 바로 계산해서 결과를 쉬지 않고 내놓습니다. 하루치를 다 모은 뒤 한 번에 계산하는 배치 처리와 맞서는 방식입니다. 결제가 들어오는 순간 이상 거래를 잡아내는 일이 대표적입니다. 영상 스트리밍이나 큰 파일을 조금씩 읽는 스트리밍과는 다른 뜻입니다.
쉽고 빠른 이해
무슨 일을 하는 방식인가 — 데이터가 한 건 들어올 때마다 바로 계산하고 결과를 고쳐 냅니다. 주문이 들어올 때마다 오늘 매출 합계를 바로 올려 내보내는 것이 스트리밍 처리입니다.
왜 이렇게 하나 — 다 모은 뒤 계산하면 답이 늦습니다. 하루치를 모으면 도난 카드로 긁은 결제를 다음 날에야 알게 됩니다. 게다가 주문처럼 끝없이 들어오는 데이터에는 「다 모인 때」가 오지 않습니다.
어떻게 도나
- 작업을 한 번 띄우면 멈추지 않고 계속 돕니다
- 데이터가 한 건 오면 계산합니다. 다음 계산에 필요한 값은 기억해 둡니다
- 바뀐 결과를 곧바로 다음 단계로 내보냅니다
대가 — 작업이 늘 떠 있어야 합니다. 기억해 둔 값은 장애가 나도 잃지 않게 지켜야 합니다. 늦게 도착하는 데이터 때문에 이미 낸 답을 고치거나 그 데이터를 버리는 결정도 따라옵니다. 그래서 답이 하루 늦어도 되는 보고서는 배치 처리가 낫습니다.
상세
가계부를 쓰는 두 사람이 있습니다. 한 사람은 영수증을 지갑에 모아 두었다가 주말에 한꺼번에 적습니다. 다른 사람은 무엇을 살 때마다 곧바로 적습니다.
앞사람은 주말이 되어야 이번 주에 얼마 썼는지 압니다. 뒷사람은 언제 물어도 지금까지 쓴 돈을 바로 답합니다.
스트리밍 처리는 뒷사람의 방식입니다. 이 절은 먼저 배치 처리와 무엇이 갈리는지 봅니다. 다음으로 끝나지 않는 작업이 어떤 모양으로 도는지 봅니다. 이어서 앞에서 본 값을 기억하는 법, 흐름을 구간으로 자르는 법, 장애에서 살아나는 법, 밀릴 때 버티는 법을 차례로 봅니다. 마지막으로 맞는 일과 안 맞는 일, 이름이 같은 다른 뜻을 짚습니다.
배치 처리와 갈리는 점
배치 처리는 데이터를 모아 두었다가 정해진 때에 한꺼번에 계산합니다. 밤마다 어제 하루치 주문을 집계하는 작업이 그렇습니다. 계산은 모인 데이터의 끝에 닿으면 끝납니다.
스트리밍 처리에는 그런 끝이 없습니다. 주문은 서비스가 도는 한 계속 들어옵니다. 계산도 그만큼 계속 돕니다. 이렇게 끝없이 들어오는 데이터를 무계 데이터라고 부릅니다. 끝이 정해진 쪽은 유계 데이터입니다.
데이터 한 건 한 건은 대개 이벤트라는 이름으로 들어옵니다. 이벤트는 「주문 하나가 결제됐다」처럼 어떤 일이 일어났다는 기록 한 건입니다. 스트리밍 처리는 이런 이벤트가 줄지어 오는 흐름, 곧 스트림을 받아 계산합니다.
두 방식을 나란히 놓으면 아래와 같습니다.
| 배치 처리 | 스트리밍 처리 | |
|---|---|---|
| 다루는 데이터 | 끝이 정해진 묶음 | 끝없이 이어지는 흐름 |
| 작업이 도는 때 | 정해진 때에 돌고 끝난다 | 한 번 띄우면 계속 돈다 |
| 결과 | 묶음마다 한 번 낸다 | 이벤트가 올 때마다 고쳐 낸다 |
| 답이 나오기까지 | 묶음이 다 모일 때까지 기다린다 | 이벤트가 도착하자마자 계산한다 |
| 다시 돌리기 | 같은 묶음을 다시 돌린다 | 지나간 흐름을 다시 읽는다 |
표에서 가장 큰 갈림은 답이 나오기까지의 시간입니다. 이벤트 하나가 들어와서 결과에 반영되기까지 걸린 시간이 지연입니다. 배치 처리에서는 묶음이 다 모일 때까지 기다린 시간이 고스란히 지연에 들어갑니다.
배치 처리가 이 기다림을 감수하는 까닭은 처리량입니다. 처리량은 정해진 시간 동안 처리한 건수입니다.
작업을 띄우고 데이터를 여는 데는 건수와 상관없이 드는 고정 비용이 있습니다. 한 번에 몰아서 계산하면 이 비용을 여러 건이 나눠 지니 처리량이 오릅니다. 스트리밍 처리는 그 이득을 일부 내주고 한 건이 기다리는 시간을 줄이는 쪽을 고릅니다.
표의 마지막 줄은 결과를 다시 내야 할 때의 이야기입니다. 계산 코드의 버그를 고쳤다면 이미 낸 결과도 고친 코드로 다시 내야 합니다. 배치 처리는 남아 있는 묶음을 다시 돌리면 됩니다. 스트리밍 처리는 지나간 이벤트가 어딘가에 남아 있어야 그 지점부터 다시 읽을 수 있습니다.
끝나지 않는 작업의 모양
스트리밍 처리 작업은 세 부분으로 짭니다. 이벤트를 받아 오는 입구, 이벤트를 계산하는 단계들, 결과를 내보내는 출구입니다.
입구는 흔히 소스라고 부릅니다. 계산 단계는 연산자입니다. 출구는 싱크입니다.
소스는 대개 메시지 큐나 이벤트 스트림입니다. 메시지 큐는 보내는 쪽이 넣은 메시지를 받는 쪽이 꺼내 갈 때까지 맡아 두는 중간 저장소입니다.
이벤트 스트림은 일어난 일을 순서대로 이어 붙여 두고 여러 곳이 각자 따라 읽게 하는 기록입니다. 앞에서 본 스트림이 흘러가는 이벤트 자체라면, 이벤트 스트림은 그 흐름을 담아 두는 저장소입니다. 읽어 간 뒤에도 이벤트가 남아 있어서 지나간 흐름을 다시 읽을 수 있습니다.
결제 이벤트로 이상 거래를 잡는 작업이라면 이렇게 생깁니다.
flowchart TD
A["소스 · 결제 이벤트 흐름"] --> B["연산자 · 카드 번호로 이벤트를 나눈다"]
B --> C["연산자 · 카드마다 최근 10분 결제 횟수를 센다"]
C --> D{"10분에 다섯 번을 넘었나"}
D -->|넘었다| E["싱크 · 경보 큐로 보낸다"]
D -->|아니다| F["다음 이벤트를 기다린다"]
이벤트 하나가 올 때마다 위에서 아래로 한 번 지나갑니다. 횟수를 카드마다 따로 세려고 먼저 카드 번호로 나눕니다. 나누는 법은 뒤의 소절에서 봅니다.
횟수가 기준을 넘으면 싱크가 경보를 담당 쪽이 읽는 큐에 넣습니다. 작업 자체는 끝나는 일 없이 다음 이벤트를 기다리며 이 일을 되풀이합니다.
앞에서 본 것을 기억하는 상태
연산자는 두 부류로 갈립니다. 한 부류는 이벤트 한 건만 보고 일을 끝냅니다. 금액 단위를 바꾸거나 필요 없는 이벤트를 걸러 내는 일은 앞에 무엇이 왔는지 몰라도 됩니다.
다른 부류는 앞에서 본 것을 기억해야 합니다. 합계를 내거나 횟수를 세는 일이 그렇습니다. 연산자가 이렇게 들고 있는 값을 상태라고 합니다. 상태를 들고 하는 계산은 상태 저장 처리입니다.
아래 자바 코드가 상태 하나로 매출 합계를 냅니다. orders 는 주문이 올 때마다 하나씩 내주는 끝없는 흐름입니다. emit 은 결과를 다음 단계로 보내는 함수라고 하겠습니다. 주문 세 건이 10000원, 15000원, 6000원 순서로 들어왔습니다.
long total = 0;
for (Order o : orders) { // 안 끝남
total += o.amount();
emit(total); // 10000, 25000, 31000
}
total 이 이 작업의 상태입니다. 반복문은 흐름에 끝이 없어 빠져나오지 않습니다. 결과는 마지막에 한 번 나오지 않고 주문이 올 때마다 하나씩 나옵니다.
상태는 대개 작업이 도는 기계의 메모리나 로컬 디스크에 둡니다. 그 기계가 죽으면 상태도 함께 사라질 수 있습니다. 상태를 장애에서 지키는 법은 뒤의 소절에서 봅니다.
끝없는 흐름을 구간으로 자르기
상태를 끝없이 키울 수는 없습니다. 「지금까지 모든 주문의 합계」는 숫자 하나라 괜찮습니다. 하지만 「카드마다 지금까지의 모든 결제」를 들고 있으면 상태가 끝없이 불어납니다.
그래서 흐름 위에 구간을 정하고 그 안에서만 계산하는 일이 흔합니다. 이 구간이 윈도입니다. 앞 그림의 「최근 10분」이 윈도입니다. 구간이 끝나면 그 구간의 상태는 버려도 됩니다.
윈도를 자를 때는 어느 시각을 기준으로 삼을지가 문제입니다. 이벤트에는 일이 일어난 시각과 작업에 도착한 시각이 따로 있습니다. 앞을 이벤트 시간, 뒤를 처리 시간이라고 부릅니다. 지하철에서 넣은 주문은 전파가 끊긴 동안 휴대폰에 머물다가 몇 분 늦게 도착합니다. 그러면 두 시각이 몇 분씩 벌어집니다.
이벤트 시간으로 자르면 늦게 올 이벤트를 언제까지 기다릴지 정해야 합니다. 작업은 「이 시각 전에 일어난 일은 거의 다 도착했다」고 추정하는 시각 하나를 들고 갑니다. 이 시각이 워터마크입니다.
워터마크는 이벤트가 들어오면서 앞으로 나아갑니다. 워터마크가 윈도의 끝 시각을 지나면 작업은 그 윈도를 닫고 결과를 냅니다.
워터마크보다 늦게 도착하는 이벤트도 생깁니다. 이런 지연 도착 데이터는 버리거나, 이미 낸 결과를 고쳐 다시 내거나, 따로 모아 둡니다.
죽었다 살아날 때
작업은 몇 달씩 돌기도 합니다. 그동안 기계가 죽거나 배포 때문에 작업을 내렸다 올리는 일이 생깁니다. 이때 상태와 「어디까지 읽었나」를 함께 잃으면 합계가 0부터 다시 시작합니다.
그래서 작업은 주기적으로 체크포인트를 남깁니다. 이 맥락의 체크포인트는 그 순간의 상태와 소스에서 읽은 위치를 함께 적은 저장본입니다. 작업이 도는 기계가 아니라 밖의 저장소에 둬서 기계가 죽어도 남습니다.
읽은 위치는 흔히 오프셋이라는 번호로 적습니다. 소스에 쌓인 이벤트에 앞에서부터 매긴 순번입니다. 작업이 죽으면 마지막 체크포인트를 불러와 거기 적힌 오프셋 다음부터 다시 읽습니다.
sequenceDiagram
participant 소스
participant 작업
participant 저장소
작업->>저장소: 체크포인트 · 오프셋 100 · 합계 50만 원
소스->>작업: 오프셋 101~105 이벤트
Note over 작업: 결과를 내보낸 뒤 죽는다
작업->>저장소: 마지막 체크포인트를 불러온다
저장소-->>작업: 오프셋 100 · 합계 50만 원
소스->>작업: 오프셋 101~105 를 다시 보낸다
그림에서 오프셋 101부터 105까지의 이벤트는 두 번 처리됐습니다. 죽기 전에 한 번 계산해 결과까지 내보냈습니다. 살아난 뒤에 다시 계산했습니다. 상태는 체크포인트로 돌아갔으니 맞습니다. 하지만 밖으로 내보낸 결과는 두 번 나간 셈입니다.
이 겹침을 어떻게 다루느냐에 따라 보장이 세 가지로 갈립니다.
| 보장 | 하는 일 | 대가 |
|---|---|---|
| 최대 한 번 | 다시 읽지 않는다 | 두 번은 없지만 장애 때 이벤트가 빠진다 |
| 최소 한 번 | 다시 읽는다 | 빠지지는 않지만 결과가 두 번 반영될 수 있다 |
| 정확히 한 번 | 다시 읽되 결과에는 한 번만 남긴다 | 상태 저장과 결과 내보내기를 한 덩이로 묶어야 해서 조율할 일이 늘어난다 |
마지막 줄은 이름과 달리 이벤트를 한 번만 읽는다는 뜻이 아닙니다. 다시 읽어도 결과에는 한 번만 남는다는 뜻입니다.
흔한 방법은 체크포인트를 남길 때 그동안 내보낸 결과도 함께 확정하는 것입니다. 체크포인트 전에 죽으면 상태도 결과도 둘 다 없던 일이 됩니다. 그래서 다시 읽어도 결과가 겹쳐 남지 않습니다.
최소 한 번을 고르면 결과를 받는 쪽이 멱등성을 갖추게 짭니다. 멱등성은 같은 입력을 여러 번 받아도 한 번 받은 것과 결과가 같은 성질입니다. 이벤트마다 고유 번호를 두고 이미 본 번호면 건너뛰는 식입니다.
들어오는 속도를 못 따라갈 때
이벤트가 처리하는 속도보다 빨리 들어오면 작업이 밀립니다. 아직 못 읽은 이벤트가 소스에 쌓이고 그만큼 결과가 늦어집니다.
작업 가운데 소스에서 이벤트를 꺼내 읽는 부분을 컨슈머라고 합니다. 메시지 큐에서 「받는 쪽」이라 하고, 체크포인트에서 오프셋을 따라 다시 읽는다고 한 것이 바로 컨슈머입니다.
가장 최근에 들어온 이벤트의 오프셋과 컨슈머가 지금 읽는 오프셋의 차이가 컨슈머 랙입니다. 아직 못 읽은 이벤트가 몇 건인지를 뜻합니다. 작업이 밀리는지 볼 때 가장 먼저 보는 숫자입니다.
작업 안의 한 단계가 밀릴 때는 백프레셔가 걸립니다. 뒤 단계가 감당 못 하면 앞 단계에 속도를 늦추라는 신호가 거슬러 올라가는 장치입니다. 앞 단계가 덜 보내니 중간에서 메모리가 넘치지 않습니다.
속도를 늦춰도 끝내 못 따라가면 일을 나눕니다. 이벤트를 카드 번호 같은 키로 나눠 여러 기계가 나눠 맡게 하는 일이 파티셔닝입니다. 같은 키의 이벤트는 같은 기계로 가니 카드마다 세는 상태도 한 기계 안에 모입니다.
맞는 일과 안 맞는 일
결과가 늦으면 쓸모가 줄어드는 일에 씁니다. 도난 카드로 긁은 결제는 바로 막아야 합니다. 다음 날 알면 이미 돈이 빠져나간 뒤입니다. 실시간 순위, 서버 오류 경보, 한 데이터베이스의 변경을 다른 저장소로 곧바로 옮기는 변경 데이터 캡처도 같은 이유로 스트리밍 처리를 씁니다.
하루에 한 번 보는 보고서에는 잘 안 맞습니다. 답이 하루 늦어도 괜찮다면 배치 처리가 짜기도 쉽고 다시 돌리기도 쉽습니다. 늦게 도착하는 데이터도 다음 날 아침이면 대부분 들어와 있습니다.
전체를 한 번에 봐야 하는 계산도 어렵습니다. 모든 주문을 금액순으로 정렬하는 일은 흐름이 끝나야 답이 정해집니다. 끝이 없는 흐름에서는 그 답이 영영 확정되지 않습니다.
운영에 드는 품도 따져야 합니다. 작업이 늘 떠 있어야 합니다. 상태와 체크포인트와 컨슈머 랙도 계속 지켜봐야 합니다. 그래서 둘 사이에서 몇 초마다 작은 묶음을 잘라 배치로 돌리는 마이크로 배치를 고르기도 합니다.
이름이 같은 다른 스트리밍
「스트리밍」은 다른 뜻으로도 많이 쓰입니다. 큰 파일이나 서버 응답을 한 번에 메모리에 올리지 않고 조각조각 읽고 쓰는 것도 스트리밍이라고 부릅니다. 영상을 내려받으면서 바로 재생하는 것도 스트리밍입니다.
두 쓰임은 데이터를 어떻게 나르느냐를 말합니다. 스트리밍 처리는 끝없이 오는 데이터를 어떻게 계산하느냐를 말합니다. 이 편은 뒤쪽을 다룹니다.
관련 항목
스트리밍 처리와 맞세워지는 처리 방식
배치 처리 · 마이크로 배치 · 실시간 처리 · 증분 처리 · 람다 아키텍처 · 카파 아키텍처
스트리밍 처리가 다루는 데이터의 갈래
무계 데이터 · 유계 데이터 · 이벤트 · 스트림 · 이벤트 스트림
스트리밍 처리에 이벤트를 실어 오는 통로
메시지 큐 · 발행-구독 · 로그 · 변경 데이터 캡처 · Apache Kafka
스트리밍 처리가 흐름을 자르는 시간 개념
윈도 · 텀블링 윈도 · 슬라이딩 윈도 · 세션 윈도 · 이벤트 시간 · 처리 시간 · 워터마크
스트리밍 처리가 장애를 견디는 장치와 보장
상태 저장 처리 · 체크포인트 · 오프셋 · 최대 한 번 전달 · 최소 한 번 전달 · 정확히 한 번 처리 · 멱등성
스트리밍 처리를 재는 성능 지표
스트리밍 처리가 밀릴 때 쓰는 장치
스트리밍 처리에서 자주 나는 장애
지연 도착 데이터 · 중복 처리 · 순서 뒤바뀜 · 데이터 쏠림
스트리밍 처리가 지난 이벤트를 다시 돌리는 방법
백필 · 재처리 · 이벤트 재생 · 이벤트 소싱
스트리밍 처리를 구현한 처리 엔진
Apache Flink · Apache Beam · Spark Structured Streaming · Kafka Streams · Apache Storm
스트리밍 처리와 이름이 겹치는 이웃 개념
스트리밍 응답 · 미디어 스트리밍 · 바이트 스트림 · 청크 전송 인코딩
스트리밍 처리가 속하는 상위 분류
다른 이름: stream processing · streaming processing · 스트림 처리 · 스트림 프로세싱 · 이벤트 스트림 처리