PySpark 느린 잡 디버깅 — Spark UI와 DAG 읽는 법
"느린데 어디가 느린지 모르겠다"를 끝내는 가이드. Spark UI 의 Jobs·Stages·Tasks·SQL 탭을 읽어 스큐·스필·잘못된 조인·셔플 폭발을 식별하고, EXPLAIN 으로 실행 계획을 진단하는 절차를 정리합니다.
의사가 청진기 없이 환자를 진찰하지 않듯, 데이터 엔지니어도 Spark UI 없이 느린 잡을 고치려 해서는 안 됩니다. "잡이 느린데 어디가 느린지 모르겠다"는 상황, 여러분도 겪어 보셨죠? 무작정 executor.memory 를 올리거나 파티션 수를 바꿔보는 추측성 튜닝은 시간만 낭비합니다. Spark UI 를 읽을 줄 알면, 느린 원인을 5분 안에 짚어낼 수 있습니다.
이 글에서 배우는 것
- Spark UI 의 Jobs·Stages·Tasks·SQL·Executors 탭이 각각 무엇을 보여주는지
- 태스크 분위수 통계에서 스큐와 스필을 읽어내는 방법
- SQL 탭과
EXPLAIN으로 잘못된 조인과 셔플 낭비를 진단하는 법- AQE 가 런타임에 계획을 어떻게 바꾸는지 확인하는 방법
- 증상별로 어느 탭을 열고 무엇을 처방해야 하는지
1. 진단의 출발 — 위에서 아래로
Spark UI 는 계층 구조입니다. 마치 도시 지도를 점점 확대하듯, 위에서 아래로 좁혀 들어가면 됩니다.
| 탭 | 답하는 질문 |
|---|---|
| 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(작은 쪽 복제) vsSortMergeJoin(양쪽 셔플). 작은 테이블인데 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. 진단 절차 (권장 순서)
아래 흐름도는 느린 잡을 만났을 때 여러분이 걸어야 할 경로를 정리한 것입니다. 위에서 아래로 따라가면 대부분의 원인이 드러납니다.
9. 증상 → 화면 → 처방
| 증상 | 어디서 보나 | 처방 |
|---|---|---|
| 한 태스크만 안 끝남 | Tasks: Max Duration | 스큐 해결 |
| 잡이 전반적으로 느림 | Stages: Shuffle 크기 | 셔플 줄이기(버킷팅·사전집계) |
| 간헐적 OOM | Tasks: Spill / Executors: GC | 파티션↑, 메모리 점검 |
| 조인이 느림 | SQL: 조인 노드 | broadcast 유도 |
| 스캔이 느림 | SQL: Scan PushedFilters | 프루닝·pushdown 복구 |
| 태스크가 너무 많음/적음 | Stages: task 수 | shuffle.partitions/AQE |
10. 정리
| 탭 | 핵심 지표 | 잡아내는 문제 |
|---|---|---|
| Stages | Shuffle Read/Write | 셔플 병목 |
| Tasks | Max vs Median, Spill | 스큐, 메모리 |
| SQL | 조인 방식, Exchange | 잘못된 조인, 셔플 |
| EXPLAIN | PushedFilters | 프루닝 깨짐 |
| Executors | GC, 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 엔지니어링 팀