Apache Spark 동작 원리
Apache Spark는 대규모 데이터 처리를 위해 메모리 기반의 빠른 연산을 제공하는 분산 컴퓨팅 엔진임.
스파크가 내부적으로 어떻게 애플리케이션을 실행하고, 자원을 스케줄링하며, 데이터를 처리하는지 이해하는 것은 성능 최적화와 효율적인 운영을 위해 매우 중요함.
전체 아키텍처 개요
1. Spark Application
사용자가 작성한 스파크 애플리케이션(예: Scala, Python, R, Java 코드)임.
SparkContext(Spark 1.x) 또는 SparkSession(Spark 2.x 이상)을 생성하여, Spark 클러스터에 작업을 제출함.
2. Driver
Spark Application의 메인 프로세스로, 사용자가 작성한 코드를 해석하고, 스케줄링 및 실행을 제어함.
주요 역할은 다음과 같음.
사용자의 Transformation(변환)과 Action(실행) 호출을 받아 DAG(Directed Acyclic Graph)를 구성.
물리적 실행 계획을 결정하여 Job → Stage → Task로 분할.
Cluster Manager(Standalone, YARN, Kubernetes 등)를 통해 Executor 자원을 할당받고, Task를 배포.
실행 결과(메트릭 등) 수집 및 최종 결과를 사용자에게 반환.
3. Cluster Manager
Spark가 요청한 리소스(노드, CPU, 메모리 등)를 관리하고 할당해주는 역할
대표적으로 Standalone, YARN, Mesos, Kubernetes 등이 있음.
Driver는 Cluster Manager에게 “Executor n개, 각각 CPU 코어 x개, 메모리 yGB” 식으로 자원을 요청하고, Cluster Manager는 물리적으로 가능한 범위 내에서 자원을 할당해줌.
4. Executor
실제로 Task(연산)를 실행하는 프로세스임.
각 Executor는 할당받은 코어 수만큼 Task를 병렬로 실행할 수 있으며, 자체 메모리를 사용해 데이터 캐싱, 중간 결과 저장 등을 수행함.
Driver가 실행 계획을 Task 단위로 쪼개어 각 Executor에게 배포하면, Executor는 해당 Task를 수행하고 결과를 Driver에게 보고함.
RDD, DataFrame, Dataset : 추상화 모델
Spark는 기본적으로 RDD(Resilient Distributed Dataset)라는 불변(Immutable)의 분산 데이터 컬렉션을 통해 데이터를 다룸.
1. RDD
Spark가 처음 등장했을 때부터 사용된 핵심 추상화.
낮은 수준의 API이지만 유연성이 높고, 분산 처리 시 발생하는 장애 복구(라인리지) 메커니즘을 포함함.
2. DataFrame / Dataset
Spark SQL 엔진(Catalyst)을 활용하여 테이블 형태의 구조적 API를 제공함.
옵티마이저를 통해 실행 계획을 더욱 효과적으로 최적화해주므로, 최근에는 DataFrame/Dataset이 더 널리 쓰임.
Spark는 사용자가 작성한 Transformation(예: map, filter, groupByKey 등)을 Lazy하게 기록해두고, 최종적으로 Action(예: count, collect, save)이 호출될 때만 실제로 계산을 수행함.
이때 내부적으로 RDD 혹은 DataFrame/Dataset의 연산 흐름이 DAG 형태로 표현되고, Spark 엔진이 이를 최적화하여 실행계획을 확정함.
논리적/물리적 실행 계획과 DAG 스케줄링
1. 논리적 실행 계획
사용자가 호출한 Transformation(연속된 RDD 연산, DataFrame 쿼리 등)을 기반으로 논리적 계획(Logical Plan)을 수립함.
DataFrame/Dataset에서는 Catalyst 옵티마이저가 Projection/Filter Pushdown, Predicate Pushdown 등 각종 최적화를 수행함.
2. 물리적 실행 계획
논리적 계획을 물리적 연산(Shuffle, Scan, Join, Sort 등)으로 구체화함.
RDD API에서는 DAG(Directed Acyclic Graph) 형태로, 각 RDD 변환 과정을 연결함.
DataFrame도 최종적으로 RDD 형태로 변환되어 DAG가 구성됨.
3. Stage와 Task 분할
DAG Scheduler는 Shuffle 경계를 기준으로 여러 개의 Stage로 분할함.
Shuffle이 일어나는 Transformation(reduceByKey, groupByKey, join 등)을 만나면 이전 단계와 나눠짐.
각 Stage 내에서는 파티션 단위로 병렬 처리가 가능하므로, Stage는 여러 개의 Task로 구성됨.
예를 들어, 파티션 100개라면, 보통 100개의 Task.
Stage는 순차적으로 실행되며, Stage 간 Shuffle 데이터 교환이 발생함.
실행 시퀀스 : Driver, Cluster Manager, Executor 간 상호작용
1. Driver 초기화
사용자가 Spark Application(메인 함수 등)을 실행하면, 내부에서 SparkSession 혹은 SparkContext를 생성함.
Cluster Manager와 통신하여 Executor를 여러 개 요청함.
2. Executor 할당
Cluster Manager는 물리적인 클러스터 상태(가용 노드, CPU, 메모리)를 파악하여, 필요한 만큼 Executor 프로세스를 실행시킴.
Executor들은 Driver와 통신 채널(RPC 등)을 열고, 작업을 받을 준비를 함.
3. Transformation, Action 호출
사용자가 Spark API(예: df.filter(...).groupBy(...).count())를 호출하면, Driver 내부에서 DAG가 구성됨.
Action이 호출되기 전까지는 실제 연산이 일어나지 않고, 최종 Action 시점에 DAG Scheduler가 동작하여 Job을 스케줄링함.
4. Job → Stage → Task 생성
Driver는 DAG를 분석해, 각각의 Shuffle 경계마다 Stage를 나누고, Stage별로 파티션 단위 Task를 생성함.
Stage 0이 끝난 뒤 Stage 1이 시작되는 식으로 의존 관계(Dependency)가 있는 순서대로 실행됨.
5. Task 배포 및 실행
Driver는 Task를 Executor들에게 분산 배치함.
주로 데이터가 존재하는 노드 근처에 Task를 배치하여 데이터 로컬리티를 높임.
각 Executor는 해당 Task를 실행하고, 중간 결과(메모리 혹은 디스크 저장 등)를 생성함.
6. Shuffle
Shuffle 경계가 있는 Stage에서는 Shuffle Write(맵 단계) -> Shuffle Read(리듀스 단계)가 발생함.
Executor 디스크(또는 External Shuffle Service)에 Shuffle 파일을 저장하고, 다음 Stage의 Executor가 이를 네트워크로 가져와서(Shuffle Read) 연산을 이어감.
7. 결과 수집 및 종료
모든 Stage의 Task가 끝나면 Action의 최종 결과가 Driver로 전달됨.
또는 외부 스토리지/HDFS/DB 등에 저장함.
애플리케이션이 종료되면 Driver, Executor 프로세스가 정리(shutdown)되고, 자원은 Cluster Manager에 반환됨.
메모리 기반 연산과 캐싱/퍼시스팅
Spark의 강점 중 하나는 메모리를 적극적으로 활용해 빠른 속도를 낸다는 점임.
1. 메모리 기반 연산
맵리듀스(Hadoop)와 달리, 중간 결과를 디스크 대신 메모리에 상주시켜 I/O 비용을 크게 줄임.
단, 데이터 크기가 메모리를 초과하면 자동으로 디스크 스필링(spilling)이 발생할 수 있으며, 이를 줄이기 위한 메모리 튜닝이 중요함.
2. 캐싱(또는 퍼시스팅)
자주 재사용되는 중간 RDD나 DataFrame을 메모리에 캐싱(cache())하거나 퍼시스트(persist())하면, 동일한 데이터를 여러 번 계산하지 않아도 되므로 성능 향상이 큼.
스토리지 레벨(MEMORY_ONLY, MEMORY_AND_DISK, DISK_ONLY 등)을 결정해, 메모리 사용량과 디스크 사용량 간 트레이드오프를 조절할 수 있음.
장애 복구와 RDD Lineage
Spark는 분산 환경에서 노드 장애가 발생해도, RDD의 Lineage 그래프를 통해 재연산을 수행하여 데이터를 복구할 수 있음.
Lineage는 다음과 같음.
1. RDD가 어떤 변환 과정을 거쳐 생성되었는지(부모 RDD, Transformation 등)를 DAG 형태로 추적하고 있음.
2. 특정 파티션이 손실되면, Spark는 라인리지 정보를 참조해 그 파티션만 다시 계산하여 복구함.
3. 별도의 중간 파일 전부를 저장할 필요 없이 최소한의 메타정보만으로 장애 복구가 가능하므로, 분산 환경에서 매우 유연하고 빠름.
성능 최적화 요인
1. Shuffle 최소화
Shuffle는 네트워크 I/O와 디스크 I/O를 일으키는 가장 비싼 연산임.
reduceByKey, mapSideCombine, broadcast join 등의 기법을 활용해 Shuffle 발생 횟수나 데이터 크기를 줄이는 것이 중요함.
2. 파티션 수 조정
spark.default.parallelism, spark.sql.shuffle.partitions 등을 조정하여, 너무 많은 파티션 혹은 너무 적은 파티션을 피해야 함.
클러스터 리소스, 데이터 크기에 따라 적절히 실험적으로 튜닝함.
3. 데이터 스큐(Data Skew) 처리
특정 키에 데이터가 편중되면, Shuffle 시 한 Executor가 과부하에 걸려 전체 성능이 저하됨.
스큐 키를 분산하기 위한 테크닉(가짜 키 추가, Skew Join 최적화 등)을 적용해야 함.
4. 메모리 및 직렬화/압축 설정
Spark에서 기본적으로 Kryo 직렬화, Snappy/LZ4 압축 등을 사용해 네트워크 I/O를 줄임.
Executor 메모리를 충분히 할당하고, GC 튜닝, Spill 최소화를 통해 CPU 및 I/O 병목을 줄임.
5. 코드 최적화
UDF(User-Defined Function) 사용 시, Python/Scala/Java 간 Overhead, Vectorized UDF 등 Spark SQL 최적화 방안을 활용하면 성능을 높일 수 있음.
요약
1. Driver가 사용자 코드를 해석해 DAG를 만들고, 이를 Stage와 Task로 분할하여 Executor에 배포.
2. Cluster Manager를 통해 자원을 할당받으며, 각 Executor가 실제 연산(맵, 필터, 조인 등)을 수행
3. Shuffle가 Stage 경계를 형성하며, 네트워크/디스크 I/O가 크게 발생하는 만큼 성능에 중요한 영향을 미침
3. RDD Lineage를 통해 장애 발생 시 최소한의 재연산으로 복구 가능
4. 메모리 기반 연산과 Lazy Evaluation 구조 덕분에 빠르고 유연한 대규모 데이터 처리가 가능
5. 최적화 지점: Shuffle 최소화, 파티션 튜닝, 스큐 방지, 메모리/직렬화 설정, 코드 최적화 등
결국 Spark의 동작원리를 이해한다는 것은, 분산 환경에서 어떠한 방식으로 데이터가 흐르고, 스케줄링 및 자원 관리가 이루어지며, 어떻게 장애복구와 최적화가 동작하는지를 파악하는 것임.
이러한 원리에 대한 이해가 뒷받침되면, 애플리케이션 성능과 안정성을 크게 향상시킬 수 있음.
'Data Engineering > Spark' 카테고리의 다른 글
[Spark] Adaptive Query Execution (0) | 2025.01.17 |
---|---|
[Spark] Apache Spark의 Job, Stage, Task 구조 (0) | 2025.01.17 |
[Spark] 스파크에서 Shuffle 개념 (0) | 2025.01.17 |
[Spark] 클러스터 매니저 종류 (0) | 2025.01.17 |
[Spark] Executor 개념 (0) | 2025.01.17 |