MapReduce · Tez · Spark, 무엇이 어떻게 다른가
저번 포스팅에서는, 하둡 에코시스템에 어떤 프로젝트들이 있는지 한 번 훑어보았습니다. 그 글의 처리 계층을 정리하면서 MapReduce → Tez → Spark 로 이어지는 흐름을 한 줄씩만 적고 넘어갔는데, 정작 이 셋이 무엇이 어떻게 다른지는 제대로 짚지 못했습니다.
세 엔진은 "YARN 에게 자원을 받아 분산 처리를 수행한다" 는 같은 자리를 놓고 세대가 교체되어 온 관계입니다. 그래서 겉으로 보면 하는 일이 비슷해 보이는데, 실행 모델을 들여다보면 꽤 다릅니다. 이번에는 그 차이를 공부한 내용을 정리해보려 합니다.
1. 세 엔진은 무엇을 놓고 경쟁했는가
먼저 짚어둘 것이 있습니다. 이 셋은 저장소를 놓고 경쟁한 것이 아닙니다. 셋 다 HDFS 위의 데이터를 읽고, 셋 다 YARN 에게 컨테이너를 받아 돌 수 있습니다. 갈리는 지점은 "받은 자원 위에서 연산을 어떤 모양으로 표현하고 실행하느냐" 입니다.
그리고 그 모양이 달라진 이유는 대체로 하나였습니다. 앞 세대에서 불편했던 지점을 메우려다 보니 다음 세대가 나왔습니다.
- MapReduce : Map 과 Reduce 라는 두 단계만으로 모든 연산을 표현합니다. 단순하고 튼튼하지만, 두 단계로 안 끝나는 일은 잡을 여러 개 이어 붙여야 합니다.
- Tez : "왜 굳이 두 단계로 쪼개야 하는가" 라는 문제의식에서 나왔습니다. 연산 전체를 하나의 DAG 로 표현해 잡을 이어 붙이는 비용을 없앴습니다.
- Spark : 같은 DAG 를 쓰되, 중간 데이터를 재사용할 수 있게 만들고 배치 밖의 영역(스트리밍·머신러닝·SQL)까지 하나의 프레임워크로 덮었습니다.
아래에서는 셋을 차례로 보고, 그다음에 같은 기준으로 나란히 놓고 비교해보겠습니다.
2. MapReduce
Map Reduce 기본 이론과 실습 에서 자세히 다루었으니, 여기서는 뒤의 두 엔진과 비교하는 데 필요한 부분만 짚겠습니다.
2.1. 잡 하나는 반드시 두 단계다
MapReduce 의 실행 단위인 잡(Job) 하나는 Map → Shuffle → Reduce 로 고정되어 있습니다. 표현력이 이 틀 안으로 제한된다는 뜻입니다. 문제는 현실의 분석이 두 단계로 끝나는 경우가 드물다는 점입니다. 예를 들어 "집계한 뒤 그 결과를 다른 테이블과 조인하고, 다시 정렬해서 상위 N 개를 뽑는다" 같은 작업은 잡 세 개로 쪼개야 합니다.
그리고 잡과 잡 사이의 중간 결과는 HDFS 에 씁니다. 여기서 비용이 두 번 발생합니다.
- 디스크 쓰기와 읽기 : 중간 결과를 HDFS 에 쓰고, 다음 잡이 그것을 다시 읽습니다.
- 복제 : HDFS 에 쓰는 이상 복제본까지 만들어집니다. 잠깐 쓰고 버릴 데이터를 세 벌씩 저장하는 셈입니다.
2.2. 태스크마다 JVM 이 새로 뜬다
MapReduce 는 태스크 하나마다 컨테이너를 받아 JVM 을 새로 띄우고, 끝나면 반납합니다. 태스크가 수천 개면 JVM 기동도 수천 번입니다. 태스크 하나가 몇 분씩 걸리는 무거운 배치에서는 이 비용이 묻힙니다. 그런데 몇 초 만에 끝나는 가벼운 질의에서는 실제 연산보다 기동 비용이 더 커지는 일이 생깁니다. Hive 쿼리가 아무리 단순해도 응답에 수십 초가 걸리던 이유가 여기에 있습니다.
2.3. 그래도 남아 있는 강점
그렇다고 MapReduce 가 열등하기만 한 것은 아닙니다. 단계마다 결과가 디스크에 남기 때문에 장애에 매우 강합니다. 태스크 하나가 실패해도 그 태스크만 다시 돌리면 되고, 메모리에 무엇을 얼마나 들고 있는지 신경 쓸 일이 없어 예측 가능성이 높습니다.
정리하면 : MapReduce 의 한계는 표현력(두 단계) 과 단계 사이의 디스크 왕복, 그리고 기동 비용 세 가지입니다. 뒤의 두 엔진은 이 셋을 각각 다른 정도로 해결합니다.
3. Apache Tez
Tez 는 "MapReduce 의 실행 방식만 바꾸자" 는 접근입니다. 사용자가 직접 Tez 코드를 짜는 경우는 드물고, 주로 Hive 나 Pig 가 내부적으로 고르는 실행 엔진으로 만나게 됩니다.
3.1. 연산 전체를 하나의 DAG 로 표현한다
Tez 는 연산을 Map 과 Reduce 의 반복이 아니라, 정점(Vertex)과 간선(Edge)으로 이루어진 방향성 비순환 그래프로 표현합니다. 정점이 처리 단계이고, 간선이 데이터가 흘러가는 방향입니다. 앞서 예로 든 "집계 → 조인 → 정렬" 작업을 생각해보면 차이가 분명해집니다.
- MapReduce : 잡 3개. 잡이 끝날 때마다 HDFS 에 쓰고, 다음 잡이 그것을 읽습니다.
- Tez : DAG 1개. 정점 사이의 데이터는 HDFS 를 거치지 않고 로컬 디스크나 네트워크로 직접 전달됩니다.
HDFS 왕복과 복제가 사라지는 것이 Tez 의 가장 큰 이득입니다.
3.2. 컨테이너를 재사용한다
Tez 는 세션(Session) 단위로 AM(ApplicationMaster)과 컨테이너를 띄워두고 여러 작업에 걸쳐 재사용합니다. 질의가 들어올 때마다 JVM 을 새로 띄우지 않으니 기동 비용이 크게 줄어듭니다. Hive 에서 실행 엔진을 mr 에서 tez 로 바꾸기만 해도 같은 쿼리가 빨라지는 이유의 상당 부분이 여기에 있습니다.
3.3. 실행 중에 그래프를 조정한다
Tez 에는 실행 계획을 돌려보면서 고치는 기능도 있습니다. 대표적인 것이 리듀서 개수 자동 조정입니다. MapReduce 에서는 리듀서 개수를 사람이 미리 정해야 했습니다. 그런데 실제로 데이터가 얼마나 나올지는 돌려봐야 압니다. 너무 적게 잡으면 리듀서 하나가 과부하가 되고, 너무 많이 잡으면 빈 리듀서가 잔뜩 생깁니다.
Tez 는 앞 단계의 출력 크기를 보고 뒤 단계의 병렬도를 실행 중에 조정할 수 있습니다. 사람이 미리 맞춰야 했던 값을 엔진이 데이터를 보고 정하는 셈입니다.
3.4. 그럼에도 Tez 가 주인공이 되지 못한 이유
Tez 는 MapReduce 의 문제를 상당 부분 해결했지만, 넓게 쓰이지는 못했습니다. 저는 두 가지 이유 때문이라고 이해했습니다.
- 사용자가 직접 쓰는 물건이 아니었다 : Tez 는 사실상 Hive 의 실행 엔진이었습니다. 사용자에게 노출되는 API 가 친절하지 않았고, "Tez 로 개발한다" 는 감각이 없었습니다.
- 배치 질의를 빠르게 하는 데까지였다 : 중간 데이터를 다음 단계로 넘기는 것은 잘했지만, 그 데이터를 붙들어 두었다가 나중에 다시 쓰는 개념은 없었습니다. 그래서 같은 데이터를 수십 번 반복해 읽는 머신러닝 같은 작업에서는 여전히 이득이 없었습니다.
두 번째 항목이 바로 Spark 가 파고든 자리입니다.
4. Apache Spark
Spark 는 Tez 와 같은 DAG 기반이지만, 접근하는 범위가 훨씬 넓습니다. 실행 엔진을 바꾸는 데서 그치지 않고, 프로그래밍 모델 자체를 새로 제시했습니다.
4.1. RDD 와 지연 실행
Spark 의 출발점은 RDD(Resilient Distributed Dataset) 입니다. 클러스터에 흩어져 있는 데이터를 하나의 컬렉션처럼 다루는 추상화인데, 여기에 map, filter, join 같은 변환을 이어 붙여 연산을 표현합니다.
중요한 것은 이 변환들이 호출하는 즉시 실행되지 않는다는 점입니다. Spark 는 변환을 기록만 해두었다가, count 나 collect 같은 액션이 호출되는 순간 그동안 쌓인 변환 전체를 보고 실행 계획을 세웁니다.
전체를 미리 볼 수 있으니 최적화의 여지가 생깁니다. 예를 들어 뒤에 필터가 있으면 그것을 앞으로 당겨 읽는 양 자체를 줄일 수 있습니다.
4.2. 중간 데이터를 붙들어 둘 수 있다
Tez 와 갈리는 결정적인 지점입니다. Spark 는 persist() 나 cache() 로 중간 결과를 메모리에 올려두고 여러 번 재사용할 수 있습니다. 이것이 왜 중요한지는 반복 연산을 생각하면 분명해집니다. 머신러닝 학습은 같은 데이터를 수십, 수백 번 훑습니다. MapReduce 라면 매 반복마다 HDFS 에서 다시 읽어야 하고, Tez 도 작업이 끝나면 중간 데이터를 붙들어 두지 않습니다.
반면 Spark 는 첫 반복에서 한 번 읽어 메모리에 올려두고, 그다음부터는 그것을 씁니다. Spark 가 머신러닝과 대화형 분석 쪽에서 먼저 자리를 잡은 이유가 여기에 있습니다.
4.3. 익스큐터가 오래 살아 있다
Spark 는 애플리케이션이 시작될 때 익스큐터(Executor)를 확보하고, 그 애플리케이션이 끝날 때까지 붙들고 있습니다. 단계가 바뀌어도 같은 JVM 을 계속 씁니다. Tez 의 컨테이너 재사용과 비슷해 보이지만 결이 다릅니다. Tez 는 컨테이너를 재사용하는 것이고, Spark 는 그 위에 올려둔 데이터까지 함께 유지합니다. 4.2 의 캐시가 가능한 것도 익스큐터가 오래 살아 있기 때문입니다.
4.4. 하나의 프레임워크로 여러 영역을 덮는다
Spark 는 배치 밖으로도 나갔습니다.
- Spark SQL : SQL 로 질의하면 Catalyst 옵티마이저가 실행 계획을 세웁니다.
- Structured Streaming : 들어오는 스트림을 작은 배치로 끊어 같은 엔진으로 처리합니다.
- MLlib : 분산 머신러닝 알고리즘을 제공합니다.
- GraphX : 그래프 연산을 다룹니다.
같은 API 와 같은 실행 엔진 위에서 이 넷을 섞어 쓸 수 있다는 점이 Spark 가 표준이 된 큰 이유입니다. 하나의 파이프라인 안에서 SQL 로 집계하고 그 결과로 모델을 학습시키는 일이, 도구를 갈아타지 않고 가능해졌습니다.
5. 자주 오해하는 부분
비교 자료를 읽다 보면 "MapReduce 는 디스크, Spark 는 메모리" 라는 한 줄 요약을 자주 만납니다. 공부하면서 이 문장이 꽤 위험하다는 것을 알게 되었습니다.
5.1. Spark 도 셔플은 디스크에 쓴다
Spark 라고 해서 모든 데이터가 메모리에만 머무는 것이 아닙니다. 스테이지 사이의 셔플 데이터는 로컬 디스크에 씁니다. 이 점은 MapReduce 와 다르지 않습니다. 그럼 무엇이 다른가 하면, 쓰는 위치와 횟수입니다.
- MapReduce : 잡 사이의 중간 결과를 HDFS 에 쓰고, 복제까지 합니다.
- Spark · Tez : 스테이지 사이의 데이터를 로컬 디스크에 쓰고, 복제하지 않습니다.
즉 Spark 의 이득은 "디스크를 안 쓴다" 가 아니라 "분산 파일 시스템을 거치지 않는다" 입니다.
5.2. 그렇다면 메모리는 어디에서 이득인가
메모리가 진짜 힘을 발휘하는 곳은 같은 데이터를 여러 번 쓸 때입니다. 4.2 에서 본 캐시가 그것입니다. 반대로 데이터를 한 번만 훑고 끝나는 단순 배치라면 Spark 와 Tez 의 차이는 생각보다 크지 않습니다. 이런 작업에서 Spark 를 쓰면서 메모리를 넉넉히 잡아두면, 쓰지도 않을 메모리를 클러스터에서 점유하게 되는 셈입니다.
5.3. 메모리는 공짜가 아니다
Spark 는 메모리를 쓰는 만큼 메모리 관리의 책임도 사용자에게 넘어옵니다. 익스큐터 메모리, 파티션 수, 셔플 파티션 수 같은 값을 잘못 잡으면 OOM 이 나거나 디스크로 스필(spill)이 일어나 오히려 느려집니다.
MapReduce 가 느린 대신 예측 가능하다고 말하는 이유가 여기에 있습니다. 단계마다 디스크에 떨구니 튜닝할 것이 적고, 한 번 돌아간 잡은 대체로 계속 돌아갑니다.
6. 한눈에 비교하기
지금까지 본 것을 같은 기준으로 나란히 놓으면 다음과 같습니다.
| 기준 | MapReduce | Tez | Spark |
|---|---|---|---|
| 연산 표현 | Map · Reduce 두 단계 | DAG | DAG |
| 여러 단계 처리 | 잡을 여러 개 이어 붙임 | DAG 하나 | DAG 하나 |
| 단계 사이 데이터 | HDFS (복제됨) | 로컬 디스크 · 네트워크 | 로컬 디스크 · 네트워크 |
| 중간 데이터 재사용 | 불가 | 불가 | 캐시로 가능 |
| 프로세스 수명 | 태스크마다 새 JVM | 세션 단위 재사용 | 애플리케이션 단위 유지 |
| 실행 중 계획 조정 | 없음 | 병렬도 자동 조정 | 적응형 질의 실행(AQE) |
| 주 사용 방식 | 직접 작성 · Hive 백엔드 | Hive · Pig 백엔드 | 직접 작성 · SQL · 스트리밍 |
| 다루는 범위 | 배치 | 배치 질의 | 배치 · 스트리밍 · ML · 그래프 |
| 튜닝 부담 | 낮음 | 중간 | 높음 |
| 장애 대응 | 태스크 재실행 | 정점 재실행 | 계보(lineage) 기반 재계산 |
6.1. 언제 무엇을 쓰는가
- MapReduce : 새로 선택할 이유는 거의 없습니다. 다만 오래된 클러스터에 남아 도는 잡을 읽고 고쳐야 하는 일은 여전히 있습니다.
- Tez : Hive 를 쓰는 환경에서 실행 엔진으로 만납니다. 직접 고를 일은 드물지만, Hive 가 느릴 때 실행 엔진이 무엇인지 확인해볼 값어치는 있습니다.
- Spark : 새로 만드는 파이프라인의 기본 선택지입니다. 특히 같은 데이터를 반복해 쓰거나, 배치와 스트리밍을 한 코드베이스에서 다뤄야 한다면 사실상 대안이 없습니다.
7. 조금 더 들어가보기 : 셔플은 실제로 무엇을 하는가
세 엔진의 차이를 실행 모델로 정리했는데, 그 차이가 가장 크게 드러나는 자리가 셔플입니다. 셔플이 실제로 디스크와 네트워크에서 무엇을 하는지까지 내려가면, 앞에서 본 성능 차이가 숫자로 설명됩니다.
7.1. 셔플이 하는 일
같은 키를 한곳에 모으려면, 모든 노드가 모든 노드에게 데이터를 보내야 합니다. 노드가 N 대면 통신 경로가 N 제곱에 가깝게 늘어납니다. 이 과정은 대략 이렇습니다.
- 분류 : 각 맵 태스크가 자기 출력을 목적지별로 나눕니다. 키의 해시로 어느 리듀스 파티션에 갈지 정합니다.
- 정렬과 스필 : 메모리 버퍼에 모으다가 차면 정렬해서 디스크에 떨굽니다. 이것을 스필이라고 합니다.
- 병합 : 태스크가 끝나면 떨궈둔 조각들을 합쳐 하나의 파일로 만듭니다.
- 가져오기 : 리듀스 쪽이 각 맵 태스크에게서 자기 몫을 네트워크로 당겨옵니다.
- 다시 병합 : 여러 곳에서 받은 것을 합쳐 정렬된 순서로 만듭니다.
셔플은 디스크 쓰기와 읽기, 정렬, 네트워크 전송이 전부 들어 있는 구간입니다. 분산 처리에서 가장 비싼 단계인 이유가 이것입니다.
7.2. 세 엔진에서 무엇이 다른가
핵심 차이는 이 결과물이 어디에 놓이고 얼마나 오래 사는가입니다.
- MapReduce : 셔플 자체는 로컬 디스크를 쓰지만, 잡과 잡 사이의 결과는 HDFS 에 씁니다. 그러면 복제까지 일어납니다. 잠깐 쓰고 버릴 데이터를 세 벌 저장하는 셈입니다.
- Tez : DAG 안에서 정점 사이의 데이터를 로컬 디스크와 네트워크로 직접 넘깁니다. HDFS 왕복과 복제가 사라집니다. 앞 단계가 끝나기 전에 뒤 단계가 받기 시작할 수도 있습니다.
- Spark : 셔플 파일은 역시 로컬 디스크에 씁니다. 여기는 Tez 와 크게 다르지 않습니다. 차이는 그 뒤에 있습니다. 결과를 캐시해두면 다음 작업이 셔플 없이 그것을 재사용합니다.
앞에서 "Spark 도 셔플은 디스크에 쓴다" 고 한 부분이 여기서 구체화됩니다. 메모리의 이득은 셔플 구간이 아니라 재사용 구간에 있습니다.
7.3. 정렬이 필요한가
MapReduce 는 리듀서에 넘기기 전에 항상 키로 정렬합니다. 리듀서가 같은 키를 한 번에 받아 처리하는 모델이기 때문입니다. 그런데 집계만 하는 작업에는 정렬이 필요 없습니다. 해시로 묶기만 하면 됩니다. 정렬은 비교 횟수가 데이터 크기에 로그를 곱한 만큼 늘어나므로 공짜가 아닙니다.
Spark 가 초기에 해시 기반 셔플을 썼던 것도 이 때문이었습니다. 다만 해시 방식은 맵 태스크마다 리듀스 파티션 수만큼 파일을 만들어 파일 수가 폭증하는 문제가 있었고, 그래서 정렬 기반으로 돌아와 태스크당 파일 하나에 구간을 나눠 담는 방식이 되었습니다.
정렬 비용과 파일 개수 사이의 절충이었던 셈입니다. 앞서 여러 번 본 "개수가 문제" 라는 이야기가 여기서도 나옵니다.
7.4. 컨테이너 재사용이 왜 그렇게 큰 차이를 만드는가
Tez 와 Spark 가 빠른 이유 중 자주 과소평가되는 것이 이 부분입니다. JVM 을 새로 띄우는 데에는 프로세스 생성, 클래스 로딩, JIT 컴파일 예열이 따라옵니다. 짧은 태스크에서는 이 준비 시간이 실제 연산보다 길 수 있습니다.
컨테이너를 재사용하면 이 비용이 한 번만 발생합니다. 게다가 JIT 이 이미 최적화해둔 코드를 그대로 쓰므로 두 번째 태스크부터는 더 빠릅니다. Spark 가 대화형 분석에 잘 맞는 이유도 여기에 있습니다. 같은 애플리케이션 안에서 쿼리를 반복하면, 익스큐터도 살아 있고 JIT 도 예열되어 있고 캐시도 남아 있습니다.
7.5. 대규모에서 무엇이 문제가 되는가
- 파티션 수를 잘못 잡으면 양쪽이 다 나쁩니다. 너무 많으면 파일과 태스크 관리 비용이, 너무 적으면 파티션 하나가 커져 스필이 늘어납니다.
- 한 키가 거대하면 어떤 엔진도 못 쪼갭니다. 자동 조정 기능은 "여러 키가 섞여 큰 파티션" 만 나눌 수 있습니다.
- 셔플 데이터를 들고 있던 노드가 사라지면 앞 단계를 다시 계산해야 합니다. 자원을 탄력적으로 회수하는 환경에서 특히 문제가 됩니다.
- 드라이버나 AM 이 태스크 수만큼 상태를 추적하므로, 파티션을 수십만으로 늘리면 조정자 쪽이 먼저 막힙니다.
셔플을 줄이는 것이 튜닝의 대부분이라는 말이 여기서 나옵니다. 브로드캐스트 조인도, 분산 키 설계도, 미리 집계하는 것도 결국 이 구간을 건너뛰거나 줄이려는 시도입니다.
결론
세 엔진을 나란히 놓고 정리하면서 가장 크게 느낀 점은, 성능 차이를 만든 것이 "메모리" 라는 한 단어가 아니었다는 것입니다. 처음에는 저도 Spark 가 빠른 이유를 메모리로만 이해하고 있었는데, 실제로 갈라지는 지점은 연산을 몇 개의 작업으로 쪼개는가, 그 사이 데이터를 어디에 두는가, 프로세스를 얼마나 오래 살려두는가 세 가지였습니다. 메모리는 그중 두 번째와 세 번째에 걸쳐 있는 하나의 수단이었지, 그 자체가 원인은 아니었습니다.
Tez 를 들여다본 것도 도움이 되었습니다. Tez 는 지금 새로 선택할 일이 거의 없는 기술이지만, Spark 가 해결한 것과 해결하지 않은 것을 가르는 기준선 역할을 해주었습니다. Tez 는 MapReduce 의 "단계를 이어 붙이는 비용" 을 걷어냈고, Spark 는 거기에 더해 "중간 데이터를 다시 쓰는 길" 을 열었습니다. Tez 가 어디까지 갔는지를 알고 나니 Spark 의 캐시가 왜 결정적이었는지가 비로소 이해되었습니다.
또 하나 마음에 남은 것은, 빠른 것이 항상 나은 것은 아니라는 점입니다. MapReduce 는 느린 대신 튜닝할 것이 적고 예측 가능합니다. Spark 는 빠른 대신 메모리를 사람이 책임져야 합니다. 어느 쪽이 맞는지는 워크로드가 정하는 것이지, 세대가 정하는 것이 아니라는 생각이 들었습니다.
공부 참고 자료
📑 1. Apache Hadoop MapReduce Tutorial
📑 5. Spark RDD Programming Guide