ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spark] 파티션 전략과 성능 최적화
    공부 2026. 4. 27. 01:29

    파티션 전략 개요

    Apache Spark에서 파티션(Partition)은 데이터를 분산 처리하는 기본 단위입니다. 파티션의 수와 크기, 분배 방식에 따라 성능이 크게 달라지므로 파티션 전략을 이해하고 올바르게 적용하는 것이 매우 중요합니다.


    1. 파티션이란?

    Spark는 데이터를 여러 파티션으로 나누어 각 Executor의 Task가 하나의 파티션을 담당하는 방식으로 동작합니다.

    • 파티션 수가 너무 적으면 병렬성이 낮아져 일부 Executor가 유휴 상태가 됩니다.
    • 파티션 수가 너무 많으면 Task 스케줄링 오버헤드가 증가하여 오히려 성능이 저하됩니다.
    • 따라서 데이터 크기와 클러스터 자원에 맞는 적정 파티션 수를 유지하는 것이 핵심입니다.

    2. repartition(100)은 어떻게 데이터를 나누는가?

    repartition(100)과 같이 숫자를 지정하면 데이터를 100개 파티션으로 나눈다는 뜻입니다. 그런데 실제로 어떤 기준으로 각 Row를 파티션에 배정하는지가 핵심입니다. Spark는 크게 두 가지 방식을 사용합니다.

    2-1. Round-Robin 방식 — repartition(n) 컬럼 미지정

    컬럼을 지정하지 않으면 RoundRobinPartitioning이 적용됩니다. Row를 순서대로 돌아가며 각 파티션에 하나씩 배분하는 방식입니다.

    Row1 → P0,  Row2 → P1,  Row3 → P2
    Row4 → P0,  Row5 → P1,  Row6 → P2  ...
    • 행 수(Row Count) 기준으로 균등 분배됩니다.
    • 바이트 크기는 보장하지 않습니다. Row 하나가 1KB인 파티션과 1MB인 파티션이 생길 수 있습니다.
    • 전체 셔플(Full Shuffle)이 발생합니다.
    df.repartition(100)
    # 100개 파티션, 각 파티션에 (전체 행 수 / 100)개의 Row가 배분됨

    2-2. Hash 방식 — repartition(n, col) 컬럼 지정

    컬럼을 지정하면 HashPartitioning이 적용됩니다. 해당 컬럼 값의 해시를 파티션 수로 나눈 나머지(modulo)가 파티션 번호가 됩니다.

    파티션 번호 = MurMur3_hash(컬럼 값) % 파티션 수
    • 같은 키 값은 반드시 같은 파티션에 들어갑니다. 조인·집계 시 셔플 없이 처리하기 위해 사용합니다.
    • 특정 키에 데이터가 몰리면 파티션 크기가 불균형해집니다(→ 스큐 문제 발생).
    df.repartition(100, "user_id")
    # user_id의 hash % 100 → 파티션 번호 결정
    # 같은 user_id를 가진 Row는 항상 같은 파티션에 모임

    2-3. 방식 비교 요약

    구분 repartition(n) repartition(n, col) coalesce(n)
    내부 방식 Round-Robin Hash (MurMur3) 파티션 병합
    분배 기준 행 수 균등 키 해시값 인접 파티션 병합
    셔플 여부 전체 셔플 전체 셔플 셔플 없음
    키 동일성 보장 X O (같은 키 → 같은 파티션) X
    스큐 가능성 낮음 높음 (키 쏠림 시) 낮음

    3. repartition vs coalesce

    repartition

    • 지정한 수만큼 파티션을 새롭게 재분배합니다.
    • 전체 셔플(Full Shuffle)이 발생하므로 비용이 높습니다.
    • 파티션 수를 늘리거나 데이터를 균등하게 재분배해야 할 때 사용합니다.
    df.repartition(200)
    df.repartition(200, "join_key")  # 특정 컬럼 기준 해시 분배

    coalesce

    • 파티션 수를 줄이는 데 사용합니다.
    • 셔플 없이 인접 파티션을 병합하므로 비용이 낮습니다.
    • 단, 데이터 편향(Skew)이 발생할 수 있으므로 주의가 필요합니다.
    df.coalesce(50)  # 파티션 수 축소, 셔플 없음
    상황 권장 방법
    파티션 수를 늘려야 할 때 repartition(n)
    필터링 이후 파티션을 줄일 때 coalesce(n)
    특정 키 기준으로 균등 분배해야 할 때 repartition(n, "key_column")

    4. 파티셔닝(Partitioning) vs 버케팅(Bucketing)

    파티셔닝 (partitionBy)

    • 컬럼 값 기준으로 디렉토리를 나눠 데이터를 저장합니다.
    • 카디널리티가 낮은 컬럼(예: 날짜, 국가, 상태값)에 적합합니다.
    • 파티션 프루닝(Partition Pruning)이 가능하여 불필요한 파일 읽기를 방지합니다.
    df.write.partitionBy("year", "month").parquet("path/to/output")

    버케팅 (bucketBy)

    • 해시 함수로 데이터를 고정 크기 버킷으로 분산하여 저장합니다.
    • 카디널리티가 높은 컬럼(예: user_id, order_id)에 적합합니다.
    • 같은 컬럼과 버킷 수로 버케팅된 테이블 간 조인 시 셔플이 제거됩니다.
    df.write.bucketBy(64, "user_id").sortBy("user_id").saveAsTable("bucketed_table")

    5. 적정 파티션 수와 크기

    권장 파티션 크기: 파티션 하나의 크기는 128MB ~ 256MB가 최적입니다.

    # 파티션 수 산정 공식
    # 방법 1: 데이터 크기 기준
    파티션 수 = 총 데이터 크기 / 128MB ~ 256MB
    
    # 방법 2: 클러스터 자원 기준
    파티션 수 = Executor 수 × Executor당 코어 수 × 2~4
    
    # 셔플 파티션 수 설정
    spark.conf.set("spark.sql.shuffle.partitions", 200)

    6. 데이터 스큐(Skew) 문제와 해결 방법

    데이터 스큐는 특정 파티션에 데이터가 집중되어 해당 Task만 오래 걸리는 현상입니다.

    스큐 진단: Spark UI > Stages 탭에서 Task 실행 시간 분포를 확인합니다. 최장 Task 시간이 중앙값 대비 5배 이상이면 스큐를 의심합니다.

    6-1. 솔팅(Salting) 기법

    스큐된 키에 임의의 숫자(Salt)를 붙여 여러 파티션에 분산시키는 방법입니다.

    from pyspark.sql.functions import rand, col, lit, concat, explode, array
    
    salt_factor = 10
    
    # 큰 테이블에 salt 추가
    df_large = df_large.withColumn("salt", (rand() * salt_factor).cast("int"))
    df_large = df_large.withColumn("salted_key", concat(col("join_key"), lit("_"), col("salt")))
    
    # 작은 테이블을 salt_factor배로 복제
    salts = array([lit(i) for i in range(salt_factor)])
    df_small = df_small.withColumn("salt", explode(salts))
    df_small = df_small.withColumn("salted_key", concat(col("join_key"), lit("_"), col("salt")))
    
    result = df_large.join(df_small, "salted_key")

    6-2. AQE Skew Join 자동 처리 (권장)

    spark.conf.set("spark.sql.adaptive.enabled", "true")
    spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
    spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5")
    spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "268435456")

    7. 동적 파티션 프루닝(Dynamic Partition Pruning, DPP)

    DPP란?

    동적 파티션 프루닝은 조인 전략이 아니라, 조인 성능을 높이기 위한 최적화 기법입니다. 작은 차원 테이블의 필터링 결과를 활용하여 큰 팩트 테이블에서 불필요한 파티션을 아예 읽지 않도록 합니다.

    DPP 적용 조건

    • 팩트 테이블이 조인 키 컬럼으로 파티셔닝되어 있어야 합니다.
    • 차원 테이블 쪽에 선택적인 WHERE 조건이 있어야 합니다.
    • Equi-Join (=) 조건에서만 동작합니다.
    • Star Schema 구조에서 가장 효과적입니다.
    spark.conf.set("spark.sql.optimizer.dynamicPartitionPruning.enabled", "true")  # 기본 활성화
    구분 Predicate Pushdown Dynamic Partition Pruning
    적용 시점 컴파일 타임 런타임
    조건 출처 고정 필터 (WHERE) 조인 상대 테이블의 필터 결과
    테이블 조건 없음 조인 키로 파티셔닝 필요
    적합 패턴 단순 필터 쿼리 Star Schema 조인

    8. AQE와 파티션 자동 최적화

    Spark 3.2부터 기본 활성화된 AQE는 런타임 통계를 기반으로 파티션을 자동으로 최적화합니다.

    spark.conf.set("spark.sql.adaptive.enabled", "true")
    spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
    spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "134217728")  # 128MB
    spark.conf.set("spark.sql.adaptive.coalescePartitions.minPartitionSize", "1048576")  # 1MB
    설정 키 기본값 설명
    spark.sql.adaptive.enabled true (3.2+) AQE 마스터 스위치
    spark.sql.adaptive.coalescePartitions.enabled true 셔플 후 파티션 자동 병합
    spark.sql.adaptive.advisoryPartitionSizeInBytes 64MB 목표 파티션 크기
    spark.sql.adaptive.skewJoin.enabled true 스큐 조인 자동 처리
    spark.sql.adaptive.skewJoin.skewedPartitionFactor 5 스큐 판단 배수
    spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes 256MB 스큐 판단 최소 크기

    9. 파티션 최적화 운영 체크리스트

    설계 단계

    • 파티셔닝 컬럼은 카디널리티가 낮은 컬럼(날짜, 상태값 등)으로 선택하였습니까?
    • 조인/집계가 잦은 고카디널리티 컬럼은 버케팅을 고려하였습니까?
    • Star Schema 구조에서 팩트 테이블을 조인 키로 파티셔닝하여 DPP를 활성화하였습니까?

    개발 단계

    • AQE가 활성화되어 있습니까? (spark.sql.adaptive.enabled = true)
    • 셔플 파티션 초기값을 충분히 크게 설정하였습니까? (AQE가 런타임에 자동 축소)
    • 대용량 필터링 이후 coalesce()로 파티션 수를 정리하였습니까?
    • 스큐가 예상되는 조인에서 AQE skewJoin 설정을 확인하였습니까?

    모니터링

    • Spark UI > Stages 탭에서 Task 실행 시간 분포를 확인하고 있습니까?
    • 최장 Task 시간이 중앙값 대비 5배 이상이라면 스큐를 의심합니다.
    • 파티션 크기가 128MB ~ 256MB 범위에 있는지 확인합니다.
    • 실행 계획에서 dynamicpruningexpression으로 DPP 적용 여부를 확인합니다.

    성능 튜닝 기준

    • 파티션 크기를 128MB ~ 256MB로 유지하면 처리 시간이 20~50% 개선될 수 있습니다.
    • 솔팅 기법 사용 시 salt_factor는 스큐 비율에 맞게 조정합니다 (10:1 스큐 → salt_factor=10).
    • 동일 컬럼·버킷 수로 버케팅된 테이블 간 조인은 셔플을 제거하여 성능을 크게 향상시킵니다.

    댓글