Apache Spark Shuffle
Apache Spark에서 Shuffle은 매우 중요한 핵심 메커니즘 중 하나임.
대규모 데이터를 분산 환경에서 처리할 때 발생하는 ‘데이터 재분배(Data Redistribution) 또는 재파티셔닝(Shuffle)’ 과정을 의미함.
Shuffle 단계에서는 특정 Transformation(예: reduceByKey, groupByKey, join 등)을 실행하기 위해, 클러스터의 여러 노드에 분산되어 있던 데이터를 필요한 형태로 재배치해야 함.
이때 대규모 네트워크 I/O와 디스크 I/O가 발생하므로, Spark 성능과 자원 활용도에 큰 영향을 미침.
Shuffle의 기본 개념
1. 데이터 재분배(Partition Shuffling)
Spark는 RDD(혹은 Dataset/DataFrame)를 여러 파티션으로 나누어 병렬 처리함.
그러나 작업(Transformation)에 따라 기존 파티션 구조로는 계산이 불가능해지는 경우가 있음.
예를 들어, reduceByKey 연산을 통해 같은 키(key)에 해당하는 데이터들을 한 노드에서 모아야 할 때, 기존 파티션 경계를 넘어서 데이터를 옮겨야 함.
이 과정을 ‘Shuffle’라고 부름.
2. Stage 경계
Spark의 물리적 실행계획에서 Shuffle은 Stage(스테이지)의 경계를 발생시킴.
즉, Shuffle이 일어나는 지점에서 Job이 여러 Stage로 나뉘고, 해당 Stage 간에는 Shuffle 데이터를 서로 주고받음.
예를 들어, map -> reduceByKey -> map -> ... 형태의 처리과정에서 reduceByKey 연산이 들어가기 전에 Shuffle이 발생하고, 그 지점이 Stage가 나뉘는 곳이 됨.
3. 비용
Shuffle에서는 데이터가 네트워크를 통해 여러 Executor 노드로 전송되고, 중간 결과가 디스크에 기록되며, 필요 시 역직렬화나 압축/해제를 동반함.
네트워크 전송 + 디스크 I/O = 비용이 매우 큼
Spark 작업에서 가장 중요한 성능 병목 지점 중 하나임.
Shuffle 동작 방식 (고수준 메커니즘)
Spark에서 Shuffle은 크게 Map 단계와 Reduce(혹은 Shuffle Read) 단계로 나뉨.
1. Map 단계(Shuffle Write)
Shuffle을 일으키는 Transformation(예: reduceByKey) 앞에 존재하는 Task를 ‘Map Task’라고 함.
각 Map Task는 파티션 안의 레코드를 확인하면서, “어떤 키(또는 파티션)로 보내야 하는가” 를 결정함.
키/파티션에 따라 데이터를 여러 개의 ‘Bucket(파일 분할 단위)’으로 나누어, (디스크)파일로 저장함.
이를 Shuffle Write라고 하며, 결과물 파일을 흔히 ‘Shuffle 파일’이라고 부름.
2. Reduce 단계(Shuffle Read)
다음 Stage에서 실행되는 Task(‘Reduce Task’)는 각자 필요한 파티션(또는 키)에 해당하는 Shuffle 파일 조각들을 읽어와서(Shuffle Read), 최종 연산(reduce, group, join 등)을 수행함.
Spark는 Reduce Task가 필요한 데이터의 위치(Shuffle 파일의 메타데이터)를 파악하고 네트워크를 통해 해당 파일 조각(혹은 블록)을 받아옴.
Shuffle 매니저의 종류와 특징
Spark 내부에는 Shuffle 과정을 관리하는 Shuffle Manager가 존재하여, Shuffle 파일 생성 및 읽기 과정을 제어함.
Spark 버전에 따라 몇 가지 구현이 있었지만, 현재(2.x ~ 3.x 기준) 주로 Sort-based Shuffle(혹은 Tungsten Sort Shuffle)이 기본값으로 설정됨.
1. Hash-based Shuffle (Deprecated)
초기 Spark 버전에서 사용하던 방식.
각 Map Task가 각 Reduce Task별로 별도의 파일을 생성(파일 개수가 매우 많아짐)하여, 확장성 이슈가 존재했음.
Spark 1.2 이후 기본 구현에서 제외되었고, Sort-based Shuffle이 도입됨.
2. Sort-based Shuffle
현재 대부분의 Spark 버전에서 기본값으로 사용하는 Shuffle.
Map Task가 출력 데이터를 (키 기준) 정렬(Sort)한 뒤, 연속된 구간을 파티션 단위로 나누어 저장함.
각 Task당 하나의 Shuffle 파일(또는 소수의 파일)만 생성하므로, 파일 개수가 줄어들고 성능이 개선됨.
3. Tungsten Sort Shuffle
Spark의 Tungsten 엔진 최적화 프로젝트와 함께 Sort-based Shuffle을 더 최적화한 구현임.
Unsafe Row 포맷, 메모리 관리 기법 개선 등을 통해 CPU 및 메모리를 더 효율적으로 활용함.
기본 Sort-based Shuffle의 상위 호환 격임.
Shuffle 파일 관리와 External Shuffle Service
Shuffle 파일(Shuffle Write 결과물)은 보통 Spark Executor 프로세스의 디스크(local storage)에 저장됨.
그러나 Executor가 종료되면(예: 동적 자원 할당에 의해 스케일 다운), 해당 Executor 노드에 존재하던 Shuffle 파일에 접근할 수 없게 되는 문제가 있음.
이를 해결하기 위해 External Shuffle Service를 사용함.
1. External Shuffle Service
Spark Executor가 종료되더라도, Shuffle 파일을 외부 프로세스가 계속 서비스해줄 수 있도록 도와주는 서비스임.
Yarn, Kubernetes 등의 환경에서 Shuffle 데이터를 안정적으로 관리하기 위해 자주 사용됨.
Shuffle 파일은 Executor가 아닌, 해당 노드에서 동작하는 별도의 External Shuffle Service 프로세스가 보관/서빙함.
나중에 Reduce Task가 Shuffle 데이터를 읽어올 때, External Shuffle Service를 통해 네트워크 전송을 수행함.
2. Shuffle Block Fetch
Reduce Task는 Shuffle 파일에서 자신이 필요한 블록들만 골라서 받아옴(블록 단위 전송).
Spark 3.x에서는 Push-based Shuffle(Shuffle 데이터의 일부를 미리 밀어넣는 최적화) 등 추가적인 개선 기능도 실험적으로 제공됨.
Shuffle 성능 최적화
Shuffle는 Spark 애플리케이션에서 가장 비용이 큰 연산 중 하나이므로, 아래와 같은 최적화 전략을 고려할 수 있음.
1. 파티션 수(Shuffle Partition)의 적절한 설정
spark.sql.shuffle.partitions(DataFrame API) 또는 spark.default.parallelism(RDD API) 설정을 적절히 조절해야 함.
너무 많으면: Shuffle 파일이 지나치게 많이 생성 -> 메타데이터 오버헤드 및 디스크 I/O 증가
너무 적으면: 병렬성이 떨어져서 실행 시간이 느려짐
데이터 크기, 클러스터 자원 상황 등을 고려해 실험적으로 튜닝함.
2. Wide Transformation(Shuffle 연산)의 최소화
mapPartitions, map과 같이 Shuffle이 없는(또는 적은) 연산을 최대한 활용하고,
불필요한 groupByKey, repartition, join을 줄이거나, mapSideCombine이 가능한 API(reduceByKey, aggregateByKey) 등을 사용함.
예를 들어, groupByKey 대신 reduceByKey 사용하면 Shuffle되는 데이터양 대폭 감소함.
3. Data Skew(데이터 스큐) 처리
특정 키에 데이터가 몰리는 스큐(Skew) 현상은 Shuffle 단계에서 특정 노드에 부하가 집중되는 문제를 유발함.
Skew가 심한 키는 사전에 다른 키로 분산(mapping)시키거나, Spark에서 제공하는 Skew Join 최적화를 적용할 수 있음.
예를 들어, Spark SQL의 spark.sql.adaptive.skewJoin.enabled 옵션 등.
4. 메모리 및 네트워크 튜닝
Executor 메모리를 충분히 주어, 메모리 스필(spill)을 줄임.
Shuffle 데이터가 크면 Spark가 디스크로 spilling하는 횟수가 늘어나므로, 필요한 경우 spark.memory.fraction 등의 파라미터를 조정함.
클러스터 네트워크 대역폭, I/O 성능(SSD 디스크) 개선도 큰 도움이 됨.
5. Compression 및 Serializer 설정
Shuffle 시 직렬화/압축 과정을 거치므로, Spark에서 사용하는 Serializer(Kryo, Java Serializer)와 압축 코덱(LZ4, Snappy 등)을 적절히 선택하여 CPU vs. I/O 트레이드오프를 조율할 수 있음.
대역폭이 제한적이거나 네트워크 비용이 큰 환경에서는 압축을 강화하는 것이 좋을 수 있으나, CPU 사용량도 늘어남.
6. Push-based Shuffle(실험적 기능)
Spark 3.x 이상에서 제공되는 기능으로, Map 단계가 끝날 때 일부 Shuffle 데이터를 미리 Reduce측에 밀어넣어(“Push”) 두어, Shuffle Read 부담을 낮추는 방식임.
아직은 실험적이지만 점차 발전 중이며, 대규모 클러스터에서 성능 향상을 기대할 수 있음.
정리
1. Shuffle = 분산 데이터 재분배
키/파티션 단위로 데이터를 모으기 위해 네트워크 및 디스크에 큰 부하가 발생하는 작업.
Stage 경계가 생기고, Spark의 성능 병목이 되기 쉬운 지점.
2. Shuffle Manager & Shuffle Service
Spark 내부적으로 Shuffle 파일을 관리하고, (Map 단계) -> (Reduce 단계)의 파이프라인을 구성.
최근에는 Sort-based Shuffle + External Shuffle Service가 대세.
3. 성능 최적화 중요
Shuffle 파티션 수, wide transformation 최소화, 데이터 스큐 방지, 메모리/네트워크 설정, 직렬화/압축 설정 등 다각적인 튜닝 필요.
Shuffle은 스파크 엔진의 동작 원리를 이해하는 핵심 요소임.
제대로 이해하고 튜닝하지 않으면 불필요하게 많은 디스크, 네트워크 I/O가 발생하여 전체 작업 시간을 크게 늘릴 수 있음.
반면 Shuffle 과정을 최소화하거나, 필요한 경우 효율적으로 수행되도록 관리해주면 대규모 데이터 처리를 안정적이고 빠르게 진행할 수 있음.
결과적으로, Shuffle을 제대로 이해하고 적절한 설정을 적용하는 것은, Spark 성능 최적화와 확장성 확보를 위해 필수적인 부분임.
'Data Engineering > Spark' 카테고리의 다른 글
[Spark] Apache Spark의 Job, Stage, Task 구조 (0) | 2025.01.17 |
---|---|
[Spark] 스파크의 동작 원리 (0) | 2025.01.17 |
[Spark] 클러스터 매니저 종류 (0) | 2025.01.17 |
[Spark] Executor 개념 (0) | 2025.01.17 |
[Spark] Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/spark/sql/SparkSession (2) | 2024.12.14 |