流水线没改,YAML 一行没动,镜像基础层也没换,构建时间从 3 分钟一路涨到 20 分钟。登上节点看监控,CPU 才用了三成,内存还有一半空着,负载曲线平得像条死线。这种"资源很闲、活很慢"的组合,在自建 Kubernetes 上跑 Tekton 的团队里出现得极其频繁,而且它几乎从来不是 Tekton 本身的问题。
原因在于 Tekton 的架构是"轻控制面 + 重执行面":控制面只有 controller、webhook、可选的 dashboard 和 triggers 这几个 Deployment,闲下来的时候内存占用以百 MB 计;真正消耗资源的是它按 PipelineRun 造出来的那一批 TaskRun Pod,以及这些 Pod 背后挂着的 workspace 卷、构建缓存目录、镜像拉取流量和日志输出。CPU 三成说明你算错的是并发维度、IO 维度和调度维度,不是算力维度。
把现象拆开看,最常落在下面几条上:
下面按"谁在吃资源 → 卷怎么选 → 缓存要不要独立 → 并发怎么算 → 镜像与出网 → 清理不做会怎样"这条线一路拆下去,每一段都给能直接拿去算的公式和数字。
先把 Tekton 的两张脸分开。控制面是 tekton-pipelines-controller、tekton-pipelines-webhook,加上可选的 dashboard、triggers-controller 与 eventlistener;执行面是 controller 按 TaskRun 定义在集群里创建的那些 Pod。这两者的资源曲线形状完全不同:控制面是一条随并发数缓慢抬升的曲线,执行面是台阶式跳变的——每来一个 TaskRun,就实打实多一份内存 request、多一份本地盘写入、多一个 Pod 槽位。
很多人以为 controller 是 CPU 密集的,其实不是。它的活是"watch 事件 → 取对象 → 判断状态 → 创建 Pod / patch 状态",典型的 IO 与串行协调型负载。卡住它的通常是三处配置,而这三处的默认值都是按小规模集群给的:
threads-per-controller 默认值是 2(以实际部署版本为准)。这意味着同一时刻最多只有 2 个协调循环在跑。按单轮协调平均 200 毫秒估算,理论吞吐在每秒 10 个对象这个量级;当 50 个 PipelineRun 同时启动、每个展开 5 个 TaskRun,队列里瞬间就有 250 个对象在等,workqueue 深度会迅速上一个量级。这三处调完之后,下一个承受压力的是 etcd。Tekton 的写放大比想象中高:一个 TaskRun 里有 10 个 step,每完成一个 step 就要 patch 一次 status,加上 start、stop、conditions、results 的更新,单个 TaskRun 生命周期内对 etcd 的写次数是几十次量级。并发 50 的时候,etcd 面对的是每秒几百次写,而 etcd 的每一次提交都要 fsync 落盘——这里对磁盘的要求是低延迟而不是大容量,控制面的盘必须是 NVMe,机械盘或者低端 SATA SSD 在这里会直接体现为流水线整体卡顿。
执行面每来一个 TaskRun,开销来自四个方向,而且四个方向都可能先撞墙:
结论很直接:控制面要独立部署,而且它的规格取向是"中等 CPU + 够用内存 + 低延迟 NVMe";执行面的规格取向是"按并发公式堆核数与内存 + 大容量本地 NVMe + 足够 Pod 槽位"。把这两类负载混在同一批节点上,后果是构建高峰期 controller 被自己的执行 Pod 抢走 CPU 和内存,workqueue 堆积,然后全流水线状态更新延迟——表现出来就是"节点看着不忙,流水线却越跑越慢"。
Tekton 的 workspace 是 Task 之间传递产物、挂载源码、挂凭证的载体。它支持三种后端,选择哪一种直接决定了磁盘 IO 路径、生命周期和并发行为。这里不能拍脑袋,三种的适用面差得很远。
emptyDir 是默认选择,也是 IO 性能最好的选择——它落在节点的本地盘上(也可以设 medium: Memory 放到内存里,但那会把内存当盘用,构建场景下一般不这么干),随机写延迟就是本地 NVMe 的延迟,编译、解压、npm install 这类海量小文件操作跑得最快。
问题出在生命周期上。emptyDir 随 Pod 创建而创建,随 Pod 删除而清空。而 Tekton 的 TaskRun Pod 在任务结束后就被回收,所以任何写在 emptyDir 里的东西,包括 Kaniko 的层缓存目录、Maven 的 .m2、Gradle 的 caches、npm 的缓存目录,全部在下一次构建时归零。
说白了:只要你的 Task 里存在"希望下次复用"的目录,emptyDir 就是错的。它适合的只有一类场景——纯工作区:拉代码、编译、出产物、产物立刻推走,中间不留下任何想复用的东西。很多团队踩的坑是"workspace 用了 emptyDir,把缓存目录也挂在 workspace 下面",于是缓存命中率恒等于 0,构建永远冷启动,3 分钟变 20 分钟有一半是这个原因。
还有两个细节:一是 emptyDir 可以设 sizeLimit,但 kubelet 对超限的检查是周期性的,不是实时的,超限后 Pod 会被驱逐而不是优雅失败,也就是说在检查间隔内它有可能先把节点盘写满;二是 emptyDir 占用的是节点盘,节点盘同时还要放镜像层,两者叠加时驱逐风险成倍上升。
PVC 有两种挂法,务必分清:persistentVolumeClaim.claimName 是引用一个已存在的卷,多次运行共享同一份数据,必须配 subPath 做目录隔离,否则并发的 PipelineRun 会互相覆盖同一个 workspace;volumeClaimTemplate 则是每次运行动态创建一个新 PVC,隔离彻底,但每跑一次就多一次存储供给请求、多一个 etcd 对象、多一个待回收的卷。
共享 PVC 路线有个绕不开的调度后果。如果底层是 ReadWriteOnce,这个卷同一时刻只能挂在一个节点上,Tekton 会为它创建 affinity assistant,把所有共享该卷的 TaskRun 拉到同一个节点。结果是:你买了 5 台执行节点,并发 10 个 PipelineRun,实际全部挤在 1 台机器上排队,剩下 4 台闲着——这正好对应开篇那个"CPU 用三成"的现象。
破解办法有三条,按优先级排:一是用 ReadWriteMany 的底层(NFS、CephFS 之类),让卷能跨节点挂载,affinity assistant 就失去了存在意义;二是把共享卷按用途拆细,减少被同一卷绑定的 TaskRun 数量;三是在确实不需要它的场景下通过设置 disable-affinity-assistant 关掉它(这个开关在 feature-flags ConfigMap 里),前提是你已经确认底层卷支持跨节点访问,否则会出现 TaskRun 挂不上卷而一直 Pending。
第三种是把 configmap 或 secret 声明为 workspace,它会被投影成目录里的文件。适用面很窄也很明确:挂配置文件、挂脚本、挂凭证、挂证书,而且全部是只读挂载。
它有三个硬限制:一是体量,etcd 对单个对象有体量上限,configmap 塞大文件会直接失败;二是更新语义,configmap 改了之后挂载点内容会更新,但正在运行的 Pod 感知时机不确定,跑构建时改它可能出现前后 step 读到不同内容的情况;三是它完全不提供写能力,任何需要落盘的目录都不能走这条路。所以判断标准就一句话:只读、几 KB 到几百 KB、内容来自配置而非构建过程,用它;否则一律不用。
这一节是整篇的核心。构建时间从 3 分钟涨到 20 分钟,绝大部分情况都可以在这里找到答案。
先把需要复用的东西列清楚,它们的共同特征是"写一次、读很多次、重建代价极高":
给缓存盘定容量不能拍脑袋说"给 500G 吧",要能算出来。方法分两步,第一步算镜像侧,第二步算依赖侧,最后乘安全系数并留出目标水位。
镜像侧:单镜像缓存 ≈ 镜像层数 × 平均层大小。按经验区间估算(需按你的实际镜像实测校准),一个业务镜像的常见层数是 10 到 15 层,未压缩状态下平均单层在 50MB 到 80MB,也就是单个镜像基线落在 0.6GB 到 1.2GB。再乘以"服务数 × 每服务保留的基线数":20 个服务、每个服务保留最近 5 个可用基线,就是 20 × 5 × 0.8GB ≈ 80GB。注意公共基础层在 registry 侧会去重,但在本地层缓存里不一定去重,所以这个估算要按不去重算,偏保守是对的。
依赖侧:依赖缓存 ≈ 单仓平均体积 × 仓库数 × (1 + 周期增长)。30 个仓库、单仓平均 800MB,就是 24GB;依赖树每半年增长 20% 到 30% 是常态,按 1.3 计就是 31GB。
合成:总量 = (镜像侧 + 依赖侧) × 增长系数 ÷ 目标水位。上面这组数字是 (80 + 31) × 1.0 ÷ 0.7 ≈ 159GB,向上取整给 200GB 到 256GB 起步。目标水位取 0.7 而不是 0.9,是因为 kubelet 的驱逐阈值默认在 15% 附近,盘用到 85% 就会驱逐 Pod,而驱逐发生在构建高峰期等于成片失败。这一点比容量本身更重要:缓存盘一定要跟系统盘、镜像盘分开,否则缓存增长会一路把节点推到驱逐线。
把上面的分析收敛成部署形态,是三层而不是一层:
三层的取舍逻辑是:控制面怕的是被抢占,所以要独占;执行面怕的是 Pod 槽位与本地 IO,所以要横向扩;缓存与镜像层怕的是盘满与网络瓶颈,所以要容量大、内网快、能独立扩容。把这三层压到一台机器上,前面所有的坑会同时出现。
| 层级 | 主要资源诉求 | 推荐部署形态 | 扩容信号 | 最容易踩的坑 |
|---|---|---|---|---|
| 控制面 controller / webhook |
中等 CPU、8–16G 内存、低延迟 NVMe;瓶颈在线程数与客户端限流,不在算力 | 2 台独立节点(或与 K8s 控制面同置),controller 副本数与 leader election buckets 同步调大 | workqueue 深度持续上百;PipelineRun 创建到 Pod 出现的时间从秒级涨到十秒级 | 和执行面混部,构建高峰期被自己的 Pod 抢走 CPU,状态更新集体延迟 |
| 控制面 etcd 与 apiserver |
fsync 延迟敏感;写放大来自 TaskRun 的 step 状态 patch | NVMe 独占盘,配额按实际量上调并开自动压缩 | etcd 数据库用量接近配额;apiserver 的 CREATE pods 耗时 P99 抬升 | 默认配额偏小,历史 TaskRun 堆积后 etcd 转只读,整个集群无法创建 Pod |
| 执行面 TaskRun Pod |
并发内存 = 单 Pod 峰值 × 并发数;Pod 槽位受 maxPods 约束 | 多台 16–32 核 / 64–128G / 本地 NVMe 横向堆,专用节点池加 taint 隔离 | 节点内存还剩很多但 Pod 调度不下去;Pending Pod 持续存在 | 把 step 内存当求和算,虚高 request 把可调度 Pod 数砍掉一半 |
| 执行面 workspace 工作区 |
4K 随机写性能;生命周期必须跟着构建步骤走 | 纯工作区用 emptyDir 走本地盘;需要跨 Task 传递时用 PVC 加 subPath | 构建耗时变长但 CPU 空闲;workspace 写入量持续增大 | 把缓存目录挂在 emptyDir 的 workspace 下,缓存命中率恒为 0 |
| 缓存层 层缓存与依赖缓存 |
大容量 + 高 IOPS;容量按镜像层数、依赖增长速率估算 | 独立机器或独立卷,RWX 优先(NFS / CephFS),与执行面同内网 | 缓存命中率下降;盘用量逼近 70%;构建冷启动比例上升 | 缓存盘和系统盘共用,缓存增长触发 kubelet 驱逐,高峰期成片失败 |
| 镜像层 内网 registry |
内网带宽与顺序写;容量受 tag 数与层去重影响 | Harbor 或轻量 registry 部署在内网,配 tag 保留规则与垃圾回收 | 拉取耗时占构建总时长比例上升;内网带宽打满 | 只做保留策略不做垃圾回收,卷一直涨;或跨机房部署把拉取延迟放大十倍 |
| 产物与日志层 对象存储与日志 |
出网流量与请求数;日志是持续增长型数据 | 内网 S3 兼容存储承接产物;Tekton Results 把历史外置到数据库 | 出网流量月度持续上升;节点日志目录增长明显 | 直接走公网对象存储,出网流量与请求数双双计费,成本不可控 |
| 清理与生命周期 | CPU 与 IO 占用都很低,但没有它就一定会出事 | CronJob 定期清理终态对象,配合 TTL 与日志轮转策略 | 终态 PipelineRun 与 TaskRun 对象数线性增长;PVC 残留堆积 | 压根没做,三个月后盘满、etcd 满、Pod 配额满同时发生 |
并发是 Tekton 选型里唯一必须先算清楚的数字,因为它同时决定了内存、CPU、Pod 槽位和本地盘四个维度。给一套能直接套的公式。
M_node = P × C + S + B
举例:P = 2.5GB,C = 8,S = 3GB,B = 20GB × 0.15 ≈ 3GB,得到 M_node ≈ 26GB,向上取标准档就是 32GB 内存的节点。注意这是"能跑"的下限,不是"跑得舒服"的规格。
N_cpu ≥ (单构建平均 CPU 使用核数 × C) ÷ 目标利用率 + 1
目标利用率取 0.7,那 1 核是留给系统和后台组件的。接上例:单构建平均吃 1.5 核,C = 8,则 1.5 × 8 ÷ 0.7 ≈ 17.1,加 1 约 18 核,实际选 16 核到 32 核的机器。这里的关键认知是:构建是突发型负载,不是稳态负载,你不能按平均利用率买到 90%,那样高峰期就是排队。
Pod 槽位:C_effective × (1 + affinity assistant 数量) ≤ maxPods - 系统 Pod 数。maxPods 默认 110,扣掉 DaemonSet、系统组件和 affinity assistant,真正留给构建的槽位通常只有 80 上下。并发上不去却看不出原因时,先看这个数。
本地盘:D_node ≥ (单次构建工作区写入量 W + 镜像解压后体积 I) × C × 1.5。W 按实测取,一个中型项目的工作区写入量在 2GB 到 8GB;I 按压缩镜像体积的 2.5 到 3 倍估。上例里 W = 4GB、I = 2GB、C = 8,得到 72GB,看起来不大,但节点盘还要同时承载镜像层缓存和日志,实际配 512GB 以上的 NVMe 才不会在高峰期被 kubelet 的镜像回收反复打断。
这一点必须讲透,否则并发一定失控。Tekton 的 PipelineRun 没有类似"最大并行数"的集群级字段,能用的手段是把 K8s 的能力组合起来:
把这些数字落到机器上,顺序是这样的:先用公式算出单节点能扛几个并发,再用"日常峰值并发 ÷ 单节点并发数"得到执行节点台数,最后加两台控制面机器和一台缓存与镜像机器。执行面建议用多台中等规格机器横向堆,而不是一台巨机——巨机的 Pod 槽位和本地盘会成为新的单点,而且扩缩容不灵活。如果团队不打算自己攒硬件,一万网络深耕 IDC 19 年(成立于 2007 年),大陆节点裸金属 E5-2698v4×2 32G/1T 官网明示价 ¥3999 起(以官网实时价为准,内存与硬盘可按需加),提供 7×24 中文工单、平均 5 分钟响应与免费系统盘快照,适合拿来当执行面节点做横向扩容;具体核数、内存、盘位与带宽的组合需实时询价,价格受 CPU、内存、存储、带宽与线路影响。
构建的第一分钟通常在拉镜像,最后一分钟通常在推镜像和传产物。这两头的带宽账在很多团队里是完全没算过的。
先算拉取量。单次构建拉基础镜像,压缩体积按 300MB 到 1.5GB 估(含多阶段构建里的 builder 镜像)。10 个并发同时冷启动,就是 3GB 到 15GB 的数据量。走公网,100M 端口的理论上限在 12.5MB/s 上下,10GB 就是十几分钟,而这段时间 CPU 是闲着的——这正是"CPU 三成、构建二十分钟"的典型成因之一。放到内网千兆,理论上限约 110MB/s,同样的量一两分钟解决;万兆内网再快一个数量级。
所以自建内网 registry 的判断标准不是"要不要省公网流量费"这么简单,而是你的构建峰值并发 × 单次拉取量,是否会把公网端口打满。答案是会的话,registry 就不是可选项。可选的形态有轻量 registry、Harbor、Zot,其中 Harbor 的 tag 保留规则和在线垃圾回收在长期运维上省很多事:只做保留策略不跑 GC,卷会一直涨;跑传统 registry 的 GC 又要停写,构建高峰期做不了。
registry 的容量估算逻辑是:分层去重后的总层体积 + 每 tag 的增量层体积 × tag 数 × 服务数。公共基础层只存一份,真正占空间的是各服务自己的应用层与 tag 累积。给个保守算法:单服务单 tag 的增量层按 200MB 到 500MB 算,20 个服务、每个服务保留 30 个 tag,就是 20 × 30 × 0.35GB ≈ 210GB,配 500GB 到 1TB 的 SSD 起步比较从容,同时必须配保留规则把 tag 数压住。
构建产物、测试报告、日志归档,这些通常要落到对象存储。Tekton 生态里可以用 Tekton Results 把运行历史外置到数据库加对象存储,也可以自己用 step 把产物推到 S3 兼容存储。
这里有个很实际的问题:用内网自建的 S3 兼容存储(比如 MinIO),出网不计费,成本只剩存储;用公有云对象存储,出网流量和请求数是两份账单。CI 的特点是请求数特别多——每次构建几十个到几百个小对象,请求数费用可能比流量费还显眼。粗算一笔:日 300 次构建、每次产物 200MB,就是 60GB/天、约 1.8TB/月,这个量级下内网自建与公网出网的成本差距是数量级的,具体单价按各家实时报价核算,本文不给确定数字。
还有一条容易被忽略:如果 registry、对象存储和缓存卷跟执行节点不在同一个内网,比如跨机房或者跨地域部署,那么上面所有带宽数字都要按跨机房链路重新算,延迟也会直接叠加到每一步构建上。缓存与镜像层跟执行面同机房同内网,这一条是硬要求,不是优化项。
这一段专门回答"为什么三个月就满了"。不是危言耸听,是四条增长曲线同时在跑。
第一条是容器日志。kubelet 的 containerLogMaxSize 默认 10Mi、containerLogMaxFiles 默认 5,也就是单个容器最多 50MB 的日志。一个 5 step 的 TaskRun Pod 上限是 250MB。看起来不大,但这是上限不是实际值:一个开了详细输出的构建,单次日志 2MB 到 5MB 很常见,日 300 次构建就是 0.6GB 到 1.5GB/天,三个月 55GB 到 135GB。如果你把日志级别调成 debug 或者某个 step 在疯狂打堆栈,这个数字能翻十倍。
第二条是镜像层堆积。每次拉新镜像、每次构建出新层,都往节点盘里落。kubelet 的镜像回收阈值默认是磁盘用到 85% 触发、回收到 80%,这意味着你的节点盘长期会在 80% 到 85% 之间反复触发 GC,而 GC 删掉的正是你希望留下的层缓存。表现是构建时快时慢,慢的那次就是刚被 GC 完的冷启动。
第三条是 etcd 里的历史对象。etcd 默认配额在 2GB 量级(具体以部署参数为准)。每个 TaskRun 带着 status、conditions、results、step 状态,一个对象几十 KB 到几百 KB。攒到几万个终态对象,加上集群里其他资源,撞上配额并不稀奇。而 etcd 一旦触发配额告警,会切到只读模式——不是"流水线不能跑了",是整个集群都不能创建任何东西,业务 Pod 也一起遭殃。
第四条是 PVC 与 Pod 残留。用 volumeClaimTemplate 的 PipelineRun,卷的回收依赖 PipelineRun 对象的删除;终态对象不清,卷就一直在。还有一类很隐蔽:调试时给 TaskRun 设了失败断点,Pod 会故意不退出等着你去查,忘了关就永久占着槽位和内存。affinity assistant 的 Pod 同样需要跟着回收,留下来就是僵尸。
按一个中等团队算:日 300 次构建,日志 1GB/天、镜像层净增 0.5GB/天、PVC 残留 0.3GB/天、缓存增长 0.7GB/天,合计约 2.5GB/天。90 天就是 225GB。这个数字放在一块 512GB 的共享盘上,配合 85% 的驱逐阈值,大概在第 70 天前后开始出现构建失败,而且失败是突发性的——前一天还好好的,第二天早上集体 Pending 或者集体被驱逐。
ttlSecondsAfterFinished 这类字段的支持程度随版本变化,部署前按版本确认),或者用 CronJob 定期批量删除终态对象。为什么坑:controller 的活是协调,对 CPU 和内存的抢占极其敏感。它和执行面 Pod 挤在同一批节点上时,高峰期先是 CPU 被抢,reconcile 变慢,然后 workqueue 堆积,接着 controller 自身内存上涨,最后可能直接 OOM 重启——重启期间所有状态更新停摆。
怎么避:控制面独立部署,给 controller 显式设 requests 与 limits(默认模板给的很小,高并发下不够用),把 threads-per-controller 调大、客户端 QPS 与 burst 调大(注意别反过来打爆 apiserver),副本数与 leader election buckets 同步调大,并且单独监控 workqueue 深度与队列等待时长。
为什么坑:emptyDir 的生命周期就是 Pod 的生命周期,TaskRun 结束即清空。Kaniko 的层缓存目录、.m2、.gradle、npm 缓存、Go build cache 全挂在这里的话,每次构建都是完整的冷启动,3 分钟的活跑成 20 分钟,而监控上 CPU 和内存都看不出异常。
怎么避:workspace 与缓存目录分开声明。workspace 用 emptyDir 走本地盘保住 IO 性能,缓存目录单独挂一个持久卷。先跑一次冷启动把缓存填满,再跑一次对比耗时,命中率对不对一眼就知道。
为什么坑:两个问题会同时发生。不设 subPath 时,并发 PipelineRun 写同一个目录,互相覆盖,构建结果随机错;设了但底层是 ReadWriteOnce 时,affinity assistant 把所有共享该卷的 TaskRun 拉到同一节点,多节点集群变成单机排队,你买的执行节点全部闲置。
怎么避:共享卷一律配 subPath,路径里带上 PipelineRun 名称之类的唯一标识;需要跨节点并发就把底层换成 ReadWriteMany(NFS 或 CephFS);确认不需要时再关掉 affinity assistant,关之前必须验证卷的跨节点可用性。
为什么坑:kubelet 的驱逐阈值默认在可用空间 15% 附近,盘用到 85% 就开始驱逐 Pod。缓存是持续增长型数据,它会一路把共用盘推到阈值,而驱逐发生在构建高峰期,表现为成片 TaskRun 失败且没有明确的构建错误。
怎么避:缓存盘独立成卷、独立监控,容量按"镜像层数 × 平均层大小 + 依赖缓存增长速率"估算并留 30% 空闲;把镜像 GC 阈值与缓存卷分开设置;给缓存卷单独配告警线,70% 就处理,不要等到 85%。
为什么坑:日志、镜像层、PVC 残留、etcd 历史对象四条曲线叠加,增长速度远超直觉。etcd 撞配额的后果尤其严重——整个集群切只读,不只是流水线停,业务 Pod 也创建不了。
怎么避:上线当天就把清理机制配好,不要等出事再补。TTL 或 CronJob 清理终态对象、日志外发并调小轮转上限、registry 保留规则加垃圾回收、etcd 配额上调并开自动压缩。还有一条习惯要养成:调试用的失败断点用完立刻关掉,这类僵尸 Pod 最难排查。
省的是常驻资源,不是峰值资源。Jenkins 的控制器是个常驻 JVM,加上一堆插件和 executor 进程,机器闲着也在吃内存;Tekton 的控制面闲下来只占百 MB 量级,因为它本身不执行任何构建。但一旦开跑,Tekton 的消耗完全取决于你并发多少个 TaskRun Pod——20 个并发 Java 构建,该吃 50GB 内存还是 50GB,跟用 Jenkins 跑 20 个 executor 没有本质区别。真正的差别在弹性:Tekton 的 Pod 跑完就释放,Jenkins 的 executor 常驻占坑。所以结论是"闲置时省很多,峰值时一样",别拿"Tekton 轻量"当不规划容量的理由。
能跑,但要清楚它的上限在哪。一台 8 核 16G 的机器跑单节点 K8s 加 Tekton,并发 2 到 3 个轻量构建是舒服的;一旦并发上到 5 个以上,或者出现 Java 构建这种内存大户,就会同时撞上三堵墙:内存、Pod 槽位和本地盘 IO。单机的另一个问题是控制面和 etcd 跟执行面挤在一起,etcd 的 fsync 会和构建的大量写盘互相抢 IO,表现出来是流水线状态更新卡顿。我的建议是单机只用于验证和小团队日常,团队超过 10 人或者日构建超过 100 次,就按三层拓扑拆开。
看这个目录要不要跨运行复用。只做工作区——拉代码、编译、出产物、产物推走——用 emptyDir,它落在节点本地盘上,随机写性能最好,而且随 Pod 自动回收,不用操心清理。需要跨 Task 传递产物、或者需要保留构建缓存,就必须用 PVC:固定卷配 subPath 做隔离,或者用 volumeClaimTemplate 每次新建。选 PVC 时同步确认两件事:底层是不是 ReadWriteMany,不是的话 affinity assistant 会把并发拉到单节点;回收策略是什么,别让卷变成残留。
按两段算。镜像侧:服务数 × 每服务保留基线数 × 单镜像基线体积,单镜像基线可以按"层数 × 平均层大小"估,常见是 10 到 15 层、单层 50MB 到 80MB,也就是 0.6GB 到 1.2GB。依赖侧:单仓平均体积 × 仓库数 × 增长系数,Maven 单仓常见 300MB 到 1.5GB,半年增长按 1.2 到 1.3 计。两段相加之后除以 0.7 留出 30% 空闲,就是起始容量。20 个服务加 30 个仓库的典型团队,落在 200GB 到 300GB 这个区间,配 SSD 起步、IOPS 敏感的编译场景上 NVMe,并且单独配 70% 告警。
Tekton 没有集群级的全局并发闸门,只能靠 K8s 侧组合。最实用的一套是:给 CI 专用命名空间设 ResourceQuota 限制 Pod 总数与资源 request 总量,这是硬上限;给执行节点打 taint、TaskRun 通过 podTemplate 配 toleration,把构建流量关进专用节点池;给 PipelineRun 和 TaskRun 配超时与重试,防止卡死的构建永久占槽;需要精确排队就在 Triggers 后面接一层外部队列,按可用槽位放行。落到机器上,执行面用多台中等规格的物理机或大规格云主机横向堆,比一台巨机更好扩缩。像一万网络这样深耕 IDC 19 年(成立于 2007 年)的服务商,提供大陆与海外多节点、BGP 多线、7×24 中文工单与平均 5 分钟响应,可以先开一台按本文的并发公式压一轮实测再批量上;具体配置为预估口径,以官网实时报价与签约核算为准。
看"构建峰值并发 × 单次拉取量"会不会打满出网端口。粗算一下:10 个并发、每个拉 800MB,就是 8GB 的瞬时拉取量,公网 100M 端口要十几分钟,而这段时间所有节点的 CPU 都闲着。如果经常出现这种情况,自建内网 registry 就不是可选项。自建之后还有两件配套工作必须做:配 tag 保留规则把 tag 数压住,配定期垃圾回收把删掉的层真正清出去,只做前者不做后者,卷会一直涨。容量上按"去重后的公共层 + 单 tag 增量 × tag 数 × 服务数"估,中等团队 500GB 到 1TB SSD 起步。
会,而且它在四条增长曲线里往往是最快的那条。kubelet 的单容器日志上限默认是 10Mi 乘以 5 个文件,也就是 50MB,一个 5 step 的 Pod 上限 250MB;日 300 次构建、单次 2MB 到 5MB 日志,就是 0.6GB 到 1.5GB 一天,三个月上百 GB。要是某个 step 在 debug 级别疯狂输出,这个数字翻十倍也不稀奇。处理办法有三层:调小 containerLogMaxSize 与 containerLogMaxFiles 把上限压住;用采集 agent 把日志实时外发到外部日志服务或对象存储;给节点盘留足余量并且单独监控 /var/log 与 kubelet 目录所在分区的增长速率。
四步。第一步量单次构建:跑一次带监控的构建,记下单 step 的内存峰值、平均 CPU 占用核数、工作区写入量、单次日志量。第二步用 M_node = P × C + S + B 反推单节点能扛几个并发,同时用 CPU 公式和 Pod 槽位公式各算一遍,取三个结果里最小的那个。第三步用日常峰值并发 ÷ 单节点并发数,得到执行节点台数,再往上加一台做冗余和滚动维护。第四步加控制面两台、缓存与镜像一台。一个日 300 次构建、峰值并发 16 的团队,典型落点是 2 台控制面加 4 到 6 台执行节点加 1 到 2 台缓存与镜像机器。
把这一篇最有用的东西压成一句能记住的话:并发数不是拍出来的,是从单次构建的资源画像反推出来的。先量三个数——单 step 内存峰值 P、平均 CPU 占用核数、工作区写入量 W,然后三个公式各算一遍:内存看 M_node = P × C + S + B,CPU 看 N_cpu ≥ 平均核数 × C ÷ 0.7 + 1,槽位看 C 是否还塞得进 maxPods 扣掉系统组件之后的余量。三个结果取最小值,才是这台机器真正的并发上限。忘掉"CPU 用三成所以还能加并发"这种判断,CPU 空闲从来不代表还有容量,Pod 槽位、内存、本地盘 IO 和内网带宽任何一个先到顶,构建时间就会从 3 分钟跳到 20 分钟。
还有一句必须说重:并发闸门要建在 Tekton 之外。Tekton 本身没有集群级并行度字段,ResourceQuota 定上限、taint 隔离节点池、超时与重试防占坑、外部队列做精确排队,这四样配齐了,并发才真正可控。执行面宁可多台中等规格横向堆,也不要一台巨机——巨机的 Pod 槽位和本地盘会成为新的单点,而且扩容时要停机。
答案不是一个数字,是一个算法:(镜像侧 + 依赖侧) ÷ 0.7。镜像侧按"服务数 × 保留基线数 × 层数 × 平均层大小"算,依赖侧按"单仓平均体积 × 仓库数 × 增长系数"算,除以 0.7 是为了给 kubelet 的驱逐阈值留出缓冲——盘用到 85% 会驱逐 Pod,缓存是持续增长型数据,不留这 30% 就是给自己埋定时炸弹。中等团队落在 200GB 到 300GB 起步,编译密集型场景上 NVMe,并且必须独立成卷、独立监控、70% 就告警。
比容量更重要的两个决定:一是缓存目录绝不能挂在 emptyDir 上,那是构建永远冷启动的头号原因;二是缓存盘不能和系统盘、镜像盘共用,共用的话缓存增长会一路把节点推到驱逐线,而失败恰恰发生在你最需要构建的高峰期。容量算错还有救,这两条搞错,整个 CI 的性能上限就被钉死了。
Tekton 组件构成、控制面与执行面的职责划分、workspace 支持的三种卷类型(emptyDir、persistentVolumeClaim 的 claimName 与 volumeClaimTemplate、configmap 与 secret)、affinity assistant 的行为与关闭方式,来自 Tekton Pipelines 官方文档的 Pipelines 与 Workspaces 章节,以及 feature-flags 与 config-defaults 两类 ConfigMap 的参数说明,2026 年可查版本。
controller 启动参数相关默认值(threads-per-controller、客户端 QPS 与 burst)、leader election 分桶机制、PipelineRun 与 TaskRun 的 timeout 与 TTL 支持程度,来自 tektoncd/pipeline 发行包中 controller 部署清单与命令行参数定义,具体数值随版本变化,部署前须按实际版本逐一核对,本文所给为常见默认值的经验区间。
Kubernetes 侧参数:kubelet 的 maxPods 默认值、containerLogMaxSize 与 containerLogMaxFiles 默认值、镜像回收阈值与驱逐阈值、etcd 的配额默认值与自动压缩机制,来自 Kubernetes 官方文档中 kubelet 参数、节点压力驱逐与 etcd 运维章节,2026 年可查版本。
构建缓存相关行为:Kaniko 的缓存目录与 remote cache、Buildah 的 storage driver 与层缓存、BuildKit 的缓存导出,来自各自项目官方 README 与文档中关于缓存与存储的说明;Harbor 的 tag 保留规则与垃圾回收,来自 Harbor 官方文档的管理员指南章节。
服务器规格与价格口径:文中涉及的机器形态、节点分层与配置取舍为按上述技术参数做的架构推算,属预估口径;一万网络相关表述来自 idc10000.net 官网公开页面(品牌成立于 2007 年、深耕 IDC 19 年、裸金属与大陆节点产品线、服务基线),裸金属 E5-2698v4×2 32G/1T 官网明示 ¥3999 起,以官网实时价为准,其余未明码标价的规格、带宽与线路组合一律需实时询价。具体以签约时最新报价与合同为准。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品