PySpark 설정 튜닝 종합 — 어떤 config를 언제 바꿔야 하는가
수많은 Spark 설정 중 실제로 효과 있는 것만 정리합니다. 익스큐터 사이징(코어·메모리·인스턴스), AQE, 셔플 파티션, 직렬화, 동적 할당, 그리고 "메모리부터 올리는" 안티패턴 대신 근거 기반으로 튜닝하는 순서를 다룹니다.
Spark 설정은 수백 개입니다. 그래서 많은 팀이 "느리면 executor.memory 올리고, 안 되면 인스턴스 늘리는" 추측성 튜닝을 반복합니다. 자원만 낭비하고 문제는 그대로입니다. 사실 실제로 효과 있는 설정은 십여 개이고, 무엇을 언제 바꿔야 하는지에는 명확한 순서가 있습니다.
이 글은 정말 중요한 Spark 설정만 골라, 무엇을 어떤 근거로 조정하는지 정리합니다. (개별 문제 — 스큐·OOM·작은 파일 — 의 상세 튜닝은 각 전용 글에서 다뤘으니, 여기서는 전체 그림과 우선순위를 봅니다.)
1. 튜닝의 황금률 — 측정하고, 가장 큰 병목부터
❌ 추측: 느리다 → memory 올린다 → 안 된다 → 인스턴스 늘린다 → 비용만↑
✅ 근거: Spark UI 측정 → 병목 식별(스큐/셔플/OOM) → 해당 설정만 조정설정을 바꾸기 전에 Spark UI 로 병목을 먼저 식별하세요(별도 글 "PySpark 느린 잡 디버깅"). 스큐인데 메모리를 올리거나, 셔플 문제인데 인스턴스를 늘리는 건 효과가 없습니다.
2. 가장 먼저 — AQE 켜기
Spark 3.x 의 Adaptive Query Execution 은 런타임에 파티션 병합·스큐 조인·조인 전략을 자동 조정합니다. 가장 적은 노력으로 가장 큰 효과를 주는 설정입니다.
spark.conf.set("spark.sql.adaptive.enabled", "true") # 마스터 스위치
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true") # 작은 파티션 자동 병합
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true") # 스큐 조인 자동 분할
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "128MB")최신 Spark 는 AQE 가 기본 활성이지만, 명시적으로 확인하세요. AQE 하나로 작은 파일·스큐·파티션 수 문제의 상당 부분이 자동 완화됩니다.
3. 익스큐터 사이징 — 가장 중요한 결정
자원 배분의 핵심은 세 값의 균형입니다: 익스큐터 코어, 메모리, 인스턴스 수.
spark.conf.set("spark.executor.cores", "4") # 익스큐터당 동시 태스크
spark.conf.set("spark.executor.memory", "8g") # JVM 힙
spark.conf.set("spark.executor.memoryOverhead", "2g")# 힙 밖(셔플·네이티브·Python)
spark.conf.set("spark.executor.instances", "20") # 익스큐터 수코어 수 — 4~5가 스위트스폿
코어 너무 적음(1~2) → 익스큐터 오버헤드 대비 비효율
코어 너무 많음(>5) → HDFS/스토리지 I/O 처리량 저하, GC 경합
→ 익스큐터당 4~5 코어가 일반적 권장메모리 — 힙과 오버헤드 구분
PySpark 는 Python 워커가 힙 밖 메모리를 쓰므로, memoryOverhead 를 넉넉히 둬야 "Container killed" 를 피합니다(별도 글 "PySpark Executor OOM 정복").
| 설정 | 의미 | 지침 |
|---|---|---|
executor.cores | 동시 태스크 | 4~5 |
executor.memory | JVM 힙 | 태스크당 충분히, GC 고려 |
executor.memoryOverhead | 힙 밖 | 힙의 ~25%, PySpark 면 더 |
executor.instances | 익스큐터 수 | 병렬도 = cores × instances |
흔한 실수: "큰 익스큐터 몇 개" vs "작은 익스큐터 많이". 너무 큰 익스큐터는 GC pause 가 길고, 너무 작으면 오버헤드가 큽니다. 중간 크기(4
5코어, 816GB) 다수가 보통 안전합니다.
4. 셔플 파티션 — 200 기본값의 함정
spark.sql.shuffle.partitions 의 기본값 200 은 거의 모든 워크로드에 안 맞습니다. 데이터가 크면 파티션당 너무 커서 OOM, 작으면 빈 파티션만 200개입니다.
# AQE 가 켜져 있으면 런타임에 자동 조정되므로 기본값 신경 덜 써도 됨
spark.conf.set("spark.sql.adaptive.enabled", "true")
# AQE 없이 수동이라면: 파티션당 ~128MB 목표로
# num = 총 셔플 데이터 / 128MB
spark.conf.set("spark.sql.shuffle.partitions", "800")AQE 의
coalescePartitions가 켜져 있으면 이 값을 크게 잡아도(예: 2000) 런타임에 적정 수로 병합해주므로 안전합니다. AQE 시대에는 이 값을 일일이 맞추는 부담이 크게 줄었습니다.
5. 직렬화·압축
# Kryo 직렬화 (Java 기본보다 빠르고 작음)
spark.conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
# 셔플 압축(기본 켜짐) — 네트워크·디스크 절약
spark.conf.set("spark.shuffle.compress", "true")
spark.conf.set("spark.shuffle.spill.compress", "true")Kryo 는 거의 항상 이득입니다. 압축은 기본 활성이며 대부분 그대로 두면 됩니다.
6. 동적 할당 — 자원 효율
고정 익스큐터 대신 부하에 따라 조절합니다(특히 공유 클러스터·K8s).
spark.conf.set("spark.dynamicAllocation.enabled", "true")
spark.conf.set("spark.dynamicAllocation.shuffleTracking.enabled", "true") # K8s 필수
spark.conf.set("spark.dynamicAllocation.minExecutors", "2")
spark.conf.set("spark.dynamicAllocation.maxExecutors", "50")
spark.conf.set("spark.dynamicAllocation.executorIdleTimeout", "60s")(K8s 에서의 셔플 추적·스팟 이슈는 별도 글 "PySpark on Kubernetes" 참고.)
7. 브로드캐스트·조인
# 자동 브로드캐스트 임계값 (기본 10MB) — 작은 차원 조인 최적화
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "50MB")
# -1 로 끄면 broadcast 안 함 (의도적 비활성 시)너무 크게 잡으면 큰 테이블이 broadcast 돼 OOM 위험(별도 글 "Broadcast 변수와 대형 Lookup"). 50~100MB 정도가 보통 안전합니다.
8. 설정 우선순위 — 무엇부터 손대나
1. AQE 켜기 (skew/coalesce/skewJoin) → 가장 큰 효과, 가장 적은 노력
2. 익스큐터 사이징 (cores 4~5, 메모리+오버헤드)
3. shuffle.partitions (AQE 있으면 크게 둬도 OK)
4. memoryOverhead (PySpark Container killed 대응)
5. 직렬화(Kryo), 동적 할당
6. autoBroadcastJoinThreshold
───
그래도 안 되면 → 코드/데이터 문제 (스큐·UDF·작은파일) — 설정으로 못 고침핵심: 설정 튜닝은 한계가 있습니다. 스큐, 잘못된 조인, 비싼 UDF, 작은 파일은 설정으로 못 고칩니다 — 코드·데이터 레이아웃을 바꿔야 합니다. 설정을 다 만져봐도 안 되면, 문제는 설정이 아니라 코드입니다.
9. 워크로드별 프로파일
| 워크로드 | 강조 설정 |
|---|---|
| 대형 ETL 배치 | 큰 익스큐터, shuffle.partitions↑, FTE |
| 대화형/짧은 쿼리 | 동적 할당, 작은 지연, broadcast |
| 스트리밍 | 트리거·백프레셔, 체크포인트, 적정 익스큐터 |
| ML 학습 | 메모리↑, 캐시, 적은 셔플 |
| 고동시성 공유 | 동적 할당, 공정 스케줄러 |
10. 정리
| 영역 | 핵심 설정 | 효과 |
|---|---|---|
| AQE | adaptive.* | 자동 스큐·파티션·조인 |
| 사이징 | cores 4~5, 메모리, 오버헤드 | 안정성·처리량 |
| 셔플 | shuffle.partitions(AQE면 덜 중요) | 파티션 크기 |
| 직렬화 | Kryo | 속도 |
| 동적 할당 | dynamicAllocation.* | 자원 효율 |
| 조인 | autoBroadcastJoinThreshold | 셔플 회피 |
Spark 설정 튜닝의 핵심은 "측정으로 병목을 찾고, 가장 효과 큰 것부터 조정하되, 설정의 한계를 인정하는 것"입니다. AQE 를 켜고 익스큐터를 합리적으로 사이징하는 것만으로 대부분의 잡이 안정화됩니다. 그래도 느리다면 — 설정을 더 만지기 전에 스큐·UDF·작은 파일 같은 코드·데이터 문제를 의심하세요. 좋은 설정은 좋은 코드를 빠르게 하지만, 나쁜 코드를 고쳐주지는 못합니다.
이 글은 Spark 3.5 기준으로 작성되었습니다. Spark 클러스터·잡의 종합 성능 튜닝이 필요하시면 언제든 문의해 주세요.
— Data Dynamics 엔지니어링 팀