1 Overview
本文主要講 Apache Spark 在 on Kubernetes 的 PodTemplate 的問題,以及也會講到 Spark Operator 里關(guān)于 PodTemplate 的問題,當(dāng)然也會講到 Apache Spark 2.2 on Kubernetes 那個(gè) Fork 的版本,感興趣的同學(xué)可以往下看看。
之前講過 Apache Spark on Kubernetes 在配置 Pod 的時(shí)候的一些限制,比如針對 Pod 的調(diào)度,想加個(gè) NodeSelector 或者 Tolerations。這在集群公用,或者有各種類型任務(wù)的集群里,是經(jīng)常會遇到的情況,而在 Spark 2.x 里是很難做到的。
目前最新 Release 的版本 2.4.5 還沒有支持通過 PodTemplate 來自定義 Pod 的配置,而社區(qū)的計(jì)劃是在 Spark 3.0 的時(shí)候?qū)⑦@一 feature 完成,他支持的方式其實(shí)也比較簡單,就是可以有一個(gè) PodTemplate 的一個(gè)文件,去描述 Driver/Executor 的 metadata/spec 字段,這樣當(dāng)然就可以在模板文件里加入跟調(diào)度需要的一些字段了,
關(guān)于 PodTemplate 可以帶來什么呢?比如說其實(shí) Apache Spark 2.2 on Kubernetes 一開始是支持 initContainer 的,當(dāng)時(shí)可以通過 spark.kubernetes.initcontainer.docker.image
來配置 Pod 的 initContainer 但是隨著版本的演進(jìn),關(guān)于 initContainer 的代碼已經(jīng)去掉了,可以想象,如果只通過幾個(gè) SparkConf 來配置 initContainer 的話,這樣限制實(shí)現(xiàn)太多了,SparkConf 的表達(dá)能力有限,如果都通過 spark.kubernetes.driver.label.*
這樣的 SparkConf 來配置的話,既不靈活,也讓 SparkConf 的配置數(shù)量急劇膨脹。
那么現(xiàn)在如果用戶想通過 initContainer 做一些事情那可以怎么辦?在 Spark 2.x 的版本里,應(yīng)該是沒有辦法的,除非通過一些迂回的辦法來實(shí)現(xiàn)原先你想通過 intContainer 達(dá)到的目標(biāo),比如說將一個(gè)文件提交下載到 Volume 并進(jìn)行掛載這類操作,又或者直接去改下源碼。
具體可以參考在 SPARK-24434 Support user-specified driver and executor pod templates 的相關(guān)討論。不論 initContainer 的邏輯怎么樣了,至少現(xiàn)在用戶可以通過 PodTemplate 來自定義 Pod,當(dāng)然包括定義需要的 initContainer,以及跟調(diào)度相關(guān)的一些字段。
2 PodTemplate
實(shí)際上,如果是在 Spark Operator 里,本身就支持 Pod Template 的配置 SparkPodSpec,也就是說,像 NodeSelector, Tolerations 之類的,可以在創(chuàng)建 CRD 對象的時(shí)候在 YAML 上添加上,比如下面的例子。
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
name: spark-pi
namespace: default
spec:
type: Scala
mode: cluster
image: gcr.io/spark/spark:v2.4.5
mainClass: org.apache.spark.examples.SparkPi
mainApplicationFile: local:///opt/spark/examples/jars/spark-examples_2.11-2.4.5.jar
nodeSelector:
key: value
所以之前的文章也有說過 Spark Operator 的配置上,會更加靈活。
而在 Apache Spark 3.0 中,PodTemplate 是需要在 spark-submit
階段將模板文件加到 spark.kubernetes.driver.podTemplateFile
或者 spark.kubernetes.executor.podTemplateFile
里的。
大家都知道 Spark 在下載依賴文件的時(shí)候,可以通過 HTTP/HDFS/S3 等協(xié)議來下載需要的文件。但是讀取 PodTemplate 的 API,目前只支持本地文件系統(tǒng)(當(dāng)然要改成支持 http 也不是很復(fù)雜),SparkConf 的配置可能如下。
# template 在本地
spark.kubernetes.driver.podTemplateFile=/opt/spark/template.yaml
spark.kubernetes.executor.podTemplateFile=/opt/spark/template.yaml
關(guān)于 Apache Spark 3.0 是如何加載這些 PodTemplate 的文件,我們可以看看源碼。在將 PodTemplate 文件加載到系統(tǒng)里的關(guān)鍵方法是是 KubernetesUtils.loadPodFromTemplate()
。
def loadPodFromTemplate(
kubernetesClient: KubernetesClient,
templateFile: File,
containerName: Option[String]): SparkPod = {
try {
// 主要的還是利用 K8S 的客戶端去 load 模板文件
// load 模板文件目前只能支持本地文件系統(tǒng),因?yàn)榈讓诱{(diào)用的是 File 接口
val pod = kubernetesClient.pods().load(templateFile).get()
// 這里需要注意會從模板里把指定 Container 撈出來
// 目的主要是撈出來 Driver 和 Executor 容器
// 否則就是以第一個(gè)容器作為 Driver/Executor 的容器
selectSparkContainer(pod, containerName)
} catch {
case e: Exception =>
logError(
s"Encountered exception while attempting to load initial pod spec from file", e)
throw new SparkException("Could not load pod from template file.", e)
}
}
通過上述方法就可以利用 PodTemplate 來做一些 Pod 的定義了,避免了大量極其繁瑣的 SparkConf 的配置。如果想在 Apache Spark 3.0 之前的版本去實(shí)現(xiàn) NodeSelector/Toleration 這些操作,直接通過 SparkConf 是不行的。后面 Driver/Executor 的 Pod 構(gòu)建就分別交給 KubernetesDriverBuilder
和 KubernetesExecutorBuilder
去做了。而在執(zhí)行 spark-submit
的環(huán)境中,需要去讀取 PodTemplate 文件,然后通過 ConfigMap 來掛載到 Driver/Executor Pod。當(dāng)然了,我覺得這樣還是不夠靈活,因?yàn)?Executor 的 PodTemplate 也可以在 Spark 鏡像里,不需要一定要在 spark-submit
的環(huán)境里,目前的做法,如果是使用本地文件的話,就必須在 spark-submit
的本地環(huán)境了,而我覺得沒必要,不過我們還是可以改成通過 http 等方式,讓本地可以讀取到這些 PodTemplate 文件的,只是你還需要一個(gè)文件服務(wù)器去放這些 PodTemplate 的文件。
因?yàn)橥ㄟ^ PodTemplate 來引導(dǎo)定義的操作相對來說是比較前置的,所以有些屬性,可能會被后面針對 Pod 的其他配置給 overwrite,在 Spark 的最新文檔的 running-on-kubernetes,可以找到那些屬性可能會被后置配置覆蓋掉。
下面是關(guān)于 Spark Driver Pod 是怎么通過各種 Step 按順序最后給構(gòu)建出來的示意圖。
val features = Seq(
new BasicDriverFeatureStep(conf),
new DriverKubernetesCredentialsFeatureStep(conf),
new DriverServiceFeatureStep(conf),
new MountSecretsFeatureStep(conf),
new EnvSecretsFeatureStep(conf),
new MountVolumesFeatureStep(conf),
new DriverCommandFeatureStep(conf),
new HadoopConfDriverFeatureStep(conf),
new KerberosConfDriverFeatureStep(conf),
new PodTemplateConfigMapStep(conf),
new LocalDirsFeatureStep(conf))
val spec = KubernetesDriverSpec(
initialPod,
driverKubernetesResources = Seq.empty,
conf.sparkConf.getAll.toMap)
features.foldLeft(spec) { case (spec, feature) =>
val configuredPod = feature.configurePod(spec.pod)
val addedSystemProperties = feature.getAdditionalPodSystemProperties()
val addedResources = feature.getAdditionalKubernetesResources()
KubernetesDriverSpec(
configuredPod,
spec.driverKubernetesResources ++ addedResources,
spec.systemProperties ++ addedSystemProperties)
}
看完整個(gè)過程,可以發(fā)現(xiàn),裝配 Driver Pod 的步驟竟然如此復(fù)雜。這個(gè)設(shè)計(jì)也是延續(xù)了 Spark 2.2 on K8S 那個(gè) Fork 的思路。
- 通過自定義鏡像,將 PodTemplate 文件置入鏡像的某個(gè)目錄中,如
/opt/spark/template.yaml
- 然后在 SparkConf 填入?yún)?shù)
spark.kubernetes.driver.podTemplateFile=/opt/spark/template/driver.yaml
- 如果 Pod 里準(zhǔn)備起其他容器,則需要在 SparkConf 指定 Driver Container 的名字,例如
spark.kubernetes.driver.podTemplateContainerName=driver-container
3 Example
下面給出一個(gè)例子,來給 Spark 的 Drvier/Executor 都加一個(gè) initContainer,將 PodTemplate 文件 template-init.yaml
放在 /opt/spark
目錄下,下面是 PodTemplate 的具體內(nèi)容,就是加一個(gè)會 sleep 1s
的 initContainer。
apiversion: v1
kind: Pod
spec:
initContainers:
- name: init-s3
image: hub.oa.com/runzhliu/busybox:latest
command: ['sh', '-c', 'sleep 1']
SparkConf 需要加上
spark.kubernetes.driver.podTemplateFile=/opt/spark/template-init.yaml
spark.kubernetes.executor.podTemplateFile=/opt/spark/template-init.yaml
運(yùn)行一個(gè) SparkPi 的例子,spark-submit
命令如下。
/opt/spark/bin/spark-submit
--deploy-mode=cluster
--class org.apache.spark.examples.SparkPi
--master=k8s://https://172.17.0.1:443
--conf spark.kubernetes.namespace=demo
--conf spark.kubernetes.driver.container.image=hub.oa.com/public/spark:v3.0.0-template
--conf spark.kubernetes.executor.container.image=hub.oa.com/public/spark:v3.0.0-template
--conf=spark.driver.cores=1
--conf=spark.driver.memory=4096M
--conf=spark.executor.cores=1
--conf=spark.executor.memory=4096M
--conf=spark.executor.instances=2
--conf spark.kubernetes.driver.podTemplateFile=/opt/spark/template-init.yaml
--conf spark.kubernetes.executor.podTemplateFile=/opt/spark/template-init.yaml
--conf=spark.kubernetes.executor.deleteOnTermination=false
local:///opt/spark/examples/jars/spark-examples_2.12-3.0.0-SNAPSHOT.jar
100
運(yùn)行結(jié)束,查看一下 Driver Pod 的 YAML 文件,發(fā)現(xiàn) initContainer 已經(jīng)加上,并且運(yùn)行正常。
...
initContainers:
- command:
- sh
- -c
- sleep 1
image: hub.oa.com/runzhliu/busybox:latest
imagePullPolicy: Always
name: init
resources: {}
...
initContainerStatuses:
- containerID: docker://526049a9a78c4b29d4e4f7b5fcc89935d44c0605bcbf427456c7d7bdf39a6172
image: hub.oa.com/runzhliu/busybox:latest
lastState: {}
name: init
ready: true
restartCount: 0
state:
terminated:
containerID: docker://526049a9a78c4b29d4e4f7b5fcc89935d44c0605bcbf427456c7d7bdf39a6172
exitCode: 0
finishedAt: "2020-04-02T00:03:35Z"
reason: Completed
startedAt: "2020-04-02T00:03:34Z"
PodTemplate 文件里,有幾個(gè)事情需要注意一下的,就是大小寫要符合 Kubernetes 的規(guī)范,比如 Pod 不能寫成 pod,initContainer 不能寫成 initcontainer,否則是不生效的。
4 Summary
Apache Spark 3.0 支持 PodTemplate,所以用戶在配置 Driver/Executor 的 Pod 的時(shí)候,會更加靈活,但是 Spark 本身是不會校驗(yàn) PodTemplate 的正確性的,所以這也給調(diào)試帶來了很多麻煩。關(guān)于 NodeSelector, Taints, Tolerations 等,這些字段在 Spark Operator 中設(shè)置,倒是比較方便的。