셔플·직렬화·압축·스케줄링 설정 총정리 — 실전 튜닝 체크리스트
Apache Spark 튜닝 옵션 카탈로그 — 공식 설정 문서 종합 (웹 리서치, 2026) — Apache Spark 공식 문서(sql-performance-tuning.html, configuration.html, Spark 4.2.0 기준) Spark 4.2.0 공식 문서 configuration.html — Serialization/Compression/Scheduling 설정 종합
이 모듈을 다 읽으면
- spark-learn-25가 개념으로 소개한 Kryo 직렬화를 실제로 켜는 구체적 설정값과 주의사항을 적용할 수 있다
- 파일을 읽을 때 초기 파티션 개수가 어떻게 정해지는지(maxPartitionBytes/openCostInBytes)를 설명할 수 있다
- 투기적 실행(speculative execution)이 무엇을 해결하는 기능이고 어떤 부작용이 있을 수 있는지 판단할 수 있다
- 이 모듈들에서 다룬 튜닝 옵션들을 하나의 점검 순서로 정리해 실전에 적용할 수 있다
spark-learn-24/25/33/34/39-42에서 다룬 개별 튜닝 주제들을 넘어, 직렬화·압축·파일 분할·투기적 실행·스케줄링까지 남은 주요 튜닝 옵션을 정리하고 마지막에 전체를 하나의 점검 체크리스트로 묶는다.
Kryo 직렬화 — 켜는 법과 놓치기 쉬운 옵션들
spark-learn-25에서 Java 직렬화가 기본이고 Kryo가 프로덕션 권장이라는 개념을 다뤘는데, Spark 4.2 기준으로도 spark.serializer의 기본값은 여전히 org.apache.spark.serializer.JavaSerializer다(Kryo가 아직도 기본이 아니라는 점은 은근히 자주 오해되는 부분이다). 켜려면 spark.serializer를 org.apache.spark.serializer.KryoSerializer로 명시해야 한다.
실무에서 자주 놓치는 두 옵션이 있다. 첫째, spark.kryoserializer.buffer(초기 버퍼 크기, 기본 64KB)와 spark.kryoserializer.buffer.max(버퍼가 늘어날 수 있는 상한, 기본 64MB) — 직렬화하려는 객체 하나가 이 상한보다 크면 "Kryo serialization failed: Buffer overflow" 에러가 나므로, 큰 객체(예: 대형 배열을 담은 커스텀 클래스)를 다룬다면 buffer.max를 미리 늘려야 한다. 둘째, spark.kryo.registrationRequired(기본 false) — true로 켜면 등록되지 않은 클래스를 직렬화하려 할 때 예외를 던져서, 어떤 커스텀 클래스가 등록 안 됐는지 빠르게 찾아낼 수 있다(등록된 클래스는 클래스 이름 전체를 매번 안 쓰고 정수 ID로 직렬화해 크기를 더 줄여준다).
핵심 포인트
- Spark 4.2 기준으로도 spark.serializer 기본값은 여전히 JavaSerializer — Kryo는 명시적으로 켜야 한다
- spark.kryoserializer.buffer(기본 64KB)/buffer.max(기본 64MB) — 객체가 buffer.max보다 크면 직렬화 실패, 대형 객체를 다루면 미리 늘려야 한다
- spark.kryo.registrationRequired(기본 false)를 켜면 미등록 클래스 직렬화 시 예외로 즉시 알려준다
압축 코덱 — lz4가 기본인 이유
spark.io.compression.codec(기본 lz4)은 RDD 파티션, 이벤트 로그, 브로드캐스트 변수, 셔플 출력 등 Spark 내부 데이터를 압축할 때 공통으로 쓰는 코덱을 지정한다. Spark는 lz4/lzf/snappy/zstd 네 가지를 제공하는데, lz4가 기본인 이유는 압축률과 압축/해제 속도 사이의 균형이 실무 워크로드 대부분에 잘 맞기 때문이다. zstd는 압축률이 더 높지만 CPU를 더 쓰므로 네트워크·디스크가 병목이고 CPU 여유가 있는 환경에서 유리하고, snappy는 CPU 오버헤드를 더 줄이고 싶을 때 고려된다 — '이게 항상 정답'인 코덱은 없고 병목이 CPU인지 I/O인지에 따라 갈린다.
핵심 포인트
- spark.io.compression.codec 기본값은 lz4 — 속도/압축률 균형
- zstd는 압축률 우선(CPU 여유 있고 I/O가 병목일 때), snappy는 CPU 오버헤드 최소화 우선
파일을 읽을 때 초기 파티션 개수는 어떻게 정해지는가
spark-learn-34에서 AQE on/off에 따른 small files problem을 실측으로 확인했는데, 애초에 파일을 읽을 때 초기 파티션(태스크) 개수를 정하는 두 설정이 있다. spark.sql.files.maxPartitionBytes(기본 128MB)는 파티션 하나에 담을 최대 바이트 수이고, spark.sql.files.openCostInBytes(기본 4MB)는 '파일 하나를 여는 데 드는 비용'을 가상의 바이트 크기로 환산한 값이다. 여러 개의 작은 파일을 하나의 Spark 파티션으로 묶을 때, 각 파일의 실제 크기에 이 openCostInBytes를 더해서 계산하기 때문에, 파일이 아주 작고 개수가 많으면(작은 파일 문제) 실제 데이터量보다 훨씬 많은 '가상 크기'가 잡혀 예상보다 파티션이 더 잘게 나뉘는 결과가 나온다 — 이게 바로 '작은 파일이 많으면 왜 태스크가 쓸데없이 많아지는가'에 대한 설정 차원의 답이다.
핵심 포인트
- maxPartitionBytes(기본 128MB) — 입력 파티션 하나의 최대 크기
- openCostInBytes(기본 4MB) — 파일 하나를 여는 비용을 가상 크기로 환산해 파티션 크기 계산에 더한다
- 작은 파일이 많으면 openCostInBytes 누적으로 실제 데이터量보다 파티션이 훨씬 잘게 나뉜다 — small files problem의 설정 차원 원인
투기적 실행(Speculative Execution) — 스트래글러 대응의 다른 축
spark-learn-24에서 다룬 데이터 스큐 외에도, 특정 태스크가 하드웨어 문제(느린 노드, 네트워크 지연)로 유독 오래 걸리는 '스트래글러' 상황이 있다. spark.speculation(기본 false)을 켜면, 한 스테이지의 태스크 중 spark.speculation.quantile(기본 0.9, 즉 90%)이 완료된 시점에 아직 안 끝난 태스크가 전체 중간값보다 spark.speculation.multiplier(기본 3배) 이상 느리면 같은 태스크를 다른 executor에서 중복 실행해, 둘 중 먼저 끝나는 결과를 쓴다.
주의할 점은 이 기능이 기본으로 꺼져 있다는 것과, 멱등적이지 않은 부수효과(외부 시스템에 쓰기 등)가 있는 작업에서는 같은 작업이 두 번 실행될 수 있어 위험할 수 있다는 점이다 — 순수 읽기·집계 위주의 배치 작업에는 비교적 안전하지만, 외부 API 호출이나 파일 append 같은 부수효과가 있는 UDF를 쓴다면 켜기 전에 멱등성을 반드시 확인해야 한다.
핵심 포인트
- spark.speculation(기본 false)을 켜면 90%(quantile) 완료 시점에 중간값의 3배(multiplier) 이상 느린 태스크를 다른 executor에서 중복 실행한다
- 스큐(데이터 쏠림)와 별개로 하드웨어성 스트래글러에 대응하는 기능
- 멱등적이지 않은 부수효과가 있는 작업에서는 중복 실행이 위험할 수 있어 켜기 전 확인 필요
실전 튜닝 체크리스트 — 이 모듈들을 하나로 묶으면
지금까지(spark-learn-24, 33, 34, 39~43) 다룬 개별 옵션들을 실제 튜닝 순서로 엮으면 대략 이렇다: (1) Spark UI의 Stages 탭에서 셔플 read/write가 큰 스테이지, Max/Median task time 격차, GC 시간, Spill to Disk를 먼저 확인해 병목 종류를 파악한다(spark-learn-24). (2) 스큐가 의심되면 AQE skewJoin 설정(모듈 41)이 켜져 있는지, 임계값이 워크로드에 맞는지 확인한다. (3) 조인이 느리면 .explain()으로 실제 조인 전략을 확인하고(spark-learn-32), 필요하면 힌트로 오버라이드한다(모듈 42). (4) 파티션 수/크기 문제라면 shuffle.partitions 고정값보다 AQE의 advisoryPartitionSizeInBytes 목표 기반 접근으로 전환한다(모듈 41). (5) 입력 단계의 작은 파일 문제라면 maxPartitionBytes/openCostInBytes를 점검한다(이 모듈). (6) executor 자체의 자원 배분(코어 수, memoryOverhead, Dynamic Allocation)은 모듈 39, 메모리 내부 동작(페이지/스필)은 모듈 40을 참고한다. (7) 그래도 특정 노드만 느리다면 speculation을 검토한다(이 모듈). (8) 직렬화 오버헤드가 크다면 Kryo를 켜고 buffer.max/registrationRequired를 점검한다(이 모듈).
이 순서의 핵심은 '옵션을 다 켜보고 뭐가 좋아지나 보는' 방식이 아니라, Spark UI가 보여주는 증상(셔플이 큰가/스큐가 있는가/GC가 긴가/스필이 있는가)에서 역으로 원인이 될 만한 설정으로 좁혀가는 진단 순서라는 점이다.
핵심 포인트
- 튜닝은 옵션을 무작위로 켜보는 게 아니라 Spark UI 증상(셔플·스큐·GC·스필)에서 역으로 원인 설정을 좁혀가는 진단 과정이다
- 이 모듈 카탈로그(24/32/33/34/39/40/41/42/43)를 참고 지도 삼아, 증상별로 어느 모듈의 어떤 설정을 봐야 하는지 빠르게 찾을 수 있다