스파크를 다루는 기술 1: MapReduce에서 RDD까지
페타 제체비치, 마르코 보나치의 『스파크를 다루는 기술』을 읽으며 정리한 내용을 세 편으로 나누어 기록해보려고 합니다. 첫 번째 글에서는 Spark를 이해하기 위한 배경으로 MapReduce와 Hadoop을 먼저 살펴보고, Spark의 기본 실행 흐름과 RDD 개념까지 정리합니다.
MapReduce란?
MapReduce는 구글의 논문인 MapReduce: Simplified Data Processing on Large Clusters에서 소개된 대규모 데이터 처리 모델입니다. 핵심은 클러스터 컴퓨팅을 조금 더 단순한 방식으로 다룰 수 있게 만드는 데 있습니다.
MapReduce의 처리 흐름은 크게 세 단계로 볼 수 있습니다.
- 잡을 잘게 분할하고, 클러스터의 여러 노드에 매핑해 분산 처리합니다.
- 각 노드는 자신에게 할당된 작업을 처리한 뒤 중간 결과를 생성합니다.
- 분할된 중간 결과를 reduce 단계에서 집계해 최종 결과를 만듭니다.
MapReduce가 해결하려는 문제는 크게 세 가지입니다.
- 병렬 처리: 작업을 잘게 나누어 동시에 처리합니다.
- 데이터 분산: 데이터를 여러 노드에 나누어 저장하고 처리합니다.
- 장애 내성: 분산 컴포넌트의 장애에 대응합니다.
예를 들어 마스터는 모든 워커 노드에 주기적으로 ping을 보냅니다. 일정 시간 이상 워커의 응답이 없다면 해당 워커에 문제가 생겼다고 판단하고, 그 워커가 맡고 있던 맵 태스크를 초기 상태로 되돌려 다른 워커에 다시 스케줄링합니다.
이 모델의 중요한 아이디어는 데이터를 처리하는 곳으로 옮기는 것이 아니라, 데이터가 저장된 곳으로 프로그램을 보내는 것입니다. 대규모 데이터에서는 네트워크 전송 비용이 크기 때문에, 가능한 한 데이터 가까이에서 연산하는 것이 중요합니다.
Word Count 예시
가장 대표적인 예시는 단어 수 세기입니다.
- map: 각 문장을 단어로 분할하고
(단어, 1)쌍 목록을 반환합니다. - shuffle phase: 같은 단어가 같은 reducer로 전달되도록 map 결과를 키별로 그룹핑합니다.
- reduce: 단어별 출현 횟수를 합산해 최종 결과를 생성합니다.
shuffle 단계는 병목이 될 수 있지만, 이후 reduce 단계에서 단어별 집계를 단순하게 처리할 수 있게 해줍니다.
Spark란?
Spark는 Hadoop의 MapReduce를 대체하는 빅데이터 처리 플랫폼입니다.
Hadoop은 분산 컴퓨팅을 위한 Java 기반 오픈소스 프레임워크입니다. 보통 Hadoop Distributed File System, 즉 HDFS와 MapReduce 처리 엔진을 함께 떠올립니다.
Spark는 범용 분산 컴퓨팅 플랫폼이라는 점에서 Hadoop과 유사합니다. 다만 대량의 데이터를 메모리에 유지하는 설계를 통해 반복 연산이나 대화형 분석에서 더 나은 성능을 기대할 수 있습니다.
Hadoop MapReduce에서는 한 잡의 계산 결과를 다른 잡에서 사용하려면 HDFS에 저장한 뒤 다시 읽어와야 합니다. 그래서 반복 알고리즘에는 비효율적인 면이 있습니다. 또한 모든 문제를 MapReduce 연산만으로 자연스럽게 분해할 수 있는 것도 아닙니다.
Spark는 이런 한계를 보완하기 위해 등장한 처리 엔진으로 볼 수 있습니다.
Spark가 적합하지 않은 경우
Spark도 모든 상황에 적합한 도구는 아닙니다.
분산 아키텍처를 사용하기 때문에 처리 시간에 어느 정도 오버헤드가 발생합니다. 대량의 데이터셋에서는 이 오버헤드가 크게 문제 되지 않지만, 작은 데이터셋이라면 다른 프레임워크가 더 효율적일 수 있습니다.
또한 Spark는 OLTP, 즉 대량의 원자성 트랜잭션을 처리하는 시스템에는 적합하지 않습니다. 대신 일괄 처리나 분석성 작업인 OLAP에 더 잘 맞습니다.
Hadoop의 핵심 아이디어
Hadoop은 크게 다음 세 가지 아이디어를 바탕으로 합니다.
- 병렬 처리(parallelization): 여러 연산을 잘게 나눕니다.
- 데이터 분산(distribution): 데이터를 여러 노드로 나누어 저장합니다.
- 장애 내성(fault tolerance): 분산 컴포넌트의 장애에 대응합니다.
Spark 역시 이런 분산 처리의 기본 전제를 공유합니다. 차이는 데이터를 어떻게 재사용하고, 어떤 방식으로 실행 계획을 구성하느냐에 있습니다.
Spark의 실행 과정
예를 들어 300MB 파일을 HDFS 클러스터에 저장한다고 해보겠습니다. HDFS는 이 파일을 128MB, 128MB, 44MB 블록으로 나누어 클러스터의 세 노드에 저장할 수 있습니다. 복제 계수를 기본값인 3으로 설정하면, HDFS는 각 블록을 다른 노드 두 곳에도 복제합니다.
Spark는 파일의 각 블록, 즉 파티션이 저장된 위치를 Hadoop에 요청합니다. 그리고 각 블록을 해당 블록이 저장된 HDFS 노드의 RAM에 로드합니다. 이를 데이터 지역성(data locality)이라고 합니다.
데이터 지역성을 활용하면 대량의 데이터를 네트워크로 옮기지 않고, 데이터가 있는 곳에 가까운 위치에서 연산할 수 있습니다.
RDD가 참조하는 분산 컬렉션은 여러 파티션의 집합입니다. 사용자는 이 컬렉션이 여러 노드에 나누어 저장되어 있다는 사실을 매번 신경 쓰지 않아도 됩니다.
예를 들어 필터링을 수행하면 필터링된 정보만 RAM에 저장됩니다. 이후 cache를 사용하면 파일을 다시 로드하지 않고도 다른 잡에서 같은 RDD를 메모리에 유지한 채 재사용할 수 있습니다. 이 필터링 작업은 여러 노드에서 병렬로 실행됩니다.
RDD
RDD는 Resilient Distributed Dataset의 약자입니다. Spark의 기본 추상화이며, 분산 환경에서 데이터를 다루기 위한 핵심 개념입니다.
RDD의 특징은 크게 세 가지입니다.
불변성
RDD는 읽기 전용 데이터셋입니다. 변환 연산자는 기존 RDD를 직접 수정하지 않고 항상 새로운 RDD 객체를 생성합니다. 즉, 한 번 생성된 RDD는 불변입니다.
복원성
RDD는 장애 내성을 갖습니다. 노드에 장애가 발생해도 RDD를 복원할 수 있습니다.
Spark는 데이터셋을 만드는 데 사용된 변환 연산자의 로그를 남깁니다. 장애가 발생하면 전체 데이터를 다시 만드는 것이 아니라, 문제가 생긴 노드가 가지고 있던 데이터셋만 다시 계산해 RDD를 복원합니다.
분산성
RDD는 하나 이상의 노드에 저장된 데이터셋입니다. 사용자는 물리적으로 데이터가 어느 노드에 있는지 직접 다루지 않고, 논리적인 컬렉션처럼 사용할 수 있습니다.
이런 특성을 위치 투명성(location transparency)이라고 볼 수 있습니다. 파일의 물리적인 조각이 여러 장소에 저장되어 있어도, 사용자는 파일 이름이나 RDD 참조를 통해 데이터에 접근합니다.
변환 연산자와 행동 연산자
Spark 연산은 크게 변환 연산자와 행동 연산자로 나눌 수 있습니다.
- 변환 연산자: 데이터를 조작해 새로운 RDD를 생성합니다. 예를 들어
filter,map이 있습니다. - 행동 연산자: 계산 결과를 실제로 반환합니다. 예를 들어
count,foreach가 있습니다.
Spark는 지연 실행(lazy evaluation)을 사용합니다. 변환 연산자를 호출했다고 해서 바로 계산하지 않습니다. 행동 연산자를 호출하는 시점에 실제 계산을 실행합니다.
이 방식 덕분에 Spark는 실행 계획을 모아두었다가 더 효율적인 방식으로 계산할 수 있습니다.
Scala for comprehension 예시
책에서는 Scala 코드도 함께 다룹니다. 예를 들어 파일에서 라인을 읽어 Set으로 만드는 코드는 다음과 같습니다.
val employees = Set() ++ (
for {
line <- fromFile(empPath).getLines
} yield line.trim
)
for 루프의 각 사이클마다 line.trim 값이 임시 컬렉션에 추가됩니다. 루프가 종료되면 이 임시 컬렉션이 반환되고, 이후 Set에 합쳐집니다.
공유 변수
분산 환경에서는 클러스터의 여러 노드가 같은 데이터를 참조해야 할 때가 있습니다. 이때 Spark의 공유 변수를 사용할 수 있습니다.
val bcEmployees = sc.broadcast(employees)
val isEmp = user => bcEmployees.value.contains(user)
공유 변수는 클러스터의 각 노드에 정확히 한 번만 전송되고, 메모리에 자동으로 캐시됩니다. 공유 변수를 사용하지 않으면 작업을 수행하는 태스크 개수만큼 같은 데이터를 반복해서 네트워크로 전송할 수 있습니다.
Spark는 P2P 프로토콜로 공유 변수를 전파합니다. 각 노드가 서로 공유 변수를 교환하며 확산시키는 방식인데, 이를 가십 프로토콜(gossip protocol)이라고도 합니다. 덕분에 마스터 실행이 크게 지연되지 않습니다.
공유 변수에 접근할 때는 반드시 value 메서드를 사용합니다.
마무리
이번 글에서는 Spark를 이해하기 위한 배경으로 MapReduce와 Hadoop을 먼저 살펴보고, Spark의 기본 실행 흐름과 RDD를 정리했습니다.
다음 글에서는 Spark 성능을 이해할 때 중요한 파티셔닝, 셔플링, RDD 의존 관계를 정리해보겠습니다.