Flink源码解析之:Flink on k8s 客户端提交任务源码分析

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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JermeryBesian

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值