← 학습 카테고리

Learn

Spark

38개 모듈 · 현재 24번째

Spark 모듈 24/38 spark-learn-24

성능 튜닝과 최적화 (스큐·셔플·Catalyst·Tungsten)

Apache Spark Data Engineering Interview Questions (blog, 50 Q&A) — (블로그, 발행처 미상) spark_ref_01.md Q21-30 (Performance Tuning & Optimization) + FAQ(파티셔닝/브로드캐스트 조인/성능 최적화/cache·persist/데이터 스큐) 종합, 수치 오류 정정 포함

이 모듈을 다 읽으면

  • 데이터 스큐가 발생하는 이유와 salting/브로드캐스트 조인을 통한 대응 전략을 설명할 수 있다
  • Catalyst 옵티마이저의 4단계와 Project Tungsten의 역할을 구분해 설명할 수 있다
  • cache()의 기본 저장 레벨이 RDD와 DataFrame/Dataset에서 다르다는 점과, autoBroadcastJoinThreshold의 정확한 기본값을 판단할 수 있다
  • Spark UI에서 셔플/스큐/GC/spill 징후를 읽고 병목을 진단할 수 있다

데이터 스큐 대응, Catalyst/Tungsten 최적화 원리, 병목 진단 방법을 다룬다. 원문 자료 내에서 서로 모순되던 cache() 기본 저장 레벨과 브로드캐스트 조인 임계값(10MB vs 100MB) 서술을 실제 Spark 기본값 기준으로 정정했다.

데이터 스큐와 salting

데이터 스큐는 일부 파티션에 다른 파티션보다 압도적으로 많은 데이터가 몰릴 때 발생한다(예: 조인 키 하나에 값이 쏠린 hot key). 대부분의 Executor는 빨리 끝나는데 스큐가 몰린 1~2개 태스크만 오래 걸리는 'straggler' 현상이 전체 잡의 완료 시간을 좌우하게 된다.

대응책으로는 조인 키에 무작위 접두사를 붙여 데이터를 여러 파티션에 분산시키는 'salting', 그리고 한쪽 테이블이 충분히 작으면 브로드캐스트 조인으로 셔플 자체를 없애는 방법이 있다. Spark 3.0+에서 AQE(Adaptive Query Execution)의 skew join 최적화가 켜져 있으면 이런 스큐를 상당 부분 자동으로 완화해주므로, 실무에서는 수동 salting보다 먼저 고려할 요소다.

핵심 포인트

  • 스큐는 특정 파티션에 데이터가 쏠려 straggler 태스크를 만든다.
  • salting(키에 랜덤 접두사 부여)과 브로드캐스트 조인이 대표적 수동 대응책이다.
  • Spark 3.0+ AQE의 skew join 최적화가 자동 완화를 제공하므로 우선 고려 대상이다.

Catalyst 옵티마이저의 4단계

Catalyst는 Spark SQL의 쿼리 최적화 엔진이다. 쿼리가 제출되면 (1) Analysis: 컬럼/테이블 참조를 카탈로그와 대조해 해석하고, (2) Logical Optimization: 상수 폴딩, 프레디케이트 푸시다운 같은 규칙 기반 최적화를 적용하고, (3) Physical Planning: 여러 물리적 실행 계획 후보를 만들어 비용 모델로 최적안을 고르고, (4) Code Generation: 선택된 계획을 JVM에서 실행할 바이트코드로 생성하는 순서를 거친다.

핵심 포인트

  • Analysis → Logical Optimization → Physical Planning → Code Generation 순서로 진행된다.
  • Physical Planning 단계에서는 비용 기반으로 여러 후보 중 하나를 선택한다.

Project Tungsten

Tungsten은 Spark의 메모리/CPU 효율을 높이기 위한 프로젝트로 두 가지 축으로 요약된다. 첫째, Off-Heap 메모리 관리로 JVM 가비지 컬렉션의 오버헤드를 우회한다. 둘째, Whole-Stage Code Generation으로 여러 연산을 하나의 함수로 합쳐 가상 함수 호출 횟수를 줄이고 CPU 캐시 지역성을 높인다.

핵심 포인트

  • Off-heap 메모리 관리로 GC 오버헤드를 줄인다.
  • Whole-Stage Code Generation으로 여러 연산을 한 함수로 합쳐 실행 효율을 높인다.

cache()/persist()의 기본 저장 레벨 — 정정

원문 자료에는 'df.cache()는 기본 저장 레벨(Memory only)을 사용한다'는 설명과, FAQ 섹션의 'cache()는 persist(MEMORY_AND_DISK)의 축약형'이라는 설명이 서로 모순되게 함께 실려 있다. 실제로는 대상 API에 따라 기본값이 다르다: RDD.cache()의 기본 저장 레벨은 MEMORY_ONLY이고, DataFrame/Dataset.cache()의 기본 저장 레벨은 MEMORY_AND_DISK(Scala Dataset과 동작을 맞추기 위해 이렇게 정해졌다)다. 같은 cache()라도 RDD API를 쓰느냐 DataFrame API를 쓰느냐에 따라 메모리가 부족할 때의 동작(디스크 스필 여부)이 달라진다는 뜻이다.

persist(storageLevel)은 MEMORY_ONLY(가장 빠르지만 메모리 부족 시 일부 파티션 유실·재계산), MEMORY_AND_DISK(메모리 부족분을 디스크로 스필), DISK_ONLY, 그리고 각각의 직렬화(_SER)·복제(_2) 변형까지 세밀하게 제어할 수 있다.

핵심 포인트

  • RDD.cache()의 기본값은 MEMORY_ONLY, DataFrame/Dataset.cache()의 기본값은 MEMORY_AND_DISK로 서로 다르다.
  • persist()는 MEMORY_ONLY/MEMORY_AND_DISK/DISK_ONLY 및 직렬화·복제 변형까지 세밀하게 지정할 수 있다.

브로드캐스트 조인과 autoBroadcastJoinThreshold — 정정

원문 본문(Q25)은 브로드캐스트 조인 기준을 '일반적으로 100MB 미만(설정 가능)'이라고 설명하지만, 같은 글의 FAQ 섹션은 'spark.sql.autoBroadcastJoinThreshold의 기본값은 10MB'라고 설명해 서로 다른 수치를 제시한다. 실제 Spark 기본값은 10MB(정확히는 10 * 1024 * 1024 바이트)가 맞다. '100MB'는 이 설정을 늘려서 쓰는 실무 관행이 반영된 수치로 보이지만 기본값 자체는 아니다.

작은 테이블 쪽이 이 임계값보다 작으면 Spark는 자동으로 해당 테이블을 각 Executor에 브로드캐스트해 셔플이 필요한 wide 조인을 narrow 연산으로 바꾼다. 자동 판단에 맡기지 않고 F.broadcast(small_df)로 명시적으로 힌트를 줄 수도 있다.

핵심 포인트

  • spark.sql.autoBroadcastJoinThreshold의 기본값은 10MB이며, 100MB는 이 값을 늘린 실무 설정값이지 기본값이 아니다.
  • 임계값 미만이면 자동으로, 또는 F.broadcast()로 명시적으로 브로드캐스트 조인을 유도할 수 있다.

Predicate Pushdown

프레디케이트 푸시다운은 WHERE 절의 필터링을 데이터 소스에 최대한 가깝게 내려보내는 최적화다. Parquet처럼 컬럼별 통계(min/max)를 담은 포맷을 읽을 때는, 전체 파일을 메모리로 올린 뒤 필터링하는 대신 스캔 단계에서부터 조건에 맞는 행만 읽어 I/O와 메모리 사용량을 크게 줄인다.

핵심 포인트

  • 필터를 데이터 소스 스캔 단계로 내려보내 불필요한 I/O를 없앤다.
  • Parquet의 min/max 통계 메타데이터 덕분에 블록 단위 스킵도 가능하다.

OOM 대응

Driver OOM은 대개 collect()로 너무 많은 데이터를 Driver 메모리로 끌어올 때 발생하며, 파일로 직접 쓰는 방식으로 피할 수 있다. Executor OOM은 spark.executor.memory를 늘리거나, 파티션 수를 늘려 태스크당 처리량을 줄이거나, spark.executor.cores를 줄여 태스크당 가용 메모리를 늘리는 방식으로 대응한다.

핵심 포인트

  • Driver OOM은 collect() 남용이 주 원인이며 파일 저장으로 회피한다.
  • Executor OOM은 executor.memory 증가, 파티션 수 증가, cores 감소 중 조합으로 대응한다.

Spark UI로 병목 식별하기

Spark UI의 Stages 탭에서 셔플 read/write가 큰 스테이지는 데이터 이동이 심하다는 신호다. Max task time과 Median task time의 격차가 크면 데이터 스큐를, GC 시간이 길면 메모리 관리에 Executor가 시달리고 있음을, Spill to Disk는 Executor 메모리가 처리 중인 데이터에 비해 부족함을 나타낸다. 튜닝을 시작하기 전에 Spark UI에서 가장 느린 스테이지부터 찾는 것이 순서다.

핵심 포인트

  • 셔플 read/write가 큰 스테이지, Max/Median task time 격차, GC 시간, Spill to Disk가 대표적 병목 신호다.
  • 튜닝은 Spark UI에서 가장 느린 스테이지를 먼저 찾는 데서 시작한다.

Parquet 포맷의 이점

Parquet은 컬럼 지향 저장 포맷으로 컬럼 프루닝(쿼리에 필요한 컬럼만 읽음), 압축 효율(같은 타입 데이터가 연속되어 압축률이 높음), min/max 통계 기반 블록 스킵을 제공한다. 스키마 진화(새 컬럼 추가)도 지원하지만, 자동으로 이루어지는 것이 아니라 읽을 때 mergeSchema 옵션을 켜야 여러 버전의 스키마가 하나로 병합된다는 점은 원문에 없는 실무상 유의점이다.

핵심 포인트

  • 컬럼 프루닝, 높은 압축률, 통계 기반 블록 스킵이 Parquet의 핵심 이점이다.
  • 스키마 진화는 자동이 아니라 mergeSchema 옵션을 켜야 적용된다.

파티션 수 튜닝과 AQE

일반적인 목표는 사용 가능한 모든 코어를 활용하되 태스크 관리 오버헤드가 병렬성 이득을 넘어서지 않는 수준이며, 코어당 2~4개 파티션이 흔한 경험칙이다. 셔플 연산의 파티션 수는 spark.sql.shuffle.partitions로 조정하며 기본값은 200이다 — 데이터가 작으면 200개가 과도한 오버헤드를 만들고, 테라바이트급이면 파티션당 데이터가 너무 커져 부족하다.

Spark 3.0부터 도입된 AQE(Adaptive Query Execution)는 실행 중 통계를 보고 셔플 파티션 수를 자동으로 조정(작은 파티션 병합)하며, Spark 3.2부터는 spark.sql.adaptive.enabled 기본값이 true로 바뀌어 별도 설정 없이도 켜져 있다는 점이 실무적으로 중요하다.

핵심 포인트

  • spark.sql.shuffle.partitions 기본값은 200이며 데이터 규모에 맞게 조정이 필요하다.
  • AQE는 Spark 3.2부터 기본적으로 활성화되어 있어 별도 설정이 없어도 동작한다.