云原生 DevOps 落地方案

点击查看目录

DevOps 简述

顾名思义,DevOps 就是开发(Development)与运维(Operations)的结合体,其目的就是打通开发与运维之间的壁垒,促进开发、运营和质量保障(QA)等部门之间的沟通协作,以便对产品进行小规模、快速迭代式地开发和部署,快速响应客户的需求变化。它强调的是开发运维一体化,加强团队间的沟通和快速反馈,达到快速交付产品和提高交付质量的目的。

DevOps 并不是一种新的工具集,而是一种思想,一种文化,用以改变传统开发运维模式的一组最佳实践。一般做法是通过一些 CI/CD(持续集成、持续部署)自动化的工具和流程来实现 DevOps 的思想,以流水线(pipeline)的形式改变传统开发人员和测试人员发布软件的方式。随着 Docker 和 Kubernetes(以下简称 k8s)等技术的普及,容器云平台基础设施越来越完善,加速了开发和运维角色的融合,使云原生的 DevOps 实践成为以后的趋势。下面我们基于混合容器云平台详细讲解下云平台下 DevOps 的落地方案。

云原生 DevOps 特点

DevOps 是 PaaS 平台里很关键的功能模块,包含以下重要能力:支持代码克隆、编译代码、运行脚本、构建发布镜像、部署 yaml 文件以及部署 Helm 应用等环节;支持丰富的流水线设置,比如资源限额、流水线运行条数、推送代码以及推送镜像触发流水线运行等,提供了用户在不同环境下的端到端高效流水线能力;提供开箱即用的镜像仓库中心;提供流水线缓存功能,可以自由配置整个流水线或每个步骤的运行缓存,在代码克隆、编译代码、构建镜像等步骤时均可利用缓存大大缩短运行时间,提升执行效率。具体功能清单如下:

  • 缓存加速:自研容器化流水线的缓存技术,通过代码编译和镜像构建的缓存复用,平均加速流水线 3~5 倍;
  • 细粒度缓存配置:任一阶段、步骤可以控制是否开启缓存及缓存路径;
  • 支持临时配置:用户无需提交即可运行临时配置,避免频繁提交配置文件污染代码仓库;
  • 开箱即用的镜像仓库;
  • 提供完整的日志功能;
  • 可视化编辑界面,灵活配置流水线;
  • 支持多种代码仓库授权:GitHub、GitLab、Bitbucket 等;
  • 多种流水线触发方式:代码仓库触发,镜像推送触发等;
  • 网络优化,加快镜像或依赖包的下载速度;

云原生 DevOps 实现

简单地说,云原生 DevOps 内部功能的设计基本上均是通过 k8s 提供的自定义 controller 功能来实现的,基本逻辑就是根据业务需要抽象出多个 CRD(Custom Resource Definition,自定义资源对象),并编写对应的 controller 来实现业务逻辑。为了实现 CI/CD 功能,我们抽象出了多个 CRD 对象,如下图所示:

图 1 CRD 对象定义
图 1 CRD 对象定义

图 1 CRD 对象定义

我们知道配置流水线通常需要对接代码仓库,包括仓库地址,仓库授权信息等,因此我们需要有 3 个 CRD 对象来记录源代码仓库的相关信息。

  • sourceCodeProviderConfig:记录仓库 OAuth Apps 的客户端 ID、客户端秘钥;
  • sourceCodeCredential:记录仓库的认证信息;
  • sourceCodeRepository:记录仓库地址等信息。

设计好了 DevOps 中与仓库相关的 3 个 CRD 对象后,我们需要再定义 3 个 CRD 对象来描述流水线相关的信息。

  • pipeline:记录该流水线的配置信息:仓库的认证信息、钩子触发配置以及项目代码地址等等;
  • pipelineExecution:记录流水线运行时信息与执行结果信息等;
  • pipelineSetting:记录整个项目下 pipeline 运行环境信息:内存、CPU 的限制,最大流水线并行运行个数等等。

pipeline 步骤功能有很多种类型,包括运行脚本、构建发布镜像、发布应用模板、部署 YAML、部署应用等等。为了提供这些功能,我们采用 Jenkins 作为底层的 CI/CD 工具,docker registry 作为镜像仓库中心,minio 作为日志存储中心等等。这些服务是运行在 pipeline 所在项目的命名空间下。综上,我们设计的 CI/CD 系统功能的实现逻辑如图所示:

图 2 逻辑示意图
图 2 逻辑示意图

图 2 逻辑示意图

如上,当第一次运行流水线时,系统会在数据面 k8s 中部署 Jenkins、minio 等基础工具的服务,同时在管理面启动一个 goroutine,实时同步数据面中流水线的作业状态到管理面的 CRD 对象中。当触发 pipeline 执行逻辑时,会产生一个 pipelineExecution CRD 对象,以记录本次运行 pipeline 的状态信息。当 goroutine(syncState)发现有新的执行实例产生时,就会通过 Jenkins 引擎接口启动 Jenkins server 端流水线作业的运行,Jenkins server 端收到信息后会启动单独的一个 Jenkins slave pod 进行流水线作业的响应。同时,goroutine(syncState)会不断地通过引擎接口轮询 pipeline 执行实例的运行情况进而更新 pipelineExecution CRD 的状态(运行成功或失败等等)。当 pipeline 执行实例发生状态变化时,就会触发其对应的 controller 业务逻辑,进而通过 Jenkins 引擎接口与 Jenkins server 通信进行不同的操作,比如,暂停流水线的运行,运行完清除不需要的资源等等。当流水线作业发生状态变化时,又会通过 goroutine(syncState)更改 pipeline 执行实例的状态,进而又触发对应的 controller 业务代码进行不同的业务逻辑处理,往复循环,直到流水线运行结束。这就是整个 pipeline 执行时的一个逻辑流程。

CRD 定义

下面是详细的 CRD 结构体讲解,敏感信息使用了’*‘代替。

pipelineSetting:该结构体保存着整个项目下所有 pipeline 的运行环境信息,比如 CPU/内存资源限额、缓存路径以及流水线运行的最大并行个数等等,不同功能的配置信息保存在不同的 CRD 下。

devops-cache-dir            12d
executor-cpu-limit          12d
executor-cpu-request        12d
executor-memory-limit       12d
executor-memory-request     12d
executor-quota              12d
...

// 比如,看下 executor-quota 详细信息
apiVersion: project.cubepaas.com/v3
kind: PipelineSetting
metadata:
  labels:
    cubepaas.com/creator: linkcloud
  name: executor-quota
  namespace: p-zwmcv
default: "2" // 默认最多可同时运行 2 个 pipeline
projectName: c-86tgg:p-zwmcv
value: "3" // 自定义设置,最多可同时运行 3 个 pipeline,没有值会取上面默认值

pipeline:该结构体记录着流水线的配置元信息,比如该流水线对接哪个项目代码、与仓库通信的认证信息以及上次该流水线运行的结果等等。如下图所示:

图 3 流水线列表
图 3 流水线列表

图 3 流水线列表

详细的结构字段讲解如下:

apiVersion: project.cubepaas.com/v3
kind: Pipeline
metadata:
  labels:
    cubepaas.com/creator: linkcloud
  name: p-d5frn
  namespace: p-zwmcv
spec:
  currentBranch: master // 流水线运行时默认代码分支
  imageWebHookToken: // 这是推送镜像时触发该流水线运行的设置信息
  - branches:
    - master
    comment: a
    imageType: harbor // 支持 harbor dockerhub aliyun 等镜像仓库
    token: 7c102c82-66d9-44c1-8718-**** // 推送镜像触发流水线运行时的 token 认证 
    trigger: nginx // 当推送 nginx 镜像时会触发流水线运行
  projectName: c-86tgg:p-zwmcv
  repositoryUrl: https://github.com/gophere/devops-go.git // 项目代码地址
  sourceCodeCredentialName: u-8sq**:p-zwmcv-github-gophere // 指向对应的用户认证信息
  triggerWebhookPush: true // 钩子操作,当 push 代码到仓库时会触发该流水线执行
status:
  lastRunState: Success // 最新一次运行的最后结果
  nextRun: 2 // 下次运行时执行实例对应的序号
  pipelineState: active // 该流水线处于有效状态
  sourceCodeCredential: // 上述已介绍,此处不再赘述
    ...
    ...
  token: e667bbb9-7230-48d4-9d29-***** // 用于代码仓库触发流水线运行时的 token 认证
  webhookId: "245901183" // 代码仓库的钩子信息

pipelineExecution:流水线执行实例,每当流水线运行一次,会产生一个该对象记录着流水线的执行结果等信息。如下图所示:

图 4 流水线执行记录列表
图 4 流水线执行记录列表

图 4 流水线执行记录列表

详细的结构字段讲解如下:

apiVersion: project.cubepaas.com/v3
kind: PipelineExecution
metadata:
  labels:
    cubepaas.com/creator: linkcloud
    pipeline.project.cubepaas.com/finish: "true"
  name: p-d5frn-2
  namespace: p-zwmcv
spec:
  branch: master // 本次运行的代码分支
  commit: f5b78969586cd90918020cb7a138fe88c7e25f9d // 代码 commitid
  message: Update .cubepaas-devops.yml // 代码 commit 说明
  pipelineConfig: // 以下是 pipeline 具体的 stage 和 step 的配置信息,每次运行时从代码仓库的配置文件(.cubepaas.devops.ymal)拉取下来填充该结构
    stages:
    - name: Clone // 克隆代码
      steps:
      - sourceCodeConfig: {}
    - name: Build // 运行脚本编译代码
      ...
  pipelineName: p-zwmcv:p-d5frn
  projectName: c-86tgg:p-zwmcv
  ref: master
  repositoryUrl: https://github.com/gophere/devops-go.git // 项目代码地址
  run: 2 // 此次运行序号
  triggerUserName: u-8sq** // 触发用户
  triggeredBy: user
status: // 以下记录着 pipeline 每个 stage 和 step 的运行结果信息
  executionState: Success
  stages:
  - ended: 2020-09-03T06:01:01Z
    state: Success
    steps:
    - ended: 2020-09-03T06:01:01Z
      state: Success
  ...
  ...

至此,我们完成了流水线功能的基础对象定义。

controller 实现

除了抽象出对应的 CRD 外,我们还需要编写对应的 controller 代码实现对应的业务逻辑,比如当 pipeline 运行时,我们需要产生 pipeline 执行实例,并实时同步其运行的状态信息等等。

当触发流水线执行逻辑时,系统会根据 pipeline CRD 对象和该流水线对应的代码仓库中的配置文件(.cubepaas.devops.ymal)产生一个 pipelineExecution CRD 对象,这时会触发 pipelineExecution 对应的 controller 运行业务逻辑。下面只摘取重要的代码逻辑,如下所示:

func (l *Lifecycle) Sync(obj *v3.PipelineExecution) (runtime.Object, error) {

    ...

    // 如果 pipeline 执行实例被暂停,则会停止流水线作业
    if obj.Status.ExecutionState == utils.StateAborted {
        if err := l.doStop(obj); err != nil {
            return obj, err
        }
    }

    // 如果 pipeline 执行实例运行完毕,则会清理流水线作业的一些资源
    // 比如,产生的 Jenkins slave pod
    if obj.Labels != nil && obj.Labels[utils.PipelineFinishLabel] == "true" {
        return l.doFinish(obj)
    }

    // 如果 pipeline 执行实例正在运行中,则直接返回,无操作
    if v3.PipelineExecutionConditionInitialized.GetStatus(obj) != "" {
        return obj, nil
    }

    // 判断流水线作业是否超出资源限额
    exceed, err := l.exceedQuota(obj)
    if err != nil {
        return obj, err
    }
    // 如果超出资源限额,则会设置当前 pipeline 执行实例为阻塞状态
    if exceed {
        obj.Status.ExecutionState = utils.StateQueueing
        obj.Labels[utils.PipelineFinishLabel] = ""

        if err := l.newExecutionUpdateLastRunState(obj); err != nil {
            return obj, err
        }

        return obj, nil
    } else if obj.Status.ExecutionState == utils.StateQueueing {
        obj.Status.ExecutionState = utils.StateWaiting
    }

    // 更新 pipeline 执行实例的状态:比如运行序号 +1
    if err := l.newExecutionUpdateLastRunState(obj); err != nil {
        return obj, err
    }
    v3.PipelineExecutionConditionInitialized.CreateUnknownIfNotExists(obj)
    obj.Labels[utils.PipelineFinishLabel] = "false"

    // 在数据面部署 pipeline 功能所需资源
    if err := l.deploy(obj.Spec.ProjectName); err != nil {
        obj.Labels[utils.PipelineFinishLabel] = "true"
        obj.Status.ExecutionState = utils.StateFailed
        v3.PipelineExecutionConditionInitialized.False(obj)
        v3.PipelineExecutionConditionInitialized.ReasonAndMessageFromError(obj, err)
    }

    // 将 configMap 存储的 docker 镜像仓库端口信息同步到 pipeline 执行实例中去。
    if err := l.markLocalRegistryPort(obj); err != nil {
        return obj, err
    }

    return obj, nil
}

其中,deploy 函数的逻辑就是第一次运行时通过判断数据面中是否存在 pipeline 的命名空间,如果存在就代表基础资源已经配置完成,直接走 reconcileRb 函数,该函数的逻辑见下面;如果不存在,就会在数据面中初始化必要的基础资源,比如:pipeline 命名空间,Jenkins docker minio 服务,配置 configMap, secret 等等。

func (l *Lifecycle) deploy(projectName string) error {
    clusterID, projectID := ref.Parse(projectName)
    ns := getPipelineNamespace(clusterID, projectID)
    // 如果该 pipeline 的 namespace 已经有了,说明下面的资源部署已经完成了,则直接走 reconcileRb 流程
    // 否则走下面的资源部署流程
    if _, err := l.namespaceLister.Get("", ns.Name); err == nil {
        return l.reconcileRb(projectName)
    } else if !apierrors.IsNotFound(err) {
        return err
    }

    // 创建 pipeline 对应的命名空间,如 p-qqxs7-pipeline
    if _, err := l.namespaces.Create(ns); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the pipeline namespace")
    }

    ...

    // 随机产生一个 token,用于配置下面的 secret
    token, err := randomtoken.Generate()

    nsName := utils.GetPipelineCommonName(projectName)
    ns = getCommonPipelineNamespace()
    // 创建用于部署 docker 镜像仓库的代理服务的命名空间
    if _, err := l.namespaces.Create(ns); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the cattle-pipeline namespace")
    }

    // 在 pipeline namespace 内创建 secret : pipeline-secret
    secret := getPipelineSecret(nsName, token)
    l.secrets.Create(secret); 

    ...

    // 获取管理面项目的系统用户 token
    apikey, err := l.systemAccountManager.GetOrCreateProjectSystemToken(projectID)

    ...

    // 在 pipeline namespace 内创建 secret: pipeline-api-key,用于数据面与管理面通信的凭证
    secret = GetAPIKeySecret(nsName, apikey)
    l.secrets.Create(secret); 

    // 调谐 docker 镜像仓库的证书配置(在控制面中)
    if err := l.reconcileRegistryCASecret(clusterID); err != nil {
        return err
    }

    // 将控制面中的 docker 镜像仓库的证书配置同步到数据面中
    if err := l.reconcileRegistryCrtSecret(clusterID, projectID); err != nil {
        return err
    }

    // 在 pipeline namespace 内创建 serviceAccount : jenkins
    sa := getServiceAccount(nsName)
    if _, err := l.serviceAccounts.Create(sa); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating a pipeline service account")
    }

    ... 

    // 在 pipeline namespace 内创建 service: jenkins
    jenkinsService := getJenkinsService(nsName)
    if _, err := l.services.Create(jenkinsService); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the jenkins service")
    }

    // 在 pipeline namespace 内创建 deployment: jenkins
    jenkinsDeployment := GetJenkinsDeployment(nsName)
    if _, err := l.deployments.Create(jenkinsDeployment); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the jenkins deployment")
    }

    // 在 pipeline namespace 内创建 service: docker-registry
    registryService := getRegistryService(nsName)
    if _, err := l.services.Create(registryService); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the registry service")
    }

    // 在 pipeline namespace 内创建 deployment: docker-registry
    registryDeployment := GetRegistryDeployment(nsName)
    if _, err := l.deployments.Create(registryDeployment); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the registry deployment")
    }

    // 在 pipeline namespace 内创建 service: minio
    minioService := getMinioService(nsName)
    if _, err := l.services.Create(minioService); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the minio service")
    }

    // 在 pipeline namespace 内创建 deployment: minio
    minioDeployment := GetMinioDeployment(nsName)
    if _, err := l.deployments.Create(minioDeployment); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the minio deployment")
    }

    // 调谐 configMap: proxy-mappings,用于配置 docker 镜像仓库代理服务的端口信息
    if err := l.reconcileProxyConfigMap(projectID); err != nil {
        return err
    }

    // 创建 secret: devops-docker-registry,存储访问 docker 仓库的认证信息
    if err := l.reconcileRegistryCredential(projectName, token); err != nil {
        return err
    }

    // 创建 daemonset: registry-proxy,每个节点部署一套 docker 镜像仓库的 nginx 代理服务
    // 可以在任意一个节点上通过不同的端口即可访问到不同的 docker 镜像仓库
    nginxDaemonset := getProxyDaemonset()
    if _, err := l.daemonsets.Create(nginxDaemonset); err != nil && !apierrors.IsAlreadyExists(err) {
        return errors.Wrapf(err, "Error creating the nginx proxy")
    }

    return l.reconcileRb(projectName)
}

reconcileRb 函数的功能就是遍历所有 namespace, 对其调谐 rolebindings, 目的是让 pipeline serviceAccount(jenkins) 对该 project 下的所有 namespace 具有所需要的操作权限,这样 Jenkins server才能够在数据面中正常提供CI/CD基础服务。

func (l *Lifecycle) reconcileRb(projectName string) error {

    ...

    var namespacesInProject []*corev1.Namespace
    for _, namespace := range namespaces {
        parts := strings.Split(namespace.Annotations[projectIDLabel], ":")
        if len(parts) == 2 && parts[1] == projectID {
            // 过滤出属于该 project 下的所有 namespace
            namespacesInProject = append(namespacesInProject, namespace)
        } else {
            // 对非该 project 下的 namespace, 清除有关该 pipeline 的 rolebinding
            if err := l.roleBindings.DeleteNamespaced(namespace.Name, commonName, &metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
                return err
            }
        }
    }

    for _, namespace := range namespacesInProject {
        // 对属于该 project 下的 namespace, 创建 rolebinding: 对 jenkins serviceAccount 绑定角色
        // 即赋予 jenkins serviceAccount 对该 project 下的所有 namespace 所需要的操作权限
        rb := getRoleBindings(namespace.Name, commonName)
        if _, err := l.roleBindings.Create(rb); err != nil && !apierrors.IsAlreadyExists(err) {
            return errors.Wrapf(err, "Error create role binding")
        }
    }

    // 赋予 jenkins serviceAccount 在 cluster 内创建和修改 namespace 的权限
    // 当部署应用时可以指定创建新的命名空间
    clusterRbs := []string{roleCreateNs, projectID + roleEditNsSuffix}
    for _, crbName := range clusterRbs {
        crb := getClusterRoleBindings(commonName, crbName)
        if _, err := l.clusterRoleBindings.Create(crb); err != nil && !apierrors.IsAlreadyExists(err) {
            return errors.Wrapf(err, "Error create cluster role binding")
        }
    }

    return nil
}

goroutine(syncState)的代码逻辑比较简单,当产生新的 pipeline 执行实例时就会启动 Jenkins server 端流水线作业的运行并实时同步其运行状态到 pipeline 执行实例中。代码逻辑如下:

func (s *ExecutionStateSyncer) syncState() {
    set := labels.Set(map[string]string{utils.PipelineFinishLabel: "false"})
    allExecutions, err := s.pipelineExecutionLister.List("", set.AsSelector())
    executions := []*v3.PipelineExecution{}
    // 遍历该 cluster 下的 pipeline 执行实例
    for _, e := range allExecutions {
        if controller.ObjectInCluster(s.clusterName, e) {
            executions = append(executions, e)
        }
    }

    for _, execution := range executions {
        if v3.PipelineExecutionConditionInitialized.IsUnknown(execution) {
            // 检查数据面 k8s 中 Jenkins pod 是否正常,正常则运行该 pipeline job
            s.checkAndRun(execution)
        } else if v3.PipelineExecutionConditionInitialized.IsTrue(execution) {
            e := execution.DeepCopy()
            // 如果已经启动了,则同步运行状态
            updated, err := s.pipelineEngine.SyncExecution(e)
            if updated {
                // 更新最新的状态到 pipelineExecution crd 中
                s.updateExecutionAndLastRunState(e);
            }
        } else {
            // 更新最新的状态到 pipelineExecution crd 中
            s.updateExecutionAndLastRunState(execution);
        }
    }

    logrus.Debugf("Sync pipeline execution state complete")
}

缓存支持

云环境下的流水线是通过启动容器来运行具体的功能步骤,每次运行流水线可能会被调度到不同的计算节点上,这会导致一个问题:容器运行完是不会保存数据的,每当流水线重新运行时,又会重新拉取代码、编译代码、下载依赖包等等,失去了本地宿主机编译代码、构建镜像时缓存的作用,大大延长了流水线运行时间,浪费很多不必要的时间、网络和计算成本等。为了提高用户使用流水线的体验,加入支持缓存的功能。

为了让流水线具有缓存功能,我们需要在流水线运行时加入持久化数据的能力。首先想到的就是 k8s 提供的本地持久化存储(即 Local Persistent Volume,以下简称 Local PV),或依赖远程存储服务器来提供持久化,远程存储效率依赖于网络,并且还需要保证远程存储高可用,这回带来很多复杂性,也一定程度上失去了缓存的作用。综合考虑,我们选择本地存储实现缓存,但是 k8s 提供的 Local PV 是需要和节点绑定在一起的,也就是说一旦流水线调度到某个节点上运行,那么下次运行还会绑定到该节点运行,虽然实现了缓存的作用,但是也造成了流水线每次只能在该节点上运行,如果有多条流水线同时跑,可能会导致该节点资源耗尽或者缓存冲突,失去了云平台本身根据资源使用情况平衡调度的特性。

因此,为了平衡缓存与调度间的关系,我们采用了挂载 hostPath Volume 方式,这样依托于 k8s 强大的容器调度能力,我们可以同时运行很多条流水线而不用担心资源耗尽或缓存冲突的问题,但是流水线每次运行时可能会被调度到不同的节点上,如果当前节点没有运行过流水线,则起不到缓存的作用。那么如何解决 hostPath Volume 缓存与调度间的尴尬关系呢?我们巧妙地利用了 k8s 提供的亲和性调度特性,当流水线运行时我们会记录当前运行节点,下次运行时通过设置 Pod 的亲和性优先调度到该节点上,随着流水线运行次数越来越多,我们会得到一个运行节点列表。如下所示:

// 按时间排序,最近运行流水线的节点排在最前面
executionScheduledInfo:
    - creationTimestamp: "2020-09-02T06:42:45Z"
      executionId: 8
      nodeName: ******
    - creationTimestamp: "2020-08-26T14:19:21Z"
      executionId: 7
      nodeName: ******
    - creationTimestamp: "2020-08-26T10:52:15Z"
      executionId: 5
      nodeName: ******
    - creationTimestamp: "2020-08-26T10:48:43Z"
      executionId: 4
      nodeName: ******
    - creationTimestamp: "2020-08-25T07:47:27Z"
      executionId: 3
      nodeName: ******
    - creationTimestamp: "2020-08-25T07:16:29Z"
      executionId: 1
      nodeName: ******
    ......

执行实例调度信息会保存到 pipeline CRD 对象中,每次运行流水线时,系统会根据节点列表设置 Pod 的亲和性,默认我们会取最近运行流水线的 10 个节点,原则是最近运行流水线的节点优先级越高。代码如下:

// 获取流水线的节点调度列表
esi := c.pipeline.Status.ExecutionScheduledInfo
nodes := make([]v1.PreferredSchedulingTerm, 0)
for i, v := range esi {
    // 设置亲和性
	n := v1.PreferredSchedulingTerm{
		Weight: int32(100 - i*10), // 最近运行的节点权重越高
		Preference: v1.NodeSelectorTerm{
			MatchExpressions: []v1.NodeSelectorRequirement{
				{
					Key:      "kubernetes.io/hostname",
					Operator: v1.NodeSelectorOpIn,
					Values:   []string{v.NodeName},
				},
			},
		},
	}
	nodes = append(nodes, n)
}
nodeAff.PreferredDuringSchedulingIgnoredDuringExecution = nodes

创新性的“Hostpath Volume + 亲和性调度”缓存设计方案,不仅实现了流水线的并发性缓存功能,而且实现复杂度低,可自由配置任一阶段、步骤的缓存开关以及缓存路径。无缓存与有缓存运行的对比如下图所示,可见通过缓存加速大大提高了流水线的运行效率。

图 5 缓存效果
图 5 缓存效果

图 5 缓存效果

HCaaS DevOps 使用

以上设计在 HCaaS 平台上得到实现(https://cubepaas.com)在 HCaaS 控制台上点击 DevOps 标签,通过代码授权后,即可通过 UI 界面轻松地编辑流水线,也可通过编辑 yaml 文件配置具体的功能步骤,如图所示:

图 6 流水线编辑
图 6 流水线编辑

图 6 流水线编辑

通过点击查看日志,你可以看到 pipeline 各个阶段运行的详细日志信息,如下图所示:

图 7 流水线运行日志
图 7 流水线运行日志

图 7 流水线运行日志

【注意】首次运行 pipeline 时系统会从网络下载 Jenkins、docker、minio 以及其他 pipeline-tools 镜像,请稍作等待。如果长时间未运行,请查看网络是否有问题。

陈月新

陈月新

某公司 DevOps 工程师,云原生爱好者。

编辑本页