Data & AI

Spark 배치 처리

meellon 2026. 7. 9. 12:00

학습 목표

Spark분산 배치 처리에서 process 레이어를 담당하는 방식을 설명할 수 있다.

transformationaction·lazy 실행을 구분할 수 있다.

partition·shuffle병렬성·비용에 미치는 영향을 설명할 수 있다.

RDDDataFrame API 선택 기준을 말할 수 있다.

일일 집계·backfill 배치 job 초안을 설계할 수 있다.

문제 상황

  • 어제 parquet 10TB — 단일 서버 pandas로 OOM, cron 12시간 초과
  • groupBy(user_id)디스크 폭발 — shuffle이 뭔지 모름
  • Spark job 로그에 200 stagescount()마다 전체 재실행
  • partition 1개로 읽고 200 executor — 199개 idle
  • Kafka에서 consume은 하는데 어제 slice 집계는 배치로 — Structured Streaming과 역할 혼동
  • 4편에서 topic·partition을 봤지만 — Spark 쪽 partition·shuffle이 별개

이번 편은 bounded 입력분산으로 읽고 변환·집계하는 Spark 배치다. Structured Streaming은 6편.

1. Spark가 process 레이어에서 하는 일

3편 배치 모델 — collect → store → process → publish. Spark는 process대표 엔진이다.

역할 Spark가 해주는 것
분산 읽기 parquet·JSON·JDBC·Kafka offset rangepartition으로 나눠 읽기
변환·집계 filter·join·groupBy를 클러스터에서 병렬 실행
장애 복구 task 실패 시 lineage재계산 (RDD·DAG)
스케일 아웃 executor 추가로 throughput 확장 (shuffle 병목 제외)
S3 raw/2026-07-28/*.parquet
        │
        ▼
Spark batch job (driver + executors)
        │
        ▼
warehouse user_day_agg · ML training set
  • Driver — job·stage 계획, 스케줄, action 결과 수집
  • Executortask 실행 — partition 청크 단위
  • Cluster manager — YARN·K8s·Standalone — 리소스 할당
  • ingest는 Kafka (4편) — Spark는 이미 쌓인 raw 또는 topic 구간배치로 읽을 수 있음

2. RDD vs DataFrame

  RDD DataFrame / Dataset
타입 RDD[T]객체 분산 컬렉션 named column·스키마
최적화 개발자가 partition 직접 Catalyst optimizer
API map·reduceByKey SQL·groupBy·DataFrame DSL
용도 레거시·비정형·세밀 제어 일반 ETL·SQL 팀 협업

# code/batch_job.example.py (발췌) — DataFrame API 권장
orders = spark.read.parquet("s3://lake/raw/orders/dt=2026-07-28")
daily = (
    orders.filter(col("status") == "PAID")
    .groupBy("user_id")
    .agg(sum("amount").alias("revenue"))
)
daily.write.mode("overwrite").parquet("s3://lake/gold/user_day_revenue/dt=2026-07-28")
  • Dataset[T] — typed row — Scala·Java에서 강타입
  • Spark SQLspark.sql("SELECT ...")동일 엔진
  • RDD — sc.textFile·map — 신규 프로젝트는 DataFrame 우선
  • UDF — Catalyst — 성능·vectorization 손해 — 표현식·built-in 우선

3. Transformation vs Action

Spark는 lazytransformation계획(DAG) 만 쌓고, action실행을 트리거한다.

종류 실행
Transformation filter, select, join, groupBy 지연 — lineage 추가
Action count, collect, write, show 즉시 job 제출
read parquet  →  filter  →  groupBy  →  write parquet
   (T)            (T)         (T)         (A) ← job runs here
  • 여러 action — DAG 재사용 안 되면 각각 full scan — cache·한 번에 write
  • debugdf.explain() — physical plan·shuffle 유무
  • collect() — driver로 전부 끌어옴 — 대용량 금지
  • write — action — partition 수 = 출력 파일 수 힌트
# 나쁜 예 — action 두 번
n = daily.count()          # full job 1
daily.write.parquet(path)  # full job 2

# better — write만, row count는 별도 메트릭 또는 write 결과
daily.write.parquet(path)

4. Partition

Partition — Spark가 나눠 읽고 쓰는 병렬 단위. Kafka partition(4편)과 이름만 같음.

개념 설명
Input partition HDFS block·parquet 파일·maxPartitionBytes로 나뉜 읽기 청크
RDD/DataFrame partition 메모리·디스크 상 논리 분할 — task 1개 ≈ partition 1개 (map stage)
repartition(n) shuffle — partition 수 변경
coalesce(n) shuffle 없이 줄이기 — 파일 merge에 유용
  • 너무 적음 — CPU 미활용 — executor 많아도 idle
  • 너무 많음 — task 오버헤드·작은 파일 난립
  • Rule of thumb — executor 코어 × 2~4 정도 partition (입력·shuffle 후 조정)
  • Kafka 배치 readassign offset range — consumer partitionSpark partition 매핑 설계
# code/partition_shuffle.example.py (발췌)
# skew 의심 시 salt key — shuffle 전
from pyspark.sql.functions import concat, col, lit, rand

salted = orders.withColumn("salt_key", concat(col("user_id"), lit("_"), (rand() * 10).cast("int")))
# ... groupBy salt_key 후 다시 user_id 로 reduce

5. Shuffle

Shuffle같은 key같은 task모으는 전역 데이터 이동. 비용이 큰 연산.

연산 shuffle
filter, map, select 없음 (narrow)
groupBy, join, repartition, distinct 있음 (wide)
orderBy (전역) 있음
Stage 1 (map):  partition 0..N  — local filter
        │
   shuffle write / read  (disk · network)
        │
Stage 2 (reduce): groupBy user_id
  • spark.sql.shuffle.partitions — 기본 200 — 데이터 크기에 맞게 (예: 50~2000)
  • Shuffle spill — memory 부족 시 디스크느림 — partition 수·메모리 튜닝
  • Skew — 특정 key에 데이터 편중 — salting·AQE skew join (Spark 3+)
  • Broadcast join — 작은 테이블 전 executor 복사 — shuffle 회피
# code/spark-job.conf.yaml (발췌)
spark:
  executor.instances: 8
  executor.cores: 4
  executor.memory: 8g
  sql.shuffle.partitions: 64
  sql.adaptive.enabled: true

6. 배치 job 설계 체크리스트

# 질문
1 입력이 bounded slice인가 (dt·offset range)
2 action은 최소화·cache 필요한가
3 groupBy·join shuffle 비용을 explain으로 확인했는가
4 출력 partition·파일 크기가 lake 권장(128MB~1GB)에 맞는가
5 job 멱등인가 — 같은 dt overwrite·partition replace
6 실패 시 재실행·backfill 경로가 있는가
# bounded slice — daily batch
dt = "2026-07-28"
raw = spark.read.parquet(f"s3://lake/raw/orders/dt={dt}")
gold = raw.groupBy("user_id").agg(sum("amount").alias("revenue"))
gold.write.mode("overwrite").parquet(f"s3://lake/gold/user_day/dt={dt}")
  • Incrementaldt partition append vs merge — 7편 ETL·9편 품질과 연결
  • Kafka → Spark batchstartingOffsets·endingOffsets하루 구간 bounded 처리
  • 비용 — shuffle 많은 job은 야간·spot executor — 2편 scale-out

정리

  • Spark 배치 — bounded raw를 분산 process → gold table
  • DataFrame — 일반 ETL·Catalyst 최적화; RDD는 예외적
  • Transformation lazyaction이 job 실행
  • Partition — 병렬 단위; shuffle — wide 연산·병목
  • Kafka partition·consumer group(4편)과 Spark partition별개 — 설계 시 둘 다 맞출 것

다음에 다룰 것

  • Spark Structured Streaming
  • watermark, trigger, sink, micro-batch vs continuous

해당 내용은 Spark Documentation (Batch Guide), Kleppmann — Designing Data-Intensive Applications (Ch 10) 를 기반으로 합니다.

'Data & AI' 카테고리의 다른 글

ETL vs ELT  (0) 2026.07.15
Spark Structured Streaming  (0) 2026.07.13
Kafka 기초  (0) 2026.07.05
Batch vs Stream  (0) 2026.07.03
데이터 규모와 아키텍처  (0) 2026.07.02