去年10月份,京东研发效能部紧跟云原生潮流,开始调研并引入 Tekton ,在内部尝试基于 Tekton 打造下一代云原生 CI 平台。
云原生概念自2015年最初被提及后, 其生态在不断壮大。与此同时,支持云原生的开源工具如雨后春笋般出现。在众多开源工具中我们把目光聚焦在了tekton上, 不仅仅因为她是K8s“亲”生, 还因为与其他工具相比较,它更加轻量、更加灵活扩展,并支持多云环境, 拥有活跃的社区。Tekton虽然还是一个挺新的项目,但是已经成为 Continuous Delivery Foundation (CDF) 四个初始项目之一。
在不到一年的时间里,我们通过对tektoncd/pipeline 工程的学习和验证,建设了jbuild等组件,并部署到业务生产环境的 Kubernetes 集群里,支持京东内部日均近万次应用构建任务。
本文将分享如何使用 Tekton 推进CI平台往云原生方向发展和落地,同时分享一些在推进过程中遇到的问题以及解决方案。
Tekton是什么?
Tekton 是一个功能强大且灵活的 Kubernetes 原生开源框架,用于创建持续集成和交付(CI/CD)系统, 实现了CI/CD 中流程的控制。通过抽象底层实现细节,用户可以跨多云平台和本地系统完成构建、测试,、部署等环节。
Tekton Pipeline中定义了几类对象,核心理念是通过定义yaml定义构建过程。下面我们来介绍在实践和落地过程中最常用的5类对象:
• Task:一个任务的执行模板,用于描述单个任务的构建过程;
• TaskRun:定义TaskRun设置具体需要运行的Task;
• Pipeline:包含多个Task, 对task进行编排(串 / 并 行);
• PipelineRun:定义PipelineRun设置具体需要运行的Pipeline;
• PipelineResource:可用于input和output的对象集合。
现状——老编译平台
架构简图
jeciService介绍
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之间的调度偶发的会出现问题;
云原生CI平台实践&落地
架构简图
jbuildService
讲解:
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成功地对用户进行了屏蔽, 用户只需要专心关注业务即可。
watchService
和 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。
增加代码如下:
ctx := injection.WithNamespaceScope(signals.NewContext(), system.GetNamespace())
sharedmain.MainWithContext(ctx, ControllerLogKey,
taskrun.NewController(images),
pipelinerun.NewController(images),
)
注:高版本修复了此问题, 增加了启动参数可以进行指定, 默认监听所有namespace。
namespace参数原文解释:
Namespace 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生成镜像即可; 此参数源码注解如下:
// DefaultThreadsPerController is the number of threads to use
// when processing the controller's workqueue. Controller binaries
// may adjust this process-wide default. For finer control, invoke
// 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的内容。
# 伪yaml
kind: Pod
metadata:
...
annotations:
tekton.dev/ready: READY
spec:
volumes:
- name: downward
downwardAPI:
items:
- path: ready
fieldRef:
apiVersion: v1
fieldPath: 'metadata.annotations[''tekton.dev/ready'']'
defaultMode: 420
containers:
- name: clone
image: ubuntu
command:
- /tekton/tools/entrypoint
args:
- '-wait_file'
- /tekton/downward/ready
- '-wait_file_content'
- '-post_file'
- /tekton/tools/0
- '-entrypoint'
- /ko-app/git-init
- '--'
- '-url'
- 'https://github.jd.com/test/te...'
- '-revision'
- "master"
- '-path'
- /workspace/test
volumeMounts:
- name: downward
mountPath: /tekton/downward
...
2) clone container中的command为/tekton/tools/entrypoint, args中-wait_file -wait_file_content -post_file -entrypoint 均为entrypoint的参数(因为在定义task时并未写这些参数, 同时也可以看entrypoint的源码也可以发现), 在查看entry point源码时看到如下逻辑
/*
file为wait_file所对应的值
expectContent为wait_file_content的值, 默认为false;
wait_file_content此参数在对task的step进行编排时判断如果是第一个step则添加, 后续step不会添加此参数
*/
func (*realWaiter) Wait(file string, expectContent bool) error {
...
for ; ; time.Sleep(waitPollingInterval) {
log.Printf("1. wait for file, time: %v", time.Now())
_/*_
获取此文件的属性,判断文件大小如果大于0则等待结束
或者expectContent为false 等待结束
*/
if info, err := os.Stat(file); err == nil {
if !expectContent || info.Size() > 0 {
log.Printf("2. finish wait for file, time: %v", time.Now())
return nil
}
}
...
因此上面在测试中出现的问题可以发现clone容器在执行的时候file中的内容为0, 因此一致在for循环无法退出, 直到file文件里面有内容才会跳出for循环开始后面执行自定义的命令。
解决办法:
经过上述的描述, 对task的step进行编排时第一个容器去掉wait_file_content即可。
老编译平台 VS 云原生CI平台
云原生CI平台上线后在编译提速上有了很可观的改善;仅仅使用三台K8sCluster的node节点,便支持了老编译中一般的日编译流量;大大减少了对jenkins的维护工作, 因此无需再有外部的服务时刻监控jenkins master是否为可用状态;同时,使用新的工具插件时, 无需再重启master, 一个镜像便可以搞定, 方便又快捷。
下图为新老编译平台job运行时长的对比分析:
未来规划
海纳百川
“海纳百川, 有容乃大”, 我们未来将会融入更多优秀的工具, 利于开发、测试、运维同学可以方便快捷地完成需求,提高工作效率,增强工作的快感和生活的幸福感。
• 丰富代码扫描功能, 可以快速定位代码问题, 做到早发现早解决;
• 完善单元测试组件, 可以满足不通语言不同架构的需要;
• 支持更多环境的编译, 满足不同语言, 不同业务的编译流程;
• 增强服务监控功能, 可以通过监控数据对服务的健康度以及使用情况进行很好的展示;
• 增加线上不同环境的部署, 打通测试-开发-部署全流程。
在编译上我们致力于提升编译率, 同时完善编译失败的信息提示, 最终做到根据错误直接给出对应的解决方案。
在部署上我们将支持更多的发布方式(蓝绿部署, 金丝雀部署等), 满足用户在不同的场景下可以更加轻松的进行发布同时降低回滚率。
点-线-面
打造全方面的平台, 既可以单独部署开源服务(比如, 快速部署一个mysql、tomcat),又可以满足开发在不同开发阶段的不同需求, 同时可以满足测试同学和运维同学需求。
总结
在不到一年的时间里tekton也在快速发展,、不断完善, 我们的服务架构也会随之迭代。而在打造此服务期间, 我们也意识到项目得以顺利进行与在开源社区中得到了许多帮助息息相关, 我们将持续关注并为社区尽微薄之力。同时,我们也将致力于为研发同学打造一款助力工作提升工作快感的平台。