- Published on
Apache Spark 실전 운영 가이드: 성능 튜닝, 셔플, 스큐, AQE, 스트리밍 운영
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- Spark에서 실제로 중요한 실행 모델
- 셔플, 파티셔닝, 데이터 스큐가 대부분을 결정한다
- AQE, 조인 전략, 캐시는 어떻게 써야 하는가
- Structured Streaming 운영에서 자주 망가지는 지점
- 프로덕션 튜닝을 위한 실전 워크플로우
- 마무리
- References

들어가며
Apache Spark는 빠른 엔진이지만, 프로덕션에서 느려지는 이유도 꽤 예측 가능합니다. 많은 팀이 처음에는 "executor memory를 더 키우면 해결되겠지"라고 생각하지만, 실제 병목은 메모리 크기보다 셔플 패턴, 파티션 설계, 조인 전략, 파일 레이아웃, 스트리밍 상태 관리에서 더 자주 터집니다.
특히 Databricks, EMR, self-managed YARN, Kubernetes 위에서 Spark를 운영할 때 공통으로 보이는 패턴이 있습니다.
- 작은 파일이 너무 많아 스캔 오버헤드가 커진다
- skewed key 때문에 일부 태스크만 비정상적으로 오래 걸린다
- 조인 전략이 잘못 잡혀 broadcast 대신 큰 shuffle join이 발생한다
- 캐시를 무분별하게 사용해 메모리 압박과 spill이 늘어난다
- Structured Streaming에서 watermark와 state store 전략을 명확히 잡지 않아 상태가 끝없이 불어난다
이 글은 Spark 입문서가 아니라, 운영 환경에서 Spark 잡을 빠르고 안정적으로 돌리기 위한 실전 기준을 정리한 글입니다.
Spark에서 실제로 중요한 실행 모델
Spark 성능을 이해하려면 API보다 먼저 실행 모델을 봐야 합니다.
- Driver는 DAG를 만들고 스케줄링을 담당합니다.
- Executor는 실제 task를 실행합니다.
- Stage 경계는 보통 shuffle이 생기는 지점에서 나뉩니다.
- Task 수와 partition 수는 거의 직접적으로 연결됩니다.
즉, Spark 튜닝은 "노드를 몇 대 더 붙일까"보다 어디서 shuffle이 생기고 partition이 어떻게 만들어지는가를 먼저 봐야 합니다.
실무에서 가장 먼저 확인할 질문은 단순합니다.
- 이 job의 가장 느린 stage는 어디인가
- 그 stage는 shuffle read 또는 shuffle write가 큰가
- skewed task가 있는가
- input file 수와 partition 수가 지나치게 많은가
- join, aggregation, sort 중 어떤 연산이 병목인가
Spark UI에서 SQL 탭, stage 탭, task distribution을 보는 습관이 중요한 이유가 여기에 있습니다.
셔플, 파티셔닝, 데이터 스큐가 대부분을 결정한다
셔플은 언제 비싸지는가
셔플은 네트워크, 디스크 I/O, 정렬 비용이 한 번에 겹칩니다. 아래 상황에서 비용이 특히 커집니다.
groupByKey, wide aggregation, large sort- join key cardinality가 크고 데이터가 넓은 경우
- 너무 많은 작은 파티션이 생성되는 경우
- 일부 key에만 데이터가 몰린 경우
파티션 수를 볼 때 흔히 하는 실수
파티션이 너무 적으면 병렬성이 부족하고, 너무 많으면 task scheduling과 작은 파일 오버헤드가 폭증합니다. 정답은 고정값이 아니라 데이터 크기와 stage 특성에 따라 달라집니다.
운영에서는 다음 원칙이 더 실용적입니다.
- 입력 데이터 크기와 평균 task duration을 함께 본다
- 출력 파일 수를 저장소 레이아웃 관점에서 본다
spark.sql.shuffle.partitions를 기본값 그대로 두지 않는다- AQE가 켜져 있어도 upstream 데이터 품질이 나쁘면 자동 최적화에 과신하지 않는다
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.shuffle.partitions", "400")
스큐는 평균값으로 보이지 않는다
데이터 스큐는 평균 execution time만 보면 놓치기 쉽습니다. stage 평균은 괜찮아 보여도, 일부 task가 몇 배 오래 걸리면서 전체 job wall-clock time을 끌어내립니다.
대표적인 대응은 이렇습니다.
- skewed key를 식별한다
- salting이나 pre-aggregation을 검토한다
- broadcast 가능한 작은 dimension table을 분리한다
- AQE skew join handling을 켠다
- 데이터 모델 자체를 바꿔 hot key를 줄인다
스큐는 Spark 옵션만으로 해결되는 문제가 아니라, 업스트림 데이터 모델과 키 분포 문제인 경우가 많습니다.
AQE, 조인 전략, 캐시는 어떻게 써야 하는가
AQE는 기본값이 아니라 운영 가드레일이다
Adaptive Query Execution은 Spark 3 이후 실전 운영에서 사실상 기본입니다. 실행 중 통계를 바탕으로 partition coalescing, skew handling, join strategy 변경을 수행하므로, 특히 입력 분포가 매일 바뀌는 데이터 파이프라인에 유리합니다.
다만 AQE를 켰다고 모든 것이 해결되지는 않습니다.
- 통계가 너무 늦게 수집되면 이미 비싼 shuffle이 발생했을 수 있다
- upstream filter pushdown이 약하면 처리량 자체가 너무 커질 수 있다
- badly partitioned source는 AQE만으로 회복하기 어렵다
조인 전략은 명시적으로 점검해야 한다
실무에서는 다음 순서로 확인하는 편이 안전합니다.
- 작은 테이블을 broadcast 가능한가
- join key 분포가 균등한가
- sort-merge join이 정말 필요한가
- partition pruning이 먹는가
- 실행 계획에서 의도한 조인 전략이 실제로 잡혔는가
import org.apache.spark.sql.functions.broadcast
val result = factDf.join(broadcast(dimDf), Seq("customer_id"), "left")
캐시는 "많이"가 아니라 "다시 읽을 가치가 있을 때"만
캐시는 자주 재사용되는 expensive intermediate에만 쓰는 것이 맞습니다. 반대로 한 번만 읽는 데이터, 이미 columnar format으로 잘 저장된 데이터, 메모리 압박이 큰 cluster에서는 오히려 손해가 납니다.
캐시를 검토할 때는 아래를 같이 봐야 합니다.
- 재사용 횟수
- 원본 스캔 비용
- executor memory pressure
- spill 발생량
- cache invalidation 시점
cache() 호출보다 중요한 것은 언제 unpersist 할 것인가입니다.
Structured Streaming 운영에서 자주 망가지는 지점
Structured Streaming은 batch API와 유사해서 접근은 쉬우나, 운영은 오히려 더 엄격해야 합니다.
watermark 없이 stateful 연산을 키우지 말 것
stateful aggregation이나 stream-stream join에서 watermark가 없으면 state store가 끝없이 커질 수 있습니다.
val aggregated =
events
.withWatermark("event_time", "10 minutes")
.groupBy(window($"event_time", "5 minutes"), $"user_id")
.count()
운영 체크리스트는 다음과 같습니다.
- event time과 processing time을 구분한다
- watermark 지연 허용치를 비즈니스 SLA와 맞춘다
- checkpoint 저장소의 내구성과 접근 권한을 점검한다
- output mode와 sink idempotency를 확인한다
- late data 비율을 모니터링한다
스트리밍은 코드보다 운영 계약이 중요하다
실패 복구, 재처리, checkpoint 보존 기간, schema evolution, sink 중복 허용 여부가 정리되지 않으면 스트리밍 파이프라인은 장애 때마다 의미가 바뀝니다.
Spark Structured Streaming 운영 문서는 항상 다음 질문을 포함해야 합니다.
- checkpoint 위치는 어디인가
- 재배포 시 어떤 설정을 절대 바꾸면 안 되는가
- exactly-once가 아니라면 어떤 중복이 허용되는가
- backfill 전략은 무엇인가
프로덕션 튜닝을 위한 실전 워크플로우
제가 가장 실용적이라고 보는 순서는 아래와 같습니다.
- Spark UI에서 가장 느린 stage 하나만 고른다
- shuffle read/write, spill, skewed task를 먼저 본다
- 실행 계획에서 join strategy와 partition 수를 확인한다
- source file layout와 predicate pushdown을 본다
- AQE, broadcast, repartition, pre-aggregation 중 하나만 바꿔서 다시 측정한다
- throughput과 cost를 함께 본다
핵심은 한 번에 모든 파라미터를 만지지 않는 것입니다. Spark 튜닝이 어려운 이유는 옵션이 많아서가 아니라, 실험 설계를 하지 않으면 무엇이 효과가 있었는지 알 수 없기 때문입니다.
마무리
Spark 운영의 핵심은 "메모리를 더 준다"가 아니라 데이터 이동을 줄이고, skew를 제어하고, 실행 계획을 의도대로 만들고, 스트리밍 상태를 제한하는 것입니다.
특히 아래 네 가지를 팀의 기본 운영 규칙으로 두면 효과가 큽니다.
- 모든 느린 job은 Spark UI와 실행 계획으로 역추적한다
- partition 수와 output file 수를 의도적으로 설계한다
- AQE와 broadcast 전략을 기본 가드레일로 둔다
- Structured Streaming은 state, watermark, checkpoint 계약을 문서화한다
Spark는 강력한 엔진이지만, 성능은 옵션이 아니라 운영 습관에서 나옵니다.