PySpark 느린 잡 디버깅 — Spark UI와 DAG 읽는 법
"느린데 어디가 느린지 모르겠다"를 끝내는 가이드. Spark UI 의 Jobs·Stages·Tasks·SQL 탭을 읽어 스큐·스필·잘못된 조인·셔플 폭발을 식별하고, EXPLAIN 으로 실행 계획을 진단하는 절차를 정리합니다.
"잡이 느린데 어디가 느린지 모르겠다." 데이터 엔지니어가 가장 자주 하는 말입니다. 무작정 executor.memory 를 올리거나 파티션 수를 바꿔보는 추측성 튜닝은 시간만 낭비합니다. Spark UI 를 읽을 줄 알면, 느린 원인을 5분 안에 짚어낼 수 있습니다.
이 글은 Spark UI 의 각 탭을 어떻게 읽는지, 어떤 숫자가 무슨 문제를 가리키는지, 그리고 느린 잡을 진단하는 순서를 정리합니다.
1. 진단의 출발 — 위에서 아래로
Spark UI 는 계층 구조입니다. 위에서 아래로 좁혀 들어갑니다.
Jobs (전체 잡)
└─ Stages (셔플 경계로 나뉜 단계)
└─ Tasks (스테이지 안의 병렬 단위) ← 대부분의 진실이 여기 있음
SQL / DataFrame 탭 (논리/물리 계획 + 실측)| 탭 | 답하는 질문 |
|---|---|
| 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 정복").
| 태스크 신호 | 의미 | 대응 |
|---|---|---|
| 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 으로 계획 확인
UI 없이 코드에서 바로 계획을 봅니다.
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 표시 확인7. Executors 탭 — 자원과 실패
| 확인 | 신호 |
|---|---|
| Failed Tasks | 재시도·OOM 반복 |
| GC Time 비율 | 높으면 힙 압박 |
| Storage Memory | 캐시가 메모리 점유 |
| Active/Dead | 익스큐터가 죽고 있나 |
GC Time 이 태스크 시간의 상당 비율이면 힙이 과대하거나 객체가 너무 많은 것입니다.
8. 진단 절차 (권장 순서)
1. Jobs/Stages → 가장 오래 걸린 스테이지 식별
│
2. Tasks Summary → Max vs Median 비교
├─ Max ≫ Median → 스큐 (salt/broadcast)
├─ Spill 큼 → 메모리 (파티션↑)
└─ GC 큼 → 힙 조정
│
3. SQL 탭/EXPLAIN → 조인 방식·셔플·프루닝 점검
├─ 작은 테이블 SMJ → broadcast
├─ Exchange 과다 → 셔플 줄이기(버킷팅/사전집계)
└─ PushedFilters 빔 → WHERE 함수 래핑 제거
│
4. Executors → 실패·GC·자원 확인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 잡 디버깅의 핵심은 "추측하지 말고 Spark UI 를 읽는 것"입니다. Stages 에서 병목 스테이지를, Tasks 에서 스큐·스필을, SQL 탭에서 잘못된 조인을 짚는 절차만 손에 익히면, 대부분의 성능 문제는 5분 안에 원인이 드러납니다. 이 진단 능력이 있어야 앞선 글들에서 다룬 스큐·메모리·조인 튜닝을 "어디에" 적용할지 알 수 있습니다.
이 글은 Spark 3.5 기준으로 작성되었습니다. 느린 Spark 잡 진단·튜닝이나 운영 모니터링 체계 수립이 필요하시면 언제든 문의해 주세요.
— Data Dynamics 엔지니어링 팀