Spark in action on Kubernetes - Spark Operator的原理解析

前言

在上篇文章中,向你们介绍了如何使用Spark Operator在kubernetes集群上面提交一个计算做业。今天咱们会继续使用上篇文章中搭建的Playground进行调试与解析,帮助你们更深刻的理解Spark Operator的工做原理。因此若是没有浏览过上篇文章的同窗,能够经过传送门直达,先配置好Playground的环境。node

Spark Operator的内部实现

在深刻解析Spark Operator以前,咱们先补充一些关于kubernetes operator的知识。2018年能够说是kubernetes operator泛滥的一年,各类operator如雨后春笋般出现。operator是扩展kubernetes以及与kubernetes集成的最佳方式之一。在kubernetes的设计理念中,有很重要的一条就是进行了抽象,好比对存储进行抽象、对应用负载进行抽象、对接入层进行抽象等等。每一个抽象又对应了各自生命周期管理的controller,开发者提交的Yaml其实是对抽象终态的描述,而controller会监听抽象的变化、解析并进行处理,最终尝试将状态修正到终态。api


那么对于在kubernetes中未定义的抽象该如何处理呢,答案就是operator。一个标准operator一般包含以下几个部分:1. CRD抽象的定义,负责描述抽象所能包含的功能。 2.CRD Controller ,负责解析CRD定义的内容以及生命周期的管理。3.clent-go的SDK,负责提供代码集成时使用的SDK。bash


有了这个知识储备,那么咱们回过头来看Spark Operator的代码,结构基本就比较明晰了。核心的代码逻辑都在pkg下,其中apis下面主要是定义了不一样版本的API;client目录下主要是自动生成的client-go的SDK;crd目录下主要是定义的两个自定义资源sparkapplication和scheduledsparkapplication的结构。controller目录下主要定义的就是这个operator的生命周期管理的逻辑;config目录下主要处理spark config的转换。了解一个Operator能力最快捷的方式,就是查看CRD的定义。在Spark Operator中定义了sparkapplication和scheduledsparkapplication两个CRD,他们之间有什么区别呢?app

sparkapplication 是对常规spark任务的抽象,做业是单次运行的,做业运行完毕后,全部的Pod会进入Succeed或者Failed的状态。而scheduledsparkapplication是对离线定时任务的一种抽象,开发者能够在scheduledsparkapplication中定义相似crontab的任务,实现spark离线任务的周期性定时调度。ui


上面这张图是Spark中kubernetes的集成图,也就是说当咱们经过spark-submit提交做业的时候,会自动生成driver pod与exector pods。那么引入了Spark Operator后,这个流程变成了什么呢?this

func (c *Controller) submitSparkApplication(app *v1beta1.SparkApplication) *v1beta1.SparkApplication {

    // prometheus的监控指标的暴露
    appToSubmit := app.DeepCopy()
    if appToSubmit.Spec.Monitoring != nil && appToSubmit.Spec.Monitoring.Prometheus != nil {
        if err := configPrometheusMonitoring(appToSubmit, c.kubeClient); err != nil {
            glog.Error(err)
        }
    }

  // 将CRD中的定义转变为spark-submit的命令
    submissionCmdArgs, err := buildSubmissionCommandArgs(appToSubmit)
    if err != nil {
        app.Status = v1beta1.SparkApplicationStatus{
            AppState: v1beta1.ApplicationState{
                State:        v1beta1.FailedSubmissionState,
                ErrorMessage: err.Error(),
            },
            SubmissionAttempts:        app.Status.SubmissionAttempts + 1,
            LastSubmissionAttemptTime: metav1.Now(),
        }
        return app
    }

    // 在operator容器内经过spark-submit提交做业
    submitted, err := runSparkSubmit(newSubmission(submissionCmdArgs, appToSubmit))
    if err != nil {
        app.Status = v1beta1.SparkApplicationStatus{
            AppState: v1beta1.ApplicationState{
                State:        v1beta1.FailedSubmissionState,
                ErrorMessage: err.Error(),
            },
            SubmissionAttempts:        app.Status.SubmissionAttempts + 1,
            LastSubmissionAttemptTime: metav1.Now(),
        }
        c.recordSparkApplicationEvent(app)
        glog.Errorf("failed to run spark-submit for SparkApplication %s/%s: %v", app.Namespace, app.Name, err)
        return app
    }

    // 由于Pod的状态也会被Spark Operator进行观测,所以driver pod宕掉会被从新拉起
    // 这是和直接跑spark-submit的一大区别,提供了故障恢复的能力。
    if !submitted {
        // The application may not have been submitted even if err == nil, e.g., when some
        // state update caused an attempt to re-submit the application, in which case no
        // error gets returned from runSparkSubmit. If this is the case, we simply return.
        return app
    }

    glog.Infof("SparkApplication %s/%s has been submitted", app.Namespace, app.Name)
    app.Status = v1beta1.SparkApplicationStatus{
        AppState: v1beta1.ApplicationState{
            State: v1beta1.SubmittedState,
        },
        SubmissionAttempts:        app.Status.SubmissionAttempts + 1,
        ExecutionAttempts:         app.Status.ExecutionAttempts + 1,
        LastSubmissionAttemptTime: metav1.Now(),
    }
    c.recordSparkApplicationEvent(app)

    // 经过service暴露spark-ui
    service, err := createSparkUIService(app, c.kubeClient)
    if err != nil {
        glog.Errorf("failed to create UI service for SparkApplication %s/%s: %v", app.Namespace, app.Name, err)
    } else {
        app.Status.DriverInfo.WebUIServiceName = service.serviceName
        app.Status.DriverInfo.WebUIPort = service.nodePort
        // Create UI Ingress if ingress-format is set.
        if c.ingressURLFormat != "" {
            ingress, err := createSparkUIIngress(app, *service, c.ingressURLFormat, c.kubeClient)
            if err != nil {
                glog.Errorf("failed to create UI Ingress for SparkApplication %s/%s: %v", app.Namespace, app.Name, err)
            } else {
                app.Status.DriverInfo.WebUIIngressAddress = ingress.ingressURL
                app.Status.DriverInfo.WebUIIngressName = ingress.ingressName
            }
        }
    }
    return app
}
复制代码

其实到此,咱们就已经基本了解Spark Operator作的事情了,首先定义了两种不一样的CRD对象,分别对应普通的计算任务与定时周期性的计算任务,而后解析CRD的配置文件,拼装成为spark-submit的命令,经过prometheus暴露监控数据采集接口,建立Service提供spark-ui的访问。而后经过监听Pod的状态,不断回写更新CRD对象,实现了spark做业任务的生命周期管理。spa

Spark Operator的任务状态机

当咱们了解了Spark Operator的设计思路和基本流程后,还须要深刻了解的就是sparkapplication的状态都包含哪些,他们之间是如何进行转换的,由于这是Spark Operator对于生命周期管理加强最重要的部分。设计


一个Spark的做业任务能够经过上述的状态机转换图进行表示,一个正常的做业任务经历以下几个状态:3d

New -> Submitted -> Running -> Succeeding -> Completed
复制代码

而当任务失败的时候会进行重试,若重试超过最大重试次数则会失败。也就是说若是在任务的执行过程当中,因为资源、调度等因素形成Pod被驱逐或者移除,Spark Operator都会经过自身的状态机状态转换进行重试。调试

Spark Operator的状态排查

咱们已经知道了Spark Operator最核心的功能就是将CRD的配置转换为spark-submit的命令,那么当一个做业运行不预期的时候,咱们该如何判断是哪一层出现的问题呢?首先咱们要判断的就是spark-submit时所生成的参数是不是预期的,由于CRD的Yaml配置虽然能够加强表达能力,可是提升了配置的难度与出错的可能性。

func runSparkSubmit(submission *submission) (bool, error) {
    sparkHome, present := os.LookupEnv(sparkHomeEnvVar)
    if !present {
        glog.Error("SPARK_HOME is not specified")
    }
    var command = filepath.Join(sparkHome, "/bin/spark-submit")

    cmd := execCommand(command, submission.args...)
    glog.V(2).Infof("spark-submit arguments: %v", cmd.Args)
    if _, err := cmd.Output(); err != nil {
        var errorMsg string
        if exitErr, ok := err.(*exec.ExitError); ok {
            errorMsg = string(exitErr.Stderr)
        }
        // The driver pod of the application already exists.
        if strings.Contains(errorMsg, podAlreadyExistsErrorCode) {
            glog.Warningf("trying to resubmit an already submitted SparkApplication %s/%s", submission.namespace, submission.name)
            return false, nil
        }
        if errorMsg != "" {
            return false, fmt.Errorf("failed to run spark-submit for SparkApplication %s/%s: %s", submission.namespace, submission.name, errorMsg)
        }
        return false, fmt.Errorf("failed to run spark-submit for SparkApplication %s/%s: %v", submission.namespace, submission.name, err)
    }

    return true, nil
}
复制代码

默认状况下Spark Operator会经过glog level=2等级对外输出每次做业提交后转换的提交命令。而默认状况下,glog的level即为2,所以经过检查Spark Operator的Pod日志能够协助开发者快速排查问题。此外在sparkapplication上面也会经过event的方式进行状态的记录,上述状态机之间的转换都会经过event的方式体如今sparkapplication的对象上。掌握这两种方式进行问题排查,能够节省大量排错时间。

最后

使用Spark Operator是在kubernetes上实践spark的最佳方式,和传统的spark-submit相比提供了更多的故障恢复与可靠性保障,而且提供了监控、日志、UI等能力的集成与支持。在下一篇中,会为你们介绍在kubernetes集群中,提交spark做业时的如何使用外部存储存储的最佳实践。


本文做者:莫源

原文连接

本文为云栖社区原创内容,未经容许不得转载。

相关文章
相关标签/搜索