Blog
pysparksparkspark-uidebuggingperformancedata-engineering

PySpark 느린 잡 디버깅 — Spark UI와 DAG 읽는 법

"느린데 어디가 느린지 모르겠다"를 끝내는 가이드. Spark UI 의 Jobs·Stages·Tasks·SQL 탭을 읽어 스큐·스필·잘못된 조인·셔플 폭발을 식별하고, EXPLAIN 으로 실행 계획을 진단하는 절차를 정리합니다.

Data Dynamics2026年6月5日13 min read
This post is not yet translated. The original Korean version is shown below.

의사가 청진기 없이 환자를 진찰하지 않듯, 데이터 엔지니어도 Spark UI 없이 느린 잡을 고치려 해서는 안 됩니다. "잡이 느린데 어디가 느린지 모르겠다"는 상황, 여러분도 겪어 보셨죠? 무작정 executor.memory 를 올리거나 파티션 수를 바꿔보는 추측성 튜닝은 시간만 낭비합니다. Spark UI 를 읽을 줄 알면, 느린 원인을 5분 안에 짚어낼 수 있습니다.

이 글에서 배우는 것

  • Spark UI 의 Jobs·Stages·Tasks·SQL·Executors 탭이 각각 무엇을 보여주는지
  • 태스크 분위수 통계에서 스큐와 스필을 읽어내는 방법
  • SQL 탭과 EXPLAIN 으로 잘못된 조인과 셔플 낭비를 진단하는 법
  • AQE 가 런타임에 계획을 어떻게 바꾸는지 확인하는 방법
  • 증상별로 어느 탭을 열고 무엇을 처방해야 하는지

1. 진단의 출발 — 위에서 아래로

Spark UI 는 계층 구조입니다. 마치 도시 지도를 점점 확대하듯, 위에서 아래로 좁혀 들어가면 됩니다.

Loading diagram…
답하는 질문
Jobs어떤 잡/액션이 오래 걸리나
Stages어느 스테이지가 병목인가
Tasks스큐·스필이 있나 (핵심)
SQL조인 방식·셔플·스캔이 적절한가
Executors자원·GC·실패가 정상인가

2. Stages 탭 — 병목 스테이지 찾기

오래 걸리는 잡을 클릭해 스테이지로 들어가면, 각 스테이지의 소요 시간과 셔플 양이 보입니다. 여러 스테이지 중 유독 긴 것 하나가 전체 잡을 붙잡고 있는 경우가 대부분이거든요.

확인할 것:

  • Duration 이 압도적으로 긴 스테이지 → 병목.
  • Shuffle Read / Write 가 큰 스테이지 → 셔플이 비용의 핵심.
  • Input / Output → 데이터가 어디서 들어오고 나가나.

직관: 셔플(Shuffle Read/Write)이 큰 스테이지가 보이면, 거기서 조인·groupBy 가 데이터를 대량으로 재분배하고 있다는 뜻입니다. 셔플은 네트워크·디스크를 쓰므로 잡 시간의 대부분을 차지하는 경우가 많습니다.

3. Tasks 탭 — 가장 중요한 진실

스테이지를 클릭해 Summary Metrics(태스크 분위수 통계) 를 봅니다. 여기서 대부분의 문제가 드러납니다. 마치 성적표의 최솟값·중앙값·최댓값을 비교하듯이, 숫자 차이가 클수록 문제가 있다는 신호입니다.

스큐 식별

Metric          Min    25th   Median  75th   Max
Duration        2s     3s     3s      4s     8min   ← Max 가 Median 의 160배!
Shuffle Read    50MB   52MB   51MB    53MB   9GB    ← 한 태스크만 거대

Max 가 Median 의 수십~수백 배라면 데이터 스큐입니다. 소수 태스크가 전체를 끌고 있습니다. (해결은 별도 글 "PySpark 데이터 스큐 완전 정복".)

스필 식별

Spill (Memory)  /  Spill (Disk)  컬럼에 큰 값
→ execution 메모리 부족으로 디스크로 흘리는 중 → 느림

Spill 이 크면 메모리가 빠듯하다는 신호입니다. 파티션을 잘게 하거나 메모리를 확보해 봅시다(별도 글 "PySpark Executor OOM 정복").

⚠️ Spill(디스크로 넘침)이 발생하면 디스크 I/O 때문에 잡이 몇 배씩 느려집니다. Tasks 탭의 Spill 컬럼이 빈칸이면 정상, 수 GB 이상이면 즉시 살펴봐야 합니다.

태스크 신호의미대응
Max ≫ Median (Duration)스큐salt/broadcast
Spill 큼메모리 부족파티션↑, 캐시↓
GC Time 큼힙 압박힙·객체 조정
태스크 수가 200 고정기본 셔플 파티션shuffle.partitions 조정/AQE

4. SQL 탭 — 실행 계획과 실측

DataFrame/SQL 잡은 SQL 탭에 쿼리별 실행 그래프가 그려지고, 각 노드에 실제 행 수·시간이 붙습니다. 가장 강력한 진단 화면입니다. 계획 문서와 현장 실측치를 동시에 보여주기 때문에, "계획대로 됐는가"를 한눈에 확인할 수 있거든요.

확인 포인트:

  • 조인 방식: BroadcastHashJoin(작은 쪽 복제) vs SortMergeJoin(양쪽 셔플). 작은 테이블인데 SMJ 면 broadcast 가 안 된 것 — 셔플 낭비.
  • Exchange(셔플) 노드 수: 많을수록 재분배 비용↑.
  • number of output rows: 어느 노드에서 행이 폭증하는지(조인 부풀림).
  • Scan 노드의 필터/프루닝: pushdown·파티션 프루닝이 됐는지.
== Exchange(셔플)가 많고, 작은 테이블이 SortMergeJoin 으로 처리됨
→ broadcast 임계값을 올리거나 broadcast() 힌트로 셔플 제거

5. EXPLAIN 으로 계획 확인

Spark UI 를 열기 전에 코드에서 바로 계획을 확인하고 싶을 때는 explain() 을 사용하면 됩니다. 잡을 실제로 실행하지 않아도 조인 방식이나 셔플 발생 여부를 미리 볼 수 있습니다.

df.explain(mode="formatted")    # 읽기 좋은 포맷
# 또는
df.explain(True)                # 논리/최적화/물리 계획 모두

핵심 키워드:

키워드의미
BroadcastHashJoin작은 쪽 복제 조인(좋음, 셔플 없음)
SortMergeJoin양쪽 셔플 조인(큰 테이블끼리면 정상)
Exchange셔플 발생 지점
*(n) (codegen)Whole-Stage CodeGen 단계
PartitionFilters / PushedFilters프루닝·pushdown 동작

PushedFilters 가 비어 있으면 필터가 소스로 안 내려간 것입니다 — WHERE 절에서 컬럼을 함수로 감쌌는지 의심해 보세요.

6. AQE 확인 — 런타임 적응

AQE 가 켜져 있으면(spark.sql.adaptive.enabled=true), SQL 탭의 계획이 런타임에 바뀝니다(AdaptiveSparkPlan). 스큐 조인 분할·파티션 병합이 적용됐는지 여기서 확인할 수 있습니다.

spark.conf.set("spark.sql.adaptive.enabled", "true")
# UI SQL 탭에서 AdaptiveSparkPlan, coalesced/skew 표시 확인

팁: AQE 는 실행 도중 실제 통계를 보고 계획을 수정합니다. SQL 탭에서 AdaptiveSparkPlan 노드가 보이면 AQE 가 동작 중이라는 뜻이고, 그 안에 실제 적용된 최적화 내역이 담겨 있습니다.

7. Executors 탭 — 자원과 실패

확인신호
Failed Tasks재시도·OOM 반복
GC Time 비율높으면 힙 압박
Storage Memory캐시가 메모리 점유
Active/Dead익스큐터가 죽고 있나

GC Time 이 태스크 시간의 상당 비율을 차지한다면, 힙이 과대하게 점유됐거나 단명하는 객체가 너무 많이 생성되고 있는 겁니다.

8. 진단 절차 (권장 순서)

아래 흐름도는 느린 잡을 만났을 때 여러분이 걸어야 할 경로를 정리한 것입니다. 위에서 아래로 따라가면 대부분의 원인이 드러납니다.

Loading diagram…

9. 증상 → 화면 → 처방

증상어디서 보나처방
한 태스크만 안 끝남Tasks: Max Duration스큐 해결
잡이 전반적으로 느림Stages: Shuffle 크기셔플 줄이기(버킷팅·사전집계)
간헐적 OOMTasks: Spill / Executors: GC파티션↑, 메모리 점검
조인이 느림SQL: 조인 노드broadcast 유도
스캔이 느림SQL: Scan PushedFilters프루닝·pushdown 복구
태스크가 너무 많음/적음Stages: task 수shuffle.partitions/AQE

10. 정리

핵심 지표잡아내는 문제
StagesShuffle Read/Write셔플 병목
TasksMax vs Median, Spill스큐, 메모리
SQL조인 방식, Exchange잘못된 조인, 셔플
EXPLAINPushedFilters프루닝 깨짐
ExecutorsGC, Failed자원·안정성

한 문장으로: "추측하지 말고 Spark UI 를 읽어라" 가 느린 잡 디버깅의 핵심입니다.

마치며 — 핵심 요약

  • Stages 탭에서 Duration 과 Shuffle 크기를 보고 병목 스테이지를 골라냅니다.
  • Tasks 탭의 Max vs Median 격차가 수십 배 이상이면 스큐, Spill 컬럼이 크면 메모리 부족입니다.
  • SQL 탭에서는 작은 테이블이 SortMergeJoin 으로 처리되는지, Exchange 가 필요 이상으로 많은지 확인합니다.
  • EXPLAIN 으로 PushedFilters 가 비어 있으면 WHERE 절 함수 래핑을 제거해 pushdown 을 복구하면 됩니다.
  • AQE 를 켜두면 런타임에 스큐 분할·파티션 병합이 자동 적용되며, SQL 탭에서 AdaptiveSparkPlan 으로 확인할 수 있습니다.
  • 이 진단 순서(Stages → Tasks → SQL → Executors)를 손에 익히면, 대부분의 성능 문제는 5분 안에 원인이 보입니다.

추측으로 낭비한 시간을 Spark UI 읽는 습관 하나로 되찾을 수 있습니다. 여러분의 다음 잡 디버깅이 훨씬 짧아지길 바랍니다.


이 글은 Spark 3.5 기준으로 작성되었습니다. 느린 Spark 잡 진단·튜닝이나 운영 모니터링 체계 수립이 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀