CHAPTER 03 · 분산처리와 Spark 실행 원리

Spark는 어떻게
나누어 계산할까?

1부터 100까지의 합을 여러 계산 자원에 나누어 구합니다. 환경과 전체 코드를 확인하고, 데이터가 나뉘고 합쳐지는 과정을 배웁니다. Worker와 코어 수를 바꾸어 실행 구조와 처리 시간을 비교합니다.

환경·전체 코드분산 처리·DataFrame실행 관찰·성능 비교

A 코드와 개념 해설

환경과 전체 코드를 먼저 확인한 뒤, 분산 처리 원리와 코드의 역할을 차례로 읽습니다.

Full Source Code

실행 환경과 파일을 준비하고 전체 코드의 흐름을 확인합니다. 각 줄의 역할은 이어지는 절에서 설명합니다.

실습 자료 한 번에 받기

실행 관찰 실습

B에서 사용할 환경 설정, 합계 프로그램, 제출·검사 코드 5개입니다.

실행 관찰 ZIP 받기

자원·성능 비교 실습

C에서 사용할 환경 설정, 시간 측정 프로그램, 제출 코드 3개입니다.

성능 비교 ZIP 받기

두 ZIP을 같은 대상 폴더에 압축 해제하면 BigData/실습자료 아래에 두 실습 폴더가 나란히 생깁니다. 압축 파일 안에서 바로 실행하지 말고 먼저 전체 압축을 푸세요.

두 ZIP을 내려받은 폴더의 PowerShell에서 다음 명령을 실행해도 됩니다. 새 spark-labs 폴더를 만든 뒤 이 장의 시작 위치인 BigData로 이동합니다.

Expand-Archive .\spark-execution-lab.zip -DestinationPath .\spark-labs
Expand-Archive .\spark-performance-lab.zip -DestinationPath .\spark-labs
cd .\spark-labs\BigData

필요한 환경

파일을 저장할 위치

ZIP의 압축을 풀면 아래 구조가 만들어집니다. 기존 학습 폴더가 있다면 해당 파일을 그대로 사용할 수 있습니다. 개별 파일을 받는 경우에도 같은 구조로 저장하세요.

이름이 같은 두 compose.yaml은 용도가 다르므로 각각의 폴더에 둡니다.

BigData/
└─ 실습자료/
   ├─ 2장_실습자료/          # 1~100 합계와 실행 구조 관찰
   │  ├─ compose.yaml
   │  └─ apps/
   │     ├─ run.sh
   │     ├─ first_app.py
   │     ├─ check_pyspark.py
   │     └─ check_network.py
   └─ 3장_실습자료/          # 큰 입력의 성능 비교
      ├─ compose.yaml
      └─ apps/
         ├─ run_performance.sh
         └─ performance_app.py

명령에서 말하는 BigData 폴더는 위 구조에서 실습자료를 바로 포함하는 폴더입니다. 파일 탐색기로 이 폴더를 연 뒤 주소창에 powershell을 입력하고 Enter를 누르면 해당 위치에서 시작할 수 있습니다.

합계 실습은 코드가 숫자를 생성하므로 별도 입력 파일이 필요하지 않습니다. 단어 집계의 세 줄 예시는 표의 변화를 설명하며, 실행용 입력 파일은 해당 절의 보충 실습에서 받습니다.

두 실습의 폴더와 화면 주소
목적실행 폴더브라우저 주소
1~100 합계와 실행 구조 관찰실습자료/2장_실습자료Master localhost:8080
Driver localhost:4040
Worker·코어 수에 따른 성능 비교실습자료/3장_실습자료Master localhost:18080
Driver localhost:24040

Master는 사용할 계산 자원을 관리하고, Worker는 그 자원을 제공합니다. Driver는 제출한 프로그램의 계산을 지휘합니다. 각 역할과 화면의 의미는 뒤에서 차례로 설명합니다.

전체 파일 목록

  1. 실행 관찰 · compose.yaml — Spark를 실행할 컨테이너와 연결 주소를 정합니다. 두 Worker로 시작하는 명령은 뒤의 실행 준비에서 안내합니다.
  2. 실행 관찰 · apps/run.sh — Python 프로그램을 Spark에 제출하는 Bash 스크립트입니다. 전체 코어 상한을 2로 지정합니다.
  3. 실행 관찰 · apps/first_app.py — 1부터 100까지의 합을 5050과 비교하고, 실행 상태를 볼 수 있도록 잠시 기다린 뒤 종료합니다.
  4. 환경 검사 · apps/check_pyspark.py — 컨테이너 안의 PySpark 버전과 간단한 로컬 계산을 확인합니다.
  5. 환경 검사 · apps/check_network.py — 서비스 이름을 주소로 찾을 수 있는지, 필요한 통신 포트에 연결할 수 있는지 확인합니다.
  6. 성능 비교 · compose.yaml — Worker를 1·2·4·8개로 복제할 수 있는 환경을 만듭니다. 실행 관찰용 설정과 별도 폴더에 저장합니다.
  7. 성능 비교 · apps/run_performance.sh — 이번 프로그램이 사용할 전체 코어 상한을 환경변수로 받아 전달합니다.
  8. 성능 비교 · apps/performance_app.py — 같은 합계를 반복 계산해 정답을 검사하고, 각 실행 시간과 중앙값을 출력합니다.

아래는 내려받을 파일과 같은 전체 내용입니다. 두 실습 모두 환경 설정 → 제출 스크립트 → 계산 코드 순서로 읽습니다. 각 파일의 역할은 이어지는 절에서 설명합니다.

실행 관찰 · compose.yaml

Spark를 실행할 컨테이너와 연결 주소를 정합니다. 두 Worker로 시작하는 명령은 뒤의 실행 준비에서 안내합니다.

파일 다운로드 · compose.yaml

name: bigdata-week02

x-spark: &spark
  image: spark:4.1.3-scala2.13-java21-python3-ubuntu
  environment:
    PYSPARK_PYTHON: python3
  init: true
  stop_grace_period: 30s

services:
  spark-master:
    <<: *spark
    hostname: spark-master
    entrypoint: ["/opt/spark/bin/spark-class"]
    command: ["org.apache.spark.deploy.master.Master", "--host", "spark-master", "--port", "7077", "--webui-port", "8080"]
    ports:
      - "127.0.0.1:8080:8080"
    healthcheck:
      test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080', timeout=3)"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 20s

  spark-worker-1:
    <<: *spark
    hostname: spark-worker-1
    entrypoint: ["/opt/spark/bin/spark-class"]
    command: ["org.apache.spark.deploy.worker.Worker", "--host", "spark-worker-1", "--cores", "1", "--memory", "1g", "--webui-port", "8081", "--work-dir", "/tmp/spark-work", "spark://spark-master:7077"]
    ports:
      - "127.0.0.1:8081:8081"
    depends_on:
      spark-master:
        condition: service_healthy

  spark-worker-2:
    <<: *spark
    hostname: spark-worker-2
    entrypoint: ["/opt/spark/bin/spark-class"]
    command: ["org.apache.spark.deploy.worker.Worker", "--host", "spark-worker-2", "--cores", "1", "--memory", "1g", "--webui-port", "8081", "--work-dir", "/tmp/spark-work", "spark://spark-master:7077"]
    ports:
      - "127.0.0.1:8082:8081"
    depends_on:
      spark-master:
        condition: service_healthy

  spark-client:
    <<: *spark
    hostname: spark-client
    entrypoint: ["/bin/bash", "-lc"]
    command: ["exec sleep infinity"]
    ports:
      - "127.0.0.1:4040:4040"
    volumes:
      - ./apps:/opt/spark/apps:ro
    depends_on:
      spark-master:
        condition: service_healthy

실행 관찰 · apps/run.sh

Python 프로그램을 Spark에 제출하는 Bash 스크립트입니다. 전체 코어 상한을 2로 지정합니다.

파일 다운로드 · run.sh

#!/usr/bin/env bash
set -euo pipefail
exec /opt/spark/bin/spark-submit \
  --master spark://spark-master:7077 \
  --deploy-mode client \
  --driver-memory 512m \
  --executor-memory 512m \
  --executor-cores 1 \
  --total-executor-cores 2 \
  --conf spark.dynamicAllocation.enabled=false \
  --conf spark.driver.host=spark-client \
  --conf spark.driver.bindAddress=0.0.0.0 \
  --conf spark.driver.port=39000 \
  --conf spark.blockManager.port=39001 \
  /opt/spark/apps/first_app.py "$@"

실행 관찰 · apps/first_app.py

1부터 100까지의 합을 5050과 비교하고, 실행 상태를 볼 수 있도록 잠시 기다린 뒤 종료합니다.

파일 다운로드 · first_app.py

"""Week 2: submit, verify a known answer, observe the UI, and stop."""
import argparse
import time
from pyspark.sql import SparkSession

parser = argparse.ArgumentParser()
parser.add_argument("--wait-seconds", type=int, default=120)
args = parser.parse_args()

spark = SparkSession.builder.appName("Week02-First-App").getOrCreate()
try:
    spark.sparkContext.setLogLevel("WARN")
    print(f"MASTER={spark.sparkContext.master}", flush=True)
    print(f"APP_ID={spark.sparkContext.applicationId}", flush=True)
    assert spark.sparkContext.master == "spark://spark-master:7077"

    # Four input partitions. DataFrame syntax is covered next week.
    numbers = spark.range(1, 101, 1, 4)
    total = numbers.selectExpr("sum(id) AS total").first()["total"]
    print(f"SUM_1_TO_100={total}", flush=True)
    assert total == 5050, f"Unexpected result: {total}"
    print("CHECK=PASS", flush=True)
    print(f"UI observation: {max(0, args.wait_seconds)} seconds", flush=True)
    time.sleep(max(0, args.wait_seconds))
finally:
    spark.stop()
print("APP_STOPPED", flush=True)

환경 검사 · apps/check_pyspark.py

컨테이너 안의 PySpark 버전과 간단한 로컬 계산을 확인합니다.

파일 다운로드 · check_pyspark.py

import pyspark
from pyspark.sql import SparkSession

print(f"PYSPARK_VERSION={pyspark.__version__}", flush=True)
assert pyspark.__version__ == "4.1.3"
spark = SparkSession.builder.appName("Week02-Image-Check").getOrCreate()
try:
    assert spark.range(1).count() == 1
    print("PYSPARK_IMAGE_CHECK=PASS", flush=True)
finally:
    spark.stop()

환경 검사 · apps/check_network.py

서비스 이름을 주소로 찾을 수 있는지, 필요한 통신 포트에 연결할 수 있는지 확인합니다.

파일 다운로드 · check_network.py

"""Run after all four Compose services have started. No extra container needed."""
import socket
import sys

names = ("spark-master", "spark-worker-1", "spark-worker-2", "spark-client")
failures = 0
for name in names:
    try:
        print(f"DNS {name} -> {socket.gethostbyname(name)}", flush=True)
    except OSError as exc:
        failures += 1
        print(f"DNS_FAIL {name}: {exc}", flush=True)

for name, port in (("spark-master", 7077), ("spark-worker-1", 8081), ("spark-worker-2", 8081)):
    try:
        with socket.create_connection((name, port), timeout=3):
            print(f"TCP {name}:{port} -> OK", flush=True)
    except OSError as exc:
        failures += 1
        print(f"TCP_FAIL {name}:{port}: {exc}", flush=True)

print("NETWORK_CHECK=" + ("PASS" if failures == 0 else "FAIL"), flush=True)
print("TCP success alone does not verify Spark registration or application correctness.")
sys.exit(0 if failures == 0 else 1)

성능 비교 · compose.yaml

Worker를 1·2·4·8개로 복제할 수 있는 환경을 만듭니다. 실행 관찰용 설정과 별도 폴더에 저장합니다.

파일 다운로드 · compose.yaml

name: bigdata-week03-scale

x-spark: &spark
  image: spark:4.1.3-scala2.13-java21-python3-ubuntu
  environment:
    PYSPARK_PYTHON: python3
  init: true
  stop_grace_period: 30s

services:
  spark-master:
    <<: *spark
    hostname: spark-master
    entrypoint: ["/opt/spark/bin/spark-class"]
    command: ["org.apache.spark.deploy.master.Master", "--host", "spark-master", "--port", "7077", "--webui-port", "8080"]
    ports:
      - "127.0.0.1:18080:8080"
    healthcheck:
      test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080', timeout=3)"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 20s

  spark-worker:
    <<: *spark
    entrypoint: ["/bin/bash", "-lc"]
    command:
      - >-
        exec /opt/spark/bin/spark-class
        org.apache.spark.deploy.worker.Worker
        --host "$$HOSTNAME"
        --cores 1
        --memory 1g
        --webui-port 8081
        --work-dir /tmp/spark-work
        spark://spark-master:7077
    depends_on:
      spark-master:
        condition: service_healthy

  spark-client:
    <<: *spark
    hostname: spark-client
    entrypoint: ["/bin/bash", "-lc"]
    command: ["exec sleep infinity"]
    ports:
      - "127.0.0.1:24040:4040"
    volumes:
      - ./apps:/opt/spark/apps:ro
    depends_on:
      spark-master:
        condition: service_healthy

성능 비교 · apps/run_performance.sh

이번 프로그램이 사용할 전체 코어 상한을 환경변수로 받아 전달합니다.

파일 다운로드 · run_performance.sh

#!/usr/bin/env bash
set -euo pipefail

TOTAL_EXECUTOR_CORES="${TOTAL_EXECUTOR_CORES:-1}"

exec /opt/spark/bin/spark-submit \
  --master spark://spark-master:7077 \
  --deploy-mode client \
  --driver-memory 512m \
  --executor-memory 512m \
  --executor-cores 1 \
  --total-executor-cores "${TOTAL_EXECUTOR_CORES}" \
  --conf spark.dynamicAllocation.enabled=false \
  --conf spark.driver.host=spark-client \
  --conf spark.driver.bindAddress=0.0.0.0 \
  --conf spark.driver.port=39000 \
  --conf spark.blockManager.port=39001 \
  /opt/spark/apps/performance_app.py "$@"

성능 비교 · apps/performance_app.py

같은 합계를 반복 계산해 정답을 검사하고, 각 실행 시간과 중앙값을 출력합니다.

파일 다운로드 · performance_app.py

"""Measure the same Spark aggregation with different cluster resources."""

import argparse
import statistics
import time

from pyspark.sql import SparkSession


parser = argparse.ArgumentParser()
parser.add_argument("--end", type=int, default=1_000_000_001)
parser.add_argument("--partitions", type=int, default=128)
parser.add_argument("--repetitions", type=int, default=3)
parser.add_argument("--wait-seconds", type=int, default=0)
args = parser.parse_args()

if args.end <= 1:
    parser.error("--end must be greater than 1")
if args.partitions <= 0:
    parser.error("--partitions must be greater than 0")
if args.repetitions <= 0:
    parser.error("--repetitions must be greater than 0")

spark = SparkSession.builder.appName("Week03-Scaling-Experiment").getOrCreate()

try:
    spark.sparkContext.setLogLevel("WARN")
    print(f"MASTER={spark.sparkContext.master}", flush=True)
    print(f"APP_ID={spark.sparkContext.applicationId}", flush=True)
    print(f"RANGE=1..{args.end - 1}", flush=True)
    print(f"PARTITIONS={args.partitions}", flush=True)
    print(f"REPETITIONS={args.repetitions}", flush=True)

    numbers = spark.range(1, args.end, 1, args.partitions)
    expected = (args.end - 1) * args.end // 2
    elapsed_values = []

    for run_number in range(1, args.repetitions + 1):
        started = time.perf_counter()
        total = numbers.selectExpr("SUM(id) AS total").first()["total"]
        elapsed = time.perf_counter() - started

        assert total == expected, f"Unexpected result: {total}"
        elapsed_values.append(elapsed)
        print(f"RUN_{run_number}_SECONDS={elapsed:.3f}", flush=True)

    median_seconds = statistics.median(elapsed_values)
    print(f"SUM_RESULT={total}", flush=True)
    print(f"MEDIAN_SECONDS={median_seconds:.3f}", flush=True)
    print("CHECK=PASS", flush=True)
    print(f"UI observation: {max(0, args.wait_seconds)} seconds", flush=True)
    time.sleep(max(0, args.wait_seconds))
finally:
    spark.stop()

print("APP_STOPPED", flush=True)

제공 실습: 실행 관찰용 파일 5개와 성능 비교용 파일 3개. 환경 참고: Docker Desktop Windows 설치 · Spark 프로그램 제출.

분산 처리 원리부터 읽기

01

큰 일을 나누면 무엇이 달라질까

분산처리는 큰 데이터를 나누어 여러 계산 자원이 함께 처리하는 방식입니다. 한 컴퓨터의 용량이나 처리 속도가 부족할 때 필요합니다.

학습용 비유 · 시험지 채점 나누기

시험지 네 묶음을 조교 두 명이 채점하면, 두 묶음은 처리하고 나머지는 기다립니다. 데이터 조각 수와 동시에 일할 자원 수는 다릅니다.

두 조교가 각각 시험지 한 묶음을 채점하고, 나머지 묶음은 옆에서 대기하는 미니어처 장면
AI 생성 학습용 비유 · 실제 Spark 화면이 아닙니다. 시험지 묶음은 데이터 조각, 조교는 동시에 일할 자원을 나타냅니다.

계산에 쓰는 자원

여러 컴퓨터의 자원을 쓰면 용량과 처리량을 늘리고, 실패한 작업 일부를 다시 실행하도록 설계할 수 있습니다.

자원을 늘리는 두 방법
방법실제 구조제약
Scale-up · 한 대 강화한 컴퓨터의 CPU와 메모리를 늘립니다. 실습에서는 기존 계산용 컨테이너의 자원 설정 증가에 대응합니다.장비 한 대가 지원하는 용량과 비용에 한계가 있습니다.
Scale-out · 대수 증가컴퓨터를 추가하고 작업을 나눕니다. 실습에서는 Worker 컨테이너 수를 늘려 구조를 관찰합니다.통신, 작업 배정, 결과 모으기와 장애 처리가 필요합니다.

데이터를 나누는 수와 일할 자원 수

Task(태스크)는 데이터 조각 하나를 처리하는 작업입니다. 이번 실습처럼 Task 하나가 코어 하나를 쓰면, 동시 실행 상한은 준비된 Task 수와 사용 가능한 코어 수 중 작은 값입니다.

나누고 모으는 데도 비용이 듭니다

한 PC 안의 여덟 Worker

모든 컨테이너는 같은 Windows PC의 CPU와 메모리를 공유합니다. 여러 물리 서버의 성능·장애 환경과는 다릅니다. 작은 데이터는 분산처리 준비 비용 때문에 단일 프로세스가 더 빠를 수도 있습니다.

입력 작업이 4개, 사용 가능한 코어가 2개라면 동시에 실행할 수 있는 작업은?

출처: 3장 「분산처리와 Spark 실행 원리」 슬라이드 1–8 (파일 순서 기준).

02

계산을 맡는 엔진, Spark

Apache Spark는 작업 배정과 데이터 이동을 관리하며 분산 계산을 실행하는 처리 엔진입니다. 수업에서는 Python으로 Spark를 사용하는 PySpark를 씁니다.

학습용 비유 · 재료 창고와 조리실

파일 저장소가 재료를 보관하는 창고라면, Spark는 재료를 가공하는 조리실에 해당합니다. 저장과 계산의 역할을 구분하는 비유입니다.

Spark가 등장한 배경

Hadoop의 MapReduce는 모아 둔 데이터를 한 번에 처리하는 대규모 Batch(배치) 방식입니다. 반복 분석에서 데이터를 다시 읽고 중간 결과를 디스크에 쓰는 비용을 줄이려는 요구가 Spark의 배경이 되었습니다.

강의 연혁: 2009년 UC Berkeley AMPLab에서 시작 → 2010년 공개 → 2014년 Apache 최상위 프로젝트 승격.

계산을 계획하고 재사용하는 기능

데이터 플랫폼의 역할 구분
역할강의에 나온 예Spark와의 관계
수집Kafka, 파일, CDC(데이터 변경 수집)처리할 입력을 제공합니다.
저장HDFS(분산 파일 시스템), 객체 저장소, Iceberg·Delta와 같은 표 관리 기술Spark가 데이터를 읽고 처리 결과를 씁니다.
자원 관리Standalone, YARN, Kubernetes응용 프로그램에 계산 자원을 배정합니다. 이번에는 Spark의 Standalone을 씁니다.
처리SparkETL(추출·변환·적재), SQL, 지속적으로 들어오는 데이터 처리, 기계학습용 특징 데이터 준비를 수행합니다.
일정과 활용Airflow 등 실행 일정 도구, 분석용 저장소, BI(업무 분석), ML(기계학습)작업 순서를 관리하거나 결과를 활용합니다.

어떤 일에 쓰일까

큰 표를 합치는 Join(조인), 집계, 데이터 정제, 반복 분석에 활용합니다. 짧은 응답 시간이 필요한 서비스나 OLTP(온라인 거래 처리) 데이터베이스를 대신하는 도구는 아닙니다.

강의에 소개된 기업 사례와 관리형 서비스

아래는 강의 당시 소개된 사례입니다.

  • Uber: 일괄 데이터 처리와 예측
  • Pinterest: 광고 분석과 과거 데이터 재적재
  • Airbnb: 결제·재무·기계학습 데이터 처리
  • Paytm: 분석·보고서 파이프라인

관리형 Spark 사례로 Amazon EMR, Google Cloud의 Managed Service for Apache Spark, Microsoft Fabric Spark, Databricks Runtime을 소개합니다.

  • 관리형 클러스터: 사업자가 설치·운영을 담당합니다.
  • Serverless: 서비스가 작업에 필요한 자원을 할당합니다.
  • 함께 쓰는 도구: 지연이 짧은 데이터 흐름 처리에는 Flink, 대화형 SQL에는 Trino나 분석용 데이터베이스를 소개합니다.
확인 질문

Spark가 종료되면 영구 저장한 결과 파일도 사라질까요? 외부 저장소에 쓴 결과는 계산 엔진이 종료되어도 남을 수 있습니다. 결과를 저장한 위치를 확인하세요.

출처: 3장 「분산처리와 Spark 실행 원리」 슬라이드 9–17.

03

자원을 준비하는 역할과 계산하는 역할

컨테이너가 켜져 있어도 계산은 아직 시작되지 않았을 수 있습니다. 자원을 준비하는 역할과 제출한 프로그램을 계산하는 역할은 서로 다릅니다.

역할을 두 쌍으로 나누면 다음과 같습니다. Master와 Worker는 쓸 수 있는 계산 자원을 준비합니다. 프로그램을 제출하면 Driver가 작업을 지휘하고, Executor가 나누어 받은 숫자를 계산합니다.

학습용 비유 · 강의실 배정과 수업 진행

시설 담당자는 강의실을 배정하고 수업 담당자는 수업을 진행합니다. 이처럼 Master는 자원을 배정하고 Driver는 계산 작업을 조정합니다.

실행 위치와 프로그램을 구분하기

네 프로세스의 역할과 수명

이번 Standalone·client mode 실습의 네 프로세스
프로세스실행 위치역할과 수명
Masterspark-masterWorker 등록과 자원 배정을 담당합니다. 클러스터가 켜져 있는 동안 유지됩니다.
Worker각 Worker 컨테이너자원을 보고하고 Executor를 시작·감시합니다. Application 종료 후에도 유지됩니다.
Driverspark-client사용자 코드와 실행 계획을 관리하고 Task를 보냅니다. 제출 후 시작해 프로그램 종료 시 끝납니다.
ExecutorWorker 컨테이너 내부Application을 위해 시작되어 Task를 계산하고 중간 데이터를 관리합니다. Application 종료 시 정리됩니다.

위 그림은 학습용 구조 모형입니다. 준비부터 계산까지 다음 순서로 진행됩니다.

  1. 준비: Master와 Worker가 켜지고 Client는 대기합니다.
  2. 제출: spark-submit이 Driver를 시작합니다.
  3. 자원 할당: 자원을 받은 Worker에서 Executor가 시작됩니다.
  4. 계산: Executor가 Driver에서 작업을 받고 결과를 보냅니다. Master가 모든 데이터를 중계하지는 않습니다.

Driver 안에서 사용하는 진입점

합계 프로그램이 종료된 뒤에도 Worker가 켜져 있어도 될까요?

출처: 3장 「분산처리와 Spark 실행 원리」 슬라이드 18–22, 26, 28–29. 구조 보충: Apache Spark 4.1.3 공식 실행 구조.

04

DataFrame · 표가 합계 숫자가 되는 과정

데이터가 여러 곳에 나뉘어 있어도 열 이름으로 계산을 지정할 수 있습니다. DataFrame(데이터프레임)은 이름이 있는 열로 구성된 표 형태의 분산 데이터입니다.

학습용 비유 · 나누어 보관한 성적표

성적표를 여러 조교에게 나누어 주어도 “점수 열의 합계”라는 요청은 같습니다. 점수 열은 표의 구조이고, 조교에게 나눈 묶음은 데이터 조각입니다.

1. 행·열·스키마와 Partition 구분하기

Python 변수 numbers는 이 분산 표를 다루는 객체를 가리킵니다. 모든 행을 Python 리스트로 가져오는 것은 아닙니다. Spark는 데이터 생성과 변환 계획을 관리하고, 실행이 필요해지면 Executor가 조각별로 처리하게 합니다.

2. 실제 코드에서 입력 표 만들기

읽기용 Python 코드입니다. first_app.py에서 이미 만든 spark를 사용하며, PowerShell에 직접 입력하지 않습니다.

numbers = spark.range(1, 101, 1, 4)
입력 표의 논리적 모습 · 이해를 위한 일부 행, 실제 실행 출력 아님
행 설명id · long
시작값1
다음 값2
중간 행 생략3부터 99까지
마지막 값100

표는 100행 × 1열이고 입력 Partition은 4개입니다. 위 표는 숫자 범위를 보여 주는 예시이며, DataFrame의 출력 순서는 명시적으로 정렬해야 보장됩니다.

3. 한 줄의 합계 코드를 네 단계로 풀기

합계를 구하는 코드는 다음 한 줄입니다.

total = numbers.selectExpr("sum(id) AS total").first()["total"]

아래는 같은 계산을 설명용 변수로 나눈 코드입니다. 실습 파일은 그대로 실행합니다.

summary = numbers.selectExpr("sum(id) AS total")
row = summary.first()
total = row["total"]
assert total == 5050, f"Unexpected result: {total}"
변수별 자료형과 예상 값 · 실제 Spark 실행 결과와 구분
변수자료형뜻과 예상 내용
numbersDataFrameid 열에 1부터 100까지 있는 입력 표
summaryDataFrametotal 열의 한 행짜리 합계 표를 정의한 계획. 계산 결과는 5050
rowRow계산 후 받은 한 행 Row(total=5050)
totalPython 정수행 안에서 꺼낸 숫자 5050
Python 변수 이름과 Spark 열 이름

total = row["total"]에서 왼쪽 total은 Python 변수 이름이고, 따옴표 안의 "total"은 AS total로 붙인 열 이름입니다. 이름은 같아도 역할은 다릅니다.

4. 표의 구조와 실행 결과를 확인하는 도구

아래는 코드 이해를 위한 선택적 확인 예시입니다. show()도 실행을 요청하므로, 실습의 Job·Task 수를 관찰할 때는 파일에 코드를 추가하지 마세요.

numbers.printSchema()   # 열 이름과 자료형 확인
summary.explain()       # 합계를 구할 실행 계획 확인
summary.show()          # 결과를 계산해 표 형태로 출력

이 합계 예제에서 printSchema()와 explain()은 구조와 계획을 보여 주고, show()는 실제 결과를 계산해 출력합니다.

예상 결과의 핵심: 입력 스키마의 열은 id: long이고, 합계 표는 total 열 하나에 5050이 들어 있는 한 행입니다.

다음 절에서는 계획을 만드는 Transformation과 결과를 요구하는 Action을 구분합니다.

합계 코드의 first()가 가져오는 것은 무엇인가요?

출처: 3장 「분산처리와 Spark 실행 원리」 슬라이드 23, 32–34 · 배포 코드. 표와 단계별 변수는 학습용 보충입니다. API 참고: DataFrame·스키마 · range · first.

05

계획을 만들고, 결과가 필요할 때 계산하기

Spark는 변환 계획을 모아 두었다가 결과가 필요할 때 계산합니다. 여러 처리 단계를 함께 보고 실행 방법을 정하기 위해서입니다.

학습용 비유 · 주문서를 모은 뒤 조리하기

주문서에 요리를 적는 것은 계획이고, 조리는 실행입니다. 변환은 계획을 만들고 실행 요청은 결과를 요구합니다. 주문서 줄 수가 실제 작업 수와 같지는 않습니다.

계획과 실행 요청

계산을 나누는 단위
단위의미합계 예제
Partition데이터를 나눈 조각1부터 100을 네 조각으로 나눕니다.
TaskExecutor가 한 Partition을 처리하는 실행 단위입력 조각별 합계를 계산합니다.
JobAction의 결과를 구하려고 시작하는 계산 단위first()로 합계 결과를 요구합니다.
Stage데이터 재배치 경계를 기준으로 나뉜 Task 묶음부분 합계 단계와 최종 합계 단계가 있습니다.

계산 단위의 포함 관계

Application → Job → Stage → Task 순으로 포함됩니다. Shuffle(셔플)은 다음 계산에 맞춰 데이터를 재배치하는 과정이며 Stage를 나누는 경계가 됩니다.

화면의 Job 수가 코드의 Action 호출 수와 항상 같지는 않습니다.

이번 합계 예제에서 계산 결과를 실제로 요구하는 호출은?

출처: 3장 「분산처리와 Spark 실행 원리」 슬라이드 23–27, 45–48.

06

단어 세기 · 코드마다 표가 어떻게 바뀔까

단어별 등장 횟수를 구하려면 문장을 단어 행으로 나누고 같은 단어를 모아 셉니다. 문장 읽기 → 단어 분리 → 집계 → 저장 순서로 표가 바뀝니다.

학습용 비유 · 문장 카드를 잘라 단어별로 모으기

문장 카드를 단어마다 잘라 같은 단어끼리 쌓고, 더 두꺼운 묶음부터 놓는 과정입니다. 실제 Spark에서는 문자열을 나누고, 행을 늘리고, 그룹별로 집계한 뒤 정렬합니다.

1. 단어 집계의 전체 코드

파일을 읽고 저장하는 흐름을 이해하는 예제

아래 코드는 HDFS(여러 컴퓨터에 파일을 나누어 저장하는 파일 시스템)의 입력을 가정합니다. 이 장의 환경에는 HDFS가 없으므로 여기서는 연산의 흐름을 읽습니다. 직접 실행하려면 이 절 끝의 문장 파일 실습을 사용하세요.

from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.appName("df-wordcount").getOrCreate()
lines = spark.read.text("hdfs:///user/$USER/week10/input")
words = (lines
    .select(F.explode(F.split(F.col("value"), r"\s+")).alias("word"))
    .where(F.col("word") != ""))
result = words.groupBy("word").count().orderBy(F.desc("count"))
result.write.mode("overwrite").parquet("hdfs:///user/$USER/week10/df-output")
spark.stop()

2. read.text · 한 줄을 한 행으로 읽기

lines = spark.read.text("hdfs:///user/$USER/week10/input")

기본 텍스트 읽기는 줄마다 한 행을 만들고, 내용은 value라는 문자열 열에 넣습니다. 아래부터는 학습용으로 가정한 세 줄을 사용합니다. 직접 실행해 얻은 출력은 아닙니다.

가정한 입력 · lines의 value 열
입력 줄value · 문자열
첫째 줄spark spark
둘째 줄data spark
셋째 줄"" · 빈 문자열

3. 문장을 단어 행으로 바꾸기

words = (lines
    .select(F.explode(F.split(F.col("value"), r"\s+")).alias("word"))
    .where(F.col("word") != ""))

괄호가 많을 때는 가장 안쪽 함수부터 읽습니다. 다음 함수들이 만든 열 표현식을 select에 전달하면, 그 열을 가진 새 DataFrame을 정의합니다.

첫 문장 하나의 중간 모습 · 학습용 예상값
단계값 또는 행구조 변화
입력"spark spark"문자열 하나, 한 행
split["spark", "spark"]문자열 배열, 아직 한 행
explode와 aliasword="spark"인 행 2개배열의 원소가 각각 별도 행

세 줄 전체에 적용하면 빈 문자열 제거 후 spark, spark, data, spark의 4개 행이 남습니다. 여기까지는 어떤 단어를 셀지 정하는 변환 계획입니다.

4. groupBy와 count · 같은 단어를 모아 세기

result = words.groupBy("word").count().orderBy(F.desc("count"))
result를 계산하면 얻을 학습용 예상 표
word · 문자열count · 정수
spark3
data1
같은 count라는 이름, 다른 반환값
  • words.groupBy("word").count(): 단어별 개수의 DataFrame을 만드는 변환입니다.
  • words.count(): 전체 행 수를 계산해 Python 정수를 돌려주는 Action입니다. 위 가정에서는 4입니다.

5. write.parquet · 저장할 때 계산 실행하기

result.write.mode("overwrite").parquet("hdfs:///user/$USER/week10/df-output")
spark.stop()

6. 표의 변환이 Stage와 Shuffle로 연결되는 지점

  1. Stage A: 입력 조각에서 단어 분리, 빈 문자열 제거, 부분 집계를 수행합니다.
  2. 첫 Shuffle: 여러 조각에 있던 같은 단어의 집계 정보를 모읍니다. Stage B에서 단어별 최종 개수를 구합니다.
  3. 두 번째 Shuffle: 횟수 기준 전체 정렬에 맞게 데이터를 재배치합니다. Stage C에서 정렬하고 저장합니다.

위 단계는 단어 집계와 정렬의 학습용 모형입니다. 실제 Job·Stage 수는 실행 계획에 따라 달라지며, 코드 한 줄이 반드시 Stage 하나에 대응하지는 않습니다.

위 단어 집계 코드에서 결과 계산을 요구하는 부분은?

출처: 3장 「분산처리와 Spark 실행 원리」 슬라이드 23–25. 전체 코드는 슬라이드 24, 입력과 중간 표는 학습용 보충입니다. API 참고: explode · 그룹별 count · 전체 count.

파일을 받아 같은 연산 실행하기

합성 문장으로 단어를 세는 보충 실습에는 입력 텍스트, 전체 Python 코드, 예상 CSV가 있습니다. HDFS 없이 local[2]에서 입력·중간 표·Parquet 저장과 재읽기를 확인합니다. 클러스터 Worker 비교는 별도의 분산 실습에서 진행합니다.

07

실제 코드 · 명령이 5050을 출력하기까지

환경 설정·제출 스크립트·계산 코드를 연결해 실행 흐름을 확인합니다. 각 프로세스가 시작되고, 합계를 계산·검증한 뒤 종료되는 순서입니다.

학습용 비유 · 실험실 배치도와 실험 지시서

배치도는 사용할 장비를 준비하고, 지시서는 실험을 시작하며, 실험식은 계산을 정합니다. 설정 파일과 제출 스크립트, Python 코드도 역할이 나뉩니다.

1. compose.yaml · 계산을 시작할 환경 준비

파일에서 먼저 확인할 연결

docker compose up으로 컨테이너를 시작한 상태와 Application을 제출한 상태를 구분하세요. 계산 제출은 다음 명령으로 별도 수행합니다.

2. PowerShell 명령 · Client 안의 run.sh 호출

실행 흐름을 읽는 예시입니다. 실제 실행은 다음 환경 점검을 마친 뒤 진행합니다.

docker compose exec spark-client bash /opt/spark/apps/run.sh --wait-seconds 300

3. run.sh · spark-submit으로 Driver 시작

앞의 run.sh 전체 코드는 다음과 같이 동작합니다. spark-submit은 계산 프로그램을 Spark에 제출하는 명령입니다. 계산 자원의 위치와 사용량을 옵션으로 전달합니다.

제출 옵션의 대상과 실제 의미
옵션대상과 뜻
--master spark://spark-master:7077Driver가 연결할 내부 Master 주소
--deploy-mode client프로그램을 제출한 위치에서 Driver 실행
--driver-memory 512mDriver의 JVM(자바 실행 환경) 메모리
--executor-memory 512mExecutor 하나의 JVM 메모리
--executor-cores 1Executor 하나가 사용할 코어 수
--total-executor-cores 2이 Application 전체가 사용할 Executor 코어의 상한

4. --conf · Executor 조정과 내부 통신

내부 통신 주소와 브라우저로 보는 Driver UI의 localhost:4040는 용도가 다릅니다.

5. first_app.py · 계산 전후의 흐름

first_app.py 전체 코드는 인자 읽기 → Spark 연결 → 합계 계산 → 정답 확인 → 화면 관찰 → 종료 순서입니다. 앞에서 배운 표 연산이 이 과정의 계산 부분에 들어갑니다.

6. 인자 읽기와 SparkSession 생성

7. 연결 확인 → DataFrame 계산 → 정답 검증

8. UI 대기 → 자원 정리 → 다음 실습

프로그램이 끝나도 Master와 Worker 컨테이너는 다음 제출을 기다릴 수 있습니다. 뒤의 실습에서 Application 종료와 클러스터 종료를 따로 관찰합니다.

9. 성능 비교 코드 · 전체 코어 상한 전달

run_performance.sh 전체 코드는 처음 합계 실습의 제출 스크립트와 같은 구조입니다. 달라지는 것은 전체 코어 상한을 고정된 숫자 대신 환경변수로 받는 부분입니다. C의 성능 비교 명령에서 이 값을 전달합니다.

10. 같은 계산을 반복하고 시간 측정하기

앞의 performance_app.py 전체 코드 중 시간 측정 부분을 발췌한 코드입니다. SparkSession 생성과 인자 검사는 포함하지 않습니다.

numbers = spark.range(1, args.end, 1, args.partitions)
expected = (args.end - 1) * args.end // 2
elapsed_values = []

for run_number in range(1, args.repetitions + 1):
    started = time.perf_counter()
    total = numbers.selectExpr("SUM(id) AS total").first()["total"]
    elapsed = time.perf_counter() - started

    assert total == expected, f"Unexpected result: {total}"
    elapsed_values.append(elapsed)
    print(f"RUN_{run_number}_SECONDS={elapsed:.3f}", flush=True)

median_seconds = statistics.median(elapsed_values)
print(f"SUM_RESULT={total}", flush=True)
print(f"MEDIAN_SECONDS={median_seconds:.3f}", flush=True)
print("CHECK=PASS", flush=True)
print(f"UI observation: {max(0, args.wait_seconds)} seconds", flush=True)
time.sleep(max(0, args.wait_seconds))

정상 실행에서는 관찰 대기 후 finally 블록에서 spark.stop()을 호출해 세션을 종료합니다. 오류가 나도 종료를 시도하며, 정상 경로에서만 APP_STOPPED를 출력하고 프로그램이 끝납니다.

하나의 Application 안에서 같은 입력 정의로 세 번 집계하지만 cache()나 persist()는 호출하지 않습니다.

입력 크기를 조정할 때

시간이 너무 짧거나 길면 --end를 조정하되 모든 비교 조건에서 같은 값을 쓰세요. 정답은 (end - 1) × end ÷ 2로 바뀝니다. 가장 빠른 한 번만 고르지 말고 세 번과 중앙값을 기록합니다.

확인 질문

전체 코어 상한이 2인데 Worker 한 개만 살아 있고 코어가 1개라면? 제공 가능한 1개 코어 안에서 계산합니다. 제출 옵션의 상한이 실제 자원을 새로 만들지는 않습니다.

출처: 3장 「분산처리와 Spark 실행 원리」 슬라이드 28–37 · 「Spark Worker 확장과 성능 측정」 1–2쪽 · 실행 관찰·성능 측정 코드.

B 실제 실행 따라가기

환경을 점검하고 작은 합계 계산을 실행한 뒤, 화면에서 데이터와 자원의 관계를 확인합니다.

08

실행 전 환경과 통신 확인하기

실행 관찰 자료 ZIP 받기 · 압축을 푼 BigData 폴더에서 시작합니다.

Docker와 실습 파일을 확인하고 두 Worker를 시작한 뒤 통신을 검사합니다. 점검을 마치면 다음 절에서 1부터 100까지의 합계 프로그램을 제출합니다.

2장 실습 파일로 실행하기

2장 실습 파일을 사용합니다. compose.yaml로 계산 자원을 준비하고, 다음 절에서 run.sh를 통해 앞에서 읽은 first_app.py를 제출합니다.

학습용 비유 · 실험 전 장비 점검

실험 전에 전원과 장비 연결을 따로 점검하듯, 이미지와 네트워크 연결도 각각 검사합니다.

  1. 준비 1 · Docker와 폴더 확인

    Docker Desktop에서 Engine running을 기다린 뒤 실행하세요. 실행 위치: BigData 폴더의 PowerShell.

    docker info
    cd ".\실습자료\2장_실습자료"
    Get-ChildItem
    docker compose config --quiet
    • Docker: docker info에 Client와 Server가 모두 표시.
    • 폴더: compose.yaml과 apps 확인.
    • 설정: docker compose config --quiet가 오류 없이 종료.
  2. 준비 2 · 이미지와 PySpark 검사

    이후 실행 위치: 2장 실습 폴더의 PowerShell.

    docker image ls spark:4.1.3-scala2.13-java21-python3-ubuntu

    이미지가 없을 때만 내려받습니다.

    docker pull spark:4.1.3-scala2.13-java21-python3-ubuntu

    일회용 Client 컨테이너의 로컬 모드 · 코어 1개로 검사합니다. 분산 계산은 다음 단계에서 확인합니다.

    docker compose run --rm --no-deps --entrypoint /opt/spark/bin/spark-submit spark-client --master "local[1]" /opt/spark/apps/check_pyspark.py
    • 예상 버전: PYSPARK_VERSION=4.1.3
    • 예상 검사 결과: PYSPARK_IMAGE_CHECK=PASS
  3. 준비 3 · 두 Worker 시작하기

    실행 위치: 2장 실습 폴더의 PowerShell. 이전 Application이 실행 중이라면 먼저 종료를 기다립니다.

    현재 compose.yaml은 Master·Worker 1·Worker 2·Client 네 서비스를 정의합니다. --remove-orphans는 같은 Compose 프로젝트에서 현재 설정에 없는 서비스의 컨테이너를 제거합니다. 실행 전에 같은 프로젝트에서 유지해야 할 컨테이너가 있는지 확인하세요. Docker Compose up 공식 문서에서 옵션을 확인할 수 있습니다.

    docker compose up -d --remove-orphans spark-master spark-worker-1 spark-worker-2 spark-client
    docker compose ps -a

    현재 설정의 네 컨테이너가 실행 중인지 확인합니다.

  4. 준비 4 · 등록된 자원 확인하기

    • 컨테이너: spark-master, spark-worker-1, spark-worker-2, spark-client 모두 Up.
    • Master 상태: healthy 표시.
    • Master UI · localhost:8080: ALIVE Worker 2개, Cores Total 2 / Used 0. Application 제출 전이므로 사용 코어는 0입니다.
    제출 전 · Master · 학습용 관찰표
    UI 항목강의 예시 값해석
    ALIVE Workers2자원을 제공할 Worker 수
    Cores Total / Used2 / 0준비된 코어는 2개, 현재 사용은 0개
    Running Applications0아직 계산을 제출하지 않은 상태
    강의 사례의 학습용 관찰표이며 실제 측정값이 아닙니다. 자신의 실행 화면과 비교하세요.

    이전에 중지한 Worker의 DEAD 행이 남아 있어도 현재 ALIVE 수가 기준입니다.

  5. 준비 5 · 컨테이너 통신 확인하기

    docker compose exec spark-client python3 /opt/spark/apps/check_network.py
    • 이름 확인: 네 서비스의 DNS(이름을 주소로 찾는 기능) 검사 통과.
    • 연결 확인: Master 7077과 Worker 내부 8081 포트의 TCP 연결 통과.
    • 최종 결과: NETWORK_CHECK=PASS.
여기서 막혔다면
  • Server / pipe 오류: Docker Desktop 실행 상태 확인.
  • 설정 파일을 찾지 못함: 현재 폴더 확인.
  • 이미지 검사 실패: 이미지 태그와 로그 확인.
  • Worker가 없음: docker compose logs spark-master spark-worker-1 spark-worker-2로 진단.

로그 명령은 보충 진단용입니다. Master의 healthy는 웹 응답 확인이므로 Worker 등록도 따로 확인하세요.

확인 질문

컨테이너가 네 개 Up인데 Used Cores가 0이면 고장일까요? Application 제출 전이라면 정상입니다. 준비된 자원과 사용 중인 자원을 구분하세요.

출처: Spark 분산 실행 실습 절차서 1–3쪽, 단계 1–8 · 3장 「분산처리와 Spark 실행 원리」 슬라이드 39–41 · 이미지 검사 원본 · 네트워크 검사 원본.

다음 절로 이동할 조건: ALIVE Worker 2개, Used Cores 0, NETWORK_CHECK=PASS를 확인한 뒤, Master UI를 열어 둔 채 Application을 제출합니다.

09

두 Worker가 같은 계산에 참여하는 과정

정답 5050은 계산의 정확성을 보여 줍니다. 그 계산에 어떤 자원이 참여했는지는 Master UI와 Driver UI를 함께 봐야 알 수 있습니다.

학습용 비유 · 배정표와 작업 기록

배정표는 누가 참여했는지, 작업 기록은 무슨 일을 끝냈는지 보여 줍니다. Master UI는 자원 배정, Driver UI는 계산 기록을 확인하는 화면입니다.

실행 1 · 읽었던 합계 코드 제출하기

실행 위치: 2장 실습 폴더의 PowerShell. 앞 절의 두 Worker 준비가 끝난 상태입니다. 계산 후 300초 동안 프로그램을 유지하도록 실행합니다.

docker compose exec spark-client bash /opt/spark/apps/run.sh --wait-seconds 300

예상 출력의 주요 부분입니다. APP_ID는 실행마다 달라지고 시작 로그가 추가됩니다.

MASTER=spark://spark-master:7077
APP_ID=app-...
SUM_1_TO_100=5050
CHECK=PASS
UI observation: 300 seconds

관찰 1 · 계산 뒤 UI를 열 시간

위 메시지가 보이면 아래 화면을 여세요. APP_STOPPED가 나오면 Application과 Driver UI가 종료됩니다. PowerShell이 돌아오지 않아도 관찰 대기 중이라면 정상입니다.

관찰 2 · 화면별 역할 구분하기

Worker 2개 실행의 예시와 직접 확인할 항목
화면확인할 항목읽는 방법
Master · 8080Workers 2 Alive, Cores 2 Total / 2 Used, Week02-First-App RUNNING클러스터 자원과 실행 중인 Application 확인
Driver · 4040 / Jobs관찰 예시에서는 Job 0의 Task 4개, Job 1의 완료 Task 1개와 Skipped 4개작업의 완료 여부를 확인하고, 생략된 항목을 완료 횟수에 더하지 않기
Driver / Stages관찰 예시에서는 Stage 0의 4개, Stage 2의 1개 완료, Stage 1은 Skipped입력 계산과 최종 집계가 나뉜다는 점 확인
Driver / ExecutorsDriver 행과 Executor 2개, Executor마다 1 Core, 완료 Task 합계 5개Driver 행을 Worker 쪽 Executor 개수에서 제외
Worker 1 · 8081 / Worker 2 · 8082각 Worker의 자원과 ExecutorWorker가 제공한 자원과 그 안의 실행 프로세스 구분

관찰 3 · Master에서 사용 자원 확인

  1. 자원 확인: Master UI를 새로 고치고, 준비할 때 0이던 Used Cores가 2로 바뀌었는지 봅니다.
  2. 프로그램 확인: Running Applications의 Week02-First-App을 찾습니다. 터미널의 APP_ID와 화면의 실행 ID를 비교합니다.

대기 중에는 합계 계산이 끝났어도 Application이 살아 있어 자원 할당이 유지됩니다.

실행 중 · Master · 학습용 관찰표
UI 항목강의 예시 값해석
ALIVE Workers2두 Worker가 등록됨
Cores Total / Used2 / 2Application에 두 코어가 배정됨
ApplicationRUNNING계산 완료 후 관찰 대기 중에도 표시될 수 있음
강의 사례의 학습용 관찰표이며 실제 측정값이 아닙니다. 자신의 실행 화면과 비교하세요.

관찰 4 · Jobs에서 실행 요청의 결과 보기

Driver UI의 Jobs 탭을 엽니다. Job은 실제 계산 요청으로 만들어진 실행 단위입니다. 관찰 예시의 Job 0은 Task 4개를, Job 1은 Task 1개를 완료했고 이전 부분의 Task 4개는 Skipped로 표시됩니다.

계산 기록 · Jobs · 학습용 관찰표
UI 항목강의 예시 값해석
입력 계산완료 Task 4개네 Partition을 처리
최종 집계완료 Task 1개부분 합계를 하나로 합침
Skipped완료 수에서 제외재사용 등으로 실행하지 않은 작업 표시
강의 사례의 학습용 관찰표이며 실제 측정값이 아닙니다. 자신의 실행 화면과 비교하세요.

관찰 5 · Stages에서 나누고 모으는 과정 보기

Stages 탭에서는 Job 안의 계산 단계를 봅니다. 네 입력 Partition은 입력 Stage의 Task 네 개로 처리됩니다. 부분 합계를 모아 전체 합계 하나를 만드는 마지막 Stage에는 Task 한 개가 필요합니다.

계산 단계 · Stages · 학습용 관찰표
UI 항목강의 예시 값해석
입력 StageTask 4개입력 Partition마다 처리
집계 StageTask 1개부분 결과를 모아 합산
Stage 번호실행별로 확인번호 자체보다 처리 내용과 완료 상태를 읽기
강의 사례의 학습용 관찰표이며 실제 측정값이 아닙니다. 자신의 실행 화면과 비교하세요.

화면을 입력 Partition 4개 → 부분 계산 Task 4개 → 최종 집계 Task 1개 → 5050으로 연결해서 설명해 보세요.

관찰 6 · Executors에서 Task 수행자 확인

Executors 탭의 Driver 행은 제외하고 Worker 쪽 Executor를 셉니다.

각 Executor가 1 Core를 쓰고 두 Executor의 완료 Task 합계가 5이면, 앞의 4개 입력 계산과 1개 최종 집계가 두 계산 프로세스에 배정된 것입니다.

두 Worker · Executors · 학습용 관찰표
UI 항목강의 예시 값해석
Driver1행계산을 지휘하는 프로세스이므로 Executor 수에서 제외
계산용 Executor2개 · 각 1 Core동시에 최대 두 Task 실행
완료 Task 합계5개Executor별 배분은 실행마다 다를 수 있음
강의 사례의 학습용 관찰표이며 실제 측정값이 아닙니다. 자신의 실행 화면과 비교하세요.
Job 번호보다 계산 구조
  • 핵심 구조: 입력 Task 4개 + 최종 집계 Task 1개, 합계 5050.
  • 달라질 수 있는 표시: Job·Stage 번호와 Skipped 표시는 버전·실행 계획에 따라 다를 수 있습니다.
  • Executor별 배분: 완료 Task가 2개와 3개로 나뉘는 등 실행마다 달라질 수 있습니다.

Executors 화면에 Driver 1행과 Executor 2행이 보이면 계산용 Executor는 몇 개일까요?

출처: Spark 분산 실행 실습 절차서 3쪽, 단계 9–10 · 3장 「분산처리와 Spark 실행 원리」 슬라이드 42–48의 실행 화면.

다음 절로 이동할 조건: 5050 / PASS, Executor 2개, 완료 Task 합계 5개를 기록한 뒤 APP_STOPPED까지 기다립니다. 그다음 Worker 수만 바꿉니다.

10

Worker를 하나로 줄이면 무엇이 달라질까

입력 데이터와 분할은 그대로 두고 Worker만 줄입니다. 전체 작업 수와 동시에 실행할 작업 수가 어떻게 다른지 확인합니다.

학습용 비유 · 채점 담당자 한 명이 자리를 비우면

조교 한 명이 자리를 비워도 남은 조교가 같은 시험지 묶음을 차례로 채점할 수 있습니다. 실습도 계산이 끝난 뒤 Worker를 줄여 비교하므로, 계산 도중 장애 복구 실험은 아닙니다.

  1. 비교 1 · 종료 후 Worker 하나 줄이기

    실행 위치: 2장 실습 폴더의 PowerShell. 먼저 APP_STOPPED까지 기다린 다음 실행합니다.

    docker compose stop spark-worker-2
    docker compose ps -a

    Master UI에서 ALIVE Worker가 1개로 줄었는지 확인합니다. 두 개가 남았다면 docker compose ps -a에서 spark-worker-2가 Exited인지 확인한 뒤 Master UI를 새로 고칩니다.

  2. 비교 2 · 같은 코드 다시 실행

    입력이나 Partition 수를 바꾸지 않고 같은 명령을 실행합니다. UI observation: 300 seconds가 나오면 Master와 Driver 화면을 새로 고칩니다.

    docker compose exec spark-client bash /opt/spark/apps/run.sh --wait-seconds 300
    • 컨테이너: spark-worker-2는 Exited.
    • Master: ALIVE Worker 1개. 중지한 Worker의 DEAD 행 표시는 실행 이력에 따라 달라질 수 있습니다.
    • 계산 결과: SUM_1_TO_100=5050, CHECK=PASS.
    • Driver의 Executors 화면: Driver를 제외한 Executor 1개, 완료 Task 5개.
    한 Worker · Master · 학습용 관찰표
    UI 항목강의 예시 값해석
    ALIVE Workers1중지한 Worker의 DEAD 기록과 구분
    Cores Total / Used1 / 1한 코어로 같은 계산을 수행
    ApplicationRUNNING관찰 대기가 끝나면 종료
    강의 사례의 학습용 관찰표이며 실제 측정값이 아닙니다. 자신의 실행 화면과 비교하세요.
    한 Worker · Executors · 학습용 관찰표
    UI 항목강의 예시 값해석
    계산용 Executor1개Driver 행은 제외
    완료 Task 합계5개입력 4개와 최종 집계 1개를 차례로 처리
    합계5050Worker 수가 줄어도 입력과 계산 결과는 같음
    강의 사례의 학습용 관찰표이며 실제 측정값이 아닙니다. 자신의 실행 화면과 비교하세요.
  3. 비교 3 · 같은 기준으로 결과 정리

    두 Worker와 한 Worker에서 비교할 예상 결과
    항목Worker 2개Worker 1개
    Worker 쪽 Executor / 사용 코어2개 / 2개1개 / 1개
    입력 Partition / 입력 Task4개 / 4개4개 / 4개
    최종 집계 Task / 완료 Task 합계1개 / 5개1개 / 5개
    동시 Task 상한2개1개
    합계와 검증5050 / PASS5050 / PASS
    확인 질문

    Worker를 줄였는데 왜 입력 Task와 합계는 같을까요? 입력 데이터와 분할 수를 바꾸지 않았기 때문입니다. 바뀐 것은 동시에 처리할 자원입니다.

    100개 숫자는 너무 작으므로 이 실습의 실행 시간 차이만으로 성능을 결론 내리지 않습니다.

  4. 비교 4 · 두 Worker 복원 후 종료

    두 번째 APP_STOPPED를 확인한 뒤 실행합니다.

    docker compose start spark-worker-2
    docker compose ps -a

    Master UI에서 ALIVE Worker가 다시 2개인지 확인한 다음 환경을 정리합니다.

    docker compose down
    docker compose ps -a

    서비스 목록이 비었는지 확인하세요. 이 프로젝트의 실습 파일과 Spark 이미지는 유지되며 다음 성능 비교에서도 이미지를 재사용합니다.

출처: Spark 분산 실행 실습 절차서 3–5쪽, 단계 11–14와 결과표 · 3장 「분산처리와 Spark 실행 원리」 슬라이드 49–54.

다음 실험으로 이동할 조건: 두 Worker와 한 Worker의 정답·Task 수·동시 실행 수를 비교하고, docker compose down 뒤 서비스 목록이 비었는지 확인합니다. 다음 실험에서는 3장 파일로 입력을 늘려 성능을 비교합니다.

C 자원 사용과 성능

구조를 확인한 뒤 같은 계산의 자원 조건과 시간을 비교합니다.

11

성능 비교에 필요한 자원과 측정 조건

자원·성능 비교 자료 ZIP 받기 · 앞에서 받은 자료가 있다면 그대로 사용합니다.

작은 합계에서는 동시에 처리하는 구조를 확인했습니다. 이제 입력을 1부터 10억까지로 늘려 같은 계산을 Worker 1·2·4·8개에서 실행합니다. 입력과 반복 수를 고정하고 자원 조건만 바꾸어 시간을 비교합니다.

학습용 비유 · 같은 분량의 일을 다른 인원으로

인원 효과를 비교하려면 맡기는 일의 분량도 같아야 합니다. 실제 실험에서는 입력 범위, Partition 수, 반복 수를 고정합니다.

제공량·할당량·사용률을 따로 보기

Worker가 많다는 사실만으로 프로그램이 모든 자원을 쓰지는 않습니다. 아래 항목을 따로 기록해야 시간이 달라진 이유를 설명할 수 있습니다.

자원을 읽는 세 가지 기준
기준확인 위치뜻
준비된 자원Master의 ALIVE Workers·Total Cores클러스터가 제공할 수 있는 Worker와 코어 수입니다.
프로그램에 배정된 자원Master의 Used Cores, Driver의 Executors이번 Application에 배정된 코어와 계산 프로세스입니다. 순간 CPU 사용률을 뜻하지 않습니다.
실행 중 실제 사용진행 중 Task, 컨테이너의 CPU·메모리 통계관찰한 순간 계산이 진행 중인지, 실제 CPU·메모리를 얼마나 쓰는지 확인합니다.

준비 1 · 성능 비교 폴더로 이동

실행 위치: 앞 절을 마친 2장 실습 폴더의 PowerShell. 앞에서 실행한 클러스터를 종료한 뒤 형제 폴더로 이동합니다.

cd "..\3장_실습자료"
Get-ChildItem
Get-ChildItem .\apps
docker info
docker compose config --quiet

이 절부터 시작한다면 BigData 폴더에서 cd ".\실습자료\3장_실습자료"로 이동하세요. 이후 성능 비교의 Compose 명령은 3장 실습 폴더에서 실행합니다.

준비 2 · 모든 조건에서 같게 둘 값

모든 성능 비교에서 유지할 조건
조건값읽는 방법
입력 범위--end 10000000011부터 1,000,000,000까지. 끝값은 제외합니다.
입력 Partition--partitions 128입력을 128개로 나눕니다.
반복 수--repetitions 3하나의 Application 안에서 합계 Action을 세 번 실행합니다.
Executor각각 1 Core / 512m이번 설정에서는 할당된 코어 수와 Executor 수가 대응합니다.
Worker각각 1 Core / 1gSpark에 보고하는 자원 설정입니다. 컨테이너 자체의 엄격한 메모리 상한을 뜻하지 않습니다.
대표 시간MEDIAN_SECONDS세 번 측정값을 정렬했을 때 가운데 값입니다.

준비 3 · 측정 범위 먼저 이해하기

3장 Worker는 어떻게 복제할 수 있을까
  • 서비스: 2장의 spark-worker-1·spark-worker-2 대신 spark-worker 서비스 하나를 복제합니다.
  • 호스트 이름: 각 컨테이너의 HOSTNAME 환경변수를 사용합니다.
  • Compose의 $$: 변수를 컨테이너 안의 셸에서 해석하도록 전달합니다.
  • Worker UI 포트: 호스트 포트를 고정 공개하지 않아 복제 시 충돌을 피합니다. 관찰은 Master·Driver UI에서 진행합니다.
확인 질문

반복 시간이 9초, 4초, 5초라면 중앙값은? 5초입니다. 위 시간은 중앙값을 설명하는 가상 숫자입니다.

출처: Spark Worker 확장과 성능 측정 1–2쪽, 6쪽 · 3장 실습 파일. 측정 범위: performance_app.py 기준. 변수 표기 보충: Docker 공식 문서의 $$ 처리.

다음 절로 이동할 조건: 3장 폴더에서 설정 검사를 통과하고 코어 조건과 입력·반복 조건을 구분한 뒤, 1 / 1 조건부터 측정합니다.

12

Worker와 코어를 1개에서 8개까지 늘리기

Worker를 늘리는 명령과 Application이 사용할 코어를 늘리는 명령은 별개입니다. 두 값을 함께 바꾸고 실제 할당 상태를 확인합니다.

학습용 비유 · 좌석 확보와 좌석 신청

자리를 여덟 개 준비해도 이용자가 한 자리만 신청하면 모두 쓰지는 않습니다. Worker 수는 준비한 자원, 전체 Executor 코어 상한은 이번 프로그램의 사용 한도에 대응합니다.

실행 위치: 3장 실습 폴더의 PowerShell. 이전 실행의 APP_STOPPED를 확인한 뒤 다음 조건으로 넘어가세요.

  1. 조건 A1 · Worker 1개·전체 코어 1개

    docker compose up -d --scale spark-worker=1
    docker compose ps -a

    측정 전 확인: Master UI에서 ALIVE Worker 1개와 Total Cores 1을 확인합니다. 등록이 완료되면 아래 측정 명령을 실행하세요.

    docker compose exec -e TOTAL_EXECUTOR_CORES=1 spark-client bash /opt/spark/apps/run_performance.sh --end 1000000001 --partitions 128 --repetitions 3 --wait-seconds 0

    한 명령 안에서 세 번 계산합니다. RUN_1_SECONDS, RUN_2_SECONDS, RUN_3_SECONDS와 MEDIAN_SECONDS를 기록하세요.

  2. 조건 A2 · Worker 2개·전체 코어 2개

    docker compose up -d --scale spark-worker=2
    docker compose ps -a

    측정 전 확인: Master UI에서 ALIVE Worker 2개와 Total Cores 2를 확인합니다. 등록이 완료되면 아래 측정 명령을 실행하세요.

    docker compose exec -e TOTAL_EXECUTOR_CORES=2 spark-client bash /opt/spark/apps/run_performance.sh --end 1000000001 --partitions 128 --repetitions 3 --wait-seconds 0

    시간과 검증 결과를 기록하고, 표 B에 쓸 실제 Executor 수도 확인하세요.

    2 / 2 조건의 화면을 놓쳤다면
    1. Worker 2개를 유지합니다.
    2. 같은 측정 명령의 --wait-seconds 0만 --wait-seconds 300으로 바꿔 실행합니다.
    3. 대기 중 Driver UI에서 Driver 행을 제외한 Executor 수를 기록합니다.
    4. APP_STOPPED까지 기다린 뒤 다음 조건을 진행합니다.
  3. 조건 A3 · Worker 4개·전체 코어 4개

    같은 서비스의 개수를 4로 맞춥니다. 자원 등록을 확인한 뒤 프로그램의 코어 상한도 4로 지정합니다.

    docker compose up -d --scale spark-worker=4
    docker compose ps -a

    측정 전 확인: Master UI에서 ALIVE Worker 4개와 Total Cores 4를 확인합니다. 등록이 완료되면 아래 측정 명령을 실행하세요.

    docker compose exec -e TOTAL_EXECUTOR_CORES=4 spark-client bash /opt/spark/apps/run_performance.sh --end 1000000001 --partitions 128 --repetitions 3 --wait-seconds 0

    세 번의 측정 시간, 중앙값과 검증 결과를 기록합니다.

  4. 조건 A4 · Worker 8개·전체 코어 8개

    Worker를 8개로 늘리고 전체 코어 상한도 8로 맞춥니다. 나머지 입력과 반복 조건은 그대로 유지합니다.

    docker compose up -d --scale spark-worker=8
    docker compose ps -a

    측정 전 확인: Master UI에서 ALIVE Worker 8개와 Total Cores 8을 확인합니다. 등록이 완료되면 아래 측정 명령을 실행하세요.

    docker compose exec -e TOTAL_EXECUTOR_CORES=8 spark-client bash /opt/spark/apps/run_performance.sh --end 1000000001 --partitions 128 --repetitions 3 --wait-seconds 0

    측정값과 검증 결과를 기록합니다. PC의 CPU·메모리가 부족해 조건을 실행하지 못했다면 미측정으로 남기고 오류와 환경을 기록합니다.

기록 A · 같은 정답을 확인하고 시간 비교하기

기본 입력의 예상 정답은 아래와 같습니다. 시간은 자신의 PC에서 측정해 기록하세요.

SUM_RESULT=500000000500000000
CHECK=PASS
UI observation: 0 seconds
APP_STOPPED
기록표 A · Worker와 전체 코어 동시 증가, 시간은 직접 기록
Worker / 요청 Core예상 Executor중앙값(초)Speedup효율(%)
1 / 11개미측정1.00 (기준)100 (기준)
2 / 22개미측정계산 후 기록계산 후 기록
4 / 44개미측정계산 후 기록계산 후 기록
8 / 88개미측정계산 후 기록계산 후 기록

계산 A · 중앙값에서 Speedup과 효율 구하기

계산법을 익히는 가상 예시: Worker 1개일 때 20초, 4개일 때 10초라면 속도 향상은 20 ÷ 10 = 2배, 효율은 2 ÷ 4 × 100 = 50%입니다. 이 숫자는 실제 측정 결과가 아닙니다.

확인 질문

Worker를 4개로 늘렸지만 제출 옵션을 1 Core로 두면 표 A의 4 / 4 조건일까요? 아닙니다. Worker 제공량과 Application 코어 상한을 모두 확인해야 합니다.

출처: Spark Worker 확장과 성능 측정 2–4쪽, 단계 2–6과 결과표 A · Docker 공식 문서의 --scale 옵션.

다음 절로 이동할 조건: 표 A의 네 조건과 실제 2 / 2 Executor 수를 기록합니다. Worker는 8개를 유지하고, 마지막 실행의 APP_STOPPED 이후 전체 코어 상한만 2로 바꿉니다.

13

Worker 8개에서 전체 코어를 2개로 제한하기

Worker는 여덟 개 유지하고 전체 Executor 코어 상한만 두 개로 줄입니다. 준비된 자원과 Application이 쓰는 자원을 비교합니다.

학습용 비유 · 열린 창구와 실제 담당 창구

창구 여덟 개 중 두 곳에만 업무를 배정하면 나머지는 그 업무를 처리하지 않습니다. 이번 설정도 Executor마다 1 Core이므로 전체 2 Core 상한이 참여 수를 제한합니다.

같은 공간에 있는 작업대 여덟 개 중 두 곳에서만 사람이 서류를 정리하고, 나머지 여섯 곳은 비어 있는 모습
AI 생성 학습용 비유 · 작업대 8개는 준비된 Worker, 일을 맡은 두 곳은 이 설정에서 계산에 참여하는 Executor 2개를 나타냅니다. 실제 실행 화면이나 성능 측정 결과가 아닙니다.

전체 코어 상한이 2이고 Executor 하나가 1 Core를 쓰므로, 이 Application에는 최대 두 Executor가 배정됩니다. 비어 있는 작업대처럼 나머지 Worker가 준비되어 있어도 자동으로 이번 계산에 참여하지는 않습니다.

조건 B · Worker 8개 유지·전체 코어만 2개

실행 위치: 3장 실습 폴더의 PowerShell. Worker 8개가 등록되어 있고 이전 Application이 종료된 상태에서 실행합니다.

docker compose exec -e TOTAL_EXECUTOR_CORES=2 spark-client bash /opt/spark/apps/run_performance.sh --end 1000000001 --partitions 128 --repetitions 3 --wait-seconds 0

확인 B · 준비한 코어와 실제 할당 구분하기

학습용 모형은 자원 사용과 입력 Task의 동시 실행 상한을 보여 줍니다. 속도 예측은 아니며 최종 집계·통신 시간은 별도로 필요합니다.

기록 B · 앞의 2 / 2 조건과 연결해서 비교하기

첫 행은 앞 절에서 기록한 2 / 2 결과입니다. 둘째 행에는 방금 실행한 8 / 2 결과를 적습니다. 실제 Executor 수는 Driver 행을 제외해 기록합니다.

기록표 B · 전체 코어 2개 고정
Worker / 요청 Core실제 Executor중앙값(초)관찰
2 / 2직접 확인표 A의 2 / 2 결과모두 참여하는지 확인
8 / 2직접 확인이번 실행값 기록나머지 Worker가 이 Application 계산에 참여하는지 확인
계산이 너무 빨라 2 Core 할당 화면을 놓쳤다면

관찰 대기만 300초로 바꾸는 보충 실행입니다. UI observation: 300 seconds가 보이면 Master UI와 Driver UI를 확인하고 APP_STOPPED까지 기다리세요.

docker compose exec -e TOTAL_EXECUTOR_CORES=2 spark-client bash /opt/spark/apps/run_performance.sh --end 1000000001 --partitions 128 --repetitions 3 --wait-seconds 300

이번 설정에서 Worker 8개, 전체 Executor 2 Core라면 계산용 Executor는?

출처: Spark Worker 확장과 성능 측정 4쪽의 단계 7과 결과표 B, 6쪽 풀이 기준 · 제출 스크립트.

다음 절로 이동할 조건: 2 / 2와 8 / 2의 중앙값·실제 Executor 수를 표 B에 기록합니다. APP_STOPPED 뒤 Worker 8개를 유지한 채 이번에는 8 Core를 요청해 전체 자원 사용을 관찰합니다.

14

측정 결과를 실행 구조와 연결하기

입력과 정답이 같은지 확인하고 실제 자원 사용을 기록해야 처리 시간을 비교할 수 있습니다. 여덟 Core 실행을 관찰해 표 A와 표 B의 결과를 해석합니다.

학습용 비유 · 함께 쓰는 작업장

사람이 늘어도 통로와 작업대를 공유하면 기다림이 생깁니다. 이 실습도 한 PC의 CPU·메모리를 공유하므로 Worker 수만큼 빨라진다고 보장할 수 없습니다.

관찰 실행 · 8 / 8 조건에 UI 대기 추가

실행 위치: 3장 실습 폴더의 PowerShell. Worker 8개 상태에서 이전 실행 종료를 확인하고 진행합니다.

docker compose exec -e TOTAL_EXECUTOR_CORES=8 spark-client bash /opt/spark/apps/run_performance.sh --end 1000000001 --partitions 128 --repetitions 3 --wait-seconds 300

화면 기록 · Master → Executors → Jobs·Stages

UI observation: 300 seconds가 나오면 확인하세요.

실행 중 CPU·메모리 사용률 확인하기

계산 명령이 실행되는 동안 3장 실습 폴더에서 두 번째 PowerShell 창을 열고 아래 명령으로 컨테이너 상태를 관찰합니다. 같은 입력을 다시 관찰할 때도 앞 프로그램이 종료된 뒤 실행하세요.

docker compose stats

한 번만 확인하려면 docker compose stats --no-stream을 사용합니다. 작은 입력은 계산이 빨리 끝나 관찰 시점을 놓칠 수 있으므로, 측정값과 함께 언제 본 값인지 기록하세요.

관찰 방법: Docker 컨테이너 통계 · Spark 실행 모니터링. 자원 관찰 보충.

결과 해석 · 표 A와 표 B로 답하기

결과 정리 · 직접 측정한 값으로 설명 작성하기

결과 설명에 사용할 기록 틀

다른 문서에 복사해 직접 관찰한 값을 채우세요.

입력 end / partitions / repetitions:
PC와 Docker에 사용 가능한 CPU·메모리:
계산 중 CPU·메모리 사용과 관찰 시점:
조건별 RUN_1 / RUN_2 / RUN_3 / MEDIAN_SECONDS:
조건별 SUM_RESULT / CHECK:
Master의 ALIVE Worker / Total Cores / Used Cores:
Driver를 제외한 Executor 수와 Task 관찰:
표 A의 Speedup / 효율:
2 Worker·2 Core와 8 Worker·2 Core 비교:
8 Worker와 4 Worker 비교 및 가능한 이유:
오류 또는 미측정 조건:
실패하거나 값이 예상과 다르면
  • 먼저 확인: 3장 폴더, Master 포트 18080, 이전 Application 종료 여부.
  • Worker 부족: docker compose ps -a와 docker compose logs spark-worker 확인.
  • 코어 부족: 제출 명령의 TOTAL_EXECUTOR_CORES와 Master 자원 상태 확인.
  • Driver UI 종료: 관찰 대기 300초로 다시 실행.

로그 명령은 보충 진단용입니다.

종료 · Application 종료 후 환경 정리

APP_STOPPED를 확인한 뒤 3장 실습 폴더의 PowerShell에서 실행합니다.

docker compose down
docker compose ps -a

확장 실습의 컨테이너와 네트워크가 제거되고 서비스 목록이 비었는지 확인합니다. 계산이 끝난 것과 실습 환경을 종료한 것은 서로 다른 단계입니다.

8 Worker가 4 Worker보다 느렸다면 올바른 다음 행동은?

출처: Spark Worker 확장과 성능 측정 5–6쪽, 단계 8–10과 풀이 기준 · 성능 측정 코드.

독립적으로 실행할 보충 예제

보충 Worker 성능 비교에 전체 측정 코드, 제출 스크립트, Compose 설정과 빈 CSV 기록표가 있습니다. 작은 입력 확인부터 1·2·4·8 Worker와 Core 상한 비교까지 안내합니다.

보충 예제는 이 장과 실행 폴더·포트·기본 입력이 다릅니다. 해당 예제의 파일과 명령을 함께 사용하세요.