京东研发效能部一直紧跟云原生潮流,去年 10 月份开始调研并引入 Tekton,在内部尝试基于 Tekton 打造下一代云原生 CI 平台。
云原生概念自 2015 年最初被提及后,其生态在不断壮大。与此同时,支持云原生的开源工具如雨后春笋般出现。在众多开源工具中我们把目光聚焦在了 tekton 上, 不仅仅因为它是 K8s “亲”生,还因为与其他工具相比较,它更加轻量、更加灵活扩展,并支持多云环境,拥有活跃的社区。Tekton 虽然还是一个挺新的项目,但是已经成为 Continuous Delivery Foundation (CDF) 四个初始项目之一。
在不到一年的时间里,我们通过对 tektoncd/pipeline 工程的学习和验证,建设了 jbuild 等组件,并部署到业务生产环境的 Kubernetes 集群里,支持京东内部日均近万次应用构建任务。
本文将分享如何使用 Tekton 推进 CI 平台往云原生方向发展和落地,同时分享一些在推进过程中遇到的问题以及解决方案。
Tekton 是一个功能强大且灵活的 Kubernetes 原生开源框架,用于创建持续集成和交付(CI/CD)系统, 实现了 CI/CD 中流程的控制。通过抽象底层实现细节,用户可以跨多云平台和本地系统完成构建、测试,、部署等环节。
Tekton Pipeline 中定义了几类对象,核心理念是通过定义 yaml 定义构建过程。下面我们来介绍在实践和落地过程中最常用的5类对象:
Task:一个任务的执行模板,用于描述单个任务的构建过程。
TaskRun:定义 TaskRun 设置具体需要运行的 Task。
Pipeline:包含多个Task, 对 task 进行编排(串 / 并 行)。
PipelineRun:定义 PipelineRun 设置具体需要运行的 Pipeline。
PipelineResource:可用于 input 和 output 的对象集合。
jeci编译平台:jenkins(2.89.3) + k8sCluster(1.13)
2017年年中,我们开始对 jenkins 的 pipeline 功能进行验证,使用此功能可以直接对接 k8s。master 从工作节点转改变成仅做任务转发的节点, 实际编译工作将会在设置的 k8sCluster 中启动对应的 pod 进行编译。pod 的生命周期等同于一次编译的生命周期。此功能很快就对线上提供了服务, 节省了一批 master 节点。
服务运行近两年中, 在实际使用过程中遇到一些问题, 例如:
jenkinsfile 并不能很好的支持 shell 脚本, 有些符号需要进行转换。
需要单独学习 jenkinsfile 的语法。
新增加插件服务需要重启。
jenkins master 节点上因 job 数量太多, 导致打开界面超级慢。
新增加新 master 需要进行重新的配置。
当编译量大的时候 jenkins 与 k8s 之间的调度偶发的会出现问题。
▲架构简图▲
讲解:
BusinessScene:处理各种业务场景的业务逻辑。
TemplateEngine:抽象 yaml 模版, 根据不同的业务场景选择对应的模版进行渲染。
Tekton pipeline 提供了 PipelineResource、Task、Pipeline、TaskRun 和 PipelineRun 等几个 CRD,其中 Task/Pipeline 作为模板类型,占据非常重要的地位。pipeline 具有对 tasks 进行编排的能力,此功能是 Tekton pipeline 的核心功能之一,也是 TemplateEngine 解决的主要问题之一。
Tekton 对一个任务的执行分为三个阶段:
资源、参数输入,包括 git 代码库,task/pipeline/pipelineRun 之间参数的传递等。
执行逻辑,如 mvn clean package、docker build 等。
定义资源输出,docker push 等。
此服务对上述的三个阶段复杂的逻辑进行屏蔽, 向外透出简单的接口, 用户无需关心 task/pipeline/pipelineRun 之间参数传递,无需关心 pipeline 如何对 task 如何进行编排。
示例1:Java 应用构建完成后使用 kaniko 产生镜像并推送到镜像仓库, 这个实例相对简单,因此直接串行之行各个步骤即可, 展示了两种可行方案。
▲example1▲
示例2:一个 java 应用编译后出两个产物(image、pkg),产物分别推送到镜像仓库、云存储,然后分别部署到不同的环境中,部署完后通知测试人员。此示例与上面例子不同的是包含了并行的逻辑, 缩短了整体运行时间。
▲example2▲
经过上面的两个示例发现可以根据具体的业务进行对 task 进行自由编排。示例 2 中含有并行执行的逻辑在缩短整体运行时间的同时增加了参数传递、数据传递的复杂度,jbuild 成功地对用户进行了屏蔽, 用户只需要专心关注业务即可。
和 K8s 其它的 CRD 一样,tektoncd/pipeline 所有的 CRD 声明、实例都存储在 K8s 体系内的 etcd 组件里。这样的设计带来了历史运行数据持久化的问题, 同时 tekton 对已经运行过的 CRD 没有自动删除的功能, 当历史数据越来越多时资源将被耗尽。
watchService 解决了上面的所有问题:
清理历史资源。
持久化日志信息。
统计运行数据等。
tekton 版本0.9.0;k8s 集群版本 1.13。
1
tekton 安装后 controller 无法正常启动
问题描述:
0.9.0 版本中 controller 默认监听是 K8s 集群内所有的 namespace 下的 pod。如果配置的 K8s config 中认证不是针对所有 namespace 都有权限的场景中会导致 install te k ton 时 controller 无法正常启动。
解决办法:
修改在 controller/main.go 源码,增加了只允许监听 tekton-pipelines 这个namespace。
增加代码如下:
1ctx := injection.WithNamespaceScope(signals.NewContext(), system.GetNamespace())
2
3 sharedmain.MainWithContext(ctx, ControllerLogKey,
4 taskrun.NewController(images),
5 pipelinerun.NewController(images),
6 )
<左右滑动以查看完整代码>
注:高版本修复了此问题,增加了启动参数可以进行指定,默认监听所有 namespace。
namespace 参数原文解释:
1Namespace to restrict informer to. Optional, defaults to all namespaces.
<左右滑动以查看完整代码>
2
统计运行时长
提供了两种解决方案,可以根据实际业务需求进行选择,两种方案均已验证:
方案一:采用 watch 机制
采用 K8s 的 watch 特性, 提供一个服务做相应的业务处理, 这就需要具体业务具体分析。
事件:ADDED、 MODIFIED、DELETED、ERROR
方案二:在任务的 container 中增加回调功能
因业务场景需要,需要对执行命令的时间进行较为准确的计算,在方案一中发现采用这种方式会有一定时间的延时,因此进行了方案二的设计。
经过对 tekton 源码的学习了解 entrypoint 控制了每个 container 什么时候开始执行命令,因此我们正在 entrypoint 中增加 callback 接口,在每次执行前与执行后回调用用户指定的 API(此API是通过环境变量的传入),这样计算出的时长相对来说更加准确。
3
pipelineRun 堆积
修改 DefaultThreadsPerController 的值然后重新对 controller 生成镜像即可;此参数源码注解如下:
1// DefaultThreadsPerController is the number of threads to use
2// when processing the controller’s workqueue. Controller binaries
3// may adjust this process-wide default. For finer control, invoke
4// Run on the controller directly.
<左右滑动以查看完整代码>
4
命令被延迟执行
测试场景描述:
简单的一个 maven 类型项目的编译,包含的步骤有代码下载、代码编译。一次编译对应一个 pod, pod 中包含两个 container 分别是代码下载、代码编译。两个 container 串行执行。
三个节点的 K8s 集群,在资源充足的情况下,使用 jmeter 进行压测, 对上面的编译场景启动 500 次编译(会启动 500 个 pod)。
现象描述:
在 500 个 pod 中会出现几个 pod 的运行时长明显长于其他 pod,分别进入两个 container 中查看, 发现第一个 container (代码下载的pod,500,个,pod 使用的是同一个代码库)的运行时间比正常 pod 中第一个 container 的运行时间要长出 20s-70s 不等,有的甚至更长。
注:
1) 以上场景重复很多次并不是每次都会出现延迟执行的现象。
2)如果对源码中启动 DefaultThreadsPerController (默认为2)没有进行修改,则可能会出新 pipelineRun 堆积的情况, 属于正常现象, 可以通过增大次参数的值接近堆积的问题(修改后需要对 controller 进行重新生成镜像)。
经过对源码和对应 pod 的 yaml 文件分析可以发现 tekton 使用了 K8s 中的 downwardAPI 机制, 将 pod 中的信息以文件的形式挂载到 container 中, 例如下面的 yaml 可以发现:
1)名字为 clone 的 container 使用 volumeMounts 挂载了 downward,其他的 container 并没有挂载 downward。因此 clone 容器是想获取 pod 中 tekton.dev/ready 的内容。
1# 伪yaml
2kind: Pod
3metadata:
4…
5annotations:
6tekton.dev/ready: READY
7spec:
8volumes:
9- name: downward
10 downwardAPI:
11 items:
12 - path: ready
13 fieldRef:
14 apiVersion: v1
15 fieldPath: ‘metadata.annotations[’‘tekton.dev/ready’’]’
16 defaultMode: 420
17 containers:
18- name: clone
19 image: ubuntu
20 command:
21 - /tekton/tools/entrypoint
22 args:
23 - ‘-wait_file’
24 - /tekton/downward/ready
25 - ‘-wait_file_content’
26 - ‘-post_file’
27 - /tekton/tools/0
28 - ‘-entrypoint’
29 - /ko-app/git-init
30 - ‘–’
31 - ‘-url’
32 - ‘https://github.jd.com/test/test.git’
33 - ‘-revision’
34 - “master”
35 - ‘-path’
36 - /workspace/test
37 volumeMounts:
38 - name: downward
39 mountPath: /tekton/downward
40 …
<左右滑动以查看完整代码>
2)clone container 中的 command 为/tekton/tools/entrypoint,args 中-waitfile -waitfilecontent -postfile -entrypoint 均为 entrypoint 的参数(因为在定义 task 时并未写这些参数, 同时也可以看 entrypoint 的源码也可以发现),在查看 entry point 源码时看到如下逻辑:
1/*
2file为wait_file所对应的值
3expectContent为wait_file_content的值, 默认为false;
4wait_file_content此参数在对task的step进行编排时判断如果是第 一个step则添加, 后续step不会添加此参数
5*/
6func (realWaiter) Wait(file string, expectContent bool) error {
7…
8for ; ; time.Sleep(waitPollingInterval) {
9 log.Printf(“1. wait for file, time: %v”, time.Now())
10 /
11 获取此文件的属性,判断文件大小如果大于0则等待结束
12 或者expectContent为false 等待结束
13 */
14 if info, err := os.Stat(file); err == nil {
15 if !expectContent || info.Size() > 0 {
16 log.Printf(“2. finish wait for file, time: %v”, time.Now())
17 return nil
18 }
19 }
20…
<左右滑动以查看完整代码>
因此上面在测试中出现的问题可以发现 clone 容器在执行的时候 file 中的内容为 0,因此一致在 for 循环无法退出, 直到 file 文件里面有内容才会跳出 for 循环开始后面执行自定义的命令。
解决办法:
经过上述的描述, 对 task 的 step 进行编排时第一个容器去掉 waitfilecontent 即可。
云原生 CI 平台上线后在编译提速上有了很可观的改善;仅仅使用三台 K8sCluster 的 node 节点,便支持了老编译中一般的日编译流量;大大减少了对 jenkins 的维护工作, 因此无需再有外部的服务时刻监控 jenkins master 是否为可用状态;同时,使用新的工具插件时, 无需再重启master, 一个镜像便可以搞定,方便又快捷。
下图为新老编译平台job运行时长的对比分析:
“海纳百川,有容乃大”,我们未来将会融入更多优秀的工具,利于开发、测试、运维同学可以方便快捷地完成需求,提高工作效率,增强工作的快感和生活的幸福感。
丰富代码扫描功能,可以快速定位代码问题, 做到早发现早解决。
完善单元测试组件,可以满足不通语言不同架构的需要。
支持更多环境的编译, 满足不同语言, 不同业务的编译流程。
增强服务监控功能, 可以通过监控数据对服务的健康度以及使用情况进行很好的展示。
增加线上不同环境的部署, 打通测试-开发-部署全流程。
在编译上我们致力于提升编译率,同时完善编译失败的信息提示, 最终做到根据错误直接给出对应的解决方案。
在部署上我们将支持更多的发布方式(蓝绿部署、金丝雀部署等), 满足用户在不同的场景下可以更加轻松的进行发布同时降低回滚率。
打造全方面的平台,既可以单独部署开源服务(比如, 快速部署一个 mysql、tomcat),又可以满足开发在不同开发阶段的不同需求, 同时可以满足测试同学和运维同学需求。
在不到一年的时间里 tekton 也在快速发展、不断完善, 我们的服务架构也会随之迭代。而在打造此服务期间,我们也意识到项目得以顺利进行与在开源社区中得到了许多帮助息息相关,我们将持续关注并为社区尽微薄之力。同时,我们也将致力于为研发同学打造一款助力工作提升工作快感的平台。