성능 튜닝 (Tuning)

성능 튜닝 (Tuning)

  • 데이터 직렬화 (Data Serialization)
  • 메모리 튜닝 (Memory Tuning)
    • 메모리 관리 개요
    • 메모리 소비량 판단하기
    • 데이터 구조 튜닝
    • 직렬화된 RDD 저장
    • 가비지 컬렉션 튜닝
  • 그 밖의 고려 사항
    • 병렬성 수준 (Level of Parallelism)
    • 입력 경로의 병렬 나열
    • Reduce 태스크의 메모리 사용량
    • 큰 변수 브로드캐스팅
    • 데이터 지역성 (Data Locality)
  • 요약

Spark 대부분의 연산은 인메모리로 이뤄지기 때문에, 프로그램이 병목에 걸리는 지점은 클러스터의 어떤 자원일 수 있어요 — CPU, 네트워크 대역폭, 메모리 모두 해당하죠. 데이터가 메모리에 들어갈 정도로 작다면 대개 병목은 네트워크 대역폭이 되는데요, 때로는 RDD를 직렬화된 형태로 저장해 메모리 사용을 줄이는 등 직접 튜닝이 필요한 경우도 있어요. 이 가이드에서는 두 가지 주제를 중심으로 다룹니다. 하나는 네트워크 성능에 결정적인 역할을 하고 메모리 사용까지 줄여주는 데이터 직렬화, 다른 하나는 메모리 튜닝이에요. 그다음엔 규모가 작은 주제 몇 가지를 더 살펴볼게요.

데이터 직렬화

직렬화는 어떤 분산 애플리케이션에서든 성능에 중요한 역할을 해요. 객체를 직렬화하는 속도가 느리거나 결과물이 많은 바이트를 차지하는 포맷은 계산 전체를 크게 느려지게 만들거든요. 그래서 Spark 애플리케이션을 최적화할 때 가장 먼저 손대야 할 부분인 경우가 많아요. Spark는 '연산에서 어떤 Java 타입이든 쓸 수 있게 해주는 편의성'과 '성능' 사이에서 균형을 잡도록 설계됐는데요, 두 가지 직렬화 라이브러리를 제공합니다.

  • Java 직렬화: 기본적으로 Spark는 Java의 ObjectOutputStream 프레임워크로 객체를 직렬화해요. java.io.Serializable을 구현한 어떤 클래스든 동작하고, java.io.Externalizable을 확장해 직렬화 성능을 더 세밀하게 제어할 수도 있어요. Java 직렬화는 유연하지만 꽤 느린 편이고, 많은 클래스에서 직렬화 결과물이 커지는 경향이 있어요.
  • Kryo 직렬화: Spark는 Kryo 라이브러리(버전 4)를 사용해 객체를 더 빠르게 직렬화할 수도 있어요. Kryo는 Java 직렬화보다 훨씬 빠르고 컴팩트하며(보통 10배까지 차이), 다만 모든 Serializable 타입을 지원하지는 않고, 최상의 성능을 위해 프로그램에서 사용할 클래스를 미리 등록해야 해요.

Kryo로 바꾸려면 SparkConf로 작업을 초기화할 때 conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")를 호출하면 돼요. 이 설정은 워커 노드 사이의 셔플 데이터뿐 아니라 RDD를 디스크에 직렬화할 때 쓰는 직렬화기도 함께 정해줍니다. Kryo가 기본값이 아닌 이유는 커스텀 등록 요구사항 때문뿐인데요, 네트워크를 많이 쓰는 애플리케이션이라면 꼭 시도해 보기를 권해요. 참고로 Spark 2.0.0부터는 단순 타입, 단순 타입의 배열, 또는 string 타입의 RDD를 셔플할 때 내부적으로 Kryo 직렬화기를 사용합니다.

Spark는 Twitter chill 라이브러리의 AllScalaRegistrar에 포함된, 흔히 쓰이는 코어 Scala 클래스들에 대한 Kryo 직렬화기를 자동으로 포함해요.

자체 커스텀 클래스를 Kryo에 등록하려면 registerKryoClasses 메서드를 사용하면 됩니다.

val conf = new SparkConf().setMaster(...).setAppName(...)
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))
val sc = new SparkContext(conf)

Kryo 문서에는 커스텀 직렬화 코드를 추가하는 것 같은 더 고급 등록 옵션도 설명돼 있어요.

객체가 크다면 spark.kryoserializer.buffer 설정을 늘려야 할 수도 있어요. 이 값은 직렬화할 가장 큰 객체를 담을 수 있을 만큼 커야 합니다.

마지막으로, 커스텀 클래스를 등록하지 않아도 Kryo는 동작하지만, 객체마다 클래스 이름 전체를 저장해야 해서 낭비가 생겨요.

메모리 튜닝

메모리 사용량을 튜닝할 때는 세 가지를 함께 봐야 해요. 객체가 쓰는 메모리 (데이터셋 전체를 메모리에 넣고 싶을 수도 있죠), 그 객체에 접근하는 비용, 그리고 객체 회전(turnover)이 많을 때의 가비지 컬렉션 오버헤드입니다.

기본적으로 Java 객체는 접근이 빠른 대신, 필드 안의 '원시(raw)' 데이터보다 2~5배는 더 공간을 쉽게 차지해요. 그 이유는 몇 가지가 있어요.

  • 개별 Java 객체마다 '객체 헤더'가 있는데, 약 16바이트이며 클래스에 대한 포인터 같은 정보를 담아요. 데이터가 아주 적은 객체(가령 Int 필드 하나짜리)라면 헤더가 데이터보다 클 수도 있어요.
  • Java String은 원시 문자열 데이터에 비해 약 40바이트의 오버헤드가 있어요(Char 배열로 저장하고 길이 같은 추가 데이터를 유지하기 때문), 그리고 내부적으로 UTF-16을 써서 각 문자를 2바이트로 저장하죠. 그래서 10문자짜리 문자열은 쉽게 60바이트를 차지할 수 있어요.
  • HashMap, LinkedList 같은 일반적인 컬렉션 클래스는 연결(linked) 데이터 구조를 쓰는데, 항목마다 '래퍼(wrapper)' 객체가 있어요(예: Map.Entry). 이 객체는 헤더뿐 아니라 리스트의 다음 객체를 가리키는 포인터(보통 각 8바이트)도 갖고 있죠.
  • 원시 타입의 컬렉션은 흔히 java.lang.Integer 같은 '박싱(boxed)' 객체로 저장됩니다.

이 절에서는 먼저 Spark의 메모리 관리 개요를 보고, 그 다음에 사용자가 애플리케이션에서 메모리를 더 효율적으로 쓰기 위한 구체적인 전략을 다룰게요. 특히 객체의 메모리 사용량을 판단하는 방법과 — 데이터 구조를 바꾸거나 데이터를 직렬화된 포맷으로 저장해서 — 이를 개선하는 방법을 설명할게요. 마지막으로 Spark의 캐시 크기와 Java 가비지 컬렉터를 튜닝하는 내용도 살펴보죠.

메모리 관리 개요

Spark의 메모리 사용량은 크게 실행(execution) 메모리와 저장(storage) 메모리, 두 범주로 나뉩니다. 실행 메모리는 셔플, 조인, 정렬, 집계에서 계산에 쓰이는 메모리이고, 저장 메모리는 캐싱과 클러스터 전반의 내부 데이터 전파에 쓰이는 메모리예요. Spark에서 실행과 저장은 하나의 통합된 영역(M)을 공유해요. 실행 메모리를 쓰지 않을 때는 저장이 사용 가능한 메모리를 전부 가져갈 수 있고, 반대의 경우도 마찬가지예요. 실행은 필요하면 저장을 축출(evict)할 수 있지만, 저장 메모리 사용이 특정 임계값(R) 아래로 내려갈 때까지만 가능하죠. 다시 말해 RM 안에서 캐시된 블록이 절대 축출되지 않는 하위 영역을 나타냅니다. 구현상의 복잡성 때문에 저장은 실행을 축출할 수 없어요.

이 설계는 몇 가지 바람직한 성질을 보장합니다. 첫째, 캐싱을 쓰지 않는 애플리케이션은 전체 공간을 실행 메모리로 쓸 수 있어서 불필요한 디스크 스필(spill)을 피해요. 둘째, 캐싱을 쓰는 애플리케이션은 최소 저장 공간(R)을 확보하므로 자신의 데이터 블록이 축출되지 않도록 보호받죠. 마지막으로 이 접근은 메모리를 내부적으로 어떻게 나누는지에 대한 사용자 지식 없이도 다양한 워크로드에 대해 합리적인 기본 성능을 제공합니다.

관련 설정 두 가지가 있지만, 기본값이 대부분의 워크로드에 적용되므로 일반 사용자는 건드릴 필요가 없어요.

  • spark.memory.fraction: M의 크기를 (JVM 힙 공간 - 300MiB)의 분수로 표현한 값 (기본 0.6). 나머지 공간(40%)은 사용자 데이터 구조, Spark의 내부 메타데이터, 그리고 드물고 비정상적으로 큰 레코드가 있을 때 OOM 오류를 방지하기 위한 안전 여유분으로 남겨둡니다.
  • spark.memory.storageFraction: R의 크기를 M의 분수로 표현한 값 (기본 0.5). RM 안에서 실행 메모리가 축출할 수 없는 캐시된 블록이 위치하는 저장 공간이에요.

spark.memory.fraction 값은 이 정도 힙 공간이 JVM의 old(또는 "tenured") 세대에 여유 있게 들어맞도록 정해야 합니다. 자세한 내용은 아래의 고급 GC 튜닝 논의를 참고하세요.

메모리 소비량 판단하기

데이터셋이 필요로 하는 메모리 소비량을 측정하는 가장 좋은 방법은 RDD를 만들고 캐시에 넣은 다음, 웹 UI의 "Storage" 페이지를 보는 거예요. 그 페이지에서 RDD가 차지하는 메모리 양을 알려주죠.

특정 객체의 메모리 소비량을 추정하려면 SizeEstimatorestimate 메서드를 사용하세요. 이 방법은 메모리 사용을 줄이기 위해 여러 데이터 레이아웃을 실험해 볼 때, 그리고 브로드캐스트 변수가 각 익스큐터 힙에서 차지할 공간을 판단할 때 유용합니다.

데이터 구조 튜닝

메모리 소비를 줄이는 첫 번째 방법은 오버헤드를 추가하는 Java 특성 — 포인터 기반 데이터 구조, 래퍼 객체 같은 것 — 을 피하는 거예요. 방법은 몇 가지가 있어요.

  1. 표준 Java나 Scala 컬렉션 클래스(예: HashMap) 대신 객체의 배열과 원시 타입을 선호하도록 데이터 구조를 설계하세요. fastutil 라이브러리는 Java 표준 라이브러리와 호환되는 원시 타입용 편리한 컬렉션 클래스를 제공합니다.
  2. 가능하면 작은 객체와 포인터가 많이 들어 있는 중첩 구조는 피하세요.
  3. 키에 문자열 대신 숫자형 ID나 열거형(enum) 객체를 쓰는 것을 고려해 보세요.
  4. RAM이 32GiB 미만이라면 JVM 플래그 -XX:+UseCompressedOops를 설정해 포인터를 8바이트가 아니라 4바이트로 만들 수 있어요. 이런 옵션은 spark-env.sh에 추가할 수 있습니다.

직렬화된 RDD 저장

이런 튜닝을 해도 객체가 여전히 너무 커서 효율적으로 저장하지 못한다면, 메모리 사용을 줄이는 훨씬 간단한 방법이 있어요. RDD 지속성 API의 직렬화된 StorageLevel — 예를 들어 MEMORY_ONLY_SER — 을 써서 데이터를 직렬화된 형태로 저장하는 거죠. 그러면 Spark는 각 RDD 파티션을 하나의 큰 바이트 배열로 저장합니다. 직렬화된 형태로 저장할 때의 유일한 단점은 객체를 그때그때 역직렬화해야 해서 접근 시간이 느려진다는 점이에요. 직렬화된 형태로 데이터를 캐시할 거라면 Kryo 사용을 적극 권장합니다 — Java 직렬화(그리고 당연히 원시 Java 객체)보다 크기가 훨씬 작아지거든요.

가비지 컬렉션 튜닝

프로그램이 저장하는 RDD에 큰 '회전(churn)'이 있을 때는 JVM 가비지 컬렉션이 문제가 될 수 있어요. (그냥 RDD를 한 번 읽고 그 위에서 여러 연산만 돌리는 프로그램이라면 보통 문제가 되지 않죠.) Java가 새 객체를 위한 공간을 마련하려고 옛 객체를 내보낼 때, 모든 Java 객체를 추적하며 쓰지 않는 객체를 찾아야 해요. 여기서 기억할 핵심은 가비지 컬렉션의 비용은 Java 객체의 수에 비례한다는 점입니다. 그래서 객체가 더 적은 데이터 구조(예: LinkedList 대신 Int 배열)를 쓰면 이 비용이 크게 줄어들어요. 더 나은 방법은 위에서 설명한 대로 객체를 직렬화된 형태로 지속하는 거예요 — 이러면 RDD 파티션마다 객체가 단 하나(바이트 배열)만 있게 되죠. 다른 기법을 시도하기 전에, GC가 문제라면 가장 먼저 해볼 일이 직렬화된 캐싱을 쓰는 겁니다.

GC는 태스크의 작업 메모리(태스크를 실행하는 데 필요한 공간)와 노드에 캐시된 RDD 사이의 간섭 때문에도 문제가 될 수 있어요. 여기서는 RDD 캐시에 할당되는 공간을 제어해서 이 간섭을 완화하는 방법을 다룰게요.

GC의 영향 측정하기

GC 튜닝의 첫 단계는 가비지 컬렉션이 얼마나 자주 발생하는지와 그에 드는 시간에 대한 통계를 수집하는 거예요. Java 옵션에 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps를 추가하면 됩니다. (설정 가이드에서 Spark 작업에 Java 옵션을 전달하는 방법을 참고하세요.) 다음에 Spark 작업을 실행하면 가비지 컬렉션이 발생할 때마다 워커 로그에 메시지가 출력되는 걸 볼 수 있어요. 이 로그는 드라이버 프로그램이 아니라 클러스터의 워커 노드(작업 디렉터리의 stdout 파일)에 있다는 점을 유의하세요.

고급 GC 튜닝

가비지 컬렉션을 더 튜닝하려면 먼저 JVM 메모리 관리에 대한 기본적인 내용을 알아야 해요.

  • Java 힙 공간은 Young과 Old 두 영역으로 나뉩니다. Young 세대는 수명이 짧은 객체를 담기 위한 곳이고, Old 세대는 수명이 긴 객체를 위한 곳이에요.
  • Young 세대는 다시 [Eden, Survivor1, Survivor2] 세 영역으로 나뉩니다.
  • 가비지 컬렉션 절차를 단순화해 설명하면 이렇습니다. Eden이 가득 차면 Eden에 대해 minor GC가 실행되고, Eden과 Survivor1에서 살아남은 객체는 Survivor2로 복사됩니다. Survivor 영역은 서로 바뀌죠. 객체가 충분히 오래됐거나 Survivor2가 가득 차면 Old로 이동합니다. 마지막으로 Old가 거의 가득 차면 full GC가 호출됩니다.

Spark에서 GC 튜닝의 목표는 수명이 긴 RDD만 Old 세대에 저장하고, Young 세대가 수명이 짧은 객체를 담기에 충분한 크기를 갖도록 하는 거예요. 이렇게 하면 태스크 실행 중 만들어지는 임시 객체를 모으기 위한 full GC를 피할 수 있어요. 유용할 수 있는 몇 가지 단계는 다음과 같습니다.

  • GC 통계를 수집해 가비지 컬렉션이 너무 잦은지 확인하세요. 태스크 하나가 끝나기 전에 full GC가 여러 번 호출된다면, 태스크 실행에 쓸 메모리가 충분하지 않다는 뜻이에요.
  • minor 컬렉션은 많은데 major GC는 별로 없다면 Eden에 더 많은 메모리를 할당하는 게 도움이 돼요. Eden 크기를 각 태스크가 필요로 할 메모리 양을 약간 넘치는 수준으로 잡으면 됩니다. Eden 크기가 E라면 옵션 -Xmn=4/3*E로 Young 세대 크기를 설정할 수 있어요. (4/3으로 키우는 것은 survivor 영역이 쓰는 공간을 감안하기 위함이에요.)
  • 출력되는 GC 통계에서 OldGen이 거의 가득 찼다면 spark.memory.fraction을 낮춰 캐싱에 쓰는 메모리 양을 줄이세요. 태스크 실행을 느리게 만드는 것보다 캐시할 객체를 줄이는 편이 나아요. 또는 Young 세대의 크기를 줄이는 것을 고려해 보세요. 위처럼 -Xmn을 설정했다면 그것을 낮추면 되고, 아니라면 JVM의 NewRatio 파라미터 값을 바꿔 보세요. 많은 JVM에서 기본값이 2인데, 이는 Old 세대가 힙의 2/3을 차지한다는 뜻이에요. 이 비율이 spark.memory.fraction을 초과할 수 있을 만큼 충분히 커야 합니다.
  • Spark 4.0.0부터는 기본적으로 JDK 17을 사용하며, 이에 따라 G1GC가 기본 가비지 컬렉터가 됩니다. 익스큐터 힙이 클 때는 -XX:G1HeapRegionSize로 G1 리전 크기를 늘리는 것이 중요할 수 있어요.
  • 예를 들어 태스크가 HDFS에서 데이터를 읽는다면, 태스크가 쓰는 메모리 양은 HDFS에서 읽은 데이터 블록의 크기로 추정할 수 있어요. 압축 해제된 블록의 크기는 보통 블록 크기의 23배라는 점을 유의하세요. 그래서 태스크 34개치 작업 공간을 원하고 HDFS 블록 크기가 128MiB라면, Eden 크기를 4*3*128MiB로 추정할 수 있어요.
  • 새 설정으로 가비지 컬렉션의 빈도와 소요 시간이 어떻게 변하는지 모니터링하세요.

경험상 GC 튜닝의 효과는 애플리케이션과 사용 가능한 메모리 양에 따라 달라집니다. 훨씬 더 많은 튜닝 옵션이 온라인에 설명돼 있지만, 높은 수준에서 보면 full GC가 얼마나 자주 발생하는지 관리하는 것이 오버헤드 줄이는 데 도움이 돼요.

익스큐터용 GC 튜닝 플래그는 작업 설정에서 spark.executor.defaultJavaOptionsspark.executor.extraJavaOptions를 설정해 지정할 수 있습니다.

그 밖의 고려 사항

병렬성 수준 (Level of Parallelism)

각 연산의 병렬성 수준을 충분히 높게 설정하지 않으면 클러스터가 완전히 활용되지 않아요. Spark는 파일마다 크기에 따라 실행할 'map' 태스크 수를 자동으로 정하고(SparkContext.textFile 등의 선택적 파라미터로 제어할 수도 있지만), groupByKey, reduceByKey 같은 분산 'reduce' 연산에서는 가장 큰 부모 RDD의 파티션 수를 사용하죠. 병렬성 수준을 두 번째 인자로 넘기거나(spark.PairRDDFunctions 문서 참고), 설정 속성 spark.default.parallelism을 설정해 기본값을 바꿀 수 있어요. 일반적으로 클러스터의 CPU 코어당 태스크 2~3개를 권장합니다.

입력 경로의 병렬 나열

작업 입력에 디렉터리가 아주 많을 때는 디렉터리 나열(listing)의 병렬성을 늘려야 할 수도 있어요. 그렇지 않으면 프로세스가 아주 오래 걸릴 수 있는데, 특히 S3 같은 객체 스토리지를 상대할 때 그래요. 작업이 Hadoop 입력 포맷의 RDD(e.g., SparkContext.sequenceFile을 통한)를 다룬다면 병렬성은 spark.hadoop.mapreduce.input.fileinputformat.list-status.num-threads로 제어됩니다 (현재 기본값은 1).

Spark SQL에서 파일 기반 데이터 소스를 쓴다면 spark.sql.sources.parallelPartitionDiscovery.thresholdspark.sql.sources.parallelPartitionDiscovery.parallelism을 조정해 나열 병렬성을 개선할 수 있어요. 자세한 내용은 Spark SQL 성능 튜닝 가이드를 참고하세요.

Reduce 태스크의 메모리 사용량

때로는 RDD가 메모리에 안 들어가서가 아니라, groupByKey 안의 reduce 태스크 같은 하나의 태스크 작업 집합(working set)이 너무 커서 OutOfMemoryError가 나는 경우가 있어요. Spark의 셔플 연산(sortByKey, groupByKey, reduceByKey, join 등)은 각 태스크 안에서 그룹핑을 수행할 해시 테이블을 만드는데, 이게 커질 수 있죠. 여기서 가장 간단한 해법은 병렬성 수준을 높여서 각 태스크의 입력 집합을 작게 만드는 거예요. Spark는 하나의 익스큐터 JVM을 여러 태스크에 재사용하고 태스크 시작 비용이 낮아서 200ms 정도로 짧은 태스크도 효율적으로 지원합니다. 그래서 병렬성 수준을 클러스터의 코어 수 이상으로 안전하게 높일 수 있어요.

큰 변수 브로드캐스팅

SparkContext에서 제공하는 브로드캐스트 기능을 쓰면 각 직렬화된 태스크의 크기와 클러스터에서 작업을 시작하는 비용을 크게 줄일 수 있어요. 태스크가 드라이버 프로그램의 큰 객체(예: 정적 조회 테이블)를 안에서 사용한다면, 그것을 브로드캐스트 변수로 바꾸는 것을 고려해 보세요. Spark는 각 태스크의 직렬화된 크기를 마스터에 출력하므로, 그 값을 보고 태스크가 너무 큰지 판단하면 됩니다. 일반적으로 20KiB보다 큰 태스크는 최적화할 가치가 있을 거예요.

데이터 지역성 (Data Locality)

데이터 지역성은 Spark 작업의 성능에 큰 영향을 줄 수 있어요. 데이터와 그 데이터를 처리하는 코드가 같이 있다면 계산은 빠른 경향이 있고, 코드와 데이터가 분리돼 있다면 한쪽이 다른 쪽으로 이동해야 하죠. 보통 코드의 크기가 데이터보다 훨씬 작기 때문에, 데이터 덩어리를 옮기는 것보다 직렬화된 코드를 이리저리 옮기는 편이 더 빨라요. Spark는 스케줄링을 바로 이 데이터 지역성 원칙 중심으로 설계합니다.

데이터 지역성이란 데이터가 그것을 처리하는 코드에 얼마나 가까이 있는지를 뜻해요. 데이터의 현재 위치에 따라 여러 단계의 지역성이 있으며, 가까운 순서부터 나열하면 이렇습니다.

  • PROCESS_LOCAL: 데이터가 실행 중인 코드와 같은 JVM에 있다. 가능한 최고의 지역성이에요.
  • NODE_LOCAL: 데이터가 같은 노드에 있다. 같은 노드의 HDFS나 같은 노드의 다른 익스큐터에 있는 경우가 예시죠. 데이터가 프로세스 사이를 이동해야 해서 PROCESS_LOCAL보다 조금 느려요.
  • NO_PREF: 데이터를 어디서든 동일하게 빠르게 접근할 수 있고 지역성 선호도가 없다.
  • RACK_LOCAL: 데이터가 같은 서버 랙에 있다. 같은 랙의 다른 서버에 데이터가 있으므로, 보통 단일 스위치를 통해 네트워크로 보내야 해요.
  • ANY: 데이터가 네트워크상의 다른 곳에 있고 같은 랙에도 없다.

Spark는 모든 태스크를 최상의 지역성 수준에서 스케줄링하는 것을 선호하지만 항상 가능한 건 아니에요. 유휴 익스큐터에 처리되지 않은 데이터가 없는 상황에서는 Spark가 더 낮은 지역성 수준으로 전환합니다. 두 가지 선택지가 있죠: a) 같은 서버의 데이터에서 태스크를 시작하기 위해 바쁜 CPU가 비워질 때까지 기다리거나, b) 데이터를 그쪽으로 옮겨야 하는 더 먼 곳에서 즉시 새 태스크를 시작하는 것.

Spark가 전형적으로 하는 일은 바쁜 CPU가 비워지길 잠시 기다리는 것입니다. 타임아웃이 만료되면 데이터를 먼 곳에서 유휴 CPU로 옮기기 시작하죠. 단계 사이의 폴백을 위한 대기 타임아웃은 각각 개별적으로 또는 하나의 파라미터로 한꺼번에 설정할 수 있어요. 자세한 내용은 설정 페이지spark.locality 파라미터를 참고하세요. 태스크가 길고 지역성이 좋지 않은 것이 보인다면 이 설정을 올려야 하지만, 기본값이 보통 잘 동작합니다.

요약

이것은 Spark 애플리케이션을 튜닝할 때 알아야 할 주요 사항 — 무엇보다 데이터 직렬화와 메모리 튜닝 — 을 짚어주는 짧은 가이드였어요. 대부분의 프로그램에서는 Kryo 직렬화로 바꾸고 데이터를 직렬화된 형태로 지속하는 것만으로 흔한 성능 문제 대부분이 해결됩니다. 다른 튜닝 모범 사례는 Spark 메일링 리스트에서 물어보셔도 좋아요.