티스토리 뷰


아래 내용은 위 Wei Dong’s Blog 의 글을 번역한 내용이다.



YARN 환경에서 Spark : 메모리 용량은 어디로 갔을까?

빅데이터를 효율적으로 처리하는 것은 얼마나 많은 메모리를 가질 수 있는지, 또는 얼마나 효과적으로 가용 가능한 메모리를 제한하면서 효율적으로 사용할 수 있는지에 달려있다. 특히 Spark 에서는 그렇다. 그러나 Spark 와 YARN 에서 기본설정만으로는 메모리를 효율적으로 사용할 수 없다. 그래서 제한된 자원에서 가능한 최대의 효율을 가지려면 신중히 설정을 조절해야 한다. 이 글에서는 좋지 않은 설정(Spark on YARN) 이 얼마나 메모리를 낭비하는지 언급하고, 각 수치들을 설명하며, 이 문제를 해결하기 위한 몇가지 팁을 소개한다.

먼저 환경에 대해서 이야기 해보자. 나는 큰 회사에서 일하지도 않고, 수 천대 장비를 갖고 있지도 않다. 단지 데이터분석을 위해 10대 정도가 전부다. 오랫동안(수개월) Standalone 설정으로 테스트를 해봤지만 뻔한 결과였고, 10대 남짓한 장비로 개발과 운영장비로 사용해야 했다. 그리고 2개의 Spark app 을 병렬적으로 수행할 필요가 있었다. 이미 hadoop 환경이 있기 때문에 YARN 을 선택한 것은 자연스러운 선택이었다. 어렵지 않게 Spark 에 YARN 환경설정을 하였지만 메모리 부족으로 2번째 Spark App 을 수행할 수 없었다. 내가 접한 문제를 간단히 단일 장비에서 재현해 볼 것이다.

1. 무엇이 문제인가?

이 데모는 64GB 메모리 환경에서 수행했다. 설정은 아래와 같다.

yarn.nodemanager.resource.memory-mb = 49152    #48G
yarn.scheduler.maximum-allocation-mb = 23576      #24G
SPARK_EXECUTOR_INSTANCES = 1
SPARK_EXECUTOR_MEMORY = 18G
SPARK_DRIVER_MEMORY = 4G

YARN 설정과 Spark 설정을 각각 yarn-site.xml 과 spark-env.sh 에 하였다. 그 외에 메모리 설정은 하지 않았다.

그래서 전체 메모리는 YARN 전체 48G로, 하나의 app 에 최대 24G 로 설정했다.  하나의 App 이 24G 까지 가능하므로 Spark 가 22G(18 + 4) 를 사용할 수 있다. 그래서 2개의 Spark App 을 병렬적으로 수행할 수 있다.

Spark App 을 수행했을 때 로그에서 확인 할 수 있는 숫자와 WEB UI 정보는 아래와 같다.

  • (YARN) 메모리 전체 : 48G
  • (YARN) 메모리 사용 : 25G
  • (YARN) Container 1, 메모리 : 5120M
  • (YARN) Container 2, 메모리 : 20480M
  • (Spark) Driver 메모리 : 4480M(overhead 384MB 포함)
  • (Spark) Driver 가용 메모리 : 2.1G
  • (Spark) Executor 가용 메모리 : 9.3G



문제는 아래와 같다.

  1. Spark Driver 메모리를 4G로 설정했다. 그리고 Yarn 에 4G + 384MB(Overhead) 를 요청했다.
  2. Spark Driver 메모리는 5G 로 나온다.
  3. Driver Block Manager 가 사용가능한 메모리는 2.1G 뿐이다.

Spark 는 코드 수행을 위해 메모리의 일정부분을 잡아둔다. 그리고 Block Manager 에게 전부를 주지 않는다. 그렇더라도 여전히 뭔가 이상하다.

모든 메모리는 어디로 갔는가?

2. The Math Behind

Rule 1. YARN 은 항상 yarn.scheduler.minimum-allocation-mb 배수만큼 올림만큼 메모리를 요청한다.(기본값 1G) 그래서 4G + 384M 이 5G 로 나타난 이유이다. yarn.scheduler.minimum-allocation-mb 가 “minimum-allocation-unit-mb” 이다. 이는 97 같은 소수를 설정하면 쉽게 확인할 수 있다. 그리고 YARN 이 얼마나 메모리를 할당하는지 보라.

Rule 2. Spark 는 YARN 에 메모리를 요청하기 전에 SPARK_EXECUTOR_MEMORY/SPARK_DRIVER_MEMORY 에 Overhead(추가) 메모리를 더해 요청한다. 이는 executor 나 driver 모두 동일하다.(아래처럼)

//yarn/common/src/main/scala/org/apache/spark/deploy/yarn/YarnSparkHadoopUtil.scala
  val MEMORY_OVERHEAD_FACTOR = 0.07
  val MEMORY_OVERHEAD_MIN = 384

//yarn/common/src/main/scala/org/apache/spark/deploy/yarn/YarnAllocator.scala
  protected val memoryOverhead: Int = sparkConf.getInt("spark.yarn.executor.memoryOverhead",
    math.max((MEMORY_OVERHEAD_FACTOR * executorMemory).toInt, MEMORY_OVERHEAD_MIN))
......
      val totalExecutorMemory = executorMemory + memoryOverhead
      numPendingAllocate.addAndGet(missing)
      logInfo(s"Will allocate $missing executor containers, each with $totalExecutorMemory MB " +
        s"memory including $memoryOverhead MB overhead")

Overhead 는 필요하다. 그 이유는 JVM 이 일정량의 메모리(-Xmx) 만큼 허용했을 때, JVM 전체 메모리 사용량은 그 보다 조금 더 많을 수 있는데, YARN 은 허용된 양보다 큰 경우 프로그램을 죽이게 되기 때문이다. 소스를 수정하여 이 2개의 값(-Xmx 메모리, Overhead)을 조절할 수 있다. 

위 2개의 Rule 은 SPARK_EXECUTOR_MEMORY/SPARK_DRIVER_MEMORY 를 어떻게 설정하는지 결정한다.

Rule 3. Driver와 Executor 는 얼만큼의 메모리를 가지나.

-Xmx 옵션을 통해 JVM 최대 메모리를 제한한다. 일정부분은 Scala Runtime 이 사용하고 나머지는 다른 시스템 컴포넌트가 사용한다. Scala 프로그램은 일정량 이하의 양을 사용한다. 아래 예제에서 잘 보여준다.

$ scala -J-Xmx4g
Welcome to Scala version 2.10.3 (OpenJDK 64-Bit Server VM, Java 1.7.0_51).
Type in expressions to have them evaluated.
Type :help for more information.

scala> Runtime.getRuntime.maxMemory
res0: Long = 3817865216
scala>

Runtime 이 약 455M 정도 사용한다.(위 프로세스는 리눅스에 140.3M 의 RSS 를 포함한다. 실제로 사용되는 양보다 꽤 큰 양을 차지하고 있다.)

JVM 옵션으로 Spark Driver 에 4G 를 설정했다. 이를 spark-shell 수행을 통해 확인할 수 있다.

scala>

scala> import java.lang.management.ManagementFactory
import java.lang.management.ManagementFactory

scala> ManagementFactory.getRuntimeMXBean.getInputArguments
res0: java.util.List[String] = [-XX:MaxPermSize=128m, -Djava.library.path=/home/hadoop/hadoop-2.4.1/lib/native, -Xms4G, -Xmx4G]

scala> Runtime.getRuntime.maxMemory
res1: Long = 4116709376

scala>

Rule 4. Spark 는 어떻게 최대 가용 메모리를 결정하나?

//core/src/main/scala/org/apache/spark/storage/BlockManager.scala
/** Return the total amount of storage memory available. */
private def getMaxMemory(conf: SparkConf): Long = {
  val memoryFraction = conf.getDouble("spark.storage.memoryFraction", 0.6)
  val safetyFraction = conf.getDouble("spark.storage.safetyFraction", 0.9)
  (Runtime.getRuntime.maxMemory * memoryFraction * safetyFraction).toLong
}

4116709376(4G) X 0.6 X 0.9 = 2.07G 이다. 이 것이 2.1G 가 되는 이유이다. Executor 의 최대 가용 메모리도 이렇게 결정된다.

종합해보면, 메모리 가용량 계산식은 아래 두 가지가 된다.

  • YARN
    (설정된 메모리 + Overhead) 에 minimum-allocation-mb 배수 만큼 올림
    Overhead = max(설정된 메모리 X 0.7, 384MB)
  • Cache
    (설정된 메모리 - Runtime 이 사용하는 메모리) * spark.storage.memoryFraction * spark.storage.safetyFraction

3. 튜닝

각 Step 에서 메모리 설정량은 과대설정 된 것을 볼 수 있었다. 프로세스가 설정된 메모리만큼 사용하더라도 항상 그 만큼의 메모리를 사용할 수 있었다. 그 이유는 YARN 이 설정된 메모리를 초과하는 경우 프로세스를 중지하기 때문이다. 그래서 SPARK_EXECUTOR_MEMORY/SPARK_DRIVER_MEMORY 를 충분히 넉넉하게 설정해야 한다. 또한 Spark 수행시 사용되는 정확한 메모리 량을 결정하기 어렵기 때문에 spark.storage.memoryFraction 옵션을 설정하는 것은 의미 없을 수 있다. 그러나 Spark App 이 병렬로 수행되면서 사용하는 전체 메모리 량이 물리 메모리량을 초과한다는 것이 사실이라면, 메모리를 설정하는 가장 쉬운 방법은 실제 사용하는 것보다 크게 설정하는 것이다. yarn.nodemanager.resource.momory-mb 옵션을 물리 메모리보다 크게 설정하는 하면 된다.(YARN 에서 물리 메모리를 넘어서는지 확인하지 않음). 그리고 yarn.scheduler.minimum-allocation-mb 를 100M 처럼 작게 설정하면 도움이 된다. 그래서 Spark App 이 요청한 것보다 그렇게 많이 할당받지 않게 하기 때문이다.


공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함