Skip to main content

9. OOM 디버깅

9.1 OOM 유형별 진단

OOM 유형에러 메시지원인해결
Driver OOMjava.lang.OutOfMemoryError: Java heap space (Driver)collect(), toPandas(), 큰 BroadcastDriver 메모리 증가 또는 collect 제거
Executor OOMjava.lang.OutOfMemoryError: Java heap space (Executor)데이터 편향, 큰 파티션파티션 수 증가, 메모리 증가
Container OOMContainer killed by YARN for exceeding memory limitsOff-heap 메모리 초과spark.memory.offHeap.size 증가
GPU OOMCUDA 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:+UseG1GCG1GC (기본)G1GC 유지Spark에 최적화
-XX:G1HeapRegionSize자동16m대용량 힙에서 효율적
-XX:InitiatingHeapOccupancyPercent4535GC 일찍 시작하여 긴 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.memorySpill 발생, OOM
spark.memory.fraction↑ (0.6→0.7)캐시 많이 사용, 큰 집계
spark.memory.offHeap.sizePhoton 사용, Container OOM
spark.sql.shuffle.partitions파티션당 메모리 초과
spark.driver.memoryDriver 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 선택배치 크기 조정

참고 링크