拿一条从 docker run 到终端提示的总耗时,无法推断是拉取慢、快照准备慢、进程启动慢还是应用就绪慢。先定义测量的边界与可关联的对象,然后谈数值。

同一只 Pod 在两台节点的“启动耗时”可能相差很大:一台已有内容存储和快照,另一台要从 registry 取层;应用的探针又可能固定等待十秒。只看 kubectl wait 结束时刻,连缓存条件都无法还原,更谈不上给 OCI create 分配多少毫秒。本篇把一次启动拆成控制面可见事件、节点运行时原始事件、应用首次就绪三条时间轴;先标记哪些区间目前还不可观测,再决定能不能比较冷、热启动。

先写实验设计,后写结论

同一 probe-app 固定 image digest、节点 CPU 架构、内核与运行时版本,在专用 VM 上分冷缓存与热缓存两组。时间戳分别落在开始拉取、manifest/layer 验证结束、本机解包与 snapshot 准备结束、OCI create/start、进程首条日志、/ready=200。对同一 ID 记录事件时钟来源,跨组件不能把不同机器的墙钟未经校准就做相减。预先确定预热次数、重复次数、并发、输入大小和失败率,报告每段延迟分布而不是一次的总值;把镜像缓存和应用缓存状态同时记录。

先确定“从何时开始”

Pod YAML 创建请求进入 API server 的时刻,和 kubelet 在目标节点收到期望状态不是同一时刻。kubectl apply 命令返回表示 API 已接受资源,不能替代容器启动;Pod 的 metadata.creationTimestamp 是 API 对象时间戳,不含操作者本地请求发出前的网络耗时。一次测试至少保存客户端单调时钟 t0(发出创建命令前)、t1(apply 返回)、t2(Pod Ready 条件首次观察到)、t3(从同 Pod 进程直接调用 /ready 返回 200)。t1−t0 含 CLI/API 网络,t2−t1 含调度/节点/运行时/探针和观测轮询,t3−t2 含 CLI exec 及 HTTP;这些都不是单纯“镜像下载时间”。若等待超时,应保留超时和 Pod UID,不把失败轮次从分布中悄悄丢掉。

单调时钟仅用于同一测量进程计算区间;要和 Pod Event 的 RFC3339 时间、kubelet 日志以及 containerd 事件对齐,还需保存对应时刻的本机 UTC 与节点时钟偏移。不能把一个 Pod 对象 creationTimestamp 与另一节点的内核事件时间直接相减,然后报告微秒级分辨率。若某事件缺少可信时间来源,计时卡将该段保留 null 并在备注列写原因。应用首条 stdout 也不等于进程 exec 瞬间:stdout 缓冲、日志收集链路和 CRI 日志转发都会带来额外延迟,只有在同一时间源下校准才可以作为近似边界,而不是精确的“Python 启动耗时”。

控制面 Pod status 的 Ready condition 会经 kubelet 上报并有传播延迟,kubectl wait 也靠观察状态变化。即使 pod 内 HTTP 在 t3 才返回 200,不意味着应用严格在 t3 才可服务;首次成功探针可能早于 status 可见,或者控制终端的 GET 与 kubelet 的探针不同。为了让可比结果有共同终点,本篇固定探针路径 /ready、容器实际端口与 --ready-delay=10,并保留启动后第一次请求与最后一次 503 的原始时间;不在另一次实验把“进程开始运行”当同一个终点。若要从客户端经 Service 首次可达,则还会包含 DNS 和 EndpointSlice 传播,必须独立定义另一个指标。

pull 与 unpack 不合并为一条虚构事件

OCI Distribution 协议负责按 manifest/content digest 获取内容,containerd 的 content store 管已取得的 blob;运行时准备 rootfs 还要解包 layer、应用变更集并由 snapshotter 构建可读写视图。固定阅读 containerd v2.1.0 core/unpack/unpacker.go 可以定位其拆包和 snapshotter 的实现入口,但此源码不是本机已安装运行时的证据。crictl pull 若返回成功,也可能包含网络下载、解压和已有本地内容复用等多项工作;在缺少运行时事件/trace 时,不能硬拆出单独的“下载毫秒”“解包毫秒”。尤其不可用镜像压缩文件的大小乘网速去估算解包完成的时间,再当成真实观测结果。

Kubernetes 的某些 kubelet 指标如 kubelet_image_pull_duration_seconds、kubelet_pod_start_total_duration_seconds 或启动 SLI 具有各自口径与稳定性标签,应以实际安装版本的指标说明及启用状态为准。image pull 度量不必然覆盖 containerd 每层解包,Pod start 总时长也不等于从 kubectl apply 到终端收到 Ready 的时间;不能把两个不兼容直方图简单相减求出“runc 开销”。若要划分 OCI create/start,还需要固定运行时版本并取得同一容器 ID 的节点原始事件及时间源。只有 Pod 事件显示 Pulling/Pulled,通常只能给出粗略控制面转述,层下载和 snapshotter I/O 必须补更细原始记录。

即便节点同时有压缩 blob 与可用 snapshot,也可能因平台不同而选择了另一份 manifest:记录镜像 index digest、所选 manifest digest、uname -m 与容器实际 imageID。对多架构镜像,仅 tag 一致并不意味着节点取得相同字节;对同一 digest,部分内容已在 content store 不等于对应 snapshot 一定已准备好。必须将“内容热”“快照热”“应用热”分成独立变量,才能回答把一次启动从五秒改成三秒究竟省去了哪段;否则第二次 Pod 重建较快只说明它沿另一个未清楚界定的缓存路径走完。

冷热实验为何最容易变成伪对照

冷组需要独占、可丢弃的节点或事先核查 content store/snapshotter 的干净基线;不能在共享宿主随意 crictl rmi 或清空 imagefs,那既会影响别的任务也未必删除 content store 的 blob。热组使用同一节点、同一 digest重复创建 Pod,并记录每轮前 content/image/snapshot 的实际状态。只有同一 VM 上有可安全控制的缓存和明确节点归属时才执行五轮冷、五轮热;如果可用的只有一台共享节点,本篇宁可热组记录为候选,冷组标 NOT_RUN,不把“删除一个 Pod 再建一个”写成冷启动。

还须说明“冷”具体指哪一级缓存:registry 的代理缓存、宿主内核页缓存、containerd content store、snapshotter、镜像运行时元数据与应用自己的状态可能独立冷热。重新创建同名 Pod 不会自动删除这些缓存,imagePullPolicy: Always 也只要求检查拉取策略,不等于一定重新下载全部 layer。若 VM 管理者提供全新快照恢复的节点,在每轮开始前记录该节点唯一 ID、镜像列表、content/snapshot 状态和 registry 位置,再把它作为冷组候选;若缺镜像列表或节点其实复用了原磁盘,不能把该轮算成“真正冷缓存”。实验设计里预先写清这些拒绝条件,能防止看到有利结果后才反向挑选样本。

每轮还需锁住 CPU/内存争用、节点文件系统剩余空间、registry 速率和网络路径。如果节点在冷组下载时恰好被另一个任务打满 I/O,那么差异不应仅归因镜像结构;若热组开启两个并行 Pod,镜像层与应用请求又会相互争资源。本篇规定同一时间仅运行一只测试 Pod、probe-app 不额外启动负载,保存 Ready 前失败请求、最后状态、失败轮数和整个原始样本表。超过探针 180 秒仍未就绪的样本是失败结果,不从“成功样本均值”里偷偷移除;无论成功与否都要知道当时的 Pod UID、节点与容器 ID。

就绪延迟也可能藏着独立故障

probe-app 的 --ready-delay 使用进程内单调时钟,不受镜像下载快慢直接影响。设置十秒后,从应用启动算起的早期 /ready 应为 503,之后才为 200;如果第二次镜像无需远程拉取却仍等十秒左右,并不意味着缓存优化完全没有价值,而是总时间的主导项可能变成了应用就绪。反过来,如果首次就绪远大于十秒,也不应立即调整探针参数:先验证进程是否拿到 CPU 配额、是否反复重启、探针失败日志是否指向错误 URL,以及 Pod 所在节点的资源压力。固定参数只是建立比较入口,不会强制两次真实行为完全相同。

另一条边界是镜像在节点上已经存在,但快照未被当前 snapshotter 准备:第二次拉取可能很快,rootfs 准备仍慢。要证明这个区分,先找 content store 与 snapshot 的实体记录,再取得 create/start 的容器 ID;只有 kubelet 的一条 Pulled image Event 时,不具备定位 rootfs 构造耗时的粒度。读取某份 containerd 源码仅说明潜在事件位置,不能在没有 shim/CRI/node 日志时虚构时间戳。如果当前安装的是其它 snapshotter(如远程或 lazy 模式),分段定义还要覆盖按需加载,在首次 HTTP 时发生的 I/O 不应被排除在“启动性能”的使用者体验之外。

报告分布而不是神奇百分比

原始数据表至少含每轮阶段(冷/热)、节点、镜像 index/manifest digest、Pod/容器 ID、cache 基线、客户端 t0/t1/t2/t3、实际节点/CRI 事件区间、探针 503/200 及错误。辅助脚本 固定 ready-delay 的同篇 Pod 清单和 计时口径与缺失字段卡、附件校验配合 独占实验入口与回收 可独立下载并复用 共用源码包。计时卡的 null 表示尚未获得该段真实事件,不能在发布时默默以零代入计算。每组若有五轮,给每次实际值和失败数,再报告中位数/最小最大;五个样本不足以稳定推断 p99,也不应用无误差的一个百分比给出通用容器损耗结论。

若下游想把计时用于优化决策,要针对瓶颈提出可以被推翻的实验:认为下载慢,就在相同节点上控制内容缓存并比较真实下载事件;认为应用慢,就在相同镜像/运行时条件下只改变 ready-delay 或应用预热;认为 rootfs 慢,就核对 snapshotter 类型与实际 prepare 日志。每轮只改变一个变量,并保存错误与请求返回。优化某一段未降低总就绪时间时,可能其它段成为新瓶颈,也可能事件观测漏掉了并行工作;两种解释需要不同的后续证据,不能为了维持“优化有效”只展示最快的一轮。

迁移到另一种运行时、存储后端或多架构部署,先确认事件名、采样时钟、content/snapshot 语义和首次可用请求的定义是否一致,不直接移植本篇的阶段公式。当前云端没有 Docker/containerd/kind、专用清空的节点或镜像 digest,冷/热分组、层下载、解包、OCI create/start、真实就绪和 HTTP 全部 NOT_RUN。页面构建与 JSON 校验只证明测量说明可读、附件可取,不能当作时间分布数据。

即使未来测得完整曲线,也要把失败轮次与成功轮次放在同一报告里:若某轮因镜像拉取失败根本没有 Ready 时间,它的终点属于缺测而非零延迟;若某轮超时,报告观测上限与当时状态。拒绝把这些记录从表格静默删掉,才能让读者判断所谓“冷组更慢”是否只来自选取了容易成功的样本。

边界例是第二轮 pull 因内容已在本地而很快,服务却因独立的就绪延迟仍返回 503。把这两段之和直接叫“镜像启动慢”会误判优化目标。一次用户请求成功还可能发生在 ready 之前或之后,需用相同 readiness 条件比较。实验变量表和有界探针保证输入一致;本机无构建器、运行时或集群,所有时间分段、重复数据、性能结论 NOT_RUN,不填估算数字。

现象 测量对象 前提
冷启动慢 下载字节、解包与启动分段 先锁定缓存状态
第二次快 相同 digest 的本地 content 不能推断另一节点也快
Running 但 Ready 慢 进程/就绪计时 与镜像传输分开

练习

  1. 先预测镜像已在节点 content store、应用 ready-delay=10 时哪个阶段占主导,分别保存每段真实起止时间。
  2. 预测网络缓存已热但 snapshot 尚未准备时总时延能否靠减少镜像大小改善,按事件区间取证,不凭最终总和猜因。

上一篇:33:制品可信;下一篇:35:性能对照。

系列总目录:从进程隔离到运行时与编排。

参考资料