데이터플로우 그래프
고친 사람 github-actions[bot]
데이터플로우 그래프는 데이터 처리 작업을 단계로 나눠 적습니다. 단계 사이로 데이터가 흐르는 길도 화살표로 이어 그림처럼 함께 적어 둡니다. 실행하는 쪽은 이 그림 전체를 보고 어느 단계를 어디서 함께 돌릴지 정합니다. 컴파일러와 딥러닝 도구도 같은 이름을 쓰지만, 이 편은 큰 데이터를 여러 기계로 나눠 처리하는 일의 그래프를 다룹니다.
쉽고 빠른 이해
데이터플로우 그래프는 데이터를 어떤 순서로 손볼지 그림으로 적어 둡니다. 접속 로그를 읽고, 봇 요청을 거르고, 페이지별로 세어 저장하는 작업이라면 이 네 단계와 그 사이의 화살표가 그래프입니다.
코드를 한 줄씩 바로 실행하면 컴퓨터는 지금 줄만 압니다. 다음에 무엇이 올지 모르니 일을 여러 기계에 나눠 맡기기 어렵습니다. 단계를 이어 붙여 중간 결과를 써 두는 수고를 덜기도 어렵습니다. 그림을 먼저 받으면 전체를 보고 계획을 세울 수 있습니다.
- 코드는 단계를 이어 붙여 그림만 만듭니다. 데이터는 아직 안 움직입니다
- 실행하라는 줄을 만나면 실행하는 쪽이 그림을 받아 계획을 세웁니다
- 데이터를 조각으로 나눠 여러 기계에서 그림대로 흘려보냅니다
대가도 있습니다. 코드를 적은 줄과 실제로 도는 때가 떨어져 있어서 오류가 엉뚱한 줄에서 터집니다. 데이터가 작으면 계획을 세우고 나누는 비용이 처리하는 비용보다 더 듭니다.
상세
김밥집 주방을 떠올려 봅시다. 밥을 짓는 사람과 단무지를 써는 사람은 서로 기다리지 않습니다. 김밥을 마는 사람은 밥과 재료가 둘 다 와야 손을 댑니다. 주방장은 누가 누구를 기다리는지 알기 때문에 일을 여럿에게 나눠 맡길 수 있습니다.
데이터플로우 그래프는 처리 작업을 단계와 그 사이의 데이터 흐름으로 적어 둡니다. 웹 서버의 접속 로그로 페이지별 조회 수와 시간대별 조회 수를 낸다고 해 봅시다. 로그를 읽고, 한 줄을 필드로 쪼개고, 봇이 보낸 요청을 거른 뒤, 두 기준으로 셉니다.
이 절은 그래프를 이루는 것에서 시작해 실행하는 쪽이 그래프로 하는 일과 그 대가까지 봅니다.
flowchart TD
A["로그 읽기"] --> B["파싱 · 한 줄을 필드로 쪼갠다"]
B --> C["봇 거르기"]
C --> D["페이지별 세기"]
C --> E["시간대별 세기"]
D --> F["저장 · 페이지별 조회 수"]
E --> G["저장 · 시간대별 조회 수"]
그림에서 봇 거르기 다음에 길이 둘로 갈립니다. 페이지별 세기와 시간대별 세기는 서로의 결과를 쓰지 않습니다. 그래서 둘은 동시에 돌려도 됩니다. 위에서 아래로 한 줄씩 적은 코드에서는 이 관계가 드러나지 않습니다.
노드와 간선
그래프는 점과 점을 잇는 선으로 관계를 나타내는 구조입니다. 점을 노드라고 부릅니다. 선은 간선이라고 부릅니다.
데이터플로우 그래프에서 노드 하나는 연산 하나입니다. 연산은 데이터를 받아 손본 뒤 내놓는 한 단계입니다. 읽기, 파싱, 거르기, 세기, 저장이 다 연산입니다.
간선은 방향이 있는 화살표입니다. 한 연산이 내놓은 데이터가 다음 연산의 입력으로 들어간다는 뜻입니다. 화살표로 이어지지 않은 두 연산은 서로의 결과를 쓰지 않습니다.
들어오는 화살표가 없는 노드는 데이터를 처음 들여오는 원천입니다. 나가는 화살표가 없는 노드는 결과를 밖으로 내보내는 싱크입니다. 앞 그림에서는 로그 읽기가 원천이고 저장 둘이 싱크입니다.
그래프를 먼저 만들고 나중에 돌리는 방식
데이터플로우 그래프를 쓰는 도구에서는 코드를 적는 때와 데이터가 움직이는 때가 갈립니다. 아래 코드는 앞 그림의 왼쪽 갈래를 흔한 모양으로 적은 것입니다. 오른쪽 주석이 그 줄이 만들어 내는 것입니다.
logs = read("access.log") # 노드 1
rows = logs.map(parse) # 노드 2
human = rows.filter(not_bot) # 노드 3
views = human.count_by_page() # 노드 4
views.write("page_views") # 노드 5
run() # 이제 돈다
다섯 줄이 지나는 동안 로그는 한 줄도 읽히지 않았습니다. 각 줄은 그래프에 노드 하나와 화살표
하나를 붙였을 뿐입니다. 마지막 run() 이 그래프를 넘기면 그때 데이터가 흐르기 시작합니다.
앞에서 실행하는 쪽이라고 부른 것, 곧 그래프를 받아 데이터를 흘려보내는 프로그램을 실행 엔진이라고 부릅니다. 실행 엔진은 여러 기계에 일을 나눠 주고, 끝난 결과를 모으고, 실패한 일을 다시 시킵니다.
계산을 미뤄 두었다가 결과가 필요할 때 하는 방식을 지연 평가라고 부릅니다. 미뤄 두는 까닭은 실행 엔진이 그래프 전체를 본 다음에 계획을 세우게 하려는 것입니다. 한 줄씩 바로 돌리면 엔진은 지금 줄만 알고 다음 줄을 모릅니다.
순환이 없는 그래프
간선을 따라가다 떠나온 노드로 돌아오는 길을 순환이라고 부릅니다. 데이터플로우 그래프에는 대개 순환이 없습니다. 어떤 연산이 자기 결과를 입력으로 기다린다면 영영 시작할 수 없기 때문입니다.
방향이 있고 순환이 없는 그래프를 DAG(Directed Acyclic Graph, 방향 비순환 그래프)라고 부릅니다. 앞 그림처럼 화살표가 위에서 아래로만 흐르고 되돌아오는 길이 없으면 DAG 입니다.
DAG 의 노드는 화살표를 거스르지 않게 한 줄로 늘어놓을 수 있습니다. 앞 그림이라면 로그 읽기 → 파싱 → 봇 거르기 → 페이지별 세기 → 시간대별 세기 → 저장 둘이 그런 줄입니다. 이런 줄을 찾는 일이 위상 정렬입니다. 두 세기의 차례를 바꿔도 화살표를 거스르지 않으니 이런 줄은 여럿일 수 있습니다.
실행 엔진은 이 줄을 따라 일을 시킵니다. 앞 연산이 내놓은 데이터가 있어야 다음 연산이 돌 수 있기 때문입니다. 화살표로 이어지지 않은 두 연산, 곧 어느 쪽이 먼저여도 되는 두 연산은 동시에 돌려도 됩니다.
나눠서 함께 돌리기
큰 데이터는 한 기계에 다 안 들어갑니다. 그래서 데이터를 파티션으로 나눕니다. 파티션은 전체 데이터를 일정한 기준으로 자른 한 덩어리입니다.
실행 엔진은 같은 연산을 파티션마다 하나씩 복사해 여러 기계에서 함께 돌립니다. 파티션이 열 개면 봇 거르기가 열 군데서 동시에 돕니다. 각 복사본은 자기 파티션만 보고 끝납니다.
화살표로 이어지지 않은 연산도 함께 돌 수 있습니다. 앞 그림의 페이지별 세기와 시간대별 세기가 그렇습니다.
이어 붙이기
파싱과 봇 거르기는 둘 다 한 줄씩 보고 끝나는 연산입니다. 실행 엔진은 이런 연산이 줄지어 있으면 하나로 이어 붙입니다. 한 줄을 파싱하면 곧바로 거르기에 넘깁니다. 그다음에 다음 줄로 갑니다.
이어 붙이지 않으면 파싱 결과 전부를 어딘가에 써 두었다가 거르기가 다시 읽어야 합니다. 이어 붙이면 중간 결과를 따로 써 두지 않습니다. 이렇게 연산 여럿을 하나로 합쳐 돌리는 일을 연산자 융합이라고 부릅니다.
셔플에서 끊기는 스테이지
페이지별 세기는 같은 페이지의 줄이 한 기계에 모여야 셀 수 있습니다. 그런데 파티션은 페이지와 상관없이 잘려 있습니다. 그래서 세기 앞에서 줄을 페이지 기준으로 기계 사이에 다시 나눠야 합니다.
이렇게 페이지처럼 묶을 기준을 정해 데이터를 기계 사이에 다시 나누는 일을 셔플이라고 부릅니다. 셔플은 네트워크로 데이터를 많이 옮깁니다.
셔플 뒤의 세기는 앞의 모든 파티션이 보낸 줄을 다 받아야 시작할 수 있습니다. 한 줄씩 이어 붙여 흘릴 수 없다는 뜻입니다. 실행 엔진은 이 지점을 경계로 그래프를 몇 덩어리로 자릅니다.
이 덩어리를 스테이지라고 부릅니다. 한 스테이지 안의 연산은 이어 붙여 파티션마다 한 번에 돌립니다. 데이터를 기계 사이로 주고받는 것은 스테이지와 스테이지 사이뿐입니다.
flowchart TD
subgraph S1["스테이지 1 · 읽기 · 파싱 · 거르기를 이어 붙인다"]
P1["파티션 1"]
P2["파티션 2"]
P3["파티션 3"]
end
subgraph S2["스테이지 2 · 페이지별 세기 · 저장"]
C1["페이지 묶음 1"]
C2["페이지 묶음 2"]
end
P1 --> C1
P1 --> C2
P2 --> C1
P2 --> C2
P3 --> C1
P3 --> C2
위 스테이지의 파티션 셋은 서로 기다리지 않고 각자 돕니다. 두 스테이지 사이에 엇갈린 화살표 여섯 개가 셔플입니다. 파티션 1 에 있던 줄도 페이지에 따라 두 묶음 중 한쪽으로 갑니다. 같은 페이지는 언제나 같은 묶음으로 가므로 묶음마다 제 페이지를 끝까지 셀 수 있습니다.
실패한 조각만 다시 계산하기
기계 여러 대로 오래 돌리면 그중 한 대가 도중에 멈추는 일이 생깁니다. 처음부터 다시 돌리면 멀쩡히 끝난 일까지 버리게 됩니다.
그래프에는 각 파티션이 어느 입력에서 어떤 연산을 거쳐 나왔는지가 남아 있습니다. 실행 엔진은 이 기록을 따라 잃어버린 파티션만 화살표를 거슬러 올라가 다시 계산합니다.
이 방법은 연산이 같은 입력에 언제나 같은 결과를 내야 통합니다. 이 성질을 결정성이라고 부릅니다. 연산 안에서 현재 시각이나 난수를 읽으면 다시 계산한 파티션이 처음과 달라집니다.
거슬러 올라갈 길이 길면 다시 계산하는 데도 오래 걸립니다. 그래서 중간 결과를 디스크에 저장해 두고 거기서 다시 시작하기도 합니다. 이렇게 중간 상태를 저장해 두는 일을 체크포인트라고 부릅니다.
배치와 스트림
그래프는 데이터에 끝이 있든 없든 같은 모양으로 적습니다. 끝이 있는 데이터를 모아 한 번에 처리하는 방식이 배치 처리입니다. 어제 하루치 로그를 밤에 한 번 세는 일이 배치 처리입니다.
끝없이 들어오는 데이터를 들어오는 대로 처리하는 방식은 스트림 처리입니다. 스트림 처리에서는 노드가 전부 늘 떠 있습니다. 데이터는 한 건씩 또는 작은 묶음으로 화살표를 따라 계속 흐릅니다.
스트림에서는 뒤 노드가 처리하는 속도보다 앞 노드가 보내는 속도가 빠르면 그 사이에 데이터가 쌓입니다. 그래서 뒤 노드가 앞 노드에게 보내는 속도를 늦추라고 알립니다. 이 신호를 백프레셔라고 부릅니다.
워크플로 그래프와의 차이
데이터 일에는 화살표 그래프가 하나 더 나옵니다. 「데이터베이스에서 데이터를 꺼내는 작업이 끝나야 그것을 손보는 작업을 시작한다」처럼 작업 사이의 순서를 적은 그래프입니다. 이렇게 순서를 정해 둔 작업 묶음을 워크플로라고 부릅니다.
워크플로를 정한 순서와 시각에 맞춰 돌리는 일은 오케스트레이션이라고 부릅니다. 매일 새벽 꺼내는 작업을 띄운 뒤, 그것이 끝나면 손보는 작업을 이어 띄우는 식입니다.
두 그래프는 둘 다 DAG 라서 모양이 비슷합니다. 다른 것은 노드와 화살표가 가리키는 것입니다.
| 데이터플로우 그래프 | 워크플로 그래프 | |
|---|---|---|
| 노드 | 연산 하나 · 거르기, 세기 | 작업 하나 · 스크립트 한 벌, 질의 한 벌 |
| 화살표의 뜻 | 앞 연산의 출력이 뒤 연산의 입력이 된다 | 앞 작업이 끝나야 뒤 작업을 시작한다 |
| 화살표로 넘어가는 것 | 데이터 자체 | 끝났다는 신호와 성공·실패 |
워크플로 그래프의 노드 하나 안에 데이터플로우 그래프가 들어 있기도 합니다. 데이터를 손보는 작업 노드가 실행 엔진에 데이터플로우 그래프 하나를 넘겨 돌리는 식입니다.
쓰는 경우와 치르는 대가
데이터플로우 그래프는 한 기계에 안 들어가는 데이터를 여러 기계로 나눠 처리할 때 씁니다. 나누고 합치고 다시 계산하는 일을 실행 엔진이 맡으므로, 코드는 무엇을 할지만 적습니다.
대가의 첫째는 디버깅입니다. 파싱 함수에 버그가 있어도 rows = logs.map(parse) 줄은 아무 문제 없이
지나갑니다. 오류는 run() 에서 터집니다. 그 오류 메시지에는 실행 엔진 안쪽의 이름이 섞여 나옵니다.
둘째는 작은 데이터에서의 비용입니다. 계획을 세우고 파티션을 나누고 기계에 일을 보내는 데 시간이 듭니다. 한 기계의 메모리에 다 들어가는 데이터라면 평범한 코드로 한 줄씩 처리하는 쪽이 대개 빨리 끝납니다.
셋째는 코드에 한 번 적은 연산이 꼭 한 번만 돈다는 보장이 없다는 것입니다. 실행 엔진은 연산을 파티션마다 복사해 돌립니다. 실패한 조각은 다시 돌립니다. 연산 안에서 바깥 변수를 고치거나 외부에 메시지를 보내는 코드는 몇 번, 어느 기계에서 돌지 알 수 없습니다.
다른 분야의 데이터플로우 그래프
같은 이름을 다른 분야에서도 씁니다. 연산을 노드로, 데이터가 오가는 길을 간선으로 적는다는 뼈대는 같습니다. 노드와 간선이 무엇을 가리키는지가 분야마다 다릅니다.
| 분야 | 노드 | 간선 | 그래프로 하는 일 |
|---|---|---|---|
| 큰 데이터 처리 | 읽기·거르기·세기 같은 연산 | 데이터가 흐르는 길 | 여러 기계로 나눠 돌린다 |
| 컴파일러 | 덧셈·곱셈 같은 연산 하나 | 한 연산의 결과값을 다른 연산이 쓴다 | 명령 순서를 바꾸거나 함께 실행할 연산을 찾는다 |
| 딥러닝 도구 | 행렬 곱 같은 연산 | 다차원 배열이 흐르는 길 | 학습에 필요한 미분값을 그래프를 거슬러 계산한다 |
딥러닝 쪽에서는 같은 그래프를 계산 그래프라고도 부릅니다.
관련 항목
데이터플로우 그래프를 이루는 구성 요소
노드 · 간선 · 원천 · 싱크 · 파티션 · 스테이지
데이터플로우 그래프가 따르는 그래프 구조
실행 엔진이 그래프에 적용하는 기법
지연 평가 · 연산자 융합 · 셔플 · 데이터 병렬성 · 체크포인트 · 결정성 · 백프레셔 · 실행 계획
데이터플로우 그래프를 돌리는 처리 방식
배치 처리 · 스트림 처리 · MapReduce · 파이프라인 · 변환
데이터플로우 그래프로 작업을 적는 도구
Apache Spark · Apache Beam · Apache Flink · Google Cloud Dataflow · Dask · TensorFlow
데이터플로우 그래프와 헷갈리는 이웃 그래프
워크플로 · 오케스트레이션 · Apache Airflow · 제어 흐름 그래프 · 계산 그래프
데이터플로우 그래프가 속하는 상위 분류
다른 이름: dataflow graph · data flow graph · 데이터 흐름 그래프 · 데이터플로 그래프