Apache Spark의 Executor
Apache Spark에서 “Executor”는 클러스터 환경에서 실제로 작업(Task)을 실행하고, 데이터 처리를 담당하는 프로세스를 의미함.
즉, Spark Driver가 분산 처리 계획을 세우고 Task를 스케줄링하면, 실제 연산은 Executor가 수행함.
Spark 애플리케이션이 실행되는 동안, 각 Executor는 애플리케이션에 할당된 자원을 사용하여 RDD나 DataFrame, Dataset 등을 메모리에 적재하고 계산을 수행함.
Executor의 생성 및 배포 방식
Spark 애플리케이션이 시작되면, 다음과 같은 흐름으로 Executor가 배포됨.
1. Driver 시작
사용자가 Spark Application(예: spark-submit)을 제출하면, 클러스터 매니저(YARN, Kubernetes, Mesos, Standalone 등)가 Spark Driver 프로세스를 실행함.
Driver는 전체 애플리케이션을 제어하고, Job과 Stage를 분해하여 Task를 생성, 스케줄링함.
2. Cluster Manager에게 Executor 요청
Driver는 애플리케이션에 필요한 리소스(코어, 메모리 등)에 대한 요구 사항을 클러스터 매니저에게 전달함.
클러스터 매니저는 노드(물리/가상 머신)에 Executor 프로세스를 생성하기 위한 자원 할당을 진행함.
3. Executor 생성 및 등록
할당받은 노드에서 Spark Executor 프로세스가 시작됨.
Executor는 Driver에게 자신이 실행 중임을 알려(등록) 후, Task를 받아서 실행할 준비를 함.
4. Executor 수명 주기
Spark Application(Driver) 관점에서 각 Executor는 애플리케이션이 종료될 때까지(또는 동적으로 스케일 다운/업될 때까지) 유지됨.
Application이 종료되면 모든 Executor는 함께 종료됨.
Executor의 내부 구조 및 역할
Executor는 크게 다음과 같은 주요 컴포넌트로 구성됨.
1. Task 실행기(Task Runner)
Spark Driver가 스케줄링하여 전송하는 각 Task를 실제로 실행하는 모듈임.
RDD 연산 또는 DataFrame 연산 등 Spark의 논리 연산을 물리적으로 수행함.
2. 메모리 관리자(Executor Memory Manager)
Task 실행에 필요한 데이터를 메모리에 캐싱하고 관리하는 역할을 수행함.
Spark 1.x 시절에는 legacy 방식을 사용했으나, Spark 2.x 이상에서는 UnifiedMemoryManager가 주로 사용되어, Execution과 Storage 메모리 풀을 통합하여 동적으로 관리함.
3. Shuffle 서비스(Shuffle I/O)
Shuffle 과정에서 데이터 읽기/쓰기(블록 전달)를 담당함.
Shuffle 파일은 로컬 디스크 등에 저장되며, 다른 Executor에서 필요한 데이터를 요청하면 이를 전달함.
4. Block Manager
RDD 블록, BroadCast 변수, Shuffle 데이터를 효율적으로 저장하고 관리하는 컴포넌트임.
캐싱 정책, 디스크 및 메모리 사용 정책 등에 의해 성능 최적화가 이뤄짐.
5. 통신 모듈(Netty 기반 RPC)
Driver와 통신하여 Task를 수신, 상태를 보고, 결과를 전송하는 모듈임.
Spark 내부적으로 Netty 기반의 RPC를 사용하여 Driver ↔ Executor 간 메시지를 주고받음.
Executor 자원 설정과 동작 메커니즘
Spark Executor가 사용하는 메모리는 크게 다음과 같이 구분할 수 있음.
Spark 2.x ~ 3.x 기준, UnifiedMemoryManager 가정함.
1. Execution Memory
Shuffle, Join, Sort, Aggregation과 같은 연산을 수행하기 위해 필요한 임시 버퍼로 사용됨.
Task가 실행 중에 데이터 구조를 생성할 때 사용하며, 사용이 끝나면 반환됨.
2. Storage Memory
RDD나 Dataset을 .cache() 혹은 .persist() 했을 때 데이터를 저장하는 공간임.
Broadcast 변수를 저장하거나, Shuffle 데이터를 읽고 쓸 때도 사용됨.
3. Unified Memory
Execution과 Storage를 구분하되, 상황에 따라 유동적으로 메모리를 할당할 수 있는 통합 메모리 풀 구조임.
Execution 메모리가 부족할 경우 Storage 영역을 일부 축소하여 확보하고, 반대 경우도 가능함.
4. Other Memory(오버헤드)
JVM 자체의 메타데이터, 스레드 스택, 네이티브 라이브러리 등에서 사용하는 영역임.
Spark 설정에서 memoryOverhead로 잡아두지 않으면 Executor가 OOM에 쉽게 걸릴 수 있음.
Executor에서의 Shuffle 동작
Shuffle는 Spark의 분산 처리에서 가장 비용이 큰 연산 중 하나임.
Executor가 Shuffle를 수행할 때 고려해야 할 사항은 다음과 같음.
1. Map Task
Shuffle 맵 단계에서는 키(또는 파티션 기준)에 따라 데이터를 로컬 디스크에 파일 형태로 분할해 씀.
예를 들면, Sort-based Shuffle 시, 각 파티션별로 구분함.
Task가 완료되면 Shuffle 파일의 메타데이터 정보를 Driver에게 보고함.
2. Reduce Task
Reduce 단계에서 다른 Executor가 생성한 Shuffle 파일에 접근하여 필요한 파티션만 네트워크로 받아옴.
데이터를 불러와서 Join, Aggregation, Sort 등을 수행 후 결과를 다시 저장하거나 반환함.
3. External Shuffle Service
YARN 클러스터 등에서 Executor를 재시작하거나 교체해도 Shuffle 파일을 유지하기 위해 외부 Shuffle 서비스(External Shuffle Service, ESS)를 쓰기도 함.
ESS가 별도 프로세스로 동작하여 Shuffle 파일을 관리하므로, Executor 재시작 시에도 Shuffle 데이터를 재사용할 수 있음.
성능 및 최적화 전력
Executor가 업무 수행의 핵심이므로, 성능 최적화를 위해서는 아래와 같은 전략들이 많이 활용됨.
1. Executor의 병렬도 조정
적절한 코어 수와 Executor 수를 설정하여 전체 Cluster 리소스를 균형있게 사용해야 함.
코어가 너무 많으면 GC 부하가 커질 수 있고, 너무 적으면 병렬도가 낮아 애플리케이션 전체 실행 시간이 늘어남.
2. 메모리 튜닝
--executor-memory, spark.executor.memoryOverhead 등을 통해 충분한 메모리를 할당하되, 노드 리소스 한계를 넘지 않도록 조정함.
GC 튜닝(예: G1GC, CMS, ZGC 등)이나, 사용 JVM 버전(자바 8 vs 11 vs 17)에 따라도 성능이 달라짐.
3. 데이터 로컬리티(Data Locality)
RDD나 Partition이 저장된 위치와 Executor 위치가 가까울수록 네트워크 I/O를 줄일 수 있음.
스케줄러가 가능한 한 로컬리티를 만족시켜 Task를 배정할 수 있도록 파티션 수, 파일 배치 등을 관리해야 함.
4. Shuffle 최적화
Shuffle 압축(spark.shuffle.compress) 설정, Shuffle 파일 병합(Consolidate Files) 사용, Tungsten 엔진 최적화, AQE(Adaptive Query Execution) 활용 등이 있음.
Executor의 디스크 I/O 병목을 피하기 위해 SSD 사용, 네트워크 성능 튜닝 등을 고려함.
5. Dynamic Allocation 활용
작업량이 많은 시점에는 Executor를 많이 띄워 병렬도를 높이고, 작업량이 적어지면 Executor를 반납하여 리소스를 절약할 수 있음.
장시간 Idle 상태인 Executor를 유지하는 것은 비용적 비효율을 유발함.
장애 복구 측면에서의 Executor
1. Task 재시도
Executor에서 Task 실패가 발생할 경우, Driver는 동일 혹은 다른 Executor로 Task를 재할당하여 재시도함.
Spark는 lineage(RDD나 DataFrame의 연산 내역) 정보를 활용하여 데이터를 다시 생성해낼 수 있으므로, 중간 데이터 손실 시에도 복구가 가능함.
2. Executor 재시작
Executor 자체가 다운되거나 네트워크가 단절되면, Spark Driver는 클러스터 매니저를 통해 새로운 Executor를 띄울 수 있음.
External Shuffle Service를 사용할 경우, 기존 Shuffle 데이터는 별도 프로세스에서 유지되므로, Executor가 재시작되더라도 Shuffle 데이터를 재활용할 수 있음.
3. Speculative Execution(추측 실행)
어떤 Executor에서 특정 Task의 수행이 지나치게 지연되고 있으면, Driver가 동일 Task를 다른 Executor에서 동시에 실행하기도 함.
먼저 끝난 결과를 채택하고, 느린 Task는 취소하여 전체 Job의 처리 시간을 단축함.
정리
Apache Spark에서 Executor는 분산된 노드에서 실제 데이터를 계산하고, 메모리를 관리하며, Shuffle를 수행하는 “실행 엔진”임.
Driver가 분산 처리를 지휘하고, Cluster Manager가 자원을 할당하면, Executor는 각 노드에서 필요한 연산을 병렬로 실행하여 높은 처리량과 빠른 속도를 달성함.
주요 역할은 다음과 같음.
1. Task 실행: Driver가 배정한 Task를 수행하여 RDD, DataFrame 등의 연산을 처리
2. 메모리 관리: Execution/Storage 메모리를 적절히 사용 및 관리
3. Shuffle 처리: 대용량 데이터 재분배를 위한 파일 I/O와 네트워크 통신 수행
4. Fault Tolerance: 장애 발생 시 Task 재실행, Executor 재시작 등을 통해 안전한 분산 처리 보장
최적화 포인트
1. Executor의 개수, 코어, 메모리, 오버헤드 등 자원 설정
2. Shuffle, GC, 네트워크, 디스크 I/O 등의 병목 요소 제거
3. Dynamic Allocation 등 자동 스케일링을 통한 효율 극대화
Spark 애플리케이션의 성능은 Executor를 어떻게 구성하고 운용하느냐에 달려 있다고 해도 과언이 아님.
잘 튜닝된 Executor 환경은 빅데이터 처리의 효율을 크게 향상시키고, 안정적인 분산 처리를 가능케 함.