Flink on k8s 客户端提交任务源码分析
当我们需要在代码中提交Flink job到kubernetes上时,需要如何做呢?要引入什么第三方依赖?需要提供什么内容?flink是如何将job提交到k8s上的?经过了什么样的流程,内部有什么细节?
本文将通过源码来带你对上面的问题一探究竟,加深我们对Flink on k8s作业提交流程的理解,以及后续在实际的使用场景中如何自己在代码中编写提交flink job的流程。
流程图
整体的提交流程图以及相关注释如下图所示,可以大致看到提交流程中所需要的一些主要步骤和组件。接下来,我们一步步地来分析其提交流程:

一、构建FlinkConfig
既然k8s暴露了客户端提供给用户能够提交资源到其服务上,那么也就会提供一些配置参数来供用户自定义配置,提交Flink任务也是一样。提交Flink job到k8s上部署时,实际最终肯定是要生成一个对应的yaml文件,而yaml属性中肯定会存在一些可以由用户指定,或者需要配置的部分(比如容器名称、JVM参数、request/limit等)。这些属性值在构建yaml的过程中,都会封装到Flink定义的Configuration配置类中。在下游的多个方法中,都会接收Configuration对象来执行诸如创建FlinkKubeClient客户端,构建Pod/Deployment等操作。为此,我们首先来构造一个Configuration对象,并来看看可以配置哪些内容。
首先来看一下Configuration类的结构:

可以看到其核心主要就是包含了一个HashMap结构的confData属性,用来保存具体的key/value属性值。同时,该类提供了一个set方法用来向confData属性中添加键值对:
public <T> Configuration set(ConfigOption<T> option, T value) {
boolean canBePrefixMap = ConfigurationUtils.canBePrefixMap(option);
this.setValueInternal(option.key(), value, canBePrefixMap);
return this;
}
因此,我们可以使用上面的set方法来添加配置属性,比如:
// set common parameter
flinkConfig
// 设置Flink应用的名称,通常用于在Flink UI或日志中识别运行的作业。
.set(PipelineOptions.NAME, submitRequest.effectiveAppName)
// 设置Flink的部署模式。比如,它可能是local、yarn-per-job、kubernetes-application
.set(DeploymentOptions.TARGET, submitRequest.executionMode.getName)
// 配置savepoint的路径
.set(SavepointConfigOptions.SAVEPOINT_PATH, submitRequest.savePoint)
// 配置应用程序的主入口类
.set(ApplicationConfiguration.APPLICATION_MAIN_CLASS, submitRequest.appMain)
// 配置应用程序的命令行参数
.set(ApplicationConfiguration.APPLICATION_ARGS, extractProgramArgs(submitRequest))
// 配置Flink作业的固定ID
.set(PipelineOptionsInternal.PIPELINE_FIXED_JOB_ID, submitRequest.jobId)
// extract from submitRequest
flinkConfig
// 配置Flink cluster id
.set(KubernetesConfigOptions.CLUSTER_ID, submitRequest.k8sSubmitParam.clusterId)
// 配置Flink job 提交到k8s时的命名空间
.set(KubernetesConfigOptions.NAMESPACE, submitRequest.k8sSubmitParam.kubernetesNamespace)
// 配置rest service的暴露类型 LoadBalancer、ClusterIP、NodePort
.set(
KubernetesConfigOptions.REST_SERVICE_EXPOSED_TYPE,
covertToServiceExposedType(submitRequest.k8sSubmitParam.flinkRestExposedType))
// 配置Flink conf的路径
if (!flinkConfig.contains(DeploymentOptionsInternal.CONF_DIR)) {
flinkConfig.set(DeploymentOptionsInternal.CONF_DIR, s"${submitRequest.flinkVersion.flinkHome}/conf")
}
// 添加Flink容器镜像标签
flinkConfig.set(KubernetesConfigOptions.CONTAINER_IMAGE, buildResult.flinkImageTag)
// 配置k8s config文件路径
flinkConfig.set(KubernetesConfigOptions.KUBE_CONFIG_FILE, "~/.kube/config
")
...
配置JVM参数:
// 自定义方法向Configuration对象中写入JVM参数
def setJvmOptions(submitRequest: SubmitRequest, flinkConfig: Configuration): Unit = {
if (MapUtils.isNotEmpty(submitRequest.properties)) {
submitRequest.properties.foreach(x => {
val k = x._1.trim
val v = x._2.toString
if (k == CoreOptions.FLINK_JVM_OPTIONS.key()) {
// 配置应用程序运行时的全局JVM参数
flinkConfig.set(CoreOptions.FLINK_JVM_OPTIONS, v)
} else if (k == CoreOptions.FLINK_JM_JVM_OPTIONS.key()) {
// 配置JobManager运行时的JVM参数
flinkConfig.set(CoreOptions.FLINK_JM_JVM_OPTIONS, v)
} else if (k == CoreOptions.FLINK_HS_JVM_OPTIONS.key()) {
// 配置HistoryServer运行时的JVM参数
flinkConfig.set(CoreOptions.FLINK_HS_JVM_OPTIONS, v)
} else if (k == CoreOptions.FLINK_TM_JVM_OPTIONS.key()) {
// 配置TaskManager运行时的JVM参数
flinkConfig.set(CoreOptions.FLINK_TM_JVM_OPTIONS, v)
} else if (k == CoreOptions.FLINK_CLI_JVM_OPTIONS.key()) {
// 配置Flink CLI(Command Line Interface)运行时的JVM参数
flinkConfig.set(CoreOptions.FLINK_CLI_JVM_OPTIONS, v)
}
})
}
}
上面我们展示了向flinkConfig对象中添加flink相关参数、k8s环境相关参数、JVM相关参数的案例。这些参数后续会在下游的其他方法中被读取使用,用来创建相应资源的对象。
有了Configuration配置类后,就可以基于此来执行后续的一系列操作了。
二、创建KubernetesClusterDescriptor
KubernetesClusterDescriptor 是 Flink 中一个用于描述 Kubernetes 集群的类。它实现了 ClusterDescriptor 接口,该接口是表示集群描述的通用接口,它定义了获取、部署以及终止具体集群(如Yarn, Mesos, Standalone 或 Kubernetes)的方法。
KubernetesClusterDescriptor类图结构如下所示:

这个个类在Flink的源代码中负责与Kubernetes集群进行交互,包括在该集群上部署或撤销Flink集群。
其主要功能如下:
- 部署Flink集群:这个类的deploySessionCluster()、deployJobCluster()、deployApplicationCluster()方法可以分别创建一个Session模式集群和一个Per-Job模式集群(k8s不支持 Flink 1.12版本)、Application模式集群。
- 停止Flink集群:通过调用killCluster()方法,可以停止在Kubernetes集群上运行的Flink集群。
- 检索Flink集群信息:retrieve()方法可以根据提供的集群ID检索一个现有的Flink集群。
假设此次我们需要以Application模式部署Flink集群,因此我们就要创建一个KubernetesClusterDescriptor对象,然后调用deployApplicationCluster()方法执行创建。
通过上面的类图可以看到,KubernetesClusterDescriptor的构造方法需要传入Configuration对象和FlinkKubeClient对象。Configuration对象我们刚刚已经创建并初始化了,接下来就需要创建一个FlinkKubeClient对象了。
Flink提供了对应了工厂类FlinkKubeClientFactory来创建FlinkKubeClient对象,因此,我们只需要调用如下方法进行创建:
FlinkKubeClientFactory.getInstance().fromConfiguration(configuration, "client"));
进入到fromConfiguration方法中看一下做了什么事情:
public FlinkKubeClient fromConfiguration(Configuration flinkConfig, String useCase) {
final Config config;
// Kubernetes 配置文件中所需的上下文,用于配置 Kubernetes 客户端以与集群交互。 如果配置了多个上下文并且想要在不同的 Kubernetes 集群/上下文上管理不同的 Flink 集群,这可能会很有帮助
final String kubeContext = flinkConfig.getString(KubernetesConfigOptions.CONTEXT);
if (kubeContext != null) {
LOG.info("Configuring kubernetes client to use context {}.", kubeContext);
}
// 判断是否指定了kubernetes.config.file参数,如果指定了则去相应路径加载k8s config配置文件。
final String kubeConfigFile =
flinkConfig.getString(KubernetesConfigOptions.KUBE_CONFIG_FILE);
if (kubeConfigFile != null) {
LOG.debug("Trying to load kubernetes config from file: {}.", kubeConfigFile);
try {
// If kubeContext is null, the default context in the kubeConfigFile will be used.
// Note: the third parameter kubeconfigPath is optional and is set to null. It is
// only used to rewrite
// relative tls asset paths inside kubeconfig when a file is passed, and in the case
// that the kubeconfig
// references some assets via relative paths.
config =
Config.fromKubeconfig(
kubeContext,
FileUtils.readFileUtf8(new File(kubeConfigFile)),
null);
} catch (IOException e) {
throw new KubernetesClientException("Load kubernetes config failed.", e);
}
} else {
// 如果没有指定,则会去查找默认的k8s config路径,默认在~/.kube/config
LOG.debug("Trying to load default kubernetes config.");
config = Config.autoConfigure(kubeContext);
}
// 设置 Kubernetes 的命名空间,如果配置文件中没有设置 KubernetesConfigOptions.NAMESPACE,则默认为 "default"。
final String namespace = flinkConfig.getString(KubernetesConfigOptions.NAMESPACE);
LOG.debug("Setting namespace of Kubernetes client to {}", namespace);
config.setNamespace(namespace);
// 使用 config 创建了一个 DefaultKubernetesClient 实例。这个客户端可以用来和 Kubernetes API server 交互。
final NamespacedKubernetesClient client = new DefaultKubernetesClient(config);
// Kubernetes 客户端执行阻塞 IO 操作所使用的 IO 执行器池的大小(比如start/stop TaskManager Pod)
final int poolSize =
flinkConfig.get(KubernetesConfigOptions.KUBERNETES_CLIENT_IO_EXECUTOR_POOL_SIZE);
// 使用上述创建的 Kubernetes Client 和线程池创建并返回一个 Fabric8FlinkKubeClient 对象,这就是Flink用来与Kubernetes进行交互的客户端。
return new Fabric8FlinkKubeClient(
flinkConfig, client, createThreadPoolForAsyncIO(poolSize, useCase));
}
上述代码注释详细介绍了每一步的主要功能。这里的Config类是在`io.fabric8.kubernetes.


4058

被折叠的 条评论
为什么被折叠?



