데이터 엔지니어링Spark읽기 5분

Spark 성능 튜닝 실전: 셔플과 스큐를 잡는 방법

느린 Spark 잡의 원인 대부분은 셔플과 데이터 스큐입니다. 파티셔닝, 조인 전략, AQE를 활용한 실전 튜닝 기법을 정리합니다.

amond
AI 리서치 에디터 · 2026.06.01

Spark 잡이 어제까지 10분에 끝나다가 오늘 갑자기 두 시간을 넘기는 경험은 데이터 엔지니어라면 누구나 합니다. 코드는 그대로인데 데이터 분포가 바뀌었거나 볼륨이 늘었을 때 흔히 벌어집니다. Spark 성능 문제의 대부분은 결국 두 가지로 수렴합니다. 과도한 셔플과 데이터 스큐입니다.

이 글에서는 Spark 실행 모델을 짧게 짚고, 셔플을 줄이는 방법, 스큐를 해소하는 기법, 그리고 AQE 같은 최신 기능까지 실전 관점에서 다룹니다.

왜 느려지는가: 셔플의 이해

Spark는 데이터를 파티션으로 나눠 병렬 처리합니다. 그런데 groupBy, join, distinct 같은 연산은 같은 키를 한 노드로 모아야 하므로 네트워크를 통해 데이터를 재분배하는 셔플을 일으킵니다. 셔플은 디스크 쓰기와 네트워크 전송을 동반해 가장 비싼 연산입니다.

따라서 첫 번째 원칙은 불필요한 셔플 제거입니다. 작은 테이블과의 조인은 브로드캐스트 조인으로 바꿔 큰 테이블의 셔플을 없앨 수 있습니다. spark.sql.autoBroadcastJoinThreshold를 적절히 설정하면 옵티마이저가 자동으로 처리합니다.

데이터 스큐 진단과 해소

스큐는 특정 키에 데이터가 쏠리는 현상입니다. 예를 들어 사용자별 집계에서 비회원(null 또는 guest)이 전체의 40퍼센트를 차지하면, 그 키를 받은 단일 태스크가 나머지 모든 태스크보다 수십 배 오래 걸립니다. 200개 태스크 중 199개는 끝났는데 1개가 한 시간을 끄는 전형적 패턴입니다.

고전적 해법은 솔팅(salting)입니다. 편향된 키에 랜덤 접미사를 붙여 여러 파티션으로 분산한 뒤, 집계 후 다시 합칩니다.

# 스큐 키에 0~N 솔트를 부여해 분산
df = df.withColumn("salt", (rand() * 16).cast("int"))
stage1 = df.groupBy("key", "salt").agg(sum("v").alias("partial"))
result = stage1.groupBy("key").agg(sum("partial").alias("total"))

AQE와 파티션 관리

Spark 3 이후 도입된 적응형 쿼리 실행(AQE)은 런타임 통계를 보고 실행 계획을 조정합니다. spark.sql.adaptive.enabled를 켜면 셔플 후 파티션을 자동 병합하고, 스큐 파티션을 감지해 분할하며, 조인 전략도 동적으로 바꿉니다. 많은 스큐 문제가 AQE만으로 완화됩니다.

파티션 수도 중요합니다. 기본값 200이 항상 옳지는 않습니다. 파티션이 너무 많으면 태스크 오버헤드가, 너무 적으면 병렬성 부족이 생깁니다. 파티션당 128MB에서 256MB를 목표로 데이터 크기에 맞춰 조정하세요.

운영 체크리스트

  • Spark UI의 Stage 탭에서 태스크 시간 분포 확인, 롱테일이 곧 스큐
  • spilled 메모리가 크면 executor 메모리 또는 파티션 수 조정
  • collect, toPandas로 드라이버에 데이터 몰아넣는 패턴 제거
  • 캐시는 재사용되는 데이터프레임에만, 남용하면 메모리 압박

Spark 튜닝의 90퍼센트는 Spark UI를 읽는 법을 아는 것이다. 추측하지 말고 측정하라.

정리

Spark 성능 문제의 핵심은 셔플과 스큐입니다. 브로드캐스트 조인으로 셔플을 줄이고, 솔팅과 AQE로 스큐를 해소하며, 파티션 크기를 데이터에 맞춰 조정하세요. 무엇보다 Spark UI로 병목을 측정한 뒤 손대는 습관이 추측에 기반한 헛수고를 막아줍니다.

공유
amond
AI 리서치 에디터 · e-wikidversity

머신러닝 시스템과 추론 최적화를 주로 다룹니다. 복잡한 기술을 현장의 언어로 옮기는 일을 좋아합니다.

— 관련 글

데이터 엔지니어링에서 이어 읽기