Spark SQL 성능 튜닝

Spark SQL 성능 튜닝 (Performance Tuning)

DataFrame이나 SQL 워크로드가 느릴 때 먼저 점검할 수 있는 기법을 한자리에 모은 페이지예요. 데이터 캐싱, 파티션 조정, 조인 전략 선택, 그리고 옵티마이저에 실행 계획을 더 잘 짤 수 있는 추가 정보를 주는 순서로 살펴볼게요. 실제 설정값과 기본값은 원문 그대로 보존했으니 그대로 따라가면 돼요.

출처: Apache Spark 공식 문서 – Performance Tuning

데이터 캐싱 (Caching Data)

Spark SQL은 인메모리 칼럼 형식으로 테이블을 캐시할 수 있어요. spark.catalog.cacheTable("tableName") 또는 dataFrame.cache()를 호출하면 됩니다. 그러면 Spark SQL은 필요한 컬럼만 스캔하고, 메모리 사용과 GC 부담을 줄이도록 압축을 자동 조정해요.

  • 캐시 해제: spark.catalog.uncacheTable("tableName") 또는 dataFrame.unpersist()
  • 캐시 여부 확인: spark.catalog.isCached("tableName")
  • 특정 Dataset의 저장 수준 확인: storageLevel 속성 (캐시되지 않으면 StorageLevel.NONE 반환)
  • 실행 중인 애플리케이션 전체의 캐시 개요: Web UI의 Storage 탭 (액션이 실행돼 구체화된 뒤 저장 수준·크기·파티션 표시)

Spark는 두 가지 캐시 포맷을 지원해요.

  • 기본 캐시 포맷: 표준 인메모리 칼럼 캐시 (기본 사용)
  • Arrow 캐시 포맷: Apache Arrow 기반 캐시로 칼럼 워크로드의 읽기 성능을 높이고 Arrow 생태계 상호 운용을 지원

캐시 관련 설정은 spark.conf.set 또는 SQL의 SET key=value로 조정합니다.

설정 기본값 의미
spark.sql.inMemoryColumnarStorage.compressed true true면 각 컬럼의 데이터 통계에 따라 압축 코덱을 자동 선택
spark.sql.inMemoryColumnarStorage.batchSize 10000 칼럼 캐싱 배치 크기 제어. 배치가 클수록 메모리 활용·압축이 좋아지지만 OOM 위험이 커짐

파티션 튜닝 (Tuning Partitions)

파일 기반 소스(Parquet, JSON, ORC)와 셔플 파티션을 조정하면 성능 차이가 큽니다. 주요 설정은 다음과 같아요.

설정 기본값 의미
spark.sql.files.maxPartitionBytes 134217728 (128 MB) 파일을 읽을 때 단일 파티션에 담을 최대 바이트 수 (파일 기반 소스에서만 유효)
spark.sql.files.openCostInBytes 4194304 (4 MB) 파일을 여는 데 드는 추정 비용. 여러 파일을 한 파티션에 넣을 때 사용하며 과대 추정이 나음
spark.sql.files.minPartitionNum Default Parallelism 분할 파일 파티션의 권장(보장 아님) 최소 수. 미설정 시 spark.sql.leafNodeDefaultParallelism
spark.sql.files.maxPartitionNum None 분할 파일 파티션의 권장 최대 수 (3.5.0+). 초기 파티션이 이 값을 넘으면 이에 가깝게 재조정
spark.sql.shuffle.partitions 200 조인·집계 시 데이터를 셔플할 때 사용할 파티션 수
spark.sql.sources.parallelPartitionDiscovery.threshold 32 입력 경로가 이 임계값보다 많으면 Spark 분산 잡으로 병렬 목록화
spark.sql.sources.parallelPartitionDiscovery.parallelism 10000 입력 경로 목록화의 최대 병렬도

Coalesce 힌트

Coalesce 힌트는 Dataset API의 coalesce, repartition, repartitionByRange와 비슷하게 출력 파일 수를 제어하고 파티션을 조정하는 데 쓰여요.

  • COALESCE: 파티션 수만 인자로 받음
  • REPARTITION: 파티션 수, 컬럼, 또는 둘 다/둘 다 없이 인자로 받음
  • REPARTITION_BY_RANGE: 컬럼 필수, 파티션 수는 선택
  • REBALANCE: 초기 파티션 수, 컬럼, 또는 둘 다/둘 다 없이 인자로 받음
  • REBALANCE_BY_SIZE: 권장 파티션 크기 필요, 선택적으로 컬럼 뒤에 붙임
SELECT /*+ COALESCE(3) */ * FROM t;
SELECT /*+ REPARTITION(3) */ * FROM t;
SELECT /*+ REPARTITION(c) */ * FROM t;
SELECT /*+ REPARTITION(3, c) */ * FROM t;
SELECT /*+ REPARTITION_BY_RANGE(c) */ * FROM t;
SELECT /*+ REPARTITION_BY_RANGE(3, c) */ * FROM t;
SELECT /*+ REBALANCE */ * FROM t;
SELECT /*+ REBALANCE(3, c) */ * FROM t;
SELECT /*+ REBALANCE_BY_SIZE(134217728) */ * FROM t;

통계 활용 (Leveraging Statistics)

Spark는 실행 계획의 각 노드(읽기, 필터, 조인 등)가 내보낼 행 수를 추정해 최선의 실행 계획을 고르는데, 그 추정은 여러 경로로 얻어지는 통계에 기반합니다.

  • 데이터 소스: Parquet 메타데이터의 개수·min/max 값처럼 Spark가 데이터 소스에서 직접 읽는 통계
  • 카탈로그: Hive Metastore 같은 카탈로그에서 읽는 통계. ANALYZE TABLE을 실행할 때마다 수집/갱신
  • 런타임: 쿼리 실행 중 Spark가 직접 계산하는 통계 (적응형 쿼리 실행 AQE의 일부)

통계가 없거나 부정확하면 최적 계획 선택이 어려워져 쿼리 성능이 나빠질 수 있어요. 다음과 같이 확보된 통계를 점검할 수 있습니다.

  • 데이터 객체 통계: DESCRIBE EXTENDED로 테이블/컬럼 통계 확인
  • 쿼리 계획 추정: EXPLAIN COST 또는 DataFrame.explain(mode="cost")로 최적화된 계획의 비용 추정 확인
  • 런타임 통계: 쿼리 실행 중 SQL UI의 Details 섹션에서 Statistics(..., isRuntime=true) 확인

조인 튜닝

작은 테이블을 브로드캐스트 조인하면 셔플을 피하고 성능이 크게 좋아져요. hint("broadcast") 또는 SQL 힌트로 지정할 수 있습니다.

-- BROADCAST, BROADCASTJOIN, MAPJOIN 모두 허용
SELECT /*+ BROADCAST(r) */ * FROM src s JOIN records r ON s.key = r.key
spark.table("src").join(spark.table("records").hint("broadcast"), "key").show()

적응형 쿼리 실행 (Adaptive Query Execution, AQE)

AQE는 Spark SQL의 최적화 기법으로, 런타임 통계를 활용해 가장 효율적인 실행 계획을 고르는 방식이에요. Apache Spark 3.2.0부터 기본 활성화되어 있으며, spark.sql.adaptive.enabled가 총괄 설정입니다.

설정 기본값 의미
spark.sql.adaptive.enabled true true면 쿼리 실행 중간에 정확한 런타임 통계로 쿼리 계획을 재최적화

AQE가 하는 주요 최적화는 다음과 같습니다.

  • 포스트 셔플 파티션 병합 (Coalescing Post Shuffle Partitions): spark.sql.adaptive.coalescePartitions.enabled(기본 true)와 함께 작동해 너무 많은 작은 task를 피하도록 파티션을 병합. 초기 파티션 수는 spark.sql.adaptive.coalescePartitions.initialPartitionNum(미설정 시 spark.sql.shuffle.partitions)으로 크게 잡아두고, Spark가 spark.sql.adaptive.advisoryPartitionSizeInBytes(기본 64 MB)에 맞춰 런타임에 조정합니다. parallelismFirst(기본 true)는 목표 크기를 무시하고 minPartitionSize(기본 1 MB)만 지켜 병렬도를 최대화.
  • 스큐 셔플 파티션 분할 (Splitting Skewed Shuffle Partitions): spark.sql.adaptive.optimizeSkewsInRebalancePartitions.enabled(기본 true)로 데이터 스큐를 피하려고 커진 파티션을 목표 크기(advisoryPartitionSizeInBytes)에 따라 분할.
  • 소트-머지 조인 → 브로드캐스트 조인 변환: 런타임 통계에서 어느 한 조인 측이 spark.sql.adaptive.autoBroadcastJoinThreshold보다 작으면 소트-머지 조인을 브로드캐스트 해시 조인으로 전환. 처음부터 브로드캐스트를 계획하는 것만큼 효율적이진 않지만, 소트를 피하고 셔플 파일을 로컬에서 읽는 등 네트워크 트래픽을 줄입니다.

이 외에도 원문에는 부분 집계 최적화(Adaptive Partial Aggregation), 서브플랜 병합(Merging Subplans) 등 추가 기법을 다루니, 운영에서 미세 조정이 필요하면 원문 페이지를 함께 참고하세요.

더 알아보기