대상 독자: 데이터 엔지니어, Spark 실무자, Databricks 클러스터 운영자
난이도: 중급 | 소요 시간: 약 15분
Spark 잡이 갑자기 OutOfMemoryError로 죽거나, 멀쩡하던 파이프라인이 특정 데이터 크기를 넘기면서 실패하기 시작했다면, 원인은 거의 항상 메모리 구조를 제대로 이해하지 못한 데서 비롯됩니다.
이 글에서는 Databricks(Spark)의 메모리가 어떻게 구성되는지, 어떤 정책으로 관리되는지, 그리고 실무에서 자주 마주치는 오류 유형별 정확한 대응법을 다룹니다.
Spark는 Driver와 Executor 두 프로세스로 구성되며, 각각 독립적인 JVM 메모리 공간을 가집니다.
┌─────────────────────────────────────────────────────────────┐
│ Databricks 클러스터 │
│ │
│ ┌───────────────────┐ ┌──────────────────────────────┐ │
│ │ Driver │ │ Worker Node │ │
│ │ │ │ ┌────────────────────────┐ │ │
│ │ 실행 계획 수립 │ │ │ Executor JVM │ │ │
│ │ 결과 집계 │ │ │ │ │ │
│ │ spark.driver │ │ │ spark.executor.memory │ │ │
│ │ .memory │ │ │ (기본값: 노드 타입 │ │ │
│ │ │ │ │ 에 따라 자동 설정) │ │ │
│ └───────────────────┘ │ └────────────────────────┘ │ │
│ │ ┌────────────────────────┐ │ │
│ │ │ memoryOverhead (Off- │ │ │
│ │ │ Heap, Python UDF 등) │ │ │
│ │ └────────────────────────┘ │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Spark 1.6부터 적용된 Unified Memory Manager는 Executor JVM 힙을 다음 4개 영역으로 나눕니다.
block-beta
columns 1
block:heap["Executor JVM Heap (spark.executor.memory)"]
columns 1
reserved["🔒 Reserved Memory\n300 MB (고정값)\nSpark 내부 객체 저장"]
user["👤 User Memory\n(Heap - 300MB) × (1 - spark.memory.fraction)\n기본 약 25%\nUDF, 사용자 데이터 구조, 해시맵"]
block:spark_mem["⚡ Spark Memory Pool\n(Heap - 300MB) × spark.memory.fraction\n기본 약 60%\n(Execution ↔ Storage 동적 공유)"]
columns 2
exec["🔄 Execution Memory\nShuffle·Sort·Join·집계\n기본 50%"]
storage["📦 Storage Memory\nCache·Broadcast·Unroll\n기본 50%"]
end
end
① Reserved Memory (300MB, 고정)
Spark 엔진 내부 객체 저장용으로 예약된 공간입니다. 설정으로 변경할 수 없으며, Heap이 450MB 이하이면 Spark가 시작 자체를 거부합니다.
② User Memory
사용자 코드(UDF, 사용자 정의 데이터 구조, 해시맵)가 사용하는 공간입니다. Spark가 직접 관리하지 않기 때문에, 이 영역이 넘치면 java.lang.OutOfMemoryError: Java heap space가 발생합니다.
User Memory = (Heap - 300MB) × (1 - spark.memory.fraction)
기본값: (Heap - 300MB) × 0.4 ≈ 전체의 25%
③ Spark Memory Pool (Execution + Storage 동적 공유)
Execution과 Storage는 하나의 풀을 공유하며 서로 빌려 쓸 수 있습니다.
Spark Memory = (Heap - 300MB) × spark.memory.fraction [기본 0.6]
Execution 초기 할당 = Spark Memory × (1 - spark.memory.storageFraction) [기본 0.5]
Storage 초기 할당 = Spark Memory × spark.memory.storageFraction [기본 0.5]
| 메모리 영역 | 기본 비율 | 주요 사용처 |
|---|---|---|
| Execution | ~30% of Heap | Shuffle, Sort, Join, 집계 |
| Storage | ~30% of Heap | persist(), cache(), Broadcast |
| User | ~25% of Heap | UDF, 사용자 데이터 구조 |
| Reserved | 300MB (고정) | Spark 내부 |
💡 동적 차용 규칙: Execution은 Storage가 사용하지 않는 공간을 자유롭게 빌릴 수 있습니다. 반대로 Storage가 Execution 공간을 빌리려 할 때, Execution이 해당 메모리를 실제 사용 중이면 강제로 빼앗지 못합니다. Execution이 항상 우선권을 가집니다.
JVM GC 대상에 포함되지 않는 Off-Heap 메모리는 두 가지 맥락에서 등장합니다.
┌──────────────────────────────────────────────────────────┐
│ Executor 전체 메모리 │
│ │
│ ┌─────────────────────────────┐ ┌───────────────────┐ │
│ │ JVM Heap │ │ Off-Heap │ │
│ │ spark.executor.memory │ │ │ │
│ │ (위의 4개 영역) │ │ memoryOverhead │ │
│ │ │ │ (기본: executor │ │
│ └─────────────────────────────┘ │ memory × 10%, │ │
│ │ 최소 384MB) │ │
│ │ │ │
│ │ • Python/R UDF │ │
│ │ • Native libs │ │
│ │ • Netty 버퍼 │ │
│ └───────────────────┘ │
└──────────────────────────────────────────────────────────┘
| 파라미터 | 설명 | 기본값 |
|---|---|---|
spark.executor.memoryOverhead | OS·Python·Native 라이브러리용 | max(executor×10%, 384MB) |
spark.memory.offHeap.enabled | Spark 자체 Off-Heap Pool 활성화 | false |
spark.memory.offHeap.size | Spark Off-Heap Pool 크기 | 0 |
⚠️ PySpark 사용자 주의: Python UDF나 Pandas API 사용 시
memoryOverhead부족으로 컨테이너가 강제 종료되는 경우가 많습니다. PySpark 워크로드는memoryOverhead를 executor memory의 20~30%로 올리는 것을 권장합니다.
Databricks는 노드 타입별로 spark.executor.memory를 자동으로 계산합니다. 실제 사용 가능한 executor 메모리는 다음 공식을 따릅니다.
사용 가능한 Executor 메모리 = (노드 전체 RAM × 0.97 - 4,800MB) × 0.8
0.97 → 커널 오버헤드 제외
4,800MB → 노드 레벨 내부 서비스 (Databricks agent 등) 제외
0.8 → 컨테이너 OOM kill 방지를 위한 안전 마진
예시 (r5.4xlarge — 128GB RAM):
(128,000MB × 0.97 - 4,800MB) × 0.8
= (124,160MB - 4,800MB) × 0.8
= 119,360MB × 0.8
≈ 95,488MB ≈ 93GB
📌 Spark UI에서 실제 executor 메모리가 노드 RAM보다 적게 보이는 이유가 이 공식 때문입니다.
java.lang.OutOfMemoryError: Java heap space원인: Executor JVM Heap 고갈. 주로 User Memory 또는 Spark Memory 영역 초과.
[오류 메시지]
java.lang.OutOfMemoryError: Java heap space
at org.apache.spark.sql.execution...
발생 패턴:
# 흔한 원인 1: 대용량 collect()
bad_df = spark.read.parquet("huge_table").collect() # ❌ 전체 데이터를 Driver로
# 흔한 원인 2: 무분별한 cache()
df1.cache()
df2.cache()
df3.cache() # 캐시 공간 초과 시 Eviction 루프 → OOM
대응:
# ✅ collect() 대신 limit() + toPandas()
sample = spark.read.parquet("huge_table").limit(10000).toPandas()
# ✅ 사용 후 캐시 해제
df.cache()
df.count() # 액션으로 캐시 확정
# ... 사용 후
df.unpersist()
# ✅ Spark Memory 비율 조정 (Execution 쪽에 더 할당)
spark.conf.set("spark.memory.fraction", "0.8") # 기본 0.6
spark.conf.set("spark.memory.storageFraction", "0.3") # Storage 비율 줄임
# ✅ Executor 메모리 증가
spark.conf.set("spark.executor.memory", "16g")
OutOfMemoryError: GC overhead limit exceeded원인: JVM GC가 전체 시간의 98% 이상을 사용하면서도 메모리의 2% 미만만 회수할 때 발생. 사실상 메모리 부족의 다른 표현.
[오류 메시지]
java.lang.OutOfMemoryError: GC overhead limit exceeded
발생 패턴:
# 반복 루프에서 DataFrame 재생성 (GC 압박)
result = spark.createDataFrame([], schema)
for i in range(10000):
temp = spark.read.parquet(f"s3://data/part_{i}/")
result = result.union(temp) # ❌ 매 이터레이션마다 쿼리 플랜 누적
result.write.parquet("output/")
대응:
# ✅ 파일 경로 목록을 한 번에 읽기
paths = [f"s3://data/part_{i}/" for i in range(10000)]
result = spark.read.parquet(*paths)
result.write.parquet("output/")
# ✅ G1GC 활성화 (Spark 기본 GC 대비 대용량 힙에서 유리)
spark.conf.set(
"spark.executor.extraJavaOptions",
"-XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=35"
)
ExecutorLostFailure / 컨테이너 강제 종료원인: Executor가 물리 메모리 한도(JVM Heap + memoryOverhead)를 초과하면 OS 또는 YARN이 컨테이너를 강제 종료.
[오류 메시지]
ExecutorLostFailure: Container killed by YARN for exceeding
memory limits. 38.5 GB of 38 GB physical memory used.
Consider boosting spark.yarn.executor.memoryOverhead.
발생 패턴: PySpark UDF, Pandas UDF, 대용량 브로드캐스트, Native 라이브러리(BLAS 등) 사용 시.
# 위험: 대용량 테이블에 broadcast 힌트
from pyspark.sql.functions import broadcast
result = fact_table.join(
broadcast(large_dim_table), # ❌ large_dim_table이 너무 크면 OOM
"id"
)
대응:
# ✅ memoryOverhead 증가 (특히 PySpark 환경)
spark.conf.set("spark.executor.memoryOverhead", "4g") # 기본 ~10% → 명시적 증가
# ✅ 브로드캐스트 임계값 조정
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "50MB") # 기본 10MB
# 또는 브로드캐스트 완전 비활성화
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "-1")
# ✅ Off-Heap 활성화로 GC 압박 감소
spark.conf.set("spark.memory.offHeap.enabled", "true")
spark.conf.set("spark.memory.offHeap.size", "8g")
원인: Shuffle 단계에서 Execution Memory가 부족할 때, 데이터를 디스크에 임시 저장. OOM은 아니지만 성능이 급격히 저하됩니다.
flowchart LR
A["Shuffle 데이터\n(Large)"] --> B{"Execution\nMemory 여유?"}
B -- "있음" --> C["메모리 내 처리\n⚡ 빠름"]
B -- "없음" --> D["디스크에 Spill\n💾 느림 (10~100x)"]
D --> E["디스크에서 읽어\n최종 병합"]
C --> F["결과 출력"]
E --> F
Spark UI에서 Spill 확인:
Spark UI → Stages 탭 → 해당 Stage 클릭
Shuffle Write: 50 GB
Shuffle Spill (Memory): 45 GB ← 메모리에서 직렬화된 크기
Shuffle Spill (Disk): 12 GB ← 실제 디스크에 쓰인 크기
↑ 이 값이 크면 메모리 압박 신호
대응:
# ✅ Shuffle 파티션 수 증가 → 파티션당 데이터 크기 감소
spark.conf.set("spark.sql.shuffle.partitions", "400") # 기본 200
# ✅ AQE 활성화 → 런타임에 파티션 자동 조정
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
# ✅ 파티션당 최대 크기 제한 (기본 128MB)
spark.conf.set("spark.sql.files.maxPartitionBytes", str(64 * 1024 * 1024)) # 64MB
# ✅ Execution Memory 비율 늘리기 (Storage 줄이기)
spark.conf.set("spark.memory.storageFraction", "0.2") # 기본 0.5
spark.driver.maxResultSize 초과원인: collect(), toPandas(), show(n) 등으로 Driver로 대량의 데이터를 집결.
[오류 메시지]
org.apache.spark.SparkException: Job aborted due to stage failure:
Total size of serialized results of 1234 tasks (1.5 GB) is bigger
than spark.driver.maxResultSize (1.0 GB)
대응:
# ✅ 결과 크기 제한 완화 (단기 대처)
spark.conf.set("spark.driver.maxResultSize", "4g") # 기본 1g
# ✅ Driver 메모리 증가
# Cluster 설정에서: spark.driver.memory 8g
# ✅ 근본적 해결: Driver 집결 연산 제거
# ❌ Bad
all_ids = df.select("id").collect()
for row in all_ids:
process(row["id"])
# ✅ Good — Executor에서 처리
from pyspark.sql.functions import udf
process_udf = udf(lambda x: process(x))
df.withColumn("result", process_udf("id")).write.parquet("output/")
flowchart TD
A["⚠️ 메모리 관련 오류 발생"] --> B{"오류 메시지\n키워드는?"}
B -- "Java heap space\nGC overhead" --> C["📍 JVM Heap 부족\n→ Executor OOM"]
B -- "Container killed\nExceeded physical memory" --> D["📍 Total 메모리 초과\n→ memoryOverhead 부족"]
B -- "maxResultSize exceeded\nDriver OOM" --> E["📍 Driver 메모리 부족\n→ collect() 남용"]
B -- "Shuffle Spill 증가\n(느리지만 오류 없음)" --> F["📍 Execution Memory 부족\n→ 디스크 스필"]
C --> C1["spark.executor.memory ↑\nspark.memory.fraction ↑\n반복 루프 리팩터링\nG1GC 활성화"]
D --> D1["spark.executor.memoryOverhead ↑\nOff-Heap 활성화\nBroadcast 크기 점검"]
E --> E1["collect() → write() 변경\nspark.driver.maxResultSize ↑\nDriver 인스턴스 업그레이드"]
F --> F1["shuffle.partitions ↑\nAQE 활성화\nStorageFraction ↓\n파티션 재조정"]
# =============================================
# Databricks 메모리 최적화 권장 설정
# =============================================
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
# --- Execution/Storage 비율 조정 ---
# Shuffle 중심 워크로드 (ETL, 집계)
spark.conf.set("spark.memory.fraction", "0.7") # 기본 0.6
spark.conf.set("spark.memory.storageFraction", "0.3") # 기본 0.5 → Execution에 더 할당
# Cache 중심 워크로드 (반복 ML 학습)
spark.conf.set("spark.memory.fraction", "0.7")
spark.conf.set("spark.memory.storageFraction", "0.6") # Storage에 더 할당
# --- PySpark / Pandas UDF 환경 ---
spark.conf.set("spark.executor.memoryOverhead", "4g") # 기본 ~10% → 명시적 증가
spark.conf.set("spark.sql.execution.arrow.pyspark.enabled", "true") # Arrow 직렬화
# --- GC 튜닝 ---
spark.conf.set(
"spark.executor.extraJavaOptions",
"-XX:+UseG1GC "
"-XX:InitiatingHeapOccupancyPercent=35 "
"-XX:ConcGCThreads=4"
)
# --- Shuffle 튜닝 ---
spark.conf.set("spark.sql.shuffle.partitions", "400") # 기본 200
spark.conf.set("spark.sql.files.maxPartitionBytes", str(64 * 1024 * 1024))
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "50MB")
# --- Driver ---
spark.conf.set("spark.driver.maxResultSize", "2g") # 기본 1g
Spark UI 체크포인트 3가지
① Executors 탭
Storage Memory: 12.5 GB / 20 GB ← 사용률 확인
Task Time (GC): 2 min / 45 min ← GC 시간이 10% 초과 시 경보
② Stages 탭 → 특정 Stage 클릭
Shuffle Spill (Memory): 0 B ← 0이 이상적
Shuffle Spill (Disk): 0 B ← 0이 이상적
Peak Execution Memory: 4.2 GB ← Executor 메모리와 비교
③ SQL 탭 → Query Plan
Exchange (Shuffle)의 outputRows, dataSize 확인
BroadcastExchange: 예상 크기와 실제 크기 비교
Databricks 메모리 이슈는 어떤 영역에서 오류가 났는지 정확히 파악하는 것이 해결의 출발점입니다.
핵심을 정리하면 이렇습니다.
Java heap space → Executor JVM 부족 → executor.memory 증가, 코드 개선GC overhead exceeded → GC 루프 → 반복 연산 구조 변경, G1GC 전환Container killed → Total 메모리 초과 → memoryOverhead 증가maxResultSize exceeded → Driver 집결 연산 → collect() 제거무엇보다 Spark UI의 Executors·Stages 탭을 보는 습관을 들이면, 오류가 터지기 전에 메모리 압박 신호를 미리 잡아낼 수 있습니다.