Jenkins Pipeline 并发编译项目代码

编译打包脚本

从源代码管理、使用角度,项目下维护一个编译打包脚本是一个不错的方法

随着项目规模迭代,会出现编译、打包时间长的问题

可以脚本拆成独立的子阶段,借助 Jenkins 的 Pipeline 功能,实现并发处理。从而缩短整个编译打包时间

Jenkins

Jenkins 是一个开源软件项目,是基于 Java 开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件项目可以进行持续集成

官方网站: https://www.jenkins.io/

在 Jenkins 中,把一个构建任务称之为 Job

Jenkins 分布式构建

Jenkins 可以单机器节点构建 Job ,或者利用多台机器节点共同协作完成一个 Job 的构建

安装 Pipeline 插件后,会使 Jenkins 具备这种特性

通常,安装 Jenkins 时,默认选择社区推荐检查安装即可

安装 Jenkins 的那台主机,称之为 master

可以通过 Dashboard => 节点列表 => 新建节点,添加从机

master 需要能 ssh 权限,登录从机

下图,为添加了一个从节点的图示:
在这里插入图片描述

Jenkins Pipeline Job

Jenkins Pipeline Job 是一种构建任务

它按照 Pipeline 语法,把整个构建任务拆成多个 stage ,并控制这些 stage 按指定的方式去执行

比如并发执行、跨机器节点执行等等

Jenkins Pipeline 相关文档资料:

  • https://www.jenkins.io/zh/doc/book/pipeline/getting-started/
  • https://www.jenkins.io/zh/doc/book/pipeline/jenkinsfile
  • https://www.jenkins.io/doc/pipeline/examples/

Jenkins Pipeline Job 最佳实践是通过 Jenkinsfile 文件方式来编排、定义:

  • 可以纳入版本管理。类似按源代码脚本方式维护、调试
  • 可以在定义 Job 时,使用 Jenkins SCM 插件指定加载

Jenkinsfile

Jenkinsfile 是使用 groovy 语法的编排脚本

比如脚本例子:

pipeline {
    agent any

    stages {
        stage('Hello') {
            steps {
                sh 'echo "Hello World"'
            }
        }
    }
}

该脚本实际上包含了 3 种语法:

语法 说明
groovy 语法 即整个脚本书写,使用 groovy 语法
pipeline job 定义语法 例子中的 agent 、 stages 、stage 等等组织方式,是需要符合 pipeline job 的规定方式
shell 语法 通常项目会使用 linux 环境下 shell 脚本来做编译、打包、运行等脚本
在 pipeline 中,每个 stage 都可以执行些 shell 语句

如果是 windows 用户, Jenkinsfile 会加入 bat 语法,用来执行 windows 批处理脚本

Jenkinsfile 实际脚本举例

下面是实作中的 Jenkinsfile 脚本。去除了项目细节,保留整个框架,读者可以学习,如何编写一个 Jenkinsfile 脚本

def source_version = ''
def script_version = ''
def proto_version = ''
def cserver_cpp_version = ''
def final_version = ''
def build_name = ''

def labels = ['login', 'gateway', 'game']

def get_go_service_stage(label, source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name) { return stage(label) { withEnv(["label=${label }", "source_version=${source_version }", "script_version=${script_version }", "proto_version=${proto_version }", "cserver_cpp_version=${cserver_cpp_version }", "final_version=${final_version }", "build_name=${build_name }"]) { sh '''set -ex
    service=$label
    echo ${service}
    # 编译脚本,略
    '''
}}}

def get_cserver_cpp_stage(label, source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name) { return stage(label) { withEnv(["label=${label }", "source_version=${source_version }", "script_version=${script_version }", "proto_version=${proto_version }", "cserver_cpp_version=${cserver_cpp_version }", "final_version=${final_version }", "build_name=${build_name }"]) { sh '''set -ex
    # 编译脚本,略
    '''
}}}

def get_client_zip_stage(label, source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name) { return stage(label) { withEnv(["label=${label }", "source_version=${source_version }", "script_version=${script_version }", "proto_version=${proto_version }", "cserver_cpp_version=${cserver_cpp_version }", "final_version=${final_version }", "build_name=${build_name }"]) { sh '''set -ex
    # 资源打包脚本,略
    '''
}}}

def get_sdata_zip_stage(label, source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name) { return stage(label) { withEnv(["label=${label }", "source_version=${source_version }", "script_version=${script_version }", "proto_version=${proto_version }", "cserver_cpp_version=${cserver_cpp_version }", "final_version=${final_version }", "build_name=${build_name }"]) { sh '''set -ex
    # 资源打包脚本,略
    '''
}}}

def get_image_stage(label, source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name) { return stage(label) { withEnv(["label=${label }", "source_version=${source_version }", "script_version=${script_version }", "proto_version=${proto_version }", "cserver_cpp_version=${cserver_cpp_version }", "final_version=${final_version }", "build_name=${build_name }"]) { sh '''set -ex
    service=$label
    echo ${service}
    # docker 镜像制作脚本,略
    '''
}}}

def get_source_version() {
    return sh (
        script: '''echo "$(# 获取代码版本脚本,略)''',
        returnStdout: true
    ).trim()
}

def get_script_version() {
    return sh (
        script: '''echo "$(# 获取资源版本脚本,略)"''',
        returnStdout: true
    ).trim()
}

def get_proto_version() {
    return sh (
        script: '''echo "$(# 获取协议版本脚本,略)"''',
        returnStdout: true
    ).trim()
}

def get_cserver_cpp_version() {
    return sh (
        script: '''echo "$(# 获取代码版本脚本,略)"''',
        returnStdout: true
    ).trim()
}

def get_final_version(source_version, script_version, proto_version) {
    return withEnv(["source_version=${source_version }", "script_version=${script_version }", "proto_version=${proto_version }"]) { sh (script: '''
    # 计算最终版本号,略
    ''', returnStdout: true).trim()}
}

def get_build_name(final_version) {
    return withEnv(["final_version=${final_version }"]) { sh(
        script: '''echo "${BUILD_NUMBER}_${BUILD_USER}_${p4}_${version}_C_${final_version}_TAG_xxx"''',
        returnStdout: true
    ).trim()}
}

pipeline {
    agent {
        node {
            label 'backend_compiler'
        }
    }
    environment {
        P4PORT = 'xxxxx'
        P4USER = 'xxxxx'
        P4PASSWD = 'xxxxx'
        registryHost = 'xxxxx:5000/'
        GOPATH = sh( script: '''echo ${WORKSPACE}''', returnStdout: true ).trim()
        BRANCH_NAME = sh( script: '''echo ${branch}''', returnStdout: true ).trim()
    }
    stages {
        stage('set_name1') {
            steps {
                wrap([$class: 'BuildUser']) {
                        sh '''
                        buildName=${BUILD_NUMBER}_${BUILD_USER}_${p4}_${version}_C_xxx
                        touch ${WORKSPACE}/buildName.txt
                        echo ${buildName} >${WORKSPACE}/buildName.txt
                        '''
                }
                script {
                    json_file = "${env.WORKSPACE}/buildName.txt"
                    file_contents = readFile json_file
                    currentBuild.displayName = file_contents
                }
            }
        }
        stage('checkout') {
            failFast true
            parallel {
                stage('p4_checkOut') {
                    steps {
                        // 拉 P4 代码,略
                    }
                }
                stage('check_out_go_code') {
                    steps {
                        // 拉 git 代码,略
                    }
                }
            }
        }
        stage('get_version') {
            steps {
                script {
                    source_version = get_source_version()
                    script_version = get_script_version()
                    proto_version = get_proto_version()
                    cserver_cpp_version = get_cserver_cpp_version()
                    final_version = get_final_version(source_version, script_version, proto_version)
                    build_name = get_build_name(final_version)
                }
            }
        }
        stage('set_name2') {
            steps {
                script {
                    withEnv(["build_name=${build_name }"]) { sh '''
                        touch ${WORKSPACE}/buildName.txt
                        echo ${build_name} >${WORKSPACE}/buildName.txt
                        '''
                    }
                    json_file = "${env.WORKSPACE}/buildName.txt"
                    file_contents = readFile json_file
                    currentBuild.displayName = file_contents
                }
            }
        }
        stage('build') {
            steps {
                script {
                    def builders = [:]
                    builders['cserver_cpp'] = {
                        get_cserver_cpp_stage('cserver_cpp', source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name)
                    }
                    builders['client.zip'] = {
                        get_client_zip_stage('client.zip', source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name)
                    }
                    builders['sdata.zip'] = {
                        get_sdata_zip_stage('sdata.zip', source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name)
                    }
                    for (x in labels) {
                        def label = x
                        builders[label] = {
                            get_go_service_stage(label, source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name)
                        }
                    }
                    builders.failFast = true
                    parallel builders
                }
            }
        }
        stage('image') {
            steps {
                script {
                    def builders = [:]
                    builders['tools'] = {
                        get_image_stage('tools', source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name)
                    }
                    builders['all_in_one_test'] = {
                        get_image_stage('all_in_one_test', source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name)
                    }
                    if ("${env.complete}" == 'true') {
                        for (x in labels) {
                            def label = x
                            builders[label] = {
                                get_image_stage(label, source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name)
                            }
                        }
                        builders['cserver_cpp'] = {
                            get_image_stage('cserver_cpp', source_version, script_version, proto_version, cserver_cpp_version, final_version, build_name)
                        }
                    }
                    builders.failFast = true
                    parallel builders
                }
            }
        }
        stage('end') {
            steps {
                script {
                    withEnv(["build_name=${build_name }", "final_version=${final_version }"]) {
                        sh '''
                        # 结束处理
                        '''
                    }
                }
            }
        }
    post {
        changed {
            echo 'pipeline post changed'
        }
        always {
            echo 'pipeline post always'
        }
        success {
            echo 'pipeline post success'
        }
        unsuccessful {
            script {
                sh '''
                        notifyUrl=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxx
                        newNotifyUrl=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxx
                        msg="[UGS] dev 编译 【失败】 ${p4}_${version},time=$(date "+%Y-%m-%d %H:%M:%S")"
                        curl $notifyUrl -H \'Content-Type: application/json\' -d "{\\"msgtype\\": \\"text\\",\\"text\\": {\\"content\\": \\"${msg}\\", \\"mentioned_list\\":[\\"xxxxxx\\"]}}"
                        curl $newNotifyUrl -H \'Content-Type: application/json\' -d "{\\"msgtype\\": \\"text\\",\\"text\\": {\\"content\\": \\"${msg}\\"}}"
                    '''
            }
            echo 'pipeline post unsuccess'
        }
    }
}

脚本说明,可以学习到以下知识点:

  • 公共信息在 stage 间,可以通过环境变量的方式传递
  • 如何动态设置环境变量
  • groovy 中的变量如何传递给 shell
  • 如何并发执行 stage
  • 如何动态生成 stage

Jenkins Pipeline Job 多节点并发

上面举的例子,还局限于 Jenkinsfile 在某台从节点上执行

如果读者需要把多个 stage 编排到不同从节点,还需要考虑:

  • 动态公共信息,如何在多个 stage 中传递
  • 多个 stage 结果如何汇总
  • 拉取的代码源是否多节点共享问题

因为项目复杂度尚未到单个从节点无法应对的情况(目前以上 Jenkinsfile ,使原执行脚本 30 分钟,缩短至 3 分钟完成

因此,这里仅提供以下思路:

  • 使用 jenkins pipeline 的 stash 命令,汇总 stage 结果
  • 预处理公共信息,同理可以通过 stash 命令,汇总于一些文件内。其他 stage ,可先 unstash 某文件获得需要的信息
  • git p4 等源代码库拉取操作是否只要公共的定义到 1 个顶级 stage 。其他多节点并发 stage 是否可见?
    • 未实践过,暂无结论。最差的情况,多节点并发 stage 内拉取对应仓库内容

另外需要注意:并非多节点并发,一定由于单节点并发。。多节点间 stage 交互需要消耗一定资源,这里有个阈值

一般的,以下情况,可以考虑:

  • 编译打包过程本身跨平台,如需要 windows 、 linux 同时参与
  • 单节点处理能力无法达到预期(这种情况一般不多见)

以上

你可能感兴趣的:(工具,jenkins,pipeline,parallel,groovy,jenkinsfile)