빅데이터/Spark

[Deep Dive] Spark Internals: spark-submit부터 Task 실행까지의 여정

네야_IT 2026. 1. 8. 18:16
반응형

Apache Spark를 다루는 데이터 엔지니어나 아키텍트라면 한 번쯤 "도대체 내 코드가 클러스터 내부에서 어떻게 쪼개져서 실행되는 걸까?" 라는 질문을 던져보았을 것입니다.

 

특히 성능 튜닝(Performance Tuning)과 트러블슈팅은 Spark의 실행 계획(Execution Plan)과 물리적 아키텍처(Physical Architecture)를 이해하는 것에서 시작됩니다. 오늘은 Spark 애플리케이션이 제출(submit)되고 결과가 나오기까지의 내부 프로세스를 해부해 보겠습니다.

 

1. The Big Picture: 주요 컴포넌트 (The Actors)

프로세스를 이해하기 전에 무대 위의 배우들을 먼저 알아야 합니다.

  1. Driver (The Brain):
    • main() 함수를 실행하는 프로세스입니다.
    • SparkContext (또는 SparkSession)를 생성합니다.
    • 애플리케이션의 라이프사이클을 관리하고, 코드를 Task 단위로 변환하여 Executor에게 명령을 내립니다.
  2. Cluster Manager (The Negotiator):
    • 리소스(CPU, RAM)를 관리하는 주체입니다. (예: YARN, Kubernetes, Standalone, Mesos).
    • Driver의 요청을 받아 Worker Node에 컨테이너를 할당해줍니다.
  3. Executor (The Muscle):
    • 실제 Worker Node에서 실행되는 JVM 프로세스입니다.
    • Driver가 할당한 Task를 수행하고 데이터를 메모리나 디스크에 저장합니다.

 

2. Step-by-Step Execution Flow

Spark 애플리케이션이 실행되는 과정은 크게 5단계로 나눌 수 있습니다.

Phase 1: 시작과 리소스 할당 (Initialization)

spark-submit 명령어를 치면 다음과 같은 일이 발생합니다.

  1. Driver 구동: Driver 프로세스가 시작되고 SparkSession이 초기화됩니다.
  2. 리소스 요청: Driver는 Cluster Manager에게 "나 작업해야 하니까 Executor 좀 띄워줘"라고 요청합니다.
  3. Executor 등록: Cluster Manager는 Worker Node들에 Executor 프로세스를 띄웁니다. 실행된 Executor들은 Driver에게 자신의 위치와 리소스 상태를 등록(Register)합니다. 이제 Driver는 누구에게 일을 시킬지 알게 되었습니다.

Phase 2: 논리적 계획 수립 (Lazy Evaluation & Catalyst)

사용자가 df.filter(...).join(...) 코드를 작성했다고 해서 바로 실행되지 않습니다. Spark는 Action (예: count(), write(), collect())이 호출될 때까지 기다립니다(Lazy Evaluation).

Action이 호출되면, Spark SQL 엔진(Catalyst Optimizer)이 작동합니다:

  • Unresolved Logical Plan: 코드 문법만 확인한 상태. (컬럼명이 실제로 존재하는지 모름)
  • Logical Plan: 카탈로그(메타스토어)를 확인하여 테이블과 컬럼 검증.
  • Optimized Logical Plan: (중요) Catalyst가 최적화를 수행합니다.
    • Predicate Pushdown: 필터를 데이터 소스 쪽으로 밈.
    • Constant Folding: 상수 연산을 미리 계산.
    • Column Pruning: 안 쓰는 컬럼 제외.

Phase 3: 물리적 계획과 DAG 생성 (Physical Planning)

최적화된 논리 계획을 바탕으로 어떻게(How) 실행할지 결정하는 단계입니다.

  1. Physical Plan: 조인을 할 때 Sort Merge Join을 할지, Broadcast Hash Join을 할지 결정합니다.
  2. DAG Scheduler: 여기서 RDD의 의존성(Dependency)을 분석하여 Stage를 나눕니다.
    • 이때 가장 중요한 기준이 Shuffle입니다.
    • Narrow Dependency: 셔플 없음 (map, filter). 파이프라인으로 연결됨.
    • Wide Dependency: 셔플 발생 (groupBy, join). 데이터를 재분배해야 함.
    • Result: 셔플 경계를 기준으로 전체 Job이 여러 개의 Stage로 쪼개집니다.

💡 RSA Interview Point: "Stage는 셔플 경계(Shuffle Boundary)에서 나뉜다"는 점을 명확히 해야 합니다. 셔플이 많으면 Stage가 많아지고, 이는 디스크 I/O 비용 증가로 이어집니다.

Phase 4: Task 스케줄링 (Scheduling)

Stage가 만들어졌으면, 이를 실제 처리 단위인 Task로 쪼갭니다.

  1. Task의 개수: 기본적으로 파티션(Partition) 1개당 Task 1개가 생성됩니다.
  2. Task Scheduler: 생성된 TaskSet을 받아 Executor들에게 분배합니다.
    • Data Locality (데이터 지역성): Spark는 "데이터가 있는 곳으로 코드를 보낸다"는 원칙을 따릅니다. 데이터가 캐시된 노드나 HDFS 블록이 있는 노드에 우선적으로 Task를 할당합니다.
  3. Speculative Execution: 만약 특정 Task가 너무 느리다면, 다른 노드에 똑같은 Task를 하나 더 띄워서 먼저 끝나는 놈의 결과를 채택하기도 합니다.

Phase 5: 실행과 결과 (Execution)

  1. De-serialization: Executor는 Driver로부터 받은 직렬화된 코드(Closure)를 역직렬화합니다.
  2. Execution: 데이터를 읽어와 연산을 수행합니다.
    • Narrow Transformation들은 Pipeline 되어 하나의 함수처럼 연속적으로 실행됩니다. (메모리에 썼다 지웠다 하지 않음).
  3. Result: 결과값(Action의 결과)을 Driver로 보내거나(Collect), 파일 시스템에 씁니다(Write).

 

3. 심화: 파이프라이닝 (Pipelining)

Spark 성능의 핵심 중 하나는 Pipelining입니다. 예를 들어 df.map(...).filter(...).map(...)을 수행한다고 가정해 봅시다.

  • 초보자의 오해: Map 결과를 메모리에 쓰고, 그걸 다시 읽어서 Filter하고, 다시 쓰고...
  • 실제 동작 (Volcano Iterator Model / Whole-Stage Code Generation): Spark는 이 3개의 연산을 하나의 함수로 합칩니다(Fusion). 레코드 하나가 들어오면 CPU 레지스터 레벨에서 map, filter, map을 한 번에 통과합니다. 이를 통해 메모리 접근을 최소화하고 속도를 비약적으로 높입니다.
반응형