1. 现象 - Tekton 克隆代码任务慢
在执行克隆任务时,Tekton 很费时间,多仓库下一般都需要 2 分 30 秒左右。如下图:
仅克隆的流水线就需要执行 2 分钟 16 秒,而克隆脚本实际上仅执行 1-3 秒。其中大部分时间花在了哪里?能不能减少?这是本篇主要想讨论的问题。
2. 分析克隆任务的时间开销
Tekton 运行流水线时,每个 Task 都会在一个独立 Pod 中运行。在上述场景下,一个 git clone task 只克隆一个仓库,如果有 N 个代码仓库,那么就需要创建至少 N 个 Pod。
这样就出现两个优化点:
- 并行执行任务
- 缩短单个执行时间
并行克隆可以从运维侧优化,先看看单个 Pod 执行的时间序列。
下面这个例子总时长 34s,第一个容器启动花了 29s,约占 85%,克隆代码只有 1s
|
|
下面这个例子总时长 107s,第一个容器启动花了 24s,约占 22%,git-init 容器启动到执行脚本花了 78秒,约占 72%,克隆代码只有 2s
|
|
从上面的例子可以看到两点:
- 从 Pod 创建到第一个容器运行很慢,大约需要 20-30 秒
- git-init 启动之后,到开始运行克隆脚本时间不稳定
因此考虑,能不能通过加速容器启动来减少执行时间?
3. 使用 tuned 将主机 CPU 设置为高性能模式,加快容器启动
CICD 构建使用的是物理机,在交付使用时不一定对其 CPU 工作模式进行了合理设置。CPU 的工作模式会对 CPU 工作频率产生影响,有可能导致 Pod 的启动速度慢[1]。
- 查看 CPU 工作频率
|
|
- 查看 CPU 工作模式
|
|
- 切换为 aliyun Ubuntu 源
|
|
- 安装 tuned
|
|
- 启动并查看状态
|
|
- 获取当前模式
|
|
- 设置为性能模式
|
|
可选项有:
latency-performance 延迟性能优化
network-latency 网络延迟优化
network-throughput 网络吞吐量优化
throughput-performance 吞吐性能优化
virtual-guest 虚拟机优化
virtual-host 虚拟机宿主机优化
throughput-performance 下 CPU 会以最高频率运行,Pod 启动第一个容器需要 23 秒,比之前的 46 秒提升不少。
全部机器设置为性能模式之后,大量测试时发现代码克隆的总时长并不会显著降低。原因是,构建机配置为 40C/125GB,已经具有足够 CPU;虽然主机 CPU 处于省电模式,但是其大部分工作频率接近最高频率,并没有处于很低的状态。出现上面 46 秒 减少到 23 秒的优化,可能只是偶现效果,CPU 全部以最高频率工作时应该能抑制这种波动。
CPU 的性能模式有利于构建加速,提供平稳的响应时间。在构建环境下,强烈建议开启 CPU 性能模式。
4. Tekton 使用 ReadWriteMany 存储提高并行度
默认情况下 Tekton 会使用 ReadWriteOnce 存储,因为其更加通用。使用 ReadWriteMany 的前提是集群的存储系统支持这种模式。下面以 Longhorn 为例对 ReadWriteMany 进行测试:
- 安装 NFS Client
Longhorn 的 ReadWriteMany 卷依赖于 NFS Client。
|
|
- 在提交 PipelineRun 时,设置存储为 ReadWriteMany
|
|
经过测试,发现 ReadWriteMany 与 ReadWriteOnce 模式耗时没有明显差别。
原因是: 如果多个 Pod 在同一个节点上,ReadWriteOnce 模式下也允许同时访问。ReadWriteOnce、ReadOnlyMany、ReadWriteMany 指的是 Node 与 PV 的对应关系,而不是 Pod 与 PV 的对应关系 ,这可能是被很多人忽略的一点。在 Kubernetes 1.22+ 版本,新增的 ReadWriteOncePod 针对的才是 Pod 与 PV 的对应关系[2]
回到 Tekton 构建的场景,由于开启了 affinity-assistant 导致一条流水线都在一个节点执行,ReadWriteOnce 与 ReadWriteMany 模式此时差别不大。
5. 关闭 affinity-assistant 分散任务到多节点
affinity-assistant 使得单条流水线创建的 Pod 都在一个节点上。为了让 Pod 启动更快,这里尝试将克隆多个仓库的任务分散到多个节点上,以减少 IO 和创建 Pod 的压力。
5.1 关闭 Tekton 的 affinity-assistant
编辑 Tekton 的配置文件[3]:
|
|
将 disable-affinity-assistant
设置为 true。
这里发现另一个很有用的参数 pruner,能够自动清理 taskrun、pipelinerun。这给构建集群的运维提供了极大便利。
此时,同一条流水线创建的 Pod 不再强行绑定在一个节点上运行,而是可以分散到其他节点。这样做的优劣如下:
优势
- 充分利用多节点,创建 Pod 并执行任务
- 在 Pod 调度方面有更多调优、定制的空间
劣势
- 对存储系统有要求,不能用 hostpath 方式
- 增加节点之间的网络传输
- 可能导致 task 之间产物传递故障,比如上一步产生的镜像,下一步调度到其他节点之后主机上找不到
5.2 测试验证
- disable-affinity-assistant + ReadWriteOnce ,执行时间明显增长
下面是截取的部分执行时长数据:
原因在于,克隆的 Pod 被分散到多个 Node 之后,Node 之间出现了对存储使用上的竞争。也就是 Node2 上的任务需要等待 Node1 上的任务执行完成之后,才能执行。
- disable-affinity-assistant + ReadWriteMany ,执行时长无明显变化
下面是截取的部分执行时长数据:
ReadWriteMany 模式下不同 Node 上的 Pod 能同时使用存储,但是额外增加了网络开销。一加一减,整体执行时长没有太大波动。从 Pod Status 和 Log 中获取的数据,也验证了上述观点。以下为执行的时间线:
|
|
创建容器只花了 18 秒,但是克隆脚本的执行时长平均都超过 10 秒,出现明显增长。
存储这部分,还有一个优化是使用 Longhorn 的 strict-local 模式。编辑 Longhorn 的配置文件:
|
|
将 numberOfReplicas 设置为 1,将 dataLocality 设置为 strict-local。 strict-local 是 Longhorn 1.4 提供的新特性,直接使用本地 Unix Socket 代理 IO 操作,而不是网络 TCP。但在构建场景下,经过测试此处不是瓶颈。
6. 优化 Etcd 以加快集群响应
在查看系统各组件时,Etcd 的日志引起了我的注意。
6.1 将 Etcd 迁移到更快的磁盘,降低延时
- etcd 大量 warning 日志
|
|
- 通过监控可以看到 Etcd 磁盘延时很高,接近 200ms
- 关闭 Etcd 服务
|
|
- 更新 Etcd 数据目录
|
|
/data
目录挂载的是一块 SSD,而 /var/lib/
是系统盘 HDD。
- 迁移数据
|
|
- 启动 Etcd
|
|
- 使用 SSD 之后 Etcd 磁盘延时有所降低,接近 100ms 但远没有达到目标值 25ms 以下
另外一个可能的原因在于 kube-status-metrics 开启了 labels 和 annotations 采集,导致 kube-apiserver 的压力上升。因此将关掉 kube-state-metrics,再观察 Etcd 指标,但并没有看到有明显优化效果。
6.2 提高 Etcd 进程的 IO 优先级
持续集成极其消耗 CPU、Mem、IO、Network 资源,而 Tekton 的基础运行时是 Kubernetes ,Etcd 又是 Kubernetes 的存储核心。因此,有必要保持 Etcd 进程具有最高的优先级,以减少管理平面的时间消耗。
|
|
6.3 拆分 Event 事件到新的 Etcd 集群
- 部署 Etcd
这里比较特殊的是,我采用的是 Kubekey 部署的集群,默认 Etcd 证书已经包含全部集群节点 IP。因此,我直接将其中一个 Etcd 拷贝到新节点运行一个新的 Etcd 集群,修改 ETCD_INITIAL_CLUSTER_STATE=new
即可。
否则,如果 Etcd 集群采用 TLS 连接,可能得重新生成并更新 kube-apiserver 中的 Etcd 证书。
- 编辑全部 master 节点的 kube-apiserver ,添加 etcd 配置
|
|
新增如下配置[4]
|
|
等待 kube-apiserver 重启完成。
- 在新的 Etcd 集群查看节点状态
|
|
如果看到 DB SIZE
不断增加,就说明 Event 事件已经拆分到了新的 Etcd 集群。
- 查看优化效果
这是在工作时间段的监控截图,有些难以置信的是经过 SSD、剥离 Event 事件之后,Etcd 磁盘延时竟然只有 10 ms。
但 Etcd 部分的优化,可能对构建时长有 1-2 秒的优化效果,这远不是理想的结果。在分析 Kubelet 日志、源码之后,使用 NFS 是一个不错的优化点。
7. 使用 NFS 存储能有效加快带存储卷的 Pod 创建
创建时,如果对存储有依赖,Pod 会持续地等待,会导致容器创建慢。下面是一个简化之后的例子,其中,nodeName
指定了节点避免集群调度的干扰;imagePullPolicy
设置为 IfNotPresent
避免拉取镜像的干扰。
- 无存储的负载
|
|
下面是时间序列:
|
|
在镜像已经提前拉取的情况下,启动第一个容器花了 1 秒。
- 有存储的负载
|
|
下面是时间序列:
|
|
以下为 Pod 使用 PV 的流程为[5],时间点主要从 Kubelet、iscsid 日志中分析得出:
- 存储组件创建 pv -> 09:55:12
- attach 挂载到 Pod 所在 Node -> 09:55:14(iscsi connected)-09:55:25(kubelet attached)
- mount 挂载到 Pod -> 09:55:26
可以看到 attach 花费了太多时间,因此换为免 attach 的 NFS 作为后端存储。
经过测试大约能有 21 秒的加速效果。
从原来的执行时长:
2 分钟 13 秒、2 分钟 34 秒、2 分钟 37 秒、2 分钟 28 秒、2 分钟 25 秒
缩短为:
1 分钟 58 秒、2 分钟 20 秒、2 分钟 16 秒、1 分钟 58 秒、2 分钟 1 秒
在配合 disable-affinity-assistant
之后,大约又能节约 10 - 20 秒,下图是测试数据:
8. 总结
最近一直在尝试从运维的角度优化构建慢的问题。
本篇是关于 Tekton 执行克隆任务慢问题的优化。通过使用 NFS 存储,大约能减少 20 秒 Kubelet 创建 Pod 的时间;关闭 affinity-assistant
功能将单条流水线的 Pod 分散到多个节点,大约能减少 10-20 秒启动速度 ;由于测试数据集有限,目前观测到的效果是之前 2 分 30 秒的克隆流水线,现在 2 分钟以内能完成,大约有 30 秒的优化提升。当然,更快的构建方式是,一个 Pod 多仓库克隆、保持 PV 不销毁,但调整过大,不在本次运维优化范围。
以下为本文主要观点:
- CPU 高性能模式有利于 Pod 快速启动
- ReadWriteOnce、ReadWriteMany 描述的是 Pod 与 Node 的关系,ReadWriteOnce 模式下,同一个 Node 的多个 Pod 可以同时使用 PV
- 带存储卷的 Pod 启动速度比不带存储的 Pod 慢很多,大约能慢 10 多秒
- Tekton 默认配置下,一条流水线只能在一个节点构建;通过参数
disable-affinity-assistant
可以关闭这一特征,提高并行 task 的并行度 - 使用 SSD、拆分 Event 能够显著降低 Etcd 的磁盘压力、提高响应速度
- NFS 下带存储卷的 Pod 创建速度明显快于 OpenEbs、Longhorn
9. 参考
- https://zhangguanzhang.github.io/2019/04/28/k8s-java-start-time-not-same/
- https://kubernetes.io/zh-cn/docs/concepts/storage/persistent-volumes/#access-modes
- https://tekton.dev/docs/operator/tektonconfig/
- https://imroc.cc/kubernetes/best-practices/ops/etcd-optimization.html
- https://www.lixueduan.com/posts/kubernetes/14-pv-dynamic-provision-process/