Databricks 메모리 구조와 관리 정책, 오류 대응 완전 가이드

GarionNachal·2026년 3월 8일

databricks

목록 보기
35/45

대상 독자: 데이터 엔지니어, Spark 실무자, Databricks 클러스터 운영자
난이도: 중급 | 소요 시간: 약 15분


Spark 잡이 갑자기 OutOfMemoryError로 죽거나, 멀쩡하던 파이프라인이 특정 데이터 크기를 넘기면서 실패하기 시작했다면, 원인은 거의 항상 메모리 구조를 제대로 이해하지 못한 데서 비롯됩니다.

이 글에서는 Databricks(Spark)의 메모리가 어떻게 구성되는지, 어떤 정책으로 관리되는지, 그리고 실무에서 자주 마주치는 오류 유형별 정확한 대응법을 다룹니다.


1. Spark 메모리 전체 구조 한눈에 보기

Spark는 DriverExecutor 두 프로세스로 구성되며, 각각 독립적인 JVM 메모리 공간을 가집니다.

┌─────────────────────────────────────────────────────────────┐
│                   Databricks 클러스터                        │
│                                                             │
│   ┌───────────────────┐    ┌──────────────────────────────┐ │
│   │      Driver       │    │        Worker Node           │ │
│   │                   │    │  ┌────────────────────────┐  │ │
│   │  실행 계획 수립   │    │  │      Executor JVM      │  │ │
│   │  결과 집계        │    │  │                        │  │ │
│   │  spark.driver     │    │  │  spark.executor.memory │  │ │
│   │  .memory          │    │  │  (기본값: 노드 타입    │  │ │
│   │                   │    │  │   에 따라 자동 설정)   │  │ │
│   └───────────────────┘    │  └────────────────────────┘  │ │
│                             │  ┌────────────────────────┐  │ │
│                             │  │  memoryOverhead (Off-  │  │ │
│                             │  │  Heap, Python UDF 등)  │  │ │
│                             │  └────────────────────────┘  │ │
│                             └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

2. Executor 메모리 내부 구조 — Unified Memory Manager

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 HeapShuffle, Sort, Join, 집계
Storage~30% of Heappersist(), cache(), Broadcast
User~25% of HeapUDF, 사용자 데이터 구조
Reserved300MB (고정)Spark 내부

💡 동적 차용 규칙: Execution은 Storage가 사용하지 않는 공간을 자유롭게 빌릴 수 있습니다. 반대로 Storage가 Execution 공간을 빌리려 할 때, Execution이 해당 메모리를 실제 사용 중이면 강제로 빼앗지 못합니다. Execution이 항상 우선권을 가집니다.


3. Off-Heap 메모리 — JVM 바깥 영역

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.memoryOverheadOS·Python·Native 라이브러리용max(executor×10%, 384MB)
spark.memory.offHeap.enabledSpark 자체 Off-Heap Pool 활성화false
spark.memory.offHeap.sizeSpark Off-Heap Pool 크기0

⚠️ PySpark 사용자 주의: Python UDF나 Pandas API 사용 시 memoryOverhead 부족으로 컨테이너가 강제 종료되는 경우가 많습니다. PySpark 워크로드는 memoryOverhead를 executor memory의 20~30%로 올리는 것을 권장합니다.


4. Databricks 메모리 할당 공식

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보다 적게 보이는 이유가 이 공식 때문입니다.


5. 실무 메모리 오류 유형 & 대응법

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 Spill (디스크 스필)

원인: 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

❺ Driver OOM — 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/")

6. 메모리 오류 진단 플로우차트

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파티션 재조정"]

7. 실무 메모리 튜닝 설정 모음

# =============================================
# 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

8. Spark UI로 메모리 상태 확인하기

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() 제거
  • Shuffle Spill 증가 → Execution Memory 부족 → AQE 활성화, 파티션 조정

무엇보다 Spark UI의 Executors·Stages 탭을 보는 습관을 들이면, 오류가 터지기 전에 메모리 압박 신호를 미리 잡아낼 수 있습니다.


참고 자료

profile
AI를 꿈꾸는 BackEnd개발자

0개의 댓글