# 運行支持kubernetes原生調度的Spark程序
TL;DR 這個主題比較大,該開源項目也還在不斷進行中,我單獨做了一個 web 用來記錄 spark on kubernetes 的研究和最新進展見: https://jimmysong.io/spark-on-k8s
**注意**:本文中的鏡像倉庫地址 `harbor-001.jimmysong.io` 為的鏡像倉庫地址為偽裝地址,非本文中真正使用的鏡像倉庫,且該地址也不存在,請替換為您自己的鏡像倉庫。
我們之前就在 kubernetes 中運行過 standalone 方式的 spark 集群,見 [Spark standalone on kubernetes](spark-standalone-on-kubernetes.md)。
目前運行支持 kubernetes 原生調度的 spark 程序由 Google 主導,fork 自 spark 的官方代碼庫,見https://github.com/apache-spark-on-k8s/spark/ ,屬于Big Data SIG。
參與到該項目的公司有:
- Bloomberg
- Google
- Haiwen
- Hyperpilot
- Intel
- Palantir
- Pepperdata
- Red Hat
## 為何使用 spark on kubernetes
使用kubernetes原生調度的spark on kubernetes是對現有的spark on yarn/mesos的資源使用方式的革命性的改進,主要表現在以下幾點:
1. Kubernetes原生調度:不再需要二層調度,直接使用kubernetes的資源調度功能,跟其他應用共用整個kubernetes管理的資源池;
2. 資源隔離,粒度更細:原先yarn中的queue在spark on kubernetes中已不存在,取而代之的是kubernetes中原生的namespace,可以為每個用戶分別指定一個namespace,限制用戶的資源quota;
3. 細粒度的資源分配:可以給每個spark任務指定資源限制,實際指定多少資源就使用多少資源,因為沒有了像yarn那樣的二層調度(圈地式的),所以可以更高效和細粒度的使用資源;
4. 監控的變革:因為做到了細粒度的資源分配,所以可以對用戶提交的每一個任務做到資源使用的監控,從而判斷用戶的資源使用情況,所有的metric都記錄在數據庫中,甚至可以為每個用戶的每次任務提交計量;
5. 日志的變革:用戶不再通過yarn的web頁面來查看任務狀態,而是通過pod的log來查看,可將所有的kuberentes中的應用的日志等同看待收集起來,然后可以根據標簽查看對應應用的日志;
所有這些變革都可以讓我們更高效的獲取資源、更有效率的獲取資源!
## Spark 概念說明
[Apache Spark](http://spark.apache.org) 是一個圍繞速度、易用性和復雜分析構建的大數據處理框架。最初在2009年由加州大學伯克利分校的AMPLab開發,并于2010年成為Apache的開源項目之一。
在 Spark 中包括如下組件或概念:
- **Application**:Spark Application 的概念和 Hadoop 中的 MapReduce 類似,指的是用戶編寫的 Spark 應用程序,包含了一個 Driver 功能的代碼和分布在集群中多個節點上運行的 Executor 代碼;
- **Driver**:Spark 中的 Driver 即運行上述 Application 的 main() 函數并且創建 SparkContext,其中創建 SparkContext 的目的是為了準備Spark應用程序的運行環境。在 Spark 中由 SparkContext 負責和 ClusterManager 通信,進行資源的申請、任務的分配和監控等;當 Executor 部分運行完畢后,Driver負責將SparkContext 關閉。通常用 SparkContext 代表 Driver;
- **Executor**:Application運行在Worker 節點上的一個進程,該進程負責運行Task,并且負責將數據存在內存或者磁盤上,每個Application都有各自獨立的一批Executor。在Spark on Yarn模式下,其進程名稱為`CoarseGrainedExecutorBackend`,類似于 Hadoop MapReduce 中的 YarnChild。一個 `CoarseGrainedExecutorBackend` 進程有且僅有一個 executor 對象,它負責將 Task 包裝成 taskRunner,并從線程池中抽取出一個空閑線程運行 Task。每個 `CoarseGrainedExecutorBackend` 能并行運行 Task 的數量就取決于分配給它的 CPU 的個數了;
- **Cluster Manager**:指的是在集群上獲取資源的外部服務,目前有:
- Standalone:Spark原生的資源管理,由Master負責資源的分配;
- Hadoop Yarn:由YARN中的ResourceManager負責資源的分配;
- **Worker**:集群中任何可以運行Application代碼的節點,類似于YARN中的NodeManager節點。在Standalone模式中指的就是通過Slave文件配置的Worker節點,在Spark on Yarn模式中指的就是NodeManager節點;
- **作業(Job)**:包含多個Task組成的并行計算,往往由Spark Action催生,一個JOB包含多個RDD及作用于相應RDD上的各種Operation;
- **階段(Stage)**:每個Job會被拆分很多組 Task,每組任務被稱為Stage,也可稱TaskSet,一個作業分為多個階段,每一個stage的分割點是action。比如一個job是:(transformation1 -> transformation1 -> action1 -> transformation3 -> action2),這個job就會被分為兩個stage,分割點是action1和action2。
- **任務(Task)**: 被送到某個Executor上的工作任務;
- **Context**:啟動spark application的時候創建,作為Spark 運行時環境。
- **Dynamic Allocation(動態資源分配)**:一個配置選項,可以將其打開。從Spark1.2之后,對于On Yarn模式,已經支持動態資源分配(Dynamic Resource Allocation),這樣,就可以根據Application的負載(Task情況),動態的增加和減少executors,這種策略非常適合在YARN上使用spark-sql做數據開發和分析,以及將spark-sql作為長服務來使用的場景。Executor 的動態分配需要在 cluster mode 下啟用 "external shuffle service"。
- **動態資源分配策略**:開啟動態分配策略后,application會在task因沒有足夠資源被掛起的時候去動態申請資源,這意味著該application現有的executor無法滿足所有task并行運行。spark一輪一輪的申請資源,當有task掛起或等待 `spark.dynamicAllocation.schedulerBacklogTimeout` (默認1s)時間的時候,會開始動態資源分配;之后會每隔 `spark.dynamicAllocation.sustainedSchedulerBacklogTimeout` (默認1s)時間申請一次,直到申請到足夠的資源。每次申請的資源量是指數增長的,即1,2,4,8等。之所以采用指數增長,出于兩方面考慮:其一,開始申請的少是考慮到可能application會馬上得到滿足;其次要成倍增加,是為了防止application需要很多資源,而該方式可以在很少次數的申請之后得到滿足。
## 架構設計
關于 spark standalone 的局限性與 kubernetes native spark 架構之間的區別請參考 Anirudh Ramanathan 在 2016年10月8日提交的 issue [Support Spark natively in Kubernetes #34377](https://github.com/kubernetes/kubernetes/issues/34377)。
簡而言之,spark standalone on kubernetes 有如下幾個缺點:
- 無法對于多租戶做隔離,每個用戶都想給 pod 申請 node 節點可用的最大的資源。
- Spark 的 master/worker 本來不是設計成使用 kubernetes 的資源調度,這樣會存在兩層的資源調度問題,不利于與 kuberentes 集成。
而 kubernetes native spark 集群中,spark 可以調用 kubernetes API 獲取集群資源和調度。要實現 kubernetes native spark 需要為 spark 提供一個集群外部的 manager 可以用來跟 kubernetes API 交互。
### 調度器后臺
使用 kubernetes 原生調度的 spark 的基本設計思路是將 spark 的 driver 和 executor 都放在 kubernetes 的 pod 中運行,另外還有兩個附加的組件:`ResourceStagingServer` 和 `KubernetesExternalShuffleService`。
Spark driver 其實可以運行在 kubernetes 集群內部(cluster mode)可以運行在外部(client mode),executor 只能運行在集群內部,當有 spark 作業提交到 kubernetes 集群上時,調度器后臺將會為 executor pod 設置如下屬性:
- 使用我們預先編譯好的包含 kubernetes 支持的 spark 鏡像,然后調用 `CoarseGrainedExecutorBackend` main class 啟動 JVM。
- 調度器后臺為 executor pod 的運行時注入環境變量,例如各種 JVM 參數,包括用戶在 `spark-submit` 時指定的那些參數。
- Executor 的 CPU、內存限制根據這些注入的環境變量保存到應用程序的 `SparkConf` 中。
- 可以在配置中指定 spark 運行在指定的 namespace 中。
參考:[Scheduler backend 文檔](https://github.com/apache-spark-on-k8s/spark/blob/branch-2.2-kubernetes/resource-managers/kubernetes/architecture-docs/scheduler-backend.md)
## 安裝指南
我們可以直接使用官方已編譯好的 docker 鏡像來部署,下面是官方發布的鏡像:
| 組件 | 鏡像 |
| -------------------------- | ---------------------------------------- |
| Spark Driver Image | `kubespark/spark-driver:v2.1.0-kubernetes-0.3.1` |
| Spark Executor Image | `kubespark/spark-executor:v2.1.0-kubernetes-0.3.1` |
| Spark Initialization Image | `kubespark/spark-init:v2.1.0-kubernetes-0.3.1` |
| Spark Staging Server Image | `kubespark/spark-resource-staging-server:v2.1.0-kubernetes-0.3.1` |
| PySpark Driver Image | `kubespark/driver-py:v2.1.0-kubernetes-0.3.1` |
| PySpark Executor Image | `kubespark/executor-py:v2.1.0-kubernetes-0.3.1` |
我將這些鏡像放到了我的私有鏡像倉庫中了。
還需要安裝支持 kubernetes 的 spark 客戶端,在這里下載:https://github.com/apache-spark-on-k8s/spark/releases
根據使用的鏡像版本,我下載的是 [v2.1.0-kubernetes-0.3.1](https://github.com/apache-spark-on-k8s/spark/releases/tag/v2.1.0-kubernetes-0.3.1)
**運行 SparkPi 測試**
我們將任務運行在 `spark-cluster` 的 namespace 中,啟動 5 個 executor 實例。
```bash
./bin/spark-submit \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
--master k8s://https://172.20.0.113:6443 \
--kubernetes-namespace spark-cluster \
--conf spark.executor.instances=5 \
--conf spark.app.name=spark-pi \
--conf spark.kubernetes.driver.docker.image=harbor-001.jimmysong.io/library/kubespark-spark-driver:v2.1.0-kubernetes-0.3.1 \
--conf spark.kubernetes.executor.docker.image=harbor-001.jimmysong.io/library/kubespark-spark-executor:v2.1.0-kubernetes-0.3.1 \
--conf spark.kubernetes.initcontainer.docker.image=harbor-001.jimmysong.io/library/kubespark-spark-init:v2.1.0-kubernetes-0.3.1 \
local:///opt/spark/examples/jars/spark-examples_2.11-2.1.0-k8s-0.3.1-SNAPSHOT.jar
```
關于該命令參數的介紹請參考:https://apache-spark-on-k8s.github.io/userdocs/running-on-kubernetes.html
**注意:** 該 jar 包實際上是 `spark.kubernetes.executor.docker.image` 鏡像中的。
這時候提交任務運行還是失敗,報錯信息中可以看到兩個問題:
- Executor 無法找到 driver pod
- 用戶 `system:serviceaccount:spark-cluster:defaul` 沒有權限獲取 `spark-cluster` 中的 pod 信息。
提了個 issue [Failed to run the sample spark-pi test using spark-submit on the doc #478](https://github.com/apache-spark-on-k8s/spark/issues/478)
需要為 spark 集群創建一個 `serviceaccount` 和 `clusterrolebinding`:
```bash
kubectl create serviceaccount spark --namespace spark-cluster
kubectl create rolebinding spark-edit --clusterrole=edit --serviceaccount=spark-cluster:spark --namespace=spark-cluster
```
該 Bug 將在新版本中修復。
## 用戶指南
### 編譯
Fork 并克隆項目到本地:
```bash
git clone https://github.com/rootsongjc/spark.git
```
編譯前請確保你的環境中已經安裝 Java8 和 Maven3。
```bash
## 第一次編譯前需要安裝依賴
build/mvn install -Pkubernetes -pl resource-managers/kubernetes/core -am -DskipTests
## 編譯 spark on kubernetes
build/mvn compile -Pkubernetes -pl resource-managers/kubernetes/core -am -DskipTests
## 發布
dev/make-distribution.sh --tgz -Phadoop-2.7 -Pkubernetes
```
第一次編譯和發布的過程耗時可能會比較長,請耐心等待,如果有依賴下載不下來,請自備梯子。
詳細的開發指南請見:https://github.com/apache-spark-on-k8s/spark/blob/branch-2.2-kubernetes/resource-managers/kubernetes/README.md
### 構建鏡像
使用該腳本來自動構建容器鏡像:https://github.com/apache-spark-on-k8s/spark/pull/488
將該腳本放在 `dist` 目錄下,執行:
```bash
./build-push-docker-images.sh -r harbor-001.jimmysong.io/library -t v2.1.0-kubernetes-0.3.1-1 build
./build-push-docker-images.sh -r harbor-001.jimmysong.io/library -t v2.1.0-kubernetes-0.3.1-1 push
```
**注意:**如果你使用的 MacOS,bash 的版本可能太低,執行改腳本將出錯,請檢查你的 bash 版本:
```bash
bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin16)
Copyright (C) 2007 Free Software Foundation, Inc.
```
上面我在升級 bash 之前獲取的版本信息,使用下面的命令升級 bash:
```bash
brew install bash
```
升級后的 bash 版本為 `4.4.12(1)-release (x86_64-apple-darwin16.3.0)`。
編譯并上傳鏡像到我的私有鏡像倉庫,將會構建出如下幾個鏡像:
```bash
harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1
harbor-001.jimmysong.io/library/spark-resource-staging-server:v2.1.0-kubernetes-0.3.1-1
harbor-001.jimmysong.io/library/spark-init:v2.1.0-kubernetes-0.3.1-1
harbor-001.jimmysong.io/library/spark-shuffle:v2.1.0-kubernetes-0.3.1-1
harbor-001.jimmysong.io/library/spark-executor:v2.1.0-kubernetes-0.3.1-1
harbor-001.jimmysong.io/library/spark-executor-py:v2.1.0-kubernetes-0.3.1-1
harbor-001.jimmysong.io/library/spark-driver-py:v2.1.0-kubernetes-0.3.1-1
```
## 運行測試
在 `dist/bin` 目錄下執行 spark-pi 測試:
```bash
./spark-submit \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
--master k8s://https://172.20.0.113:6443 \
--kubernetes-namespace spark-cluster \
--conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \
--conf spark.executor.instances=5 \
--conf spark.app.name=spark-pi \
--conf spark.kubernetes.driver.docker.image=harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.executor.docker.image=harbor-001.jimmysong.io/library/spark-executor:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.initcontainer.docker.image=harbor-001.jimmysong.io/library/spark-init:v2.1.0-kubernetes-0.3.1-1 \
local:///opt/spark/examples/jars/spark-examples_2.11-2.2.0-k8s-0.4.0-SNAPSHOT.jar
```
詳細的參數說明見:https://apache-spark-on-k8s.github.io/userdocs/running-on-kubernetes.html
**注意:**`local:///opt/spark/examples/jars/spark-examples_2.11-2.2.0-k8s-0.4.0-SNAPSHOT.jar` 文件是在 `spark-driver` 和 `spark-executor` 鏡像里的,在上一步構建鏡像時已經構建并上傳到了鏡像倉庫中。
執行日志顯示:
```bash
2017-09-14 14:59:01 INFO Client:54 - Waiting for application spark-pi to finish...
2017-09-14 14:59:01 INFO LoggingPodStatusWatcherImpl:54 - State changed, new state:
pod name: spark-pi-1505372339796-driver
namespace: spark-cluster
labels: spark-app-selector -> spark-f4d3a5d3ad964a05a51feb6191d50357, spark-role -> driver
pod uid: 304cf440-991a-11e7-970c-f4e9d49f8ed0
creation time: 2017-09-14T06:59:01Z
service account name: spark
volumes: spark-token-zr8wv
node name: N/A
start time: N/A
container images: N/A
phase: Pending
status: []
2017-09-14 14:59:01 INFO LoggingPodStatusWatcherImpl:54 - State changed, new state:
pod name: spark-pi-1505372339796-driver
namespace: spark-cluster
labels: spark-app-selector -> spark-f4d3a5d3ad964a05a51feb6191d50357, spark-role -> driver
pod uid: 304cf440-991a-11e7-970c-f4e9d49f8ed0
creation time: 2017-09-14T06:59:01Z
service account name: spark
volumes: spark-token-zr8wv
node name: 172.20.0.114
start time: N/A
container images: N/A
phase: Pending
status: []
2017-09-14 14:59:01 INFO LoggingPodStatusWatcherImpl:54 - State changed, new state:
pod name: spark-pi-1505372339796-driver
namespace: spark-cluster
labels: spark-app-selector -> spark-f4d3a5d3ad964a05a51feb6191d50357, spark-role -> driver
pod uid: 304cf440-991a-11e7-970c-f4e9d49f8ed0
creation time: 2017-09-14T06:59:01Z
service account name: spark
volumes: spark-token-zr8wv
node name: 172.20.0.114
start time: 2017-09-14T06:59:01Z
container images: harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1
phase: Pending
status: [ContainerStatus(containerID=null, image=harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1, imageID=, lastState=ContainerState(running=null, terminated=null, waiting=null, additionalProperties={}), name=spark-kubernetes-driver, ready=false, restartCount=0, state=ContainerState(running=null, terminated=null, waiting=ContainerStateWaiting(message=null, reason=ContainerCreating, additionalProperties={}), additionalProperties={}), additionalProperties={})]
2017-09-14 14:59:03 INFO LoggingPodStatusWatcherImpl:54 - State changed, new state:
pod name: spark-pi-1505372339796-driver
namespace: spark-cluster
labels: spark-app-selector -> spark-f4d3a5d3ad964a05a51feb6191d50357, spark-role -> driver
pod uid: 304cf440-991a-11e7-970c-f4e9d49f8ed0
creation time: 2017-09-14T06:59:01Z
service account name: spark
volumes: spark-token-zr8wv
node name: 172.20.0.114
start time: 2017-09-14T06:59:01Z
container images: harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1
phase: Running
status: [ContainerStatus(containerID=docker://5c5c821c482a1e35552adccb567020532b79244392374f25754f0050e6cd4c62, image=harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1, imageID=docker-pullable://harbor-001.jimmysong.io/library/spark-driver@sha256:beb92a3e3f178e286d9e5baebdead88b5ba76d651f347ad2864bb6f8eda26f94, lastState=ContainerState(running=null, terminated=null, waiting=null, additionalProperties={}), name=spark-kubernetes-driver, ready=true, restartCount=0, state=ContainerState(running=ContainerStateRunning(startedAt=2017-09-14T06:59:02Z, additionalProperties={}), terminated=null, waiting=null, additionalProperties={}), additionalProperties={})]
2017-09-14 14:59:12 INFO LoggingPodStatusWatcherImpl:54 - State changed, new state:
pod name: spark-pi-1505372339796-driver
namespace: spark-cluster
labels: spark-app-selector -> spark-f4d3a5d3ad964a05a51feb6191d50357, spark-role -> driver
pod uid: 304cf440-991a-11e7-970c-f4e9d49f8ed0
creation time: 2017-09-14T06:59:01Z
service account name: spark
volumes: spark-token-zr8wv
node name: 172.20.0.114
start time: 2017-09-14T06:59:01Z
container images: harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1
phase: Succeeded
status: [ContainerStatus(containerID=docker://5c5c821c482a1e35552adccb567020532b79244392374f25754f0050e6cd4c62, image=harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1, imageID=docker-pullable://harbor-001.jimmysong.io/library/spark-driver@sha256:beb92a3e3f178e286d9e5baebdead88b5ba76d651f347ad2864bb6f8eda26f94, lastState=ContainerState(running=null, terminated=null, waiting=null, additionalProperties={}), name=spark-kubernetes-driver, ready=false, restartCount=0, state=ContainerState(running=null, terminated=ContainerStateTerminated(containerID=docker://5c5c821c482a1e35552adccb567020532b79244392374f25754f0050e6cd4c62, exitCode=0, finishedAt=2017-09-14T06:59:11Z, message=null, reason=Completed, signal=null, startedAt=null, additionalProperties={}), waiting=null, additionalProperties={}), additionalProperties={})]
2017-09-14 14:59:12 INFO LoggingPodStatusWatcherImpl:54 - Container final statuses:
Container name: spark-kubernetes-driver
Container image: harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1
Container state: Terminated
Exit code: 0
2017-09-14 14:59:12 INFO Client:54 - Application spark-pi finished.
```
從日志中可以看到任務運行的狀態信息。
使用下面的命令可以看到 kubernetes 啟動的 Pod 信息:
```bash
kubectl --namespace spark-cluster get pods -w
```
將會看到 `spark-driver` 和 `spark-exec` 的 Pod 信息。
## 依賴管理
上文中我們在運行測試程序時,命令行中指定的 jar 文件已包含在 docker 鏡像中,是不是說我們每次提交任務都需要重新創建一個鏡像呢?非也!如果真是這樣也太麻煩了。
#### 創建 resource staging server
為了方便用戶提交任務,不需要每次提交任務的時候都創建一個鏡像,我們使用了 **resource staging server** 。
```
kubectl create -f conf/kubernetes-resource-staging-server.yaml
```
我們同樣將其部署在 `spark-cluster` namespace 下,該 yaml 文件見 [kubernetes-handbook](https://github.com/rootsongjc/kubernetes-handbook) 的 `manifests/spark-with-kubernetes-native-scheduler` 目錄。
#### 優化
其中有一點需要優化,在使用下面的命令提交任務時,使用 `--conf spark.kubernetes.resourceStagingServer.uri` 參數指定 *resource staging server* 地址,用戶不應該關注 *resource staging server* 究竟運行在哪臺宿主機上,可以使用下面兩種方式實現:
- 使用 `nodeSelector` 將 *resource staging server* 固定調度到某一臺機器上,該地址依然使用宿主機的 IP 地址
- 改變 `spark-resource-staging-service` service 的 type 為 **ClusterIP**, 然后使用 **Ingress** 將其暴露到集群外部,然后加入的內網 DNS 里,用戶使用 DNS 名稱指定 *resource staging server* 的地址。
然后可以執行下面的命令來提交本地的 jar 到 kubernetes 上運行。
```bash
./spark-submit \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
--master k8s://https://172.20.0.113:6443 \
--kubernetes-namespace spark-cluster \
--conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \
--conf spark.executor.instances=5 \
--conf spark.app.name=spark-pi \
--conf spark.kubernetes.driver.docker.image=harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.executor.docker.image=harbor-001.jimmysong.io/library/spark-executor:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.initcontainer.docker.image=harbor-001.jimmysong.io/library/spark-init:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.resourceStagingServer.uri=http://172.20.0.114:31000 \
../examples/jars/spark-examples_2.11-2.2.0-k8s-0.4.0-SNAPSHOT.jar
```
該命令將提交本地的 `../examples/jars/spark-examples_2.11-2.2.0-k8s-0.4.0-SNAPSHOT.jar` 文件到 *resource staging server*,executor 將從該 server 上獲取 jar 包并運行,這樣用戶就不需要每次提交任務都編譯一個鏡像了。
詳見:https://apache-spark-on-k8s.github.io/userdocs/running-on-kubernetes.html#dependency-management
#### 設置 HDFS 用戶
如果 Hadoop 集群沒有設置 kerbros 安全認證的話,在指定 `spark-submit` 的時候可以通過指定如下四個環境變量, 設置 Spark 與 HDFS 通信使用的用戶:
```bash
--conf spark.kubernetes.driverEnv.SPARK_USER=hadoop
--conf spark.kubernetes.driverEnv.HADOOP_USER_NAME=hadoop
--conf spark.executorEnv.HADOOP_USER_NAME=hadoop
--conf spark.executorEnv.SPARK_USER=hadoop
```
使用 hadoop 用戶提交本地 jar 包的命令示例:
```bash
./spark-submit \
--deploy-mode cluster \
--class com.talkingdata.alluxio.hadooptest \
--master k8s://https://172.20.0.113:6443 \
--kubernetes-namespace spark-cluster \
--conf spark.kubernetes.driverEnv.SPARK_USER=hadoop \
--conf spark.kubernetes.driverEnv.HADOOP_USER_NAME=hadoop \
--conf spark.executorEnv.HADOOP_USER_NAME=hadoop \
--conf spark.executorEnv.SPARK_USER=hadoop \
--conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \
--conf spark.executor.instances=5 \
--conf spark.app.name=spark-pi \
--conf spark.kubernetes.driver.docker.image=harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.executor.docker.image=harbor-001.jimmysong.io/library/spark-executor:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.initcontainer.docker.image=harbor-001.jimmysong.io/library/spark-init:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.resourceStagingServer.uri=http://172.20.0.114:31000 \
~/Downloads/tendcloud_2.10-1.0.jar
```
詳見:https://github.com/apache-spark-on-k8s/spark/issues/408
#### 限制 Driver 和 Executor 的資源使用
在執行 `spark-submit` 時使用如下參數設置內存和 CPU 資源限制:
```bash
--conf spark.driver.memory=3G
--conf spark.executor.memory=3G
--conf spark.driver.cores=2
--conf spark.executor.cores=10
```
這幾個參數中值如何傳遞到 Pod 的資源設置中的呢?
比如我們設置在執行 `spark-submit` 的時候傳遞了這樣的兩個參數:`--conf spark.driver.cores=2` 和 `--conf spark.driver.memory=100G` 那么查看 driver pod 的 yaml 輸出結果將會看到這樣的資源設置:
```yaml
resources:
limits:
memory: 110Gi
requests:
cpu: "2"
memory: 100Gi
```
以上參數是對 `request` 值的設置,那么 `limit` 的資源設置的值又是從何而來?
可以使用 `spark.kubernetes.driver.limit.cores` 和 `spark.kubernetes.executor.limit.cores` 來設置 CPU的 hard limit。
memory limit 的值是根據 memory request 的值加上 `spark.kubernetes.executor.memoryOverhead` 的值計算而來的,該配置項用于設置分配給每個 executor 的超過 heap 內存的值(可以使用k、m、g單位)。該值用于虛擬機的開銷、其他本地服務開銷。根據 executor 的大小設置(通常是 6%到10%)。
我們可以這樣來提交一個任務,同時設置 driver 和 executor 的 CPU、內存的資源 request 和 limit 值(driver 的內存 limit 值為 request 值的 110%)。
```bash
./spark-submit \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
--master k8s://https://172.20.0.113:6443 \
--kubernetes-namespace spark-cluster \
--conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \
--conf spark.driver.memory=100G \
--conf spark.executor.memory=10G \
--conf spark.driver.cores=30 \
--conf spark.executor.cores=2 \
--conf spark.driver.maxResultSize=10240m \
--conf spark.kubernetes.driver.limit.cores=32 \
--conf spark.kubernetes.executor.limit.cores=3 \
--conf spark.kubernetes.executor.memoryOverhead=2g \
--conf spark.executor.instances=5 \
--conf spark.app.name=spark-pi \
--conf spark.kubernetes.driver.docker.image=harbor-001.jimmysong.io/library/spark-driver:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.executor.docker.image=harbor-001.jimmysong.io/library/spark-executor:v2.1.0-kubernetes-0.3.1-1 \
--conf spark.kubernetes.initcontainer.docker.image=harbor-001.jimmysong.io/library/spark-init:v2.1.0-kubernetes-0.3.1-1 \
local:///opt/spark/examples/jars/spark-examples_2.11-2.2.0-k8s-0.4.0-SNAPSHOT.jar 10000000
```
這將啟動一個包含一千萬個 task 的計算 pi 的 spark 任務,任務運行過程中,drvier 的 CPU 實際消耗大約為 3 核,內存 40G,每個 executor 的 CPU 實際消耗大約不到 1 核,內存不到 4G,我們可以根據實際資源消耗不斷優化資源的 request 值。
`SPARK_DRIVER_MEMORY` 和 `SPARK_EXECUTOR_MEMORY` 和分別作為 Driver 容器和 Executor 容器啟動的環境變量,比如下面這個 Driver 啟動的 CMD 中:
```bash
CMD SPARK_CLASSPATH="${SPARK_HOME}/jars/*" && \
env | grep SPARK_JAVA_OPT_ | sed 's/[^=]*=\(.*\)/\1/g' > /tmp/java_opts.txt && \
readarray -t SPARK_DRIVER_JAVA_OPTS < /tmp/java_opts.txt && \
if ! [ -z ${SPARK_MOUNTED_CLASSPATH+x} ]; then SPARK_CLASSPATH="$SPARK_MOUNTED_CLASSPATH:$SPARK_CLASSPATH"; fi && \
if ! [ -z ${SPARK_SUBMIT_EXTRA_CLASSPATH+x} ]; then SPARK_CLASSPATH="$SPARK_SUBMIT_EXTRA_CLASSPATH:$SPARK_CLASSPATH"; fi && \
if ! [ -z ${SPARK_EXTRA_CLASSPATH+x} ]; then SPARK_CLASSPATH="$SPARK_EXTRA_CLASSPATH:$SPARK_CLASSPATH"; fi && \
if ! [ -z ${SPARK_MOUNTED_FILES_DIR+x} ]; then cp -R "$SPARK_MOUNTED_FILES_DIR/." .; fi && \
if ! [ -z ${SPARK_MOUNTED_FILES_FROM_SECRET_DIR} ]; then cp -R "$SPARK_MOUNTED_FILES_FROM_SECRET_DIR/." .; fi && \
${JAVA_HOME}/bin/java "${SPARK_DRIVER_JAVA_OPTS[@]}" -cp $SPARK_CLASSPATH -Xms$SPARK_DRIVER_MEMORY -Xmx$SPARK_DRIVER_MEMORY $SPARK_DRIVER_CLASS $SPARK_DRIVER_ARGS
```
我們可以看到對 `SPARK_DRIVER_MEMORY` 環境變量的引用。Executor 的設置與 driver 類似。
而我們可以使用這樣的參數來傳遞環境變量的值 `spark.executorEnv.[EnvironmentVariableName]`,只要將 `EnvironmentVariableName` 替換為環境變量名稱即可。
## 參考
- [Spark動態資源分配-Dynamic Resource Allocation](http://lxw1234.com/archives/2015/12/593.htm)
- [Running Spark on Kubernetes](https://apache-spark-on-k8s.github.io/userdocs/running-on-kubernetes.html)
- [Apache Spark Jira Issue - 18278 - SPIP: Support native submission of spark jobs to a kubernetes cluster](https://issues.apache.org/jira/browse/SPARK-18278)
- [Kubernetes Github Issue - 34377 Support Spark natively in Kubernetes](https://github.com/kubernetes/kubernetes/issues/34377)
- https://github.com/rootsongjc/spark-on-kubernetes
- [Scheduler backend](https://github.com/apache-spark-on-k8s/spark/blob/branch-2.2-kubernetes/resource-managers/kubernetes/architecture-docs/scheduler-backend.md)
- [Introduction to Spark on Kubernetes - banzaicloud.com](https://banzaicloud.github.io/blog/spark-k8s/)
- [Scaling Spark made simple on Kubernetes - banzaicloud.com](https://banzaicloud.com/blog/scaling-spark-k8s/)
- [The anatomy of Spark applications on Kubernetes - banzaicloud.com](https://banzaicloud.com/blog/spark-k8s-internals/)
- [Monitoring Apache Spark with Prometheus - banzaicloud.com](https://banzaicloud.com/blog/spark-monitoring/)
- [Running Zeppelin Spark notebooks on Kubernetes - banzaicloud.com](https://banzaicloud.com/blog/zeppelin-spark-k8/)
- [Apache Spark CI/CD workflow howto - banzaicloud.com](https://banzaicloud.com/blog/pipeline-howto/)
- 序言
- 云原生
- 云原生(Cloud Native)的定義
- CNCF - 云原生計算基金會簡介
- CNCF章程
- 云原生的設計哲學
- Play with Kubernetes
- 快速部署一個云原生本地實驗環境
- Kubernetes與云原生應用概覽
- 云原生應用之路——從Kubernetes到Cloud Native
- 云原生編程語言
- 云原生編程語言Ballerina
- 云原生編程語言Pulumi
- 云原生的未來
- Kubernetes架構
- 設計理念
- Etcd解析
- 開放接口
- CRI - Container Runtime Interface(容器運行時接口)
- CNI - Container Network Interface(容器網絡接口)
- CSI - Container Storage Interface(容器存儲接口)
- Kubernetes中的網絡
- Kubernetes中的網絡解析——以flannel為例
- Kubernetes中的網絡解析——以calico為例
- 具備API感知的網絡和安全性管理開源軟件Cilium
- Cilium架構設計與概念解析
- 資源對象與基本概念解析
- Pod狀態與生命周期管理
- Pod概覽
- Pod解析
- Init容器
- Pause容器
- Pod安全策略
- Pod的生命周期
- Pod Hook
- Pod Preset
- Pod中斷與PDB(Pod中斷預算)
- 集群資源管理
- Node
- Namespace
- Label
- Annotation
- Taint和Toleration(污點和容忍)
- 垃圾收集
- 控制器
- Deployment
- StatefulSet
- DaemonSet
- ReplicationController和ReplicaSet
- Job
- CronJob
- Horizontal Pod Autoscaling
- 自定義指標HPA
- 準入控制器(Admission Controller)
- 服務發現
- Service
- Ingress
- Traefik Ingress Controller
- 身份與權限控制
- ServiceAccount
- RBAC——基于角色的訪問控制
- NetworkPolicy
- 存儲
- Secret
- ConfigMap
- ConfigMap的熱更新
- Volume
- Persistent Volume(持久化卷)
- Storage Class
- 本地持久化存儲
- 集群擴展
- 使用自定義資源擴展API
- 使用CRD擴展Kubernetes API
- Aggregated API Server
- APIService
- Service Catalog
- 資源調度
- QoS(服務質量等級)
- 用戶指南
- 資源對象配置
- 配置Pod的liveness和readiness探針
- 配置Pod的Service Account
- Secret配置
- 管理namespace中的資源配額
- 命令使用
- Docker用戶過度到kubectl命令行指南
- kubectl命令概覽
- kubectl命令技巧大全
- 使用etcdctl訪問kubernetes數據
- 集群安全性管理
- 管理集群中的TLS
- kubelet的認證授權
- TLS bootstrap
- 創建用戶認證授權的kubeconfig文件
- IP偽裝代理
- 使用kubeconfig或token進行用戶身份認證
- Kubernetes中的用戶與身份認證授權
- Kubernetes集群安全性配置最佳實踐
- 訪問Kubernetes集群
- 訪問集群
- 使用kubeconfig文件配置跨集群認證
- 通過端口轉發訪問集群中的應用程序
- 使用service訪問群集中的應用程序
- 從外部訪問Kubernetes中的Pod
- Cabin - Kubernetes手機客戶端
- Kubernetic - Kubernetes桌面客戶端
- Kubernator - 更底層的Kubernetes UI
- 在Kubernetes中開發部署應用
- 適用于kubernetes的應用開發部署流程
- 遷移傳統應用到Kubernetes中——以Hadoop YARN為例
- 最佳實踐概覽
- 在CentOS上部署Kubernetes集群
- 創建TLS證書和秘鑰
- 創建kubeconfig文件
- 創建高可用etcd集群
- 安裝kubectl命令行工具
- 部署master節點
- 安裝flannel網絡插件
- 部署node節點
- 安裝kubedns插件
- 安裝dashboard插件
- 安裝heapster插件
- 安裝EFK插件
- 生產級的Kubernetes簡化管理工具kubeadm
- 使用kubeadm在Ubuntu Server 16.04上快速構建測試集群
- 服務發現與負載均衡
- 安裝Traefik ingress
- 分布式負載測試
- 網絡和集群性能測試
- 邊緣節點配置
- 安裝Nginx ingress
- 安裝配置DNS
- 安裝配置Kube-dns
- 安裝配置CoreDNS
- 運維管理
- Master節點高可用
- 服務滾動升級
- 應用日志收集
- 配置最佳實踐
- 集群及應用監控
- 數據持久化問題
- 管理容器的計算資源
- 集群聯邦
- 存儲管理
- GlusterFS
- 使用GlusterFS做持久化存儲
- 使用Heketi作為Kubernetes的持久存儲GlusterFS的external provisioner
- 在OpenShift中使用GlusterFS做持久化存儲
- GlusterD-2.0
- Ceph
- 用Helm托管安裝Ceph集群并提供后端存儲
- 使用Ceph做持久化存儲
- 使用rbd-provisioner提供rbd持久化存儲
- OpenEBS
- 使用OpenEBS做持久化存儲
- Rook
- NFS
- 利用NFS動態提供Kubernetes后端存儲卷
- 集群與應用監控
- Heapster
- 使用Heapster獲取集群和對象的metric數據
- Prometheus
- 使用Prometheus監控kubernetes集群
- Prometheus查詢語言PromQL使用說明
- 使用Vistio監控Istio服務網格中的流量
- 分布式跟蹤
- OpenTracing
- 服務編排管理
- 使用Helm管理Kubernetes應用
- 構建私有Chart倉庫
- 持續集成與發布
- 使用Jenkins進行持續集成與發布
- 使用Drone進行持續集成與發布
- 更新與升級
- 手動升級Kubernetes集群
- 升級dashboard
- 領域應用概覽
- 微服務架構
- 微服務中的服務發現
- 使用Java構建微服務并發布到Kubernetes平臺
- Spring Boot快速開始指南
- Service Mesh 服務網格
- 企業級服務網格架構
- Service Mesh基礎
- Service Mesh技術對比
- 采納和演進
- 定制和集成
- 總結
- Istio
- 安裝并試用Istio service mesh
- 配置請求的路由規則
- 安裝和拓展Istio service mesh
- 集成虛擬機
- Istio中sidecar的注入規范及示例
- 如何參與Istio社區及注意事項
- Istio教程
- Istio免費學習資源匯總
- 深入理解Istio Service Mesh中的Envoy Sidecar注入與流量劫持
- 深入理解Istio Service Mesh中的Envoy Sidecar代理的路由轉發
- Linkerd
- Linkerd 使用指南
- Conduit
- Condiut概覽
- 安裝Conduit
- Envoy
- Envoy的架構與基本術語
- Envoy作為前端代理
- Envoy mesh教程
- SOFAMesh
- SOFAMesh中的Dubbo on x-protocol
- SOFAMosn
- 使用 SOFAMosn 構建 SOFAMesh
- 大數據
- Spark standalone on Kubernetes
- 運行支持Kubernetes原生調度的Spark程序
- Serverless架構
- 理解Serverless
- FaaS-函數即服務
- OpenFaaS快速入門指南
- 邊緣計算
- 人工智能