Kubernetes 节点的冷启动时间由操作系统引导、容器运行时拉起、Kubernetes 组件安装与配置、以及业务 Pod 镜像拉取等多个环节叠加而成。很多集群在扩容时都遇到过新节点长时间停留在 NotReady,查日志却发现 kubelet 已经启动,只是还在等 containerd 下载几个大镜像。预烘焙镜像的思路是把这些固定依赖提前做进节点镜像,让弹性扩容不再被镜像下载拖住。

如果按照默认流程,一台裸的云主机需要先安装 containerd、kubelet、kubeadm 等二进制,然后由 kubeadm join 注册进集群,注册完成后 kube-scheduler 才会把 Pod 调度上来。此时节点本地没有任何业务镜像,只能从 registry 逐个拉取。以一个 800MB 的 Java 服务镜像为例,在千兆带宽下可能只需几秒,但生产环境常常同时拉到几十个镜像,还要处理镜像层解压和快照,实际会持续三五分钟甚至更久。如果集群采用并发扩容,几十台节点同时拉镜像,还可能把镜像仓库或对象存储打满,进一步放大启动抖动。
预烘焙镜像的收益在频繁扩缩容场景尤其明显。构建时把 pause、CoreDNS、CNI 插件、基础运维 Agent 以及高频业务镜像固化进去,节点起来后 containerd 直接使用本地 content store,不需要走网络。节点从创建到 Ready 的耗时主要由云资源创建、操作系统引导和 kubelet 证书申请组成,通常能稳定在 10 到 30 秒。这个差距对突发流量弹性和批处理任务启动窗口非常关键。
一、节点启动慢的根因在哪里
节点冷启动慢主要不是单个步骤慢,而是多个串行步骤叠加后的结果。云主机创建完成后,操作系统先进入启动流程,然后 systemd 拉起 containerd,再启动 kubelet。kubelet 启动后会读取配置并向 API Server 申请证书和注册节点,只有节点状态变为 Ready,调度器才会把 Pod 分配上来。这一连串动作中,最不可控的就是镜像拉取,因为镜像大小、仓库距离、网络带宽和磁盘 I/O 都会影响实际耗时。
镜像拉取还有两个容易被忽视的成本。一是镜像层解压和快照创建,拉取 5GB 镜像不意味着只是下载 5GB 数据,解压后的内容可能膨胀到十几 GB,对磁盘随机写性能要求很高。二是仓库带宽竞争,集中扩容时所有节点同时请求同一个 registry,很容易触发限流或打满内网链路。预烘焙方案通过提前下载并导入镜像,把这两个成本都前移到了构建阶段,节点启动时只做本地读取。
当然,预烘焙并不是所有场景的最优解。如果集群规模很小,或者业务镜像更新非常频繁,预烘焙镜像本身会带来额外的构建和维护成本。但对于需要快速扩缩容的在线服务、定时批量计算以及边缘节点批量部署,预烘焙带来的启动速度提升通常远高于额外维护开销。
二、用 Packer 构建预烘焙节点镜像
构建预烘焙镜像的常用工具是 Packer,它支持从云厂商基础镜像出发,自动执行脚本并生成新的自定义镜像。构建时可以基于 Ubuntu、CentOS、Debian 等常见发行版,也可以使用云厂商提供的精简 OS。Packer 的配置使用 HCL2 或 JSON 编写,下面是一个面向 AWS 的简单示例,用来定义源镜像和构建参数。
source "amazon-ebs" "k8s-node" {
ami_name = "k8s-node-1-28-{{timestamp}}"
instance_type = "t3.large"
region = "cn-north-1"
ssh_username = "ubuntu"
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"]
}
}
build {
sources = ["source.amazon-ebs.k8s-node"]
provisioner "shell" {
script = "scripts/prepare-node.sh"
}
provisioner "shell" {
inline = [
"sudo cloud-init clean --machine-id",
"sudo rm -rf /var/lib/cloud/instances/*"
]
}
}
构建脚本要做的事情比单纯安装二进制更多。除了下载 containerd、kubelet、kubeadm 和 CNI 插件,还需要提前导入基础镜像和常用业务镜像。比如下面这个脚本会遍历 /opt/k8s/images 目录下的 tar 文件,并用 ctr 命令导入到 containerd 的 k8s.io 命名空间。这样节点启动后,kubelet 发现本地已有 Pod 所需镜像,就不会再触发远程拉取。
#!/usr/bin/env bash
set -euo pipefail
IMAGE_DIR="/opt/k8s/images"
K8S_NS="k8s.io"
for img in "${IMAGE_DIR}"/*.tar; do
echo "importing ${img}"
ctr -n "${K8S_NS}" images import "${img}"
done
ctr -n "${K8S_NS}" images list
systemctl restart kubelet
除了导入镜像,构建脚本还应该固化内核参数、关闭 swap、加载必要的内核模块,并写入 containerd 配置。这些内容如果放到节点启动后执行,要么需要额外重启服务,要么容易因为顺序问题导致 kubelet 启动失败。提前写入镜像可以把这些不确定性一次性消除。
三、预烘焙与 DaemonSet 预拉取怎么选
很多团队第一次优化节点启动时,会先采用 DaemonSet 预拉取镜像的方案。它的做法是在集群中部署一个特权 DaemonSet,每个节点启动后由该 DaemonSet 统一拉取需要预热的目标镜像。这种方案不需要维护新的节点镜像,更新更灵活,但缺点是节点仍然要在启动后等待镜像下载完成,只是把时间从业务 Pod 拉取阶段提前到了 DaemonSet 运行阶段。
| 方案 | 首次启动耗时 | 镜像更新成本 | 适用规模 |
|---|---|---|---|
| 纯启动脚本 | 高 | 低 | 小规模或测试集群 |
| DaemonSet预拉取 | 中 | 较低 | 中大规模 |
| 镜像预烘焙 | 低 | 中 | 频繁扩容、弹性伸缩 |
预烘焙的优势在于节点 Ready 之前本地已经有完整镜像,而 DaemonSet 预拉取通常需要节点先加入集群才有机会运行。资源紧张时,DaemonSet 的 Pod 还可能因为节点还未 Ready 而调度延迟。实际生产环境中,两者可以混合使用:预烘焙镜像负责固定依赖和高频基础镜像,DaemonSet 负责预热版本变化较快的业务镜像,这样既保证启动速度,又保留灵活性。
四、进一步缩短节点启动链路
镜像预烘焙之后,启动链路中还有几个可以优化的点。第一个是减少不必要的系统服务启动。云厂商基础镜像常常带有 cloud-init、更新检查、日志组件等服务,这些服务在 Kubernetes 节点上并非全部必要。通过 systemd 禁用无关服务,可以减少操作系统引导完成后到 containerd 启动之间的等待时间。
第二个是提前配置好 containerd 的镜像加速和本地缓存路径。比如把 registry mirror 写入 /etc/containerd/config.toml,这样即使少量镜像没有预烘焙,节点运行时也会走更快的镜像源。下面是一个精简配置片段,只保留 runc 运行时和镜像加速设置。
version = 2 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://mirror.ipipp.com"]
第三个点是让启动后的准备工作幂等。节点首次启动时,预烘焙镜像中的导入工作已经完成,但某些情况下还需要重新导入或清理残留。使用一个 systemd 一次性单元来封装启动后的检查逻辑,可以避免在云厂商的 user-data 脚本里写复杂逻辑。这个单元设置为在 containerd 启动之后执行,并且只在节点第一次启动时运行。
[Unit] Description=Prepare Kubernetes node images After=containerd.service Requires=containerd.service [Service] Type=oneshot ExecStart=/usr/local/bin/preload-images.sh RemainAfterExit=true TimeoutStartSec=300 [Install] WantedBy=multi-user.target
除了这些常规手段,镜像格式本身也可以继续优化。比如使用 eStargz 或 Nydus 这类支持懒加载的镜像格式,可以让节点在只下载少量元数据后就开始启动容器,其余镜像层按需拉取。不过这类方案通常需要 containerd 插件和镜像仓库协同改造,维护复杂度比预烘焙高,适合对启动速度有极致要求的场景。
五、预烘焙镜像的版本与维护
预烘焙镜像一旦进入生产,就不能把它当成一次性构建产物。Kubernetes 版本升级、containerd 安全漏洞修复、CNI 插件更新,都需要重新构建节点镜像。建议在 CI 流水线中建立镜像构建任务,用 Git tag 或 K8s 版本号作为镜像标签,并把构建结果发布到云厂商的自定义镜像列表。这样每次升级前可以先在测试集群验证新镜像,再批量替换节点。
安全更新是维护预烘焙镜像时必须考虑的问题。如果在线节点依赖系统包漏洞修复,而自定义镜像长期不更新,生产环境会面临安全风险。更稳妥的做法是设置周期构建任务,比如每月用最新的安全补丁重建一次基础镜像,同时保留最近几个可用版本,便于回滚。对于跨架构环境,还需要分别构建 amd64 和 arm64 镜像,避免某个架构的节点启动时找不到匹配镜像。
回滚策略上,预烘焙镜像比启动脚本更安全。因为节点镜像一旦发布,其内容是固定的,回滚时只需要更换节点镜像 ID 再重装节点即可,不会因为脚本版本漂移导致配置不一致。团队只要在每次构建时记录好镜像内容和依赖版本,就能在出现兼容性问题时快速定位。
Kubernetes镜像预烘焙节点启动优化修改时间:2026-10-03 09:38:34