9. OOM 디버깅
9.1 OOM 유형별 진단
| OOM 유형 | 에러 메시지 | 원인 | 해결 |
|---|---|---|---|
| Driver OOM | java.lang.OutOfMemoryError: Java heap space (Driver) | collect(), toPandas(), 큰 Broadcast | Driver 메모리 증가 또는 collect 제거 |
| Executor OOM | java.lang.OutOfMemoryError: Java heap space (Executor) | 데이터 편향, 큰 파티션 | 파티션 수 증가, 메모리 증가 |
| Container OOM | Container killed by YARN for exceeding memory limits | Off-heap 메모리 초과 | spark.memory.offHeap.size 증가 |
| GPU OOM | CUDA out of memory | 배치 사이즈 과대, 모델 과대 | 배치 사이즈 축소, Mixed Precision |
9.2 OOM 해결 체크리스트
[Driver OOM 체크리스트]
□ collect(), toPandas(), show(매우 큰 수) 사용하고 있는가?
→ 필터/집계 후 소량만 수집
□ Broadcast 변수가 너무 큰가? (> 1GB)
→ Broadcast 임계값 낮추기
□ 대시보드/노트북에서 대량 결과를 표시하는가?
→ LIMIT 추가
[Executor OOM 체크리스트]
□ Spill to Disk가 발생하는가?
→ 파티션 수 증가: spark.sql.shuffle.partitions = 2000
□ 데이터 Skew가 있는가?
→ AQE Skew Join 활성화, Salting 적용
□ 인스턴스 메모리가 부족한가?
→ Memory-optimized 인스턴스 (r5, r6i)
[GPU OOM 체크리스트]
□ 배치 사이즈가 너무 큰가?
→ 배치 사이즈 절반으로 줄이기
□ Mixed Precision을 사용하고 있는가?
→ torch.cuda.amp 적용 (메모리 50% 절감)
□ Gradient Checkpointing을 사용하는가?
→ model.gradient_checkpointing_enable() (메모리 절감)
# Driver OOM 방지: 대량 데이터 수집 대신 파일로 저장
# ❌ 위험
large_df = spark.table("catalog.schema.big_table")
pandas_df = large_df.toPandas() # OOM!
# ✅ 안전
large_df.write.format("delta").save("/tmp/result")
# 필요한 부분만 읽기
small_df = spark.read.format("delta").load("/tmp/result").filter("date = '2026-03-01'").toPandas()
10. GC 튜닝
10.1 GC 문제 진단
[GC 문제 증상]
- Spark UI의 Executor 탭에서 GC Time이 전체 시간의 10% 이상
- 태스크 실행 시간이 불규칙 (GC Pause로 인한 간헐적 지연)
- "GC overhead limit exceeded" 에러
| GC 설정 | 기본값 | 권장값 | 효과 |
|---|---|---|---|
-XX:+UseG1GC | G1GC (기본) | G1GC 유지 | Spark에 최적화 |
-XX:G1HeapRegionSize | 자동 | 16m | 대용량 힙에서 효율적 |
-XX:InitiatingHeapOccupancyPercent | 45 | 35 | GC 일찍 시작하여 긴 Pause 방지 |
-Xmx | 클러스터 설정 | 인스턴스 메모리의 60~70% | 나머지는 Off-heap/OS 용 |
# GC 튜닝 설정 (클러스터 Spark 설정)
spark.conf.set("spark.executor.extraJavaOptions",
"-XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:InitiatingHeapOccupancyPercent=35")
# GC 로그 활성화 (디버깅용)
spark.conf.set("spark.executor.extraJavaOptions",
"-XX:+UseG1GC -Xlog:gc*:file=/tmp/gc.log:time,uptime:filecount=5,filesize=10m")
10.2 GC 부담 줄이는 코딩 패턴
# ❌ GC 부담 높은 패턴: 불필요한 중간 DataFrame 생성
df1 = spark.table("table1")
df2 = df1.filter("date > '2026-01-01'")
df3 = df2.withColumn("new_col", ...)
df4 = df3.groupBy("region").agg(...)
df5 = df4.orderBy("total")
# ✅ GC 친화적 패턴: 체이닝으로 중간 참조 제거
result = (
spark.table("table1")
.filter("date > '2026-01-01'")
.withColumn("new_col", ...)
.groupBy("region").agg(...)
.orderBy("total")
)
# ✅ 사용 완료한 DataFrame 캐시 해제
cached_df.unpersist()
11. 메모리 프로파일링
11.1 Spark 메모리 구조
[Executor 메모리 구조]
전체 Executor 메모리 (spark.executor.memory)
├── Spark Memory (60%)
│ ├── Storage Memory: 캐시된 데이터, Broadcast
│ └── Execution Memory: Shuffle, Sort, Join, 집계
│
├── User Memory (40%)
│ ├── Python UDF 데이터
│ └── 사용자 데이터 구조
│
└── Reserved Memory (300MB 고정)
Off-Heap Memory (spark.memory.offHeap.size)
├── Photon 엔진 메모리
└── Arrow 직렬화 버퍼
11.2 메모리 사용 진단
# 현재 메모리 설정 확인
print(f"Executor Memory: {spark.conf.get('spark.executor.memory')}")
print(f"Driver Memory: {spark.conf.get('spark.driver.memory')}")
print(f"Memory Fraction: {spark.conf.get('spark.memory.fraction')}")
print(f"Off-Heap Size: {spark.conf.get('spark.memory.offHeap.size', 'not set')}")
# 캐시된 데이터 크기 확인
for table_name in ["table1", "table2"]:
if spark.catalog.isCached(table_name):
print(f"{table_name}: cached")
| 메모리 설정 | 조정 방향 | 시나리오 |
|---|---|---|
spark.executor.memory | ↑ | Spill 발생, OOM |
spark.memory.fraction | ↑ (0.6→0.7) | 캐시 많이 사용, 큰 집계 |
spark.memory.offHeap.size | ↑ | Photon 사용, Container OOM |
spark.sql.shuffle.partitions | ↑ | 파티션당 메모리 초과 |
spark.driver.memory | ↑ | Driver OOM, 큰 Broadcast |
참고 메모리 최적화 우선순위: 1) 불필요한 컬럼 제거 (SELECT * 금지) → 2) 파티션 수 조정 → 3) Broadcast 임계값 조정 → 4) 인스턴스 메모리 증가. 인스턴스 업그레이드는 마지막 수단으로, 코드 최적화를 먼저 시도하세요.
12. 성능 진단 체크리스트
단계별로 성능 문제를 진단하고 해결하는 가이드입니다.Step 1: 쿼리가 느린가?
□ Query Profile에서 가장 시간이 오래 걸리는 단계는?
→ Scan이 오래 걸림 → Step 2로
→ Join이 오래 걸림 → Step 3으로
→ Aggregate가 오래 걸림 → Step 4로
Step 2: 스캔 최적화
□ 읽은 데이터량 vs 필요한 데이터량 비교
→ 과도한 스캔 → Liquid Clustering 적용했는가?
→ 클러스터링 적용됨 → OPTIMIZE를 최근에 실행했는가?
→ OPTIMIZE 실행됨 → 클러스터링 키가 쿼리 패턴과 맞는가?
□ ANALYZE TABLE로 통계를 수집했는가?
□ 파일 수가 과도하지 않은가? (Small File 문제)
Step 3: 조인 최적화
□ 조인 유형 확인 (Broadcast vs SortMerge)
→ 한쪽 테이블이 작은데 SortMerge? → Broadcast 힌트 추가
→ 양쪽 모두 대용량? → Shuffle Partition 수 조정
□ 데이터 편향(Skew) 존재하는가?
→ 특정 태스크만 오래 걸림 → AQE Skew Join 설정 확인
→ 극심한 Skew → Salting 기법 적용
□ 조인 키에 NULL이 많은가?
→ NULL 키는 사전 필터링
Step 4: 집계/셔플 최적화
□ Shuffle 데이터량이 과도한가?
→ Pre-aggregate 가능한가? (서브쿼리에서 먼저 집계)
→ Shuffle Partition 수 조정 (기본 200, 데이터에 따라 조절)
□ Spill to Disk 발생하는가?
→ 메모리 확대 (Memory-optimized 인스턴스)
→ Partition 수 증가로 단위 데이터 축소
□ Photon이 활성화되어 있는가?
→ SQL 집계 워크로드에 Photon 적용 시 2~8x 향상
Step 5: 리소스 최적화
□ 클러스터/Warehouse 사이즈가 적절한가?
→ CPU 사용률 > 80% 지속 → 스케일 업
→ 메모리 사용률 > 80% → Memory-optimized 인스턴스
→ I/O 대기 높음 → Storage-optimized 인스턴스
□ Autoscaling이 적절히 동작하는가?
→ Min/Max 워커 수 검토
□ 네트워크 병목이 있는가?
→ 리전 간 데이터 전송 최소화
→ 같은 리전에 컴퓨트와 스토리지 배치
빠른 참조: 증상별 해결 매트릭스
| 증상 | 1순위 확인 | 2순위 확인 | 3순위 확인 |
|---|---|---|---|
| 쿼리 시작이 느림 | Warehouse Auto Stop 설정 | Instance Pool 사용 | Serverless 전환 |
| 스캔이 느림 | Liquid Clustering | 통계 수집 | 파일 크기 최적화 |
| 조인이 느림 | Broadcast 가능 여부 | Skew 확인 | 클러스터 사이즈 |
| 집계가 느림 | Photon 활성화 | MV 사전 계산 | Partition 수 조정 |
| Spill 발생 | 메모리 확대 | Partition 수 증가 | 쿼리 분리 |
| 스트리밍 지연 | 트리거 간격 | Checkpoint 위치 | 파일 수 제한 |
| ML 추론 느림 | 모델 최적화 | GPU 선택 | 배치 크기 조정 |