持续交付在星辰平台的应用
原文链接 http://veryyoung.me/blog/2016/11/23/continuous-delivery-at-star-platform.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
上篇文章提到了持续交付,使用持续交付好处诸多,这里就不说了。
其实在之前工作过程中,也尝试过 Jenkins 来做持续集成,但基本只用作自动打包,其他过程基本是 ssh 到服务器上进行人肉操作,或者完全交给 op 处理。
在星辰平台(属于滴滴效能平台部)这边用 Jenkins 做持续集成,效果还不错,同时持续交付的流程也处理得也必将棒。
<!-- more -->
1.代码管理
星辰平台采用了主干开发模式,即所有的代码变更和发布都在主干进行。
主干开发模式可以避免分支合并时让人抓狂的解决冲突的困境,也让持续集成变得更加及时和有效,大大提升了交付速度。
但主干开发模式最大的风险就是未完成的代码很容易被“顺带”上线,这对研发人员提出了更高的要求。 一个大任务或者 story 要切分成许多很快能完成,验收并上线的子 task,同时也要借静态代码检查、单元测试、集成测试、持续集成等来降低风险, 程序很多时候都处于中间状态,双写、新旧版本并存等都是常见的技术手段。
在必要时候需要巧妙的隐藏起来,甚至要设计很多开关,在线上的时候关掉未完成或未验收的 feature,有条件甚至可以尝试下灰度发布和 ab test。
2.数据库管理
在持续交付过程中,数据库管理是非常重要的环节,在多个环境间管理数据库也是一件非常麻烦是事情。
程序和数据库是强依赖的,只有做到自动化的数据库管理,持续集成才能有序的进行。
星辰平台对数据库管理有以下原则:
1. 不共享数据库
公用数据库是一件很头疼的事情,很容易就会出现开发人员相互覆盖彼此所做的修改的情况,导致开发过程多次被阻断,同时排查起来困难,严重影响开发效率。
2. 数据库进行版本管理
数据库最好能随着程序的交付而一起交付,并用版本控制工具管理起来。
3. 不用手工变更数据库,需要使用脚本变更,并纳入版本控制
基于以上的考虑,我们引进了 flyway,flyway 能完美满足我们上面的对数据库的要求。 有了数据库的版本控制,建立一个数据库环境非常简单了,只需建好对应的数据库,直接跑程序 run 起来,数据库结构以及一些依赖数据已经 ok 了。 对数据库有修改也非常容易,加一个 migration 脚本,所有环境都会自动执行这个变更。
3.静态代码检查
静态代码扫码可以分析出代码存在的代码 bad code style,不良的用法,以及潜在的错误或缺陷,对代码质量的保持有着重要作用。
星辰平台的代码主要由 jsx 和 Java 组成。
jsx 代码扫码采用了 ESLint,使用 npm start 启动项目之后,如果代码有 code style 问题, 会实时的在控制台显示出来。 ESLint 可以检测出 Possible Errors(潜在的错误),推荐采用最佳实践的写法,同时对 ES6 有着比较好的支持。
Java 代码静态扫码采用了 Checkstyle,Checkstyle 支持丰富的代码检查规范,比如命名规范、长度限制、重复代码、复杂度等。 我们基于 Google's Java Style Checkstyle Coverage进行符合项目习惯的修改,并且把代码检查放在了编译阶段自动进行。
每次执行任何 mvn 命令之前都会先跑一遍 Checkstyle,通过不了后面的 task 都无法进行。
4.Tests
我们没有测试人员,研发质量都得研发人员自己保证。
为了在快速迭代和经常性的重构过程中保持代码质量。我们加入了自动化测试。
目前最常见的测试有两种:单元测试和集成测试。
单元测试只测代码片段的逻辑,对外部的依赖,比如数据库、http 请求等都 mock 掉。
集成测试主要用来测对外暴露出去的接口,走真正的数据库(但不是真正运行的库,一般在库名后加个 _test)。
5.Pipeline
我们基于 Jenkins 搭建了 Pipeline(持续集成流水线),如下图所示:
流水线分为五个过程: Compile -> Tests -> Package -> Stage Deploy -> Prod Deploy.
Jenkins 会轮训我们的代码库(两分钟一次),期间如果有任何代码变更 push 到 master,持续集成流水线就会跑起来。
第一个阶段是执行代码编译(包含CheckStyle),也就是执行 mvn compile
,如果编译没通过,会失败“变红”,流水线后续都无法进行,并发邮件给相关人进行处理。
如果编译顺利的通过,会触发下游 Job 进行 Tests,也就是执行 mvn clean test
,如果测试不通过也会报警并阻碍接下来流程进行(下同)。
测试通过后会执行它的后继任务,注意,这次后继任务有多条平行流水线,每个后端模块一个,每条流水线都会执行 package 操作,也就是 mvn install
。
打包成功后会把生成的文件(前端是 html+js+css,后端是 jar 包), sync 到一台中转机上,后续的任务都用这一步生成的包了。
打包成功后会执行 stage 环境的自动发布,我们的 staging 环境部署在 Docker 容器上,每次 job 执行容器会重新构建,并把上一步生成的文件拉过去,
如果是 jar 包,则 java -jar
启动起来,如果是前端,直接放置在某个目录就行,二者都用 nginx 进行代理,然后由宿主机暴露出端口,这样外界就能访问了。
可以看出,每次 push 代码到主干,如果一切顺利,stage 环境很快就能部署上去。
stage 环境部署之后,可以进行人肉的验收工作,验收通过后,需要手工点击 Prod Deploy Job 上的按钮,进行人工上线。
点击按钮后,线上机器也会 package 阶段生成的文件,放置在在机器上,然后软连接到一个目录。
这样回滚起来也超级方便,只要点击之前 work 的流水线上的 Prod Deploy Job 上的按钮,自动改下软连接,如果是 Java jar 包,kill 掉以前的 Java 进程并重新执行就行了。
6.UAT
UAT 是 User Acceptance Test的简写,也就是用户验收测试。
当我们开发完 story 之后,会找 pm 和一个项目经理(之前是资深测试工程师,也是我们的需求提供方之一)来当用户,帮我们验收产品(在 stage 环境),如果他们的验收能通过, 则可以发布,story 算正式完结。
改进点
1.主干开发的风险规避做的不够好
我们现在每次上线之前还需要互相问问对方的部分能不能上线,这代表大家对地方的风险规避还不信任。
事实上,团队(包括我自己)对这方面也做得确实不够好,任务拆分的不够细,很容易做出对现有功能破坏性的而又没法马上完成的改动。 可能下次动手之前需要仔细想清楚,把任务切细,每次 commit 都前进一点,每次 commit 尽量能验收,能上线。 实在不行,也应该把当前修改的部分给隐藏起来,而不是冒着阻碍项目发布的风险。 对于风险比较大的改动,风险无法预估,任务不好切分,比如组件的升级等,可以考虑分支开发。
2.DB Migration 没用好
我们现在用 flyway 只用了最基本的用法,因为用得太简单粗暴,好几次上线问题和 N 多次的 staging 不可用都是由 DB Migration 造成。
flyway 还有更多用法。
比如: mvn flyway:validate
可用来检查 migration 是否能校验通过(不会执行 sql,所以语法错误之类的没法防止),比如 checksum 更改,版本冲突等问题。
mvn flyway:repair
用来删除执行失败的 Migration,代替以往的手工操作数据库,删除 schema_version 表里的记录。
3.静态代码检查需要再丰富
Checkstyle 只检查了最基本的 code style,更像只是一个纯代码风格检查工具,对代码可能隐藏的问题和不好的实践,checkstyle 可能有点无能为力。 可以考虑下引进 FindBugs、PMD之类的真正静态代码扫码工具。
4.Tests 覆盖率需要提上去
在项目早期的时候,还保持了一个很高的测试覆盖率。随着功能的迭代,覆盖率越来越低了。这个需要提上去,不然代码质量没保障。 如果有条件,可以在 CI 里面跑 Test Coverage 和 Sonar。
如果有条件,主流程上应该加上 UI 测试,来保证前端的可靠性。
5.CI 需要串行
CI 需要是串行的,以避免构建失败后定位问题难,也能让大家更重视 CI。 提交之前需要关注 CI 的状态, CI 执行完再提交代码。 或者也可以采用物理令牌,准备或者正在提交代码的人获得令牌,CI 构建完毕,视为代码提交完毕,才释放令牌
6.重视 CI
现在偶尔还会出现代码风格不符合,或者测试没跑过的代码在提交。这个应该彻底杜绝掉,在本地跑测试,没问题再提交。
对于不稳定的测试,一定要想办法修复掉。
要把 CI 构建失败作为第一优先级处理的事情,如果能快速定位问题,马上修复,commit;如果一时半会修不了,需要马上回滚,让 ci 恢复,再到本地慢慢修。
7.Pipeline 时间可以再缩短
持续集成时间越短,持续集成的效果就越好。
我们现在大概需要10分钟构建,如果串行提交,那么需要等待这么长时间。
可以尝试的优化方案:
1. 所有 job 使用同一份代码
我们现在每个模块的构建都会拉一份代码,拉代码是有网络开销的。
2. 只触发代码变更的模块和其下游。
3. 把 CI 机器从产品环境迁移出去,换一台独立的,性能强劲的机器来跑 CI。
人力比机器贵得多。