PySpark on Kubernetes — Dynamic Allocation, Shuffle, 스팟 운영
Spark 를 Kubernetes 에서 운영할 때의 실전 과제. executor Pod 모델, dynamic allocation 과 셔플 데이터 보존 문제, 스팟 인스턴스에서 셔플이 사라지는 함정, 그리고 비용을 낮추면서 안정성을 지키는 패턴을 정리합니다.
Spark 워크로드를 YARN 에서 Kubernetes 로 옮기는 흐름이 자리 잡았습니다. 컨테이너 기반 배포, 오토스케일링, 스팟 인스턴스로 인한 비용 절감이 매력적입니다. 하지만 Spark on K8s 에는 YARN 시절에 없던 고유 과제가 있습니다 — 특히 dynamic allocation 과 셔플 데이터 보존, 그리고 스팟 인스턴스에서 executor 가 사라질 때의 셔플 손실입니다.
이 글은 Spark on Kubernetes 의 실행 모델, 핵심 설정, 그리고 비용과 안정성을 동시에 잡는 운영 패턴을 정리합니다.
1. 실행 모델 — Driver 와 Executor Pod
spark-submit (--master k8s://...)
│
▼
Driver Pod ──(K8s API 로 executor Pod 생성/삭제)──┐
│ │
┌────┼────────────────┐ │
▼ ▼ ▼ │
Executor Executor Executor Pod ← driver 가 직접 관리| Spark 개념 | Kubernetes |
|---|---|
| Driver | Pod (잡 1개당 1개) |
| Executor | Pod (driver 가 생성/삭제) |
| 리소스 요청 | Pod requests/limits |
| 격리 | namespace, resource quota |
YARN 과 달리 driver 가 K8s API 로 executor Pod 를 직접 만들고 지웁니다. 별도 클러스터 매니저 없이 K8s 가 그 역할을 합니다.
2. 기본 제출과 리소스 설정
# spark-submit 예시 (개념)
# spark-submit \
# --master k8s://https://<api-server> \
# --deploy-mode cluster \
# --conf spark.kubernetes.container.image=<spark-image> \
# --conf spark.executor.instances=10 \
# ...
conf = {
"spark.kubernetes.container.image": "registry/spark:pinned-tag",
"spark.executor.instances": "10",
"spark.executor.memory": "8g",
"spark.executor.memoryOverhead": "2g", # PySpark Python 메모리 고려
"spark.executor.cores": "4",
"spark.kubernetes.executor.request.cores": "4",
}PySpark 라면 memoryOverhead 를 넉넉히 두세요 — Python 워커가 힙 밖 메모리를 쓰므로 컨테이너가 OOMKill 될 수 있습니다(별도 글 "PySpark Executor OOM 정복").
3. Dynamic Allocation — 부하에 따라 executor 조절
고정 executor 수 대신, 잡의 부하에 따라 executor 를 늘리고 줄입니다. 유휴 자원 낭비를 막습니다.
conf = {
"spark.dynamicAllocation.enabled": "true",
"spark.dynamicAllocation.shuffleTracking.enabled": "true", # K8s 핵심!
"spark.dynamicAllocation.minExecutors": "2",
"spark.dynamicAllocation.maxExecutors": "50",
"spark.dynamicAllocation.executorIdleTimeout": "60s",
}핵심: K8s 에는 YARN 의 External Shuffle Service 가 (전통적으로) 없습니다. 그래서 dynamic allocation 으로 executor 를 줄일 때, 그 executor 가 들고 있던 셔플 데이터가 사라지는 문제가 생깁니다. 이를 해결하는 것이
shuffleTracking.enabled입니다 — 셔플 데이터를 가진 executor 는 idle 여도 회수하지 않고 추적·보존합니다.
4. 가장 큰 함정 — 셔플 데이터 손실
K8s 에서 executor Pod 가 사라지면(축소, 노드 장애, 스팟 회수) 그 Pod 의 로컬 디스크에 있던 셔플 데이터도 사라집니다. 다른 executor 가 그 셔플을 읽으려 하면 FetchFailedException 이 나고, Spark 는 해당 스테이지를 재계산합니다.
Executor A 가 셔플 쓰기 → A 가 스팟 회수로 사라짐
→ Executor B 가 A 의 셔플을 읽으려 함 → FetchFailedException
→ 스테이지 재계산 (비쌈), 반복되면 잡 실패대응 전략:
| 전략 | 방법 |
|---|---|
| shuffle tracking | shuffleTracking.enabled=true 로 셔플 가진 executor 보존 |
| 셔플 데이터 외부화 | 원격 셔플 서비스(예: Celeborn 등)로 셔플을 클러스터 외부에 |
| FTE(재시도) | 단계 재계산 허용, 셔플 재생성 |
| 스팟 제한 | 셔플 무거운 잡은 온디맨드 비중↑ |
5. 스팟 인스턴스 — 비용 절감과 위험
스팟(또는 preemptible) 인스턴스는 저렴하지만 언제든 회수됩니다. Spark on K8s 에서 스팟을 안전하게 쓰는 패턴:
Driver → 온디맨드 노드 (driver 가 죽으면 잡 전체 실패 — SPOF)
Executor → 스팟 노드 (죽어도 재계산/재시도로 복구 가능)conf = {
# driver 는 온디맨드, executor 는 스팟 노드풀에 (nodeSelector)
"spark.kubernetes.driver.node.selector.node-pool": "on-demand",
"spark.kubernetes.executor.node.selector.node-pool": "spot",
}| 컴포넌트 | 배치 | 이유 |
|---|---|---|
| Driver | 온디맨드 | 죽으면 잡 전체 실패(SPOF) |
| Executor | 스팟 | 재계산/재시도로 복구 |
| 셔플 무거운 단계 | 온디맨드 비중↑ | 셔플 손실 비용 큼 |
원칙: driver 는 절대 스팟에 두지 마세요. executor 손실은 재계산으로 복구되지만, driver 손실은 잡 전체를 날립니다. (Trino 코디네이터와 같은 원리 — 별도 글 "Trino 를 Kubernetes 에 배포하기".)
6. 데이터 지역성과 I/O
K8s 의 Spark 는 보통 컴퓨트와 스토리지가 분리(S3/오브젝트 스토리지)되어 있습니다. HDFS 식 데이터 지역성이 없으므로:
- 입출력은 오브젝트 스토리지 커넥터(S3A 등) 성능에 의존 → 커넥터 튜닝(멀티파트, 연결 풀).
- 셔플·spill 용 로컬 디스크는 빠른 노드 로컬 SSD 를 확보(
spark.local.dir). - Lakehouse(Iceberg/Delta) + 오브젝트 스토리지가 자연스러운 조합.
7. 운영 — 모니터링과 격리
| 항목 | 방법 |
|---|---|
| 리소스 격리 | namespace + ResourceQuota |
| 이미지 | 버전 고정(latest 금지) |
| 로그 | driver/executor Pod 로그 수집 |
| 메트릭 | Spark UI + Prometheus(메트릭 sink) |
| 정리 | 완료된 driver Pod 정리 정책 |
여러 팀이 한 클러스터를 쓰면 namespace 와 quota 로 격리해, 한 팀의 잡이 전체를 잠식하지 못하게 합니다.
8. Spark on K8s vs YARN 요약
| 항목 | YARN | Kubernetes |
|---|---|---|
| 자원 관리 | RM/NM | K8s 스케줄러 |
| 셔플 서비스 | External Shuffle Service 내장 | 없음 → shuffle tracking/원격 셔플 |
| 오토스케일 | 제한적 | dynamic allocation + 클러스터 오토스케일러 |
| 스팟 | 제한적 | 자연스러움(단 셔플 손실 주의) |
| 멀티테넌시 | 큐 | namespace/quota |
| 데이터 지역성 | HDFS 강함 | 보통 분리(오브젝트 스토리지) |
9. 정리
| 영역 | 핵심 |
|---|---|
| 실행 모델 | driver 가 executor Pod 직접 관리 |
| dynamic allocation | shuffleTracking 필수(셔플 보존) |
| 셔플 손실 | executor 소실 = 셔플 소실 → 재계산/원격 셔플 |
| 스팟 | executor 만, driver 는 온디맨드 |
| 스토리지 | 컴퓨트-스토리지 분리, Lakehouse 조합 |
Spark on Kubernetes 의 핵심 통찰은 "YARN 의 External Shuffle Service 가 없다"는 한 가지에서 대부분의 운영 과제가 파생된다는 것입니다. dynamic allocation 의 shuffleTracking, 스팟에서의 셔플 손실, driver/executor 노드 분리 — 모두 셔플 데이터의 수명을 어떻게 다루느냐의 문제입니다. driver 는 온디맨드로 보호하고, executor 는 스팟으로 비용을 낮추되 셔플 무거운 단계는 신중히 다루면, 비용과 안정성을 함께 잡을 수 있습니다.
이 글은 Spark 3.5 기준으로 작성되었습니다. Spark on Kubernetes 마이그레이션이나 비용 최적화 설계가 필요하시면 언제든 문의해 주세요.
— Data Dynamics 엔지니어링 팀