본문 바로가기
Bigdata Components/SPARK

[SPARK] Spark의 Execuotr memory 구조

by Blue____ 2024. 1. 6.

이전 글에서는 spark의 세부 동작 개념인 Transformation과 Action을 살펴 보았습니다. 

이번 글에서는 Spark의 Executor memory 구조에 대해서 살펴 볼 예정입니다.


 

Executor memory 구조

Executor는 우선, 워커노드에 컨테이너 형태로 Memory를 할당 받게됩니다. 각 executor의 memory(JVM)는 크게 3가지 독립 영역으로 나뉘게 됩니다. 

 

On-heap memory (JVM) 영역

spark.executor.memory를 통해 설정할 수 있습니다.(ex 1g, 20g 등)

 

Off-heap memory 영역

off-heap memory는 JVM(on heap memory) 영역과는 별도로 존재하며, Spark container(Executor) 내에 위치합니다. off-heap memory는 특정(Project Tungsten의 데이터) 데이터를 저장하거나, 혹은 On-heap memory(JVM 영역)에서의 빈번한 GC로 인한 Overhead를 감소시킬 목적으로 사용됩니다.   

 

spark.memory.offHeap.enabled=true로 설정되어 있어야 사용 가능합니다. (default = false) 단, 해당 설정이 true라고 하더라도, default는 0 (bytes)입니다. 즉, spark.memory.offHeap.size를 통해 추가 설정해줘야 합니다.

 

Overhead Memory 

오버헤드 메모리는 executor에서 container의 overhead를 감안해서 여유분으로 남겨놓은 메모리 공간입니다. 보통, 전체 spark.executor.memory의 10% 정도로 설정하고, default값도 10%입니다. spark.executor.memoryOverhead 혹은  spark.executor.memoryOverheadFactor설정을 통해 size, fraction(execuotr.memory 설정)을 세팅합니다.

 

executor memory 구조

 

On-Heap Memory 4가지 영역

JVM 의 heap 영역에서 사용하는 메모리이며, Spark에서 하나의 application(app)이 task를 수행하며 사용할 수 있는 메모리입니다.

 

Reserved Memory

spark 엔진 내부동작을 위해 남겨놓는 reserved 용량, 해당 영역은 300MB가 고정으로 할당됩니다. 

 

User Memory

spark의 metadata(데이터 정보, 위치 등등), large records 등에 의한 Out Of Memory를 방지하는 목적의 영역입니다. User memory 비율은 spark.memory.fraction(default : 0.6)라는 memory 비율을 통해 정해지는데, 간단한 계산식에 아래에 설명 예정입니다.

 

Storage Memory

데이터를 cache하는 영역이며, 쉽게 말해 데이터(연산 후의 중간 데이터 등)들을 저장하는 영역으로 여기에 cache된 데이터를 가져가 아래의 Execution Memory에서 연산을 수행하게 됩니다.

 

Execution Memory

spark application 수행에 필요한 통합된 메모리이며, 풀어 설명 하면 Transformations, Actions 의 연산과정에서 사용하는 메모리 영역입니다. 연산과정의 결과를 임시로 가지고 있긴 하지만 Job이 끝나면 바로 해제하게 됩니다. 함수로는 join, sort, aggregation(Transformations, Actions의 함수들)과 같은 함수 호출을 통해 사용되는 영역입니다. 

 

영역의 비율(fraction) 계산

위에서 설명한 spark.memory.fraction은 각 메모리 영역의 비율을 설정하게 합니다. 만약 spark.memory.fraction=0.6인 경우, storage memory + execution memory 영역이 60%, User Memory 영역이 40%를 차지합니다.

 

추가로 Storage memory는 spark.memory.storageFraction(defalut=0.5) 비율에 따라 결정되며 "spark.memory.fraction * spark.memory.storageFraction"인 비율로 할당됩니다.

 

execution memory 영역 또한, spark.memory.executionFraction(defalut=0.5) 비율을 통해 결정되며, 위의 계산식에서 storageFraction을 executionFraction로 변경하면 동일한 계산식이 됩니다. 

 

 

추가 튜닝 포인트

만약 spark.memory.fraction 비율을 너무 줄일 경우, Spill이 자주 일어날 수 있습니다. 즉, 데이터를 저장할 공간이 부족해져 성능을 저해할 수 있습니다. 따라서, 최초에는 default로 설정으로 진행 후, 이후에 모니터링을 통해 튜닝을 진행하는게 일반적인 튜닝 방법입니다. 

 

spill

Spill이란, 데이터를 저장할 메모리 공간(Storage memory 혹은 execution memory)이 부족할 경우 데이터를 담을 공간이 없을때, 일어납니다.  Spill은 부족한 메모리 영역에 저장 못한 데이터를 Disk 영역(HDFS)에 저장하기 때문에 spill이 많이 일어 날 경우 성능에 매우 치명적이다 (Disk I/O 발생). 

 

해결 방법은 여러 측면을 봐야하지만 가장 대표적으로는 Partition의 사이즈를 줄이는 것으로 튜닝하는 것입니다. partition의 size를 줄이는 이유는 한번에 처리할 데이터의 양을 줄일 수 있어 메모리 영역에 저장되는 데이터 사이즈를 감소시킬수 있습니다. 하지만 partition size를 줄여, partition의 수가 늘어 나게되면, 오히려 더 많은 task가 생성/수행 되기에 실행 시간은 늘어나게 됩니다.

 

즉, 지속적인 모니터링을 통해 적절한 partition 사이즈(주로 hdfs block 사이즈에 맞춰 설정)를 설정하는 것이 중요합니다.그러나, 만약 가용 리소스가 충분하다면 executor memory(memory.fracion 비율도 함께)를 늘려주는게 가장 간단한 방법입니다.

 

 

이번 글에서는 Spark의 executor memory 구조와 튜닝 포인트을 확인 했습니다. 다음 글에서는 Spark의 partition 개념과 spark job 제출 방식에 대해서 알아볼 예정입니다.