Please enable Javascript to view the contents

优化 Tekton 执行克隆任务慢问题,节省约 30 秒

 ·  ☕ 8 分钟

1. 现象 - Tekton 克隆代码任务慢

在执行克隆任务时,Tekton 很费时间,多仓库下一般都需要 2 分 30 秒左右。如下图:

仅克隆的流水线就需要执行 2 分钟 16 秒,而克隆脚本实际上仅执行 1-3 秒。其中大部分时间花在了哪里?能不能减少?这是本篇主要想讨论的问题。

2. 分析克隆任务的时间开销

Tekton 运行流水线时,每个 Task 都会在一个独立 Pod 中运行。在上述场景下,一个 git clone task 只克隆一个仓库,如果有 N 个代码仓库,那么就需要创建至少 N 个 Pod。

这样就出现两个优化点:

  • 并行执行任务
  • 缩短单个执行时间

并行克隆可以从运维侧优化,先看看单个 Pod 执行的时间序列。

下面这个例子总时长 34s,第一个容器启动花了 29s,约占 85%,克隆代码只有 1s

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pod -> 14:36:52
    distroless-base -> 14:37:21
    distroless-base <- 14:37:21
    pipeline-entrypoint -> 14:37:21
    pipeline-entrypoint <- 14:37:21
    pipeline-git-init -> 14:37:22
        clone -> 14:37:25
        clone <->  14:37:26
    pipeline-git-init <- 14:37:26
pod <- 14:37:26

下面这个例子总时长 107s,第一个容器启动花了 24s,约占 22%,git-init 容器启动到执行脚本花了 78秒,约占 72%,克隆代码只有 2s

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pod -> 10:42:07
    distroless-base -> 10:42:31
    distroless-base <- 10:42:31
    pipeline-entrypoint -> 10:42:32
    pipeline-entrypoint <- 10:42:32
    pipeline-git-init -> 10:42:34
        clone -> 10:43:52
        clone <-> 10:43:54
    pipeline-git-init <- 10:43:54
pod <- 10:43:54

从上面的例子可以看到两点:

  • 从 Pod 创建到第一个容器运行很慢,大约需要 20-30 秒
  • git-init 启动之后,到开始运行克隆脚本时间不稳定

因此考虑,能不能通过加速容器启动来减少执行时间?

3. 使用 tuned 将主机 CPU 设置为高性能模式,加快容器启动

CICD 构建使用的是物理机,在交付使用时不一定对其 CPU 工作模式进行了合理设置。CPU 的工作模式会对 CPU 工作频率产生影响,有可能导致 Pod 的启动速度慢[1]。

  • 查看 CPU 工作频率
1
2
3
grep -i mhz /proc/cpuinfo

cpu MHz		: 1641.500
  • 查看 CPU 工作模式
1
2
3
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

powersave
  • 切换为 aliyun Ubuntu 源
1
2
3
4
cp /etc/apt/sources.list /etc/apt/sources.list.bak
sed -i "s/cn.archive.ubuntu.com/mirrors.aliyun.com/g" /etc/apt/sources.list
sed -i '/^#/d' /etc/apt/sources.list
apt-get update
  • 安装 tuned
1
apt install -y tuned tuned-utils tuned-utils-systemtap
  • 启动并查看状态
1
2
3
systemctl start tuned
systemctl enable tuned
systemctl status tuned
  • 获取当前模式
1
tuned-adm active
  • 设置为性能模式
1
tuned-adm profile throughput-performance

可选项有:

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。

1
apt-get -y install nfs-common
  • 在提交 PipelineRun 时,设置存储为 ReadWriteMany
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
	"kind": "PipelineRun",
	"apiVersion": "tekton.dev/v1beta1",
	"metadata": {
		"name": "test"
	},
	"spec": {
		"pipelineRef": {
			"name": "test"
		},
		"workspaces": [{
			"volumeClaimTemplate": {
				"spec": {
					"accessModes": ["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]:

1
kubectl edit configmap feature-flags -n tekton-pipelines

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 中获取的数据,也验证了上述观点。以下为执行的时间线:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pod -> 00:12:04
    distroless-base -> 00:12:22
    distroless-base <- 00:12:22
    pipeline-entrypoint -> 00:12:24
    pipeline-entrypoint <- 00:12:24
    pipeline-git-init -> 00:12:25
        clone -> 00:12:33 
        clone <-> 00:12:48
    pipeline-git-init <- 00:12:48
pod <- 00:12:48

创建容器只花了 18 秒,但是克隆脚本的执行时长平均都超过 10 秒,出现明显增长。

存储这部分,还有一个优化是使用 Longhorn 的 strict-local 模式。编辑 Longhorn 的配置文件:

1
kubectl -n longhorn-system edit cm longhorn-storageclass

将 numberOfReplicas 设置为 1,将 dataLocality 设置为 strict-local。 strict-local 是 Longhorn 1.4 提供的新特性,直接使用本地 Unix Socket 代理 IO 操作,而不是网络 TCP。但在构建场景下,经过测试此处不是瓶颈。

6. 优化 Etcd 以加快集群响应

在查看系统各组件时,Etcd 的日志引起了我的注意。

6.1 将 Etcd 迁移到更快的磁盘,降低延时

  • etcd 大量 warning 日志
1
2
3
4
5
6
7
journalctl -u etcd.service -f

08:42:37 node1 etcd[1091]: read-only range request "key:\"/registry/minions/node1\" " with result "range_response_count:1 size:14068" took too long (107.736306ms) to execute
08:42:37 node1 etcd[1091]: read-only range request "key:\"/registry/pods/xxx/p-cfhlvsaf16letp8btsp0-fetch--rw-7qrpm\" " with result "range_response_count:1 size:20887" took too long (103.499181ms) to execute
08:42:37 node1 etcd[1091]: read-only range request "key:\"/registry/pods/xxx/p-cfhlvi2f16letp8btsmg-docker-build-rbrzl-pod-fjk2l\" " with result "range_response_count:1 size:22666" took too long (115.939841ms) to execute
08:42:37 node1 etcd[1091]: read-only range request "key:\"/registry/tekton.dev/pipelines/xxx/p-cbqcugeb23tefrmhs5jg\" " with result "range_response_count:1 size:12536" took too long (157.681896ms) to execute
08:42:37 node1 etcd[1091]: read-only range request "key:\"/registry/pods/xxx/p-cfhlvsaf16letp8btsp0-fetch--rw-7qrpm\" " with result "range_response_count:1 size:20853" took too long (147.333596ms) to execute
  • 通过监控可以看到 Etcd 磁盘延时很高,接近 200ms

  • 关闭 Etcd 服务
1
systemctl stop etcd
  • 更新 Etcd 数据目录
1
2
3
vim /etc/etcd.env

ETCD_DATA_DIR=/data/etcd

/data 目录挂载的是一块 SSD,而 /var/lib/ 是系统盘 HDD。

  • 迁移数据
1
mv /var/lib/etcd /data
  • 启动 Etcd
1
systemctl start 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 进程具有最高的优先级,以减少管理平面的时间消耗。

1
ionice -c2 -n0 -p $(pgrep 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 配置
1
vim /etc/kubernetes/manifests/kube-apiserver.yaml

新增如下配置[4]

1
    - --etcd-servers-overrides="/events#https://5.5.5.5:2379"

等待 kube-apiserver 重启完成。

  • 在新的 Etcd 集群查看节点状态
1
2
3
4
5
6
7
etcdctl --write-out=table endpoint status

+----------------+------------------+---------+---------+-----------+-----------+------------+
|    ENDPOINT    |        ID        | VERSION | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |
+----------------+------------------+---------+---------+-----------+-----------+------------+
| 127.0.0.1:2379 | 173a84b520278cc1 |  3.4.13 |   18 MB |      true |         2 |      11223 |
+----------------+------------------+---------+---------+-----------+-----------+------------+

如果看到 DB SIZE 不断增加,就说明 Event 事件已经拆分到了新的 Etcd 集群。

  • 查看优化效果

这是在工作时间段的监控截图,有些难以置信的是经过 SSD、剥离 Event 事件之后,Etcd 磁盘延时竟然只有 10 ms。

但 Etcd 部分的优化,可能对构建时长有 1-2 秒的优化效果,这远不是理想的结果。在分析 Kubelet 日志、源码之后,使用 NFS 是一个不错的优化点。

7. 使用 NFS 存储能有效加快带存储卷的 Pod 创建

创建时,如果对存储有依赖,Pod 会持续地等待,会导致容器创建慢。下面是一个简化之后的例子,其中,nodeName 指定了节点避免集群调度的干扰;imagePullPolicy 设置为 IfNotPresent 避免拉取镜像的干扰。

  • 无存储的负载
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: Pod
metadata:
  name: test-pv
spec:
  nodeName: node1
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: nginx

下面是时间序列:

1
2
3
4
pod -> 03:01:18
    nginx -> 03:01:19
    nginx <-
pod <-

在镜像已经提前拉取的情况下,启动第一个容器花了 1 秒。

  • 有存储的负载
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: v1
kind: Pod
metadata:
  name: test-pv
spec:
  nodeName: node1
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: nginx
    volumeMounts:
    - mountPath: /data
      name: data
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: test-pv
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pv
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

下面是时间序列:

1
2
3
4
5
6
7
pvc -> 09:55:09
pv -> 09:55:12

pod -> 09:55:09
    nginx -> 09:55:27
    nginx <-
pod <-

以下为 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. 参考

  1. https://zhangguanzhang.github.io/2019/04/28/k8s-java-start-time-not-same/
  2. https://kubernetes.io/zh-cn/docs/concepts/storage/persistent-volumes/#access-modes
  3. https://tekton.dev/docs/operator/tektonconfig/
  4. https://imroc.cc/kubernetes/best-practices/ops/etcd-optimization.html
  5. https://www.lixueduan.com/posts/kubernetes/14-pv-dynamic-provision-process/

微信公众号
作者
微信公众号