知乎应用平台团队基于 Jenkins Pipeline 和 Docker 打造了一套持续集成系统。Jenkins Master 和 Slave 基于 Docker 部署,每次构建也是在容器中进行。目前有三千个 Jenkins Job,支撑着整个团队每日近万次的构建和部署量。
整个系统的设计目标是具备以下的能力:
知乎选用 Jenkins 作为构建方案,因其强大和灵活,且有非常丰富的插件可供使用和扩展。
早期,应用数量较少时,每个开发者都手动创建并维护着几个 Job,各自编写 Jenkins Job 的配置,以及手动触发构建。随着服务化以及业务类型,开发者以及 Jenkins Job 数量的增加,我们面临了以下的问题:
于是,一个能方便应用接入构建部署的系统,成为了必须。
知乎的构建工作流主要是以下两种场景:
一个 Commit 从提交到最后部署,会经历以下的环节:
每个应用的拉取代码,准备数据库,处理测试覆盖率,发送消息,候选版本的注册等通用的部分,都会由构建系统统一处理,而接入构建系统的应用,只需要在代码仓库中包含一个约定格式的配置文件。
构建系统去理解应用要做的事情靠的是约定格式的 yaml 配置文件,而我们希望这个配置文件能足够简单,声明上必要的部分,如环境、构建、测试步骤就能开始构建。
同时,也要有能力提供更多的定制功能让应用可以使用,如选择系统依赖和版本,缓存的路径,是否需要构建系统提供 MySQL 以及需要的 MySQL 版本等。以及可以根据应用的类别自动生成配置文件。
一个最简单的应用场景:
base_image: python2/jessie build: - buildout test: unittest: - bin/test --cover-package=pin --with-xunit --with-coverage --cover-xml
一个更多定制化的场景:
base_image: py_node/jessie deps: - libffi-dev build: - buildout - cd admin && npm install && gulp test: deps: - mysql:5.7 unittest: - bin/test --cover-package=lived,liveweb --with-xunit --with-coverage coverage_test: report_fpath: coverage.xml post_build: scripts: - /bin/bash scripts/release_sentry.sh artifacts: targets: - docker - tarball cache: directories: - admin/static/components - admin/node_modules
为了尽可能满足多样化的业务场景,我们主要将配置文件分为三部分:声明环境和依赖、构建相关核心环节、声明 Artifact 类型。
声明环境和依赖:
构建相关核心环节:
声明 Artifact 类型:
artifact,用于选择部署的类型,目前支持的有:
早期所有的构建都在物理机上进行,构建之前需要提前在物理机上安装好对应的系统依赖,而如果遇到所需要的版本不同时,调度和维护的成本就高了很多。随着团队业务数量和种类的增加,技术选型的演进,这样的挑战越来越大。于是构建系统整体的优化方向由物理机向 Docker 容器化前进,如今,所有构建都在干净的容器中进行,基础的语言镜像由应用自己选择。
目前镜像管理的方式是:
image
语言镜像之上,会安装上 deps
指定的系统依赖,再构建出应用的镜像,应用会在这个环境里面进行构建测试等。
语言这一层的 Dockerfile 会被严格 review,通过的镜像才能被使用,以更好了解和支持业务技术选型和使用场景。
缓存的设计
最开始构建的缓存是落在对应的 Jenkins Slave 上的,随着 Slave 数量的增多,应用构建被分配到不同 Slave 带来的代价也越来越大。
为了让 Slave 的管理更加灵活以及构建速度和 Slave 无关,我们最后将缓存按照应用使用的镜像和系统依赖作为缓存的标识,上传到 HDFS。在每次构建前拉取,构建之后再上传更新。
针对镜像涉及到的语言,我们会对常见的依赖进行缓存,如 eggs,node_modules,.ivy2/cache,.ivy2/repository。应用如果有其他的文件想要缓存,也支持在配置文件中指定。
依赖获取稳定性
在对整个构建时间的开销和不稳定因素的观察中,我们发现拉取外部依赖是个非常耗时且失败率较高的环节。
为了让这个过程更加稳定,我们做了以下的事情:
更低的排查错误的成本
本地开发和构建环境存在明显的差异,可能会出现本地构建成功但是在构建系统失败的情况。
为了让用户能够快速重现,我们在项目 docker-ssh 的基础上做了二次开发,支持直接 ssh 到容器进行调试。由于容器环境与其他人的构建相隔离,我们不必担心 SSH 权限导致的各种安全问题。构建失败的容器会多保留一天,之后便被回收。
我们希望能给接入到构建系统的提高效率的同时,也希望能推动一些标准或者好的实践,比如完善测试。
围绕着测试和测试覆盖率,我们做了以下的事情:
对于团队内或者业界的基础库,如果发现有更稳定版本或者发现有严重问题,构建系统会按照应用的重要性,从低到高提示应用去升级或者去掉对应依赖。
Job 调度策略
Jenkins Master 只进行任务的调度,而实际执行是在不同的 Jenkins Node 上。
每个 Node 会被赋予一些 label 用于任务调度,比如:mysql:5.6,mysql:5.7,common 等。构建系统会根据应用的类型分配到不同的 label,由 Jenkins Master 去进一步调度任务到对应的 Node 上。
高可用设计
集群的设计如下,一个 Node 对应的是一台物理机,上面跑了 Jenkins Slave (分别连 Master 和 Master Standby),Docker Deamon 和 MySQL(为应用提供测试的 MySQL)。
Slave 连接 Master 等待被调度,而当 Jenkins Slave 出现故障时,只需摘掉这台 Slave 的 label,后续将不会有任务调度调度上来。
而当 Jenkins Master 故障时,如果不能短时间启动起来时,集群可能就处于不可用状态了,从而影响整个构建部署。为了减少这种情况带来的不可用,我们采用了双 Master 模型,一台作为 Standby,如果其中一台出现异常就切换到另一台健康的 Master。
监控和报警
为了更好监控集群的运行状态,及时发现集群故障,我们加了一系列的监控报警,如:
在未来我们还希望完善以下的方面:
原文链接: https://zhuanlan.zhihu.com/p/45694823