[Spark] Spark Standalone과 YARN 클러스터 매니저 비교
1. 개요
Spark는 클러스터의 CPU와 메모리를 직접 관리하지 않는다. 익스큐터를 몇 개, 어느 서버에, 코어와 메모리를 얼마씩 주고 띄울지는 클러스터 매니저가 정한다. 클러스터 매니저로는 Standalone, YARN, Kubernetes 등이 있고 이 글은 Standalone과 YARN만 다룬다.
Livy로 Spark 세션을 만드는 DAG의 설정을 정리하면서 한 세션 요청 안에 Standalone에서만 읽는 키와 YARN에서만 읽는 키가 함께 들어 있는 것을 확인했다. 어떤 키가 실제로 적용되는지는 세션이 어느 매니저에서 실행되는지에 따라 달라진다. Livy를 거치면 매니저가 세션 요청에 드러나지 않아서 설정만 읽어서는 실제 익스큐터 수를 알기 어렵다.
이 글에서는 두 매니저의 구성과 자원 배분 방식, 같은 설정 키를 각 매니저가 어떻게 읽는지를 Spark 3.5 문서와 소스 코드를 기준으로 정리한다.
2. 클러스터 구성
Standalone은 Spark 배포본에 포함된 클러스터 매니저다. 마스터 프로세스 하나가 클러스터 자원을 관리하고 서버마다 실행되는 워커 프로세스가 마스터의 지시를 받아 익스큐터 JVM을 띄운다. start-master.sh와 start-worker.sh만으로 구성되며 받을 수 있는 앱은 Spark 앱뿐이다.
YARN은 Hadoop의 자원 관리 계층이다. ResourceManager가 클러스터 전체 자원과 큐를 관리하고 서버마다 실행되는 NodeManager가 컨테이너를 띄운다. Spark 앱은 먼저 ApplicationMaster 컨테이너를 하나 받고 ApplicationMaster가 ResourceManager에 익스큐터용 컨테이너를 요청한다. cluster 배포 모드에서는 드라이버가 ApplicationMaster 안에서 실행된다. 같은 NodeManager에 Hive(Tez)나 MapReduce 같은 다른 엔진의 컨테이너도 함께 올라간다.
3. 자원 배분 방식
3.1 Standalone의 코어 기준 배분
Standalone 마스터는 앱이 받을 코어 수를 기준으로 워커에 익스큐터를 배치한다. 앱 하나가 받을 수 있는 코어의 상한은 spark.cores.max로 정하고 앱이 이 값을 지정하지 않으면 마스터의 spark.deploy.defaultCores를 쓴다. spark.deploy.defaultCores의 기본값은 무한대라서 spark.cores.max 없이 제출한 앱은 클러스터에 남은 코어를 모두 받는다. 뒤에 제출된 앱은 앞의 앱이 코어를 반환할 때까지 대기한다.
워커 3대, 워커당 8코어인 클러스터에 spark.executor.cores=4로 두 앱을 차례로 제출하면 코어가 다음과 같이 배분된다.
여러 앱이 함께 쓰는 클러스터라면 마스터 쪽 spark.deploy.defaultCores를 낮춰 두면 된다. 그러면 spark.cores.max를 빠뜨린 앱도 클러스터 코어 전체를 받지 않는다.
spark.executor.cores도 배치 결과를 바꾼다. 이 값을 지정하면 워커에 코어와 메모리가 남아 있는 한 같은 앱의 익스큐터 여러 개가 한 워커에 올라간다. 지정하지 않으면 익스큐터 하나가 워커에 남은 코어를 모두 받으므로 한 번의 스케줄링에서 앱당 워커 하나에 익스큐터가 하나씩만 뜬다.
3.2 YARN의 큐 기준 배분
YARN은 큐 단위로 자원을 나눈다. Hadoop의 기본 스케줄러인 Capacity Scheduler에서는 큐마다 클러스터 자원 중 보장받는 비율(capacity)과 최대 비율(maximum-capacity)을 정한다. 다른 큐가 자원을 쓰지 않으면 최대 비율까지 더 가져다 쓰고 선점(preemption)을 켜 두면 보장 비율보다 적게 받은 큐를 위해 다른 큐의 컨테이너를 회수한다. Spark 앱은 spark.yarn.queue로 제출할 큐를 지정하고 지정하지 않으면 default 큐로 들어간다.
자원 요청 단위는 컨테이너다. 익스큐터 하나는 spark.executor.memory와 spark.executor.memoryOverhead를 더한 크기의 컨테이너로 요청된다. Capacity Scheduler의 기본 자원 계산기인 DefaultResourceCalculator는 메모리만 기준으로 자원을 나눈다. 이 설정에서는 spark.executor.cores가 큐 자원 계산에 들어가지 않고 코어까지 계산하려면 DominantResourceCalculator로 바꿔야 한다.
4. 익스큐터 수와 메모리 설정
4.1 매니저별로 읽는 설정
익스큐터 수와 크기에 관련된 설정 중 일부는 한쪽 매니저에서만 읽힌다.
| 설정 | Standalone | YARN | 용도 |
|---|---|---|---|
spark.executor.cores |
적용 | 적용 | 익스큐터 하나의 코어 수 |
spark.cores.max |
적용 | 무시 | 앱 전체의 코어 상한 |
spark.executor.instances |
무시 | 적용 | 익스큐터 수 |
spark.executor.memoryOverhead |
무시 | 적용 | 힙 밖 메모리, 컨테이너 크기에 포함 |
spark.yarn.* |
무시 | 적용 | 큐, ApplicationMaster 설정 |
Standalone에서 익스큐터 수를 정하는 설정은 spark.executor.instances가 아니다. Spark 3.5의 StandaloneSchedulerBackend는 동적 할당이 꺼져 있으면 익스큐터 수에 제한을 두지 않고 마스터에는 spark.cores.max만 전달한다. 마스터는 이 코어 상한이 찰 때까지 spark.executor.cores 크기의 익스큐터를 띄우므로 익스큐터 수는 spark.cores.max ÷ spark.executor.cores가 된다. 워커에 남은 코어나 메모리가 부족하면 이보다 적게 뜬다.
YARN에서는 반대로 spark.executor.instances가 익스큐터 수를 정하고 spark.cores.max는 읽지 않는다. 클러스터나 큐에 남은 메모리가 컨테이너를 놓기에 부족하면 나머지 익스큐터는 자원이 날 때까지 대기한다. 설정 문서에도 spark.cores.max는 Standalone과 Mesos에서 쓰는 값으로 적혀 있다.
익스큐터 수 = cores.max 4 ÷ executor.cores 2 = 2개. 워커는 남은 코어와 힙 크기(executor.memory)로 익스큐터를 놓을 자리를 판단하고 memoryOverhead와 executor.instances는 반영하지 않는다. 남은 자원은 코어 20개, 메모리 88GB다.
4.2 메모리 한도 처리
YARN NodeManager는 기본 설정에서 컨테이너의 실제 메모리 사용량을 감시한다. 익스큐터 프로세스가 컨테이너 크기를 넘으면 NodeManager가 프로세스를 종료하고 Spark 로그에는 Container killed by YARN for exceeding physical memory limits 메시지가 남는다. 컨테이너 크기는 spark.executor.memory와 spark.executor.memoryOverhead의 합이고 오프힙 메모리나 PySpark 메모리를 따로 설정하면 그 값도 더해진다. spark.executor.memoryOverhead를 지정하지 않으면 spark.executor.memory의 10%와 384MiB 중 큰 값이 쓰인다.
Standalone 워커에는 이런 감시가 없다. spark.executor.memory가 그대로 익스큐터 JVM의 -Xmx가 되어 힙 크기는 제한되지만 힙 밖에서 쓰는 메모리에는 매니저가 두는 상한이 없다. 서버 메모리가 부족해지면 OS의 OOM killer가 프로세스를 종료한다. spark.executor.memoryOverhead는 Standalone에서 읽지 않으므로 이 값을 넣어도 힙 밖 메모리가 따로 확보되지 않는다. Standalone 클러스터에서 익스큐터가 메모리 부족으로 종료된 사례는 Spark Standalone 익스큐터 메모리 부족과 Spill 원인 분석에 정리했다.
spark.executor.memory=4G와 spark.executor.memoryOverhead=1G를 넣은 같은 설정을 두 매니저가 해석한 결과는 그림 4와 같다.
디스크 스필은 이 한도와 별개로 동작한다. 스필은 힙 안의 실행 메모리 영역이 부족할 때 Spark가 데이터를 디스크에 쓰는 동작이고 그동안 힙은 -Xmx 안에 머무르므로 스필 때문에 프로세스 메모리가 컨테이너 크기를 넘지는 않는다. 컨테이너 종료는 셔플 네트워크 버퍼(Netty 다이렉트 메모리), PySpark 워커 프로세스, zstd 같은 네이티브 압축 라이브러리처럼 Spark 메모리 관리자가 세지 않는 힙 밖 메모리가 오버헤드 몫을 넘을 때 생긴다.
5. 두 매니저용 키가 섞인 세션 설정
1장에서 언급한 세션 요청에서 익스큐터와 메모리에 관련된 키 일부만 남기면 다음과 같다.
{
"kind": "pyspark",
"executorCores": 2,
"executorMemory": "4G",
"conf": {
"spark.cores.max": 4,
"spark.executor.instances": 12,
"spark.yarn.executor.memoryOverhead": "1G",
"spark.yarn.appMasterEnv.LANG": "ko_KR.UTF-8"
}
}
spark.yarn.executor.memoryOverhead는 Spark 2.3에서 spark.executor.memoryOverhead로 이름이 바뀐 키다. 지금도 대체 키로 등록되어 있어 YARN에서는 같은 설정으로 읽힌다. 이 요청을 매니저별로 해석하면 다음과 같다.
| 키 | Standalone | YARN |
|---|---|---|
executorCores: 2 |
익스큐터당 2코어 | 익스큐터당 2코어 |
spark.cores.max: 4 |
앱 코어 상한 4개 | 읽지 않음 |
spark.executor.instances: 12 |
읽지 않음 | 익스큐터 12개 |
spark.yarn.executor.memoryOverhead: 1G |
읽지 않음 | 컨테이너 크기 4G + 1G |
spark.yarn.appMasterEnv.LANG |
읽지 않음 | ApplicationMaster 환경 변수 |
동적 할당이 꺼져 있으면 이 요청은 Standalone에서 익스큐터 2개, 코어 4개로 실행된다. YARN에서는 익스큐터 12개, 코어 24개, 컨테이너 메모리 합계 60GB를 요청한다. 그림 3 계산기의 기본값이 이 요청과 같다.
Livy로 만든 세션이 어느 매니저에서 실행되는지는 요청 본문이 아니라 Livy 서버 설정 livy.spark.master가 정한다. 실행 중인 세션의 Spark UI에서 Environment 탭의 spark.master 값이 spark://로 시작하면 Standalone, yarn이면 YARN이다. 한쪽 매니저에서만 쓰는 세션이라면 다른 매니저용 키를 지워 두어야 설정에 적힌 익스큐터 수와 실제 익스큐터 수가 다르게 보이지 않는다.
6. 그 밖의 차이
| 항목 | Standalone | YARN |
|---|---|---|
| 설치 | Spark 배포본만 있으면 된다 | Hadoop(YARN) 클러스터가 필요하다 |
| 다른 엔진과 자원 공유 | 하지 않는다 | Hive, MapReduce 등과 공유한다 |
| Python 앱의 cluster 배포 모드 | 지원하지 않는다 | 지원한다 |
| 관리 노드 장애 대비 | ZooKeeper 기반 대기 마스터 또는 파일시스템 기반 복구 | ResourceManager HA |
| Kerberos 위임 토큰 배포 | 지원하지 않는다 | 지원한다 |
위임 토큰 지원 범위는 Spark 3.5 문서 기준이다. 익스큐터가 Kerberos가 적용된 HDFS나 Hive 메타스토어에 접근해야 하는 환경이라면 Standalone으로는 구성하기 어렵다.
7. 정리
- Standalone은 코어 수를 기준으로 익스큐터를 배치하고
spark.cores.max가 없으면 앱 하나가 클러스터에 남은 코어를 모두 받는다. - YARN은 큐 비율로 자원을 나누고 익스큐터를
spark.executor.memory + spark.executor.memoryOverhead크기의 컨테이너로 띄워 이 크기를 넘으면 종료한다. - 익스큐터 수는 Standalone에서
spark.cores.max ÷ spark.executor.cores로, YARN에서spark.executor.instances로 정해지고 두 매니저는 서로의 키를 읽지 않는다. - Livy 세션 요청에는 매니저가 드러나지 않으므로 Spark UI의
spark.master로 확인하고 쓰지 않는 매니저의 키는 설정에서 지운다.
Leave a comment