以 Flink 和 Spark 为代表的分布式流批计算框架的下层资源管理平台逐渐从 Hadoop 生态的 YARN 转向 Kubernetes 生态的 k8s 原生 scheduler 以及周边资源调度器,比如 Volcano 和 Yunikorn 等。这篇文章简单比较一下两种计算框架在 Native Kubernetes 的支持和实现上的异同,以及对于应用到生产环境我们还需要做些什么。
1. 什么是 Native
这里的 native 其实就是计算框架直接向 Kubernetes 申请资源。比如很多跑在 YARN 上面的计算框架,需要自己实现一个 AppMaster 来想 YARN 的 ResourceManager 来申请资源。Native K8s 相当于计算框架自己实现一个类似 AppMaster 的角色向 k8s 去申请资源,当然和 AppMaster 还是有差异的 (AppMaster 需要按 YARN 的标准进行实现)。
2. Spark on k8s 使用
提交作业
向 k8s 集群提交作业和往 YARN 上面提交很类似,命令如下,主要区别包括:
- --master 参数指定 k8s 集群的 ApiServer
- 需要通过参数 spark.kubernetes.container.image 指定在 k8s 运行作业的 image,
- 指定 main jar,需要 driver 进程可访问:如果 driver 运行在 pod 中,jar 包需要包含在镜像中;如果 driver 运行在本地,那么 jar 需要在本地。
- 通过 --name 或者 spark.app.name 指定 app 的名字,作业运行起来之后的 driver 命名会以 app 名字为前缀。当然也可以通过参数 spark.kubernetes.driver.pod.name 直接指定 dirver 的名字
$ ./bin/spark-submit \
--master k8s://https://: \
--deploy-mode cluster \
--name spark-pi \
--class org.apache.spark.examples.SparkPi \
--conf spark.executor.instances=5 \
--conf spark.kubernetes.container.image= \
local:///path/to/examples.jar
提交完该命令之后,spark-submit 会创建一个 driver pod 和一个对应的 servcie,然后由 driver 创建 executor pod 并运行作业。
deploy-mode
和在 YARN 上面使用 Spark 一样,在 k8s 上面也支持 cluster 和 client 两种模式:
- cluster mode: driver 在 k8s 集群上面以 pod 形式运行。
- client mode: driver 运行在提交作业的地方,然后 driver 在 k8s 集群上面创建 executor。为了保证 executor 能够注册到 driver 上面,还需要提交作业的机器可以和 k8s 集群内部的 executor 网络连通(executor 可以访问到 driver,需要注册)。
资源清理
这里的资源指的主要是作业的 driver 和 executor pod。spark 通过 k8s 的 onwer reference 机制将作业的各种资源连接起来,这样当 driver pod 被删除的时候,关联的 executor pod 也会被连带删除。但是如果没有 driver pod,也就是以 client 模式运行作业的话,如下两种情况涉及到资源清理:
- 作业运行完成,driver 进程退出,executor pod 运行完自动退出
- driver 进程被杀掉,executor pod 连不上 driver 也会自行退出
可以参考:
https://kubernetes.io/docs/concepts/architecture/garbage-collection/
依赖管理
前面说到 main jar 包需要在 driver 进程可以访问到的地方,如果是 cluster 模式就需要将 main jar 打包到 spark 镜像中。但是在日常开发和调试中,每次重新 build 一个镜像的 effort 实在是太大了。spark 支持提交的时候使用本地的文件,然后使用 s3 等作为中转:先上传上去,然后作业运行的时候再从 s3 上面下载下来。下面是一个实例。
...
--packages org.apache.hadoop:hadoop-aws:3.2.0
--conf spark.kubernetes.file.upload.path=s3a:///path
--conf spark.hadoop.fs.s3a.access.key=...
--conf spark.hadoop.fs.s3a.impl=org.apache.hadoop.fs.s3a.S3AFileSystem
--conf spark.hadoop.fs.s3a.fast.upload=true
--conf spark.hadoop.fs.s3a.secret.key=....
--conf spark.driver.extraJavaOptions=-Divy.cache.dir=/tmp -Divy.home=/tmp
file:///full/path/to/app.jar
Pod Template
k8s 的 controller (比如 Deployment,Job)创建 Pod 的时候根据 spec 中的 pod template 来创建。下面是一个 Job 的示例。
apiVersion: batch/v1
kind: Job
metadata:
name: hello
spec:
template:
# 下面的是一个 pod template
spec:
containers:
- name: hello
image: busybox
command: ['sh', '-c', 'echo "Hello, Kubernetes!" && sleep 3600']
restartPolicy: OnFailure
# The pod template ends here
由于我们通过 spark-submit 提交 spark 作业的时候,最终的 k8s 资源(driver/executor pod)是由 spark 内部逻辑构建出来的。但是有的时候我们想要在 driver/executor pod 上做一些额外的工作,比如增加 sidecar 容器做一些日志收集的工作。这种场景下 PodTemplate 就是一个比较好的选择,同时 PodTemplate 也将 spark 和底层基础设施(k8s)解耦开。比如 k8s 发布新版本支持一些新的特性,那么我们只要修改我们的 PodTemplate 即可,而不涉及到 spark 的内部改动。
RBAC
RBAC 全称是 Role-based access control,是 k8s 中的一套权限控制机制。通俗来说:
- RBAC 中包含了一系列的权限设置,比如 create/delete/watch/list pod 等,这些权限集合的实体叫 Role 或者 ClusterRole
- 同时 RBAC 还包含了角色绑定关系(Role Binding),用于将 Role/ClusterRole 赋予一个或者一组用户,比如 Service Account 或者 UserAccount
为了将 Spark 作业在 k8s 集群中运行起来,我们还需要一套 RBAC 资源:
- 指定 namespace 下的 serviceaccount
- 定义了权限规则的 Role 或者 ClusterRole,我们可以使用常见的 ClusterRole "edit"(对几乎所有资源具有操作权限,比如 create/delete/watch 等)
- 绑定关系
下面命令在 spark namespace 下为 serviceaccount spark 赋予了操作同 namespace 下其他资源的权限,那么只要 spark 的 driver pod 挂载了该 serviceaccount,它就可以创建 executor pod 了。
$ kubectl create serviceaccount spark
$ kubectl create clusterrolebinding spark-role --clusterrole=edit --serviceaccount=spark:spark --namespace=spark
下面做一个简单的演示:
通过如下命令提交作业 SparkPiSleep 到 k8s 集群中。
$ spark-submit --master k8s://https://: --deploy-mode cluster --class org.apache.spark.examples.SparkPiSleep --conf spark.executor.memory=2g --conf spark.driver.memory=2g --conf spark.driver.core=1 --conf spark.app.name=test12 --conf spark.kubernetes.submission.waitAppCompletion=false --conf spark.executor.core=1 --conf spark.kubernetes.container.image= --conf spark.eventLog.enabled=false --conf spark.shuffle.service.enabled=false --conf spark.executor.instances=1 --conf spark.dynamicAllocation.enabled=false --conf spark.kubernetes.namespace=spark --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark --conf spark.executor.core=1 local:///path/to/main/jar
查看 k8s 集群中的资源
$ kubectl get po -n spark
NAME READY STATUS RESTARTS AGE
spark-pi-5b88a27b576050dd-exec-1 0/1 ContainerCreating 0 2s
test12-9fd3c27b576039ae-driver 1/1 Running 0 8s
其中第一个就是 executor pod,第二个是 driver 的 pod。除此之外还创建了一个 service,可以通过该 service 访问到 driver pod,比如 Spark UI 都可以这样访问到。
$ kubectl get svc -n spark
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
test12-9fd3c27b576039ae-driver-svc ClusterIP None 7078/TCP,7079/TCP,4040/TCP 110s
下面再看一下 service owner reference,executor pod 也是类似的。
$ kubectl get svc test12-9fd3c27b576039ae-driver-svc -n spark -oyaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: "2021-08-18T03:48:50Z"
name: test12-9fd3c27b576039ae-driver-svc
namesp