QCon 演讲火热征集中,快来分享你的技术实践与洞见! 了解详情
写点什么

Jenkins 的 Pipeline 脚本在美团餐饮 SaaS 中的实践

  • 2020-02-25
  • 本文字数:7305 字

    阅读完需:约 24 分钟

Jenkins的Pipeline脚本在美团餐饮SaaS中的实践

一、背景

在日常开发中,我们经常会有发布需求,而且还会遇到各种环境,比如:线上环境(Online),模拟环境(Staging),开发环境(Dev)等。最简单的就是手动构建、上传服务器,但这种方式太过于繁琐,使用持续集成可以完美地解决这个问题,推荐了解一下Jenkins


Jenkins 构建也有很多种方式,现在使用比较多的是自由风格的软件项目(Jenkins 构建的一种方式,会结合 SCM 和构建系统来构建你的项目,甚至可以构建软件以外的系统)的方式。针对单个项目的简单构建,这种方式已经足够了,但是针对多个类似且又存在差异的项目,就难以满足要求,否则就需要大量的 job 来支持,这就存在,一个小的变动,就需要修改很多个 job 的情况,难以维护。我们团队之前就存在这样的问题。


目前,我们团队主要负责开发和维护多个 Android 项目,而且每个项目都需要构建,每个构建流程非常类似但又存在一定的差异。比如构建的流程大概如下:


  • 克隆代码;

  • 静态代码检查(可选);

  • 单元测试(可选);

  • 编译打包 APK 或者热补丁;

  • APK 分析,获取版本号(VersionCode),包的 Hash 值(apkhash)等;

  • 加固;

  • 上传测试分发平台;

  • 存档(可选);

  • 触发自动化测试(可选);

  • 通知负责人构建结果等。


整个流程大体上是相同的,但是又存在一些差异。比如有的构建可以没有单元测试,有的构建不用触发自动化测试,而且构建结果通知的负责人也不同。如果使用自由风格软件项目的普通构建,每个项目都要建立一个 job 来处理流程(可能会调用其他 job)。


这种处理方式原本也是可以的,但是必须考虑到,可能会有新的流程接入(比如二次签名),构建流程也可能存在 Bug 等多种问题。无论哪种情况,一旦修改主构建流程,每个项目的 job 都需要修改和测试,就必然会浪费大量的时间。针对这种情况,我们使用了 Pipeline 的构建方式来解决。


当然,如果有项目集成了 React Native,还需要构建 JsBundle。在 Native 修改以后,JsBundle 不一定会有更新,如果是构建 Native 的时候一起构建 JsBundle,就会造成很多资源浪费。并且直接把 JsBundle 这类大文件放在 Native 的 Git 仓库里,也不是特别合适。


本文是分享一种Pipeline的使用经验,来解决这类问题。

二、Pipeline 的介绍

Pipeline 也就是构建流水线,对于程序员来说,最好的解释是:使用代码来控制项目的构建、测试、部署等。使用它的好处有很多,包括但不限于:


  • 使用 Pipeline 可以非常灵活的控制整个构建过程;

  • 可以清楚的知道每个构建阶段使用的时间,方便构建的优化;

  • 构建出错,使用 stageView 可以快速定位出错的阶段;

  • 一个 job 可以搞定整个构建,方便管理和维护等。


Stage View


三、使用 Pipeline 构建

新建一个 Pipeline 项目,写入 Pipeline 的构建脚本,就像下面这样:



对于单个项目来说,使用这样的 Pipeline 来构建能够满足绝大部分需求,但是这样做也有很多缺陷,包括:


  • 多个项目的 Pipeline 打包脚本不能公用,导致一个项目写一份脚本,维护比较麻烦。一个变动,需要修改多个 job 的脚本;

  • 多个人维护构建 job 的时候,可能会覆盖彼此的代码;

  • 修改脚本失败以后,无法回滚到上个版本;

  • 无法进行构建脚本的版本管理,老版本发修复版本需要构建,可能和现在用的 job 版本已经不一样了,等等。

四、把 Pipeline 当代码写

既然存在缺陷,我们就要找更好的方式,其实 Jenkins 提供了一个更优雅的管理 Pipeline 脚本的方式,在配置项目 Pipeline 的时候,选择Pipeline script from SCM,就像下面这样:



这样,Jenkins 在启动 job 的时候,首先会去仓库里面拉取脚本,然后再运行这个脚本。在脚本里面,我们规定的构建方式和流程,就会按部就班地执行。构建的脚本,可以实现多人维护,还可以 Review,避免出错。


以上就算搭建好了一个基础,而针对多个项目时,还有一些事情要做,不可能完全一样,以下是构建的结构图:



如此以来,我们的构建数据来源分为三部分:job UI 界面、仓库的通用 Pipeline 脚本、项目下的特殊配置,我们分别来看一下。

job UI 界面(参数化构建)

在配置 job 的时候,选择参数化构建过程,传入项目仓库地址、分支、构建通知人等等。还可以增加更多的参数 ,这些参数的特点是,可能需要经常修改,比如灵活选择构建的代码分支。


项目配置

在项目工程里面,放入针对这个项目的配置,一般是一个项目固定,不经常修改的参数,比如项目名字,如下图:


注入构建信息

QA 提一个 Bug,我们需要确定,这是哪次的构建,或者要知道 commitId,从而方便进行定位。因此在构建时,可以把构建信息注入到 APK 之中。


  1. 把属性注入到gradle.properties


# 应用的后端环境APP_ENV=Beta# CI 打包的编号,方便确定测试的版本,不通过 CI 打包,默认是 0CI_BUILD_NUMBER=0# CI 打包的时间,方便确定测试的版本,不通过 CI 打包,默认是 0CI_BUILD_TIMESTAMP=0
复制代码


  1. 在 build.gradle 里设置 buildConfigField


#使用的是gradle.properties里面注入的值buildConfigField "String", "APP_ENV", "\"${APP_ENV}\""buildConfigField "String", "CI_BUILD_NUMBER", "\"${CI_BUILD_NUMBER}\""buildConfigField "String", "CI_BUILD_TIMESTAMP", "\"${CI_BUILD_TIMESTAMP}\""buildConfigField "String", "GIT_COMMIT_ID", "\"${getCommitId()}\""
//获取当前Git commitIdString getCommitId() { try { def commitId = 'git rev-parse HEAD'.execute().text.trim() return commitId; } catch (Exception e) { e.printStackTrace(); }}
复制代码


  1. 显示构建信息


在 App 里,找个合适的位置,比如开发者选项里面,把刚才的信息显示出来。QA 提 Bug 时,要求他们把这个信息一起带上


mCIIdtv.setText(String.format("CI 构建号:%s", BuildConfig.CI_BUILD_NUMBER));mCITimetv.setText(String.format("CI 构建时间:%s", BuildConfig.CI_BUILD_TIMESTAMP));mCommitIdtv.setText(String.format("Git CommitId:%s", BuildConfig.GIT_COMMIT_ID));
复制代码

仓库的通用 Pipeline 脚本

通用脚本是抽象出来的构建过程,遇到和项目有关的都需要定义成变量,再从变量里进行读取,不要在通用脚本里写死。


node {    try{        stage('检出代码'){//从git仓库中检出代码            git branch: "${BRANCH}",credentialsId: 'xxxxx-xxxx-xxxx-xxxx-xxxxxxx', url: "${REPO_URL}"               loadProjectConfig();          }           stage('编译'){               //这里是构建,你可以调用job入参或者项目配置的参数,比如:               echo "项目名字 ${APP_CHINESE_NAME}"               //可以判断               if (Boolean.valueOf("${IS_USE_CODE_CHECK}")) {                   echo "需要静态代码检查"               } else {                   echo "不需要静态代码检查"               }
} stage('存档'){//这个演示的Android的项目,实际使用中,请根据自己的产物确定 def apk = getShEchoResult ("find ./lineup/build/outputs/apk -name '*.apk'") def artifactsDir="artifacts"//存放产物的文件夹 sh "mkdir ${artifactsDir}" sh "mv ${apk} ${artifactsDir}" archiveArtifacts "${artifactsDir}/*" } stage('通知负责人'){ emailext body: "构建项目:${BUILD_URL}\r\n构建完成", subject: '构建结果通知【成功】', to: "${EMAIL}" } } catch (e) { emailext body: "构建项目:${BUILD_URL}\r\n构建失败,\r\n错误消息:${e.toString()}", subject: '构建结果通知【失败】', to: "${EMAIL}" } finally{ // 清空工作空间 cleanWs notFailBuild: true } } // 获取 shell 命令输出内容def getShEchoResult(cmd) { def getShEchoResultCmd = "ECHO_RESULT=`${cmd}`\necho \${ECHO_RESULT}" return sh ( script: getShEchoResultCmd, returnStdout: true ).trim()}
//加载项目里面的配置文件def loadProjectConfig(){ def jenkinsConfigFile="./jenkins.groovy" if (fileExists("${jenkinsConfigFile}")) { load "${jenkinsConfigFile}" echo "找到打包参数文件${jenkinsConfigFile},加载成功" } else { echo "${jenkinsConfigFile}不存在,请在项目${jenkinsConfigFile}里面配置打包参数" sh "exit 1" }}
复制代码


轻轻的点两下Build with Parameters -> 开始构建,然后等几分钟的时间,就能够收到邮件。


五、其他构建结构

以上,仅仅是针对我们当前遇到问题的一种不错的解决方案,可能并不完全适用于所有场景,但是可以根据上面的结构进行调整,比如:


  • 根据 stage 拆分出不同的 Pipeline 脚本,这样方便 CI 的维护,一个或者几个人维护构建中的一个 stage;

  • 把构建过程中的 stage 做成普通的自由风格的软件项目的 job,把它们作为基础服务,在 Pipeline 中调用这些基础服务等。

六、当遇上 React Native

当项目引入了 React Native 以后,因为技术栈的原因,React Native 的页面是由前端团队开发,但容器和原生组件是 Android 团队维护,构建流程也发生了一些变化。

方案对比

方案说明缺点优点
手动拷贝等JsBundle构建好了,再手动把构建完成的产物,拷贝到Native工程里面1. 每次手动操作,比较麻烦,效率低,容易出错
2. 涉及到跨端合作,每次要去前端团队主动拿JsBundle
3. Git不适合管理大文件和二进制文件
简单粗暴
使用submodule保存构建好的JsBundle直接把JsBundle放在Native仓库的一个submodule里面,由前端团队主动更新,每次更新Native的时候,直接就拿到了最新的JsBundle1. 简单无开发成本
2. 不方便单独控制JsBundle的版本
3. Git不适合管理大文件和二进制文件
前端团队可以主动更新JsBundle
使用submodule管理JsBundle的源码直接把JsBundle的源码放在Native仓库的一个submodule里面,由前端团队开发更新,每次构建Native的时候,先构构建JsBundle1. 不方便单独控制JsBundle的版本
2. 即使JsBundle无更新,也需要构建,构建速度慢,浪费资源
方便灵活
分开构建,产物存档JsBundle和Native分开构建,构建完了的JsBundle分版本存档,Native构建的时候,直接去下载构建好了的JsBundle版本1. 通过配置管理JsBundle,解放Git
2. 方便Jenkins构建的时候,动态配置需要的JsBundle版本
1. 需要花费时间建立流程
2. 需要开发Gradle的JsBundle下载插件


前端团队开发页面,构建后生成 JsBundle,Android 团队拿到前端构建的 JsBundle,一起打包生成最终的产物。 在我们开发过程中,JsBundle 修改以后,不一定需要修改 Native,Native 构建的时候,也不一定每次都需要重新构建 JsBundle。并且这两个部分由两个团队负责,各自独立发版,构建的时候也应该独立构建,不应该融合到一起。


综合对比,我们选择了使用分开构建的方式来实现。

分开构建

因为需要分开发布版本,所以 JsBundle 的构建和 Native 的构建要分开,使用两个不同的 job 来完成,这样也方便两个团队自行操作,避免相互影响。 JsBundle 的构建,也可以参考上文提到的 Pipeline 的构建方式来做,这里不再赘述。


在独立构建以后,怎么才能组合到一起呢?我们是这样思考的:JsBundle 构建以后,分版本的储存在一个地方,供 Native 在构建时下载需要版本的 JsBundle,大致的流程如下:



这个流程有两个核心,一个是构建的 JsBundle 归档存储,一个是在 Native 构建时去下载。

JsBundle 归档存储

方案缺点优点
直接存档在Jenkins上面1. JsBundle不能汇总浏览
2. Jenkins很多人可能要下载,命名带有版本号,时间,分支等,命名不统一,不方便构建下载地址
3. 下载Jenkins上面的产物需要登陆授权,比较麻烦
1. 实现简单,一句代码就搞定,成本低
自己构建一个存储服务1. 工程大,开发成本高
2. 维护起来麻烦
可扩展,灵活性高
MSS
(美团存储服务)
1. 储存空间大
2. 可靠性高,配合CDN下载速度快
3. 维护成本低, 价格便宜


这里我们选择了 MSS。 上传文件到 MSS,可以使用s3cmd,但毕竟不是每个 Slave 上面都有安装,通用性不强。为了保证稳定可靠,这里基于MSS的SDK写个小工具即可,比较简单,几行代码就可以搞定:


private static String TenantId = "mss_TenantId==";private static AmazonS3 s3Client;
public static void main(String[] args) throws IOException { if (args == null || args.length != 3) { System.out.println("请依次输入:inputFile、bucketName、objectName"); return; } s3Client = AmazonS3ClientProvider.CreateAmazonS3Conn(); uploadObject(args[0], args[1], args[2]);}
public static void uploadObject(String inputFile, String bucketName, String objectName) { try { File file = new File(inputFile); if (!file.exists()) { System.out.println("文件不存在:" + file.getPath()); return; } s3Client.putObject(new PutObjectRequest(bucketName, objectName, file)); System.out.printf("上传%s到MSS成功: %s/v1/%s/%s/%se", inputFile, AmazonS3ClientProvider.url, TenantId, bucketName, objectName); } catch (AmazonServiceException ase) { System.out.println("Caught an AmazonServiceException, which " + "means your request made it " + "to Amazon S3, but was rejected with an error response" + " for some reason."); System.out.println("Error Message: " + ase.getMessage()); System.out.println("HTTP Status Code: " + ase.getStatusCode()); System.out.println("AWS Error Code: " + ase.getErrorCode()); System.out.println("Error Type: " + ase.getErrorType()); System.out.println("Request ID: " + ase.getRequestId()); } catch (AmazonClientException ace) { System.out.println("Caught an AmazonClientException, which " + "means the client encountered " + "an internal error while trying to " + "communicate with S3, " + "such as not being able to access the network."); System.out.println("Error Message: " + ace.getMessage()); }}
复制代码


我们直接在 Pipeline 里构建完成后,调用这个工具就可以了。


当然,JsBundle 也分类型,在调试的时候可能随时需要更新,这些 JsBundle 不需要永久保存,一段时间后就可以删除了。在删除时,可以参考MSS生命周期管理。所以,我们在构建 JsBundle 的 job 里,添加一个参数来区分。


//根据TYPE,上传到不同的bucket里面def bucket = "rn-bundle-prod"if ("${TYPE}" == "dev") {  bucket = "rn-bundle-dev" //有生命周期管理,一段时间后自动删除}echo "开始JsBundle上传到MSS"//jar地址需要替换成你自己的sh "curl -s -S -L  http://s3plus.sankuai.com/v1/mss_xxxxx==/rn-bundle-prod/rn.bundle.upload-0.0.1.jar -o upload.jar"sh "java -jar upload.jar ${archiveZip} ${bucket} ${PROJECT}/${targetZip}"echo "上传JsBundle到MSS:${archiveZip}"
复制代码

Native 构建时 JsBundle 的下载

为了实现构建时能够自动下载,我们写了一个 Gradle 的插件。


首先要在 build.gradle 里面配置插件依赖:


classpath 'com.zjiecode:rn-bundle-gradle-plugin:0.0.1'
复制代码


在需要的 Module 应用插件:


apply plugin: 'mt-rn-bundle-download'
复制代码


在 build.gradle 里面配置 JsBundle 的信息:


RNDownloadConfig {    //远程文件目录,因为有多种类型,所以这里可以填多个。    paths = [            'http://msstest-corp.sankuai.com/v1/mss_xxxx==/rn-bundle-dev/xxx/',            'http://msstest-corp.sankuai.com/v1/mss_xxxx==/rn-bundle-prod/xxx/'    ]    version  = "1"//版本号,这里使用的是打包JsBundle的BUILD_NUMBER    fileName = 'xxxx.android.bundle-%s.zip' //远程文件的文件名,%s会用上面的version来填充    outFile  = 'xxxx/src/main/assets/JsBundle/xxxx.android.bundle.zip' // 下载后的存储路径,相对于项目根目录}
复制代码


插件会在 package 的 task 前面,插入一个下载的 task,task 读取上面的配置信息,在打包阶段检查是否已经存在这个版本的 JsBundle。如果不存在,就会去归档的 JsBundle 里,下载我们需要的 JsBundle。 当然,这里的 version 可以使用上文介绍的注入构建信息的方式,通过 job 参数的方式进行注入。这样在 Jenkins 构建 Native 时,就可以动态地填写需要 JsBundle 的版本了。


这个 Gradle 插件,我们已经放到到了 github 仓库,你可以基于此修改,当然,也欢迎 PR。


https://github.com/zjiecode/rn-bundle-gradle-plugin

六、总结

我们把一个构建分成了好几个部分,带来的好处如下


  • 核心构建过程,只需要维护一份,减轻维护工作;

  • 方便多个人维护构建 CI,避免 Pipeline 代码被覆盖;

  • 方便构建 job 的版本管理,比如要修复某个已经发布的版本,可以很方便切换到发布版本时候用的 Pipeline 脚本版本;

  • 每个项目,配置也比较灵活,如果项目配置不够灵活,可以尝试定义更多的变量;

  • 构建过程可视化,方便针对性优化和错误定位等。


当然,Pipeline 也存在一些弊端,比如


  • 语法不够友好,但好在 Jenkins 提供了一个比较强大的帮助工具(Pipeline Syntax);

  • 代码测试繁琐,没有本地运行环境,每次测试都需要提交运行一个 job,等等。


当项目集成了 React Native 时,配合 Pipeline,我们可以把 JsBundle 的构建产物上传到 MSS 归档。在构建 Native 的时候 ,可以动态地下载。

七、作者

  • 张杰,美团点评高级 Android 工程师,2017 年加入餐饮平台成都研发中心,主要负责餐饮平台 B 端应用开发。

  • 王浩,美团点评高级 Android 工程师,2017 年加入餐饮平台成都研发中心,主要负责餐饮平台 B 端应用开发。


2020-02-25 20:321377

评论

发布
暂无评论
发现更多内容

支持75对矿井、23 类、1100余个系统工业数据采集,云鼎科技煤矿安全监测的经验分享

TDengine

DNS在架构中的使用

EquatorCoco

架构 DNS

【网络安全】Web Hacking网络黑客手册,GitHub星标3.7K!

我再BUG界嘎嘎乱杀

黑客 网络安全 安全 WEB安全 网安

金融行业中API的挑战与未来趋势

蛙人族

API接口

体验教程:通义灵码陪你备战求职季

阿里巴巴云原生

阿里云 云原生 通义灵码

GPT-4o版「Her」终于来了!英伟达股价两周内下跌23%!|AI日报

可信AI进展

人工智能

LeetCode题解:2073. 买票需要的时间,直接计算,JavaScript,详细注释

Lee Chen

永劫光遇等40+鸿蒙原生游戏首次亮相CJ 2024 技术赋能精品游戏体验

最新动态

独“数”一帜 双证加冕!TeleDB亮相可信数据库发展大会

天翼云开发者社区

数据库

折叠想象,「天池AI IP形象征集大赛」火热进行中!

阿里云天池

阿里云 AI 图像生成

SaaS 服务:满足个性化需求

可观测技术

SaaS

观测云:构筑数字化时代的IT监控堡垒

可观测技术

监控

体验教程:通义灵码陪你备战求职季

阿里云云效

阿里云 云原生 通义灵码

如何选择最佳需求跟踪工具?8大优质系统盘点

爱吃小舅的鱼

需求管理 需求管理工具 需求跟踪

开源与商业的平衡:观测云的开放策略

可观测技术

开源

简化管理,提升效率:统一数据视角的力量

可观测技术

数据分析

体育直播平台开发与运营:差异化与选择跟随战略的实践案例

软件开发-梦幻运营部

智算引领,数耀鹭岛!天翼云与厦门电信共筑智算时代新底座!

天翼云开发者社区

云计算 智算中心 天翼云

AI+奥运:2024巴黎奥运时刻,怎么用AI技术给网友亿点震撼?

爱AI的猫猫头

人工智能 海报 AI绘画 Prompt AI视频

碳课堂|什么是碳盘查、碳核查?

AMT企源

碳管理 碳核算

Linux内存不够了?看看如何开启虚拟内存增加内存使用量

快乐非自愿限量之名

Java 数据库 Linux

实时检出率仅19%,SIEM还是网络威胁处理的“瑞士军刀”吗?

我再BUG界嘎嘎乱杀

网络安全 安全 信息安全 网安 SIEM

汽车虚拟仿真交互体验更真实,实时云渲染来助力!

3DCAT实时渲染

实时云渲染 云仿真 云交互 实时渲染云 汽车虚拟仿真

Agisoft Metashape Professional for Mac(专业3D建模软件)中文版

Mac相关知识分享

C#中常用集合类型

不在线第一只蜗牛

C# 集合 windows

海外直播APP源码技术配置说明 个性化定制海外直播平台

山东布谷科技胡月

国际版语音直播APP 社交直播APP开发 海外直播App开发 海外直播APP源码 聊天交友源码

怎么解决做海外直播的网络问题?

Ogcloud

海外直播专线 海外直播 tiktok直播专线 海外直播网络 tiktok直播网络

观测云:技术栈兼容性助力企业数字化转型

可观测技术

技术栈

Compressor for Mac(视频转码编辑工具) v4.7中文激活版

Mac相关知识分享

JPA乐观锁改悲观锁遇到的一些问题与思考

不在线第一只蜗牛

数据库 oracle 乐观锁 jap

荣誉再加码!2024可信云大会,天翼云载誉而归!

天翼云开发者社区

云计算 可信云大会

Jenkins的Pipeline脚本在美团餐饮SaaS中的实践_文化 & 方法_美团技术团队_InfoQ精选文章