拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Kubernetes 镜像预热全攻略:从 DaemonSet 到 P2P 加速的四种方案

你有没有遇到过这种场景滚动发布刚触发新 Pod 卡在ContainerCreatingkubectl describe一看正在拉取一个几个 GB 的镜像然后这一卡就是好几分钟与此同时旧 Pod 还在被逐步缩容服务容量窗口直接少了一大截。如果业务镜像本身就大或者集群节点跨机房、跨地域这个问题只会更明显。Kubernetes 节点提前拉取 / 预热镜像就是为了解决这类痛点而存在的在 Pod 真正调度到节点之前先把镜像从 Registry 拉到节点本地让调度后的容器启动变成“秒级”。这篇内容我会把业界常用的几种预热思路一次性讲清楚DaemonSet 全量预热、发布前 Job 定向预热、调度器扩展的进阶玩法、Registry 缓存与 P2P 加速以及如何把预热做成平台能力。适合正在被大镜像发布、突发扩容、批量建节点折腾的运维和平台工程师参考也适合刚接触 Kubernetes 集群管理的同学用来建立完整的镜像分发知识体系。1. 为什么“节点预热镜像”值得专门做方案1.1 镜像拉取慢的根源和影响先看最基础的问题Kubernetes 在创建 Pod 时kubelet 会通过 CRI 接口让容器运行时检查本地是否已有目标镜像。如果节点上不存在这个镜像就必须先从 Registry 拉取拉取完成后再创建容器。这个过程中 Pod 的状态一直是ContainerCreatingkubectl describe里会看到Pulling image的事件。镜像拉取慢的根源不只是网络带宽。一个完整的镜像由多层 tar 组成Kubernetes 拉镜像要做三件事从 Registry 下载各层数据、解压到本地存储、写入节点文件系统。这三步里任何一步慢都会拖后腿。打个比方这就像你临时要用一个巨大的安装包等到使用时才开始下载体验自然很差预热相当于把货提前备到本地仓库真正上架的时候直接取不用现买。举例算一笔账一个 5GB 的镜像在 100MBps 的内网带宽下理论最短传输时间是 50 秒但实际还要加上 Registry 侧限速、TCP 窗口、层解压和磁盘写入耗时翻倍是常事。如果同一时间有 20 个节点一起刷这个镜像节点之间的下载会互相争抢机架带宽Registry 出口也可能被打满最终每个节点拉取的时间比单节点拉取还要慢。更严重的是它对发布节奏的影响。滚动发布时新 Pod 迟迟不 Ready旧 Pod 按策略继续缩容服务实例数出现阶段性下降。对于不能接受容量波动的业务这种“发布期变相降级”是不能忍的。而预热镜像的价值恰恰是把拉取时间从“发布关键路径”里剥离出去发布时只管调度和启动让新 Pod 到达节点后直接就能用本地镜像。1.2 适合预热的典型场景与判断标准并不是所有集群都需要做镜像预热。如果你的镜像只有几十 MB内网带宽又很充裕拉取也就是一两秒的事没必要折腾。真正值得做预热的场景有几个特征场景特征具体表现预热能解决什么镜像体积大算法模型镜像、AI 推理镜像、部分中间件镜像达到 GB 级以上避免 Pod 启动被拉取时间拖住突发扩容HPA 或其他策略在流量高峰批量拉起几十上百个 Pod让扩容 Pod 秒级 Ready快速承接流量发布频繁一天发多次每次镜像 tag 都在变每次发布前预热新镜像避免反复等待批量加入新节点Cluster API、节点池扩容、边缘节点批量上线新节点落地即有镜像缩短就绪时间跨地域拉取节点与 Registry 距离远延迟和带宽不稳定提前在业务低峰期完成分发有一个很实用的判断标准如果 Pod 从调度到 Ready 的时间超过 30 秒而且这段时间大部分显示在ContainerCreating就值得考虑预热。如果超过 1 分钟基本就是必须处理的问题了。接下来我按方案逐个拆解。2. 方案一DaemonSet 全量预热最通用的入门做法2.1 原理与最小 YAML 模板DaemonSet 全量预热的核心逻辑很简单DaemonSet 会保证每个符合条件的节点上都运行一个 Pod而只要 Pod 里使用了目标镜像kubelet 在创建容器前就必须先把镜像拉到本地。镜像拉取完成预热目标就达到了。最小实现是创建一个 DaemonSet容器镜像指向需要预热的业务镜像容器命令让它挂住或者直接退出。因为 DaemonSet 会管理 Pod 的生命周期如果容器退出DaemonSet 会不断重建 Pod造成循环重启所以一般用sleep 3600这种命令让容器保持 Running 状态或者直接用 pause 镜像当占位容器。apiVersion: apps/v1 kind: DaemonSet metadata: name: prewarm-nginx namespace: prewarm spec: selector: matchLabels: app: prewarm-nginx template: metadata: labels: app: prewarm-nginx spec: imagePullSecrets: - name: regcred containers: - name: prewarm image: nginx:1.25 command: [sh, -c, sleep 3600] resources: requests: cpu: 10m memory: 10Mi创建后观察每个节点上的 Pod状态变成 Running说明镜像已经拉取完成。用crictl images也能直接看到镜像出现在节点本地。这个方案的优势是覆盖面全新节点加入集群后 DaemonSet 会自动在它上面创建 Pod顺带完成预热不需要额外触发。有一个细节容易被忽略Pod 的imagePullPolicy。如果镜像 tag 是固定的节点上已经存在该 tag 时IfNotPresent策略不会重新拉取预热一次就够。但如果你用latest或每次构建生成相同 tag想让节点强制刷新镜像就要把策略设为Always。预热场景下我建议尽量用不可变 tag否则预热任务本身会变成“每次都重新拉全量镜像”又慢又费流量。2.2 单个 DaemonSet 批量预热多个镜像的实战技巧实际操作中一个业务往往不止一个镜像可能同时要预热五六个、十几个。最简单的做法是每镜像一个 DaemonSet但管理起来很繁琐清理时容易漏。更实用的做法是利用 Pod 里的多个initContainer把每个要预热的镜像放进一个 initContainer主容器用 pause 占位。apiVersion: apps/v1 kind: DaemonSet metadata: name: prewarm-batch namespace: prewarm spec: selector: matchLabels: app: prewarm-batch template: metadata: labels: app: prewarm-batch spec: imagePullSecrets: - name: regcred initContainers: - name: pull-nginx image: nginx:1.25 command: [sh, -c, exit 0] - name: pull-mysql image: mysql:8.0 command: [sh, -c, exit 0] containers: - name: pause image: registry.k8s.io/pause:3.9 resources: requests: cpu: 5m memory: 5Mikubelet 在启动 initContainer 之前同样要先拉取它的镜像所以这种方式能在一个 DaemonSet 里完成多个镜像的预热。主容器只依赖 pause 镜像占用的 CPU 和内存几乎可以忽略对节点没啥影响。这里有个必须提醒的坑如果业务镜像本身不包含sh或sleep这些命令initContainer 会启动失败出现CrashLoopBackOff。但从预热角度看镜像层已经下载并存储到节点上了目标已经达成。很多人第一次遇到这种情况会以为方案有问题其实只要确认crictl images里有对应镜像预热就已经完成Pod 状态异常不需要太在意。不过为了避免告警轰炸建议在预热完成后删除这个 DaemonSet或者接受这种“异常但镜像已存在”的状态并做好记录。如果你很在意 Pod 状态干净就老老实实每个镜像一个 DaemonSet用镜像自带的入口挂住容器比如 nginx 镜像直接用默认 command 启动 nginx 也可以但要评估业务进程占用资源的问题。2.3 预热凭证、证书、磁盘与更新策略的坑DaemonSet 预热私有仓库镜像时最常见的失败原因是凭证和证书问题。凭证方面需要在 DaemonSet 的 Pod 模板里指定imagePullSecrets或者在 serviceAccount 上绑定 imagePullSecret否则私有 Registry 会返回 401 或 403。很多团队用的是同一个私有仓库但每个业务 namespace 的 Secret 不一样预热 DaemonSet 一般放在独立 namespace要确保它引用的 Secret 有权限拉取所有目标镜像否则会看到一个 Pod 能拉、另一个 Pod 拉不动的奇怪现象。证书方面如果私有 Registry 用的是自签名证书节点上的容器运行时必须先信任这个 CA。containerd 场景下要在/etc/containerd/certs.d/下配置对应域名的主机证书目录docker 场景下要把 CA 放到系统信任链或 docker 的配置里。预热时拉取失败如果报x509: certificate signed by unknown authority基本就是证书没分发到节点先解决证书再做预热。磁盘空间是另一个隐形坑。提前在节点上存放多个大镜像会直接占用/var/lib/containerd或/var/lib/docker的空间。如果节点原本磁盘就比较满预热会触发镜像 GCKubernetes 的 kubelet 默认会在磁盘使用率到 85% 左右开始清镜像清掉的正好是你刚预热好的那一批。后面我会专门讲 GC 的调优。更新策略上DaemonSet 默认的RollingUpdate在镜像列表变化后会滚动重建 Pod。因为重建时镜像层基本都存在重建速度很快不会产生大规模重新拉取所以不用担心预热 DaemonSet 更新会打满带宽。3. 方案二发布前定向预热把预热塞进发布流水线3.1 为什么要做定向预热DaemonSet 全量预热适合“不分节点、不分镜像”的无差别覆盖但它有个明显问题如果集群有几百个节点业务镜像只在其中一部分节点上跑全量预热等于把镜像推到所有节点上浪费磁盘和带宽。而且如果每次发布都用新镜像旧镜像的预热就变成无效预热还会占用节点空间。定向预热的核心思路是在 Pod 真正要被调度到某台节点之前针对性地在那台节点上提前拉好镜像。触发时机通常是三个发布前、扩容前、节点初始化时。相比全量预热它更精确浪费更少也更切合发布场景。3.2 手把手用绑定节点的 Job 实现发布前预热最落地的定向预热方式是在发布流水线里插一个预热步骤先拿到目标节点列表然后为每个节点创建一个绑定节点名的 JobJob 里的 Pod 镜像就是待预热的业务镜像。Pod 一旦调度到节点kubelet 就会拉取镜像拉完之后如果容器命令能执行就正常退出Job 标记完成。apiVersion: batch/v1 kind: Job metadata: name: prewarm-node-10-0-1-8 namespace: prewarm spec: backoffLimit: 2 template: spec: nodeName: 10.0.1.8 restartPolicy: Never imagePullSecrets: - name: regcred containers: - name: prewarm image: registry.internal/app/ml-server:v2.0 command: [sh, -c, exit 0]使用nodeName直接固定节点绕过了调度器预热 Pod 不会被调度到别的节点上。这个方案在流水线里的执行流程是通过kubectl get nodes -l label拿到目标节点列表。循环为每个节点生成上面这个 Job。等待所有 Job 完成或者设置超时时间。Job 全部成功后再触发 Deployment 滚动发布。这里有一个很实用的经验预热 Job 里的容器命令如果指向镜像里不存在的二进制Job 会失败但镜像已经拉取完毕。对预热来说Job 的成功与否不是最关键的关键是确认每个目标节点上都有目标镜像。所以我通常会在流水线里用crictl images校验而不是只看 Job 状态。当然如果目标镜像里确实有可用的sh那就让 Job 正常退出状态干净又好排查。为什么不用 Deployment 加 PDB 那套来做预热因为 Job 天然是“执行一次就结束”的语义适合发布前的一次性动作。它还能通过backoffLimit控制重试次数通过activeDeadlineSeconds控制总超时接入 CI/CD 时更友好。3.3 再进一步调度时触发预热与节点初始化时预热发布前预热适合“我知道要发哪个版本”的场景但有些场景是 Pod 可能被调度到任意节点比如突发扩容、Pod 漂移、抢占。这时候可以在调度层面做文章思路是在调度器准备把 Pod 调度到某节点之前先检查节点上有没有目标镜像没有就触发预热让 Pod 等一等再继续调度。严格来说这需要扩展 kube-scheduler。你可以实现一个调度器扩展Scheduler Extender在Filter阶段判断节点上是否有镜像如果缺少镜像可以拒绝该节点并触发预热。也可以用 webhook 在 Pod 创建前后拦截但不推荐因为 webhook 不适合做需要等待的操作容易拖垮 API Server。实际落地时更多团队选择绕开调度器用“先预热再扩容”的思路把预热放在扩容流程的前置步骤里扩容脚本先根据预计扩容的节点创建预热 Job等 Job 完成再触发 HPA 或调整副本数。这比自定义调度器简单得多也足够满足大多数场景。另一个非常实用的场景是“新节点加入时预热”。如果你用 Cluster API、Terraform 或云厂商的节点池来批量创建节点完全可以在节点的初始化脚本里在 kubelet 启动后执行一段预热命令crictl pull registry.internal/app/ml-server:v2.0这样节点加入集群时就已经带上了目标镜像首次调度也能秒级启动。这个思路尤其适合边缘节点批量上线的场景因为边缘节点到中心 Registry 的链路通常更不稳定趁节点初始化时网络相对空闲完成拉取比业务高峰再拉更靠谱。4. 方案三Registry 缓存与 P2P 加速反向解决分发问题4.1 节点本地不存镜像用缓存代理把远端拉取变成近端拉取前面两个方案都是“把镜像提前放到节点本地”还有一种思路是“让拉取本身变快”在节点和远端 Registry 之间加一层缓存代理让节点拉镜像时优先从内网缓存拉而不是每次都穿透到公网或跨地域的远端 Registry。最典型的实现是部署一个内网 Registry并打开它的 Proxy Cache拉取缓存功能比如 Harbor 的 Proxy Cache 项目。节点上的容器运行时配置镜像加速地址把某个上游 Registry 的请求指向内网缓存。第一次拉取时缓存仓库会回源到远端把镜像存下来后续所有节点再拉同一个镜像时就直接从内网缓存下载。containerd 的配置示例version 2 [plugins.io.containerd.grpc.v1.cri.registry] [plugins.io.containerd.grpc.v1.cri.registry.mirrors] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.docker.io] endpoint [https://harbor.internal/v2proxy/dockerhub] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.registry.example.com] endpoint [https://harbor.internal/v2proxy/example]修改后需要重启 containerd 生效。如果是 docker 运行时的旧版本集群可以在/etc/docker/daemon.json里配置registry-mirrors但要注意 Docker CLI 的registry-mirrors通常只对 Docker Hub 生效对自定义私有仓库不生效containerd 的 mirror 配置则可以对任意 Registry 生效这也是现在 Kubernetes 从 1.24 开始默认使用 containerd 之后更推荐的玩法。缓存代理解决的是“多次拉取同一镜像只回源一次”的问题但它第一次回源还是慢。所以它和预热是互补关系预热时配合内网缓存预热速度会更快业务高峰节点首次拉取时至少不会全部拥到外网。4.2 Dragonfly P2P 加速大镜像分发的另一种解法如果你有几十上百个节点同时拉取同一个大镜像即使有缓存代理缓存仓库的出口带宽也可能成为瓶颈。这时候可以考虑用 P2P 的方式做分发代表项目是 CNCF 的 Dragonfly。Dragonfly 的思路是用 Peer 节点互相分片传输一个节点从远端 Registry 拉取镜像的分片后其他节点可以从这个节点下载分片而不是都去请求 Registry。整套组件一般包括 scheduler、dfdaemon 客户端和 seed peer 种子节点。通过 containerd 的 proxy 插件拦截拉取请求让镜像流量走 P2P 网络。严格来说 Dragonfly 不是“预热镜像”它解决的是大镜像并发分发慢的问题。但和预热配合起来效果很好预热时用 Dragonfly 加速避免预热本身占用太长时间发布时即使有少量节点没被预热覆盖到临时拉取也不会太慢。很多中大型团队的实际方案就是“少量预热 Dragonfly 兜底”既保证关键路径启动快又不用每个节点都提前塞镜像。4.3 预热、缓存、P2P 三者的定位对比三者经常被放在一起讨论但定位完全不同手段核心思想优势局限节点预热提前把镜像放到节点本地Pod 启动无需拉取最快占节点磁盘预热有延迟镜像列表变化要重新预热Registry 缓存让节点从内网缓存拉取镜像多次拉取只回源一次简单省事首次回源慢缓存仓库可能成为新的瓶颈P2P 加速节点之间互相分享镜像分片并发分发效率高节省出口带宽部署复杂需要额外的组件和运维选型时可以先问自己是希望“节点上一定有镜像”还是“节点拉镜像时很快”前者选预热后者选缓存或 P2P。很多场景下两者并不冲突可以用组合拳。5. 方案四把预热工具化做成定时任务和平台能力5.1 一个简单的镜像预热服务该有哪些模块如果集群规模到了几十上百个节点发布频繁镜像列表经常变靠人手工创建 DaemonSet 或 Job 就不现实了。这时候值得做一个内部镜像预热服务哪怕是精简版。它的核心模块大致是这几块镜像列表管理一个可配置的待预热镜像列表可以是 ConfigMap、数据库或者 Git 仓库里的文件包含镜像地址、tag、目标节点范围。执行器根据镜像列表生成预热任务。比较简单的方式是生成 DaemonSet全量预热或批量 Job定向预热也可以直接通过节点初始化脚本触发。校验器任务执行后还需要确认每个目标节点上确实存在对应镜像。可以通过在节点上执行crictl images或ctr -n k8s.io images list并解析结果也可以再创建一个探测 Pod 看它是否秒级 Ready。清理器预热任务完成后删除 DaemonSet 或 Job清理掉预热用的临时 Pod但保留节点上的镜像不删除。状态上报把每次预热的节点覆盖数、成功失败数、耗时汇总成报告供发布流水线判断是否放行。整个服务不必很复杂核心就是“镜像列表驱动任务生成任务完成后校验镜像存在”。很多团队的预热平台就是从这么一个小小的控制器长出来的后续再叠加调度策略、权限隔离和审计。5.2 定时预热与发布前预热如何组合预热平台在日常运行中通常有两种触发模式。第一种是定时预热适合镜像列表相对固定的场景。比如每天凌晨业务低峰期用一个 CronJob 把第二天的发布候选镜像全部刷一遍到指定节点。好处是不占用发布窗口节点上始终保持有最近的镜像发布时碰运气也能秒起。坏处是如果发布时用的镜像 tag 是当天早晨才构建出来的凌晨那次定时预热就无效了。第二种是发布前预热适合镜像变化快的场景。CI/CD 流水线里镜像构建并推送到 Registry 之后马上触发一次定向预热预热完成后再跑 Kubernetes 滚动发布。这个过程把“预热”和“发布”串成了前后依赖关系发布的关键路径里没有拉镜像的等待。我见过的比较成熟的组合是新镜像构建完成后立刻触发全量或定向预热把镜像在业务低峰期或发布准备阶段就分发到位发布流水线启动时再快速校验一下目标节点是否已有镜像缺少的节点现场补预热。这样既有覆盖面的兜底又有精确性的保障。5.3 预热效果怎么验证判断镜像是否真的在节点上很多同学预热完只看了 DaemonSet 的 Pod 状态是 Running就觉得万事大吉结果正式发布时 Pod 还是卡在拉镜像排查半天发现 tag 对不上。所以要养成“验证镜像在节点上是否存在”的习惯。containerd 环境下验证命令是crictl images | grep registry.internal/app/ml-server:v2.0也可以直接进节点用 ctr 看不经过 CRI 的镜像列表ctr -n k8s.io images list | grep registry.internal/app/ml-server:v2.0要注意 crictl 输出的 IMAGE 字段可能是“短名”也可能是完整 digestgrep 时尽量用完整镜像名和 tag避免误判。另一个更贴近业务视角的验证方式是在目标节点上手动创建一个 Pod镜像就是待验证镜像观察它是否在几秒内进入 Running。如果能秒级 Ready说明镜像本地已存在。这个方法虽然稍重但能同时验证“镜像存在”和“容器能正常启动”比单纯看镜像列表更可靠。6. 所有方案横评到底怎么选型6.1 六个维度对比表把前面几套方案放在一起对比看得更清楚方案原理适用场景实时性资源占用运维复杂度典型失败点DaemonSet 全量预热每节点跑预热 Pod 拉目标镜像集群规模中等、镜像列表稳定、不在乎全量覆盖节点加入后自动触发实时性好每个节点都会额外占磁盘很低YAML 即可大镜像多时磁盘爆满、并发拉取打满带宽发布前 Job 定向预热为指定节点创建绑定 nodeName 的 Job发布频繁、目标节点明确、想省磁盘发布前触发流程化控制只占目标节点磁盘低需要流水线配合Job 状态和镜像存在性不一致时容易误判调度器扩展触发调度阶段判断缺镜像再预热平台团队、需要全自动覆盖所有调度场景高但有延迟取决于预热策略高不建议轻易自己写实现复杂边界情况多Registry 缓存代理节点拉镜像走内网缓存多节点、跨地域、镜像经常变化首次拉取仍慢后续快缓存仓库占存储节点本地磁盘少占中需要部署维护缓存仓库首次回源慢、缓存仓库带宽瓶颈Dragonfly P2P节点间 P2P 分享镜像分片大镜像、大规模并发分发并发场景越久越快节点会承担 peer 流量中高组件较多部署复杂需要压测验证节点初始化脚本预热新节点初始化时 crictl pull批量建节点、边缘节点上线节点就绪即有镜像只占新节点磁盘低嵌入节点模板即可初始化脚本失败或网络不稳时会漏6.2 场景化选型建议如果只想要一个立刻能用的方案优先选 DaemonSet 全量预热它最简单覆盖面也够。你只需要维护一个预热镜像列表把它们塞进 DaemonSet 的 initContainer 或拆成多个 DaemonSet 就行。如果发布频繁镜像 tag 经常变化优先做发布前 Job 定向预热把它接到 CI/CD 流水线里。这样做能精确控制预热范围不浪费节点磁盘也方便和发布审批流程结合。如果节点数量大、跨地域、Registry 链路差优先考虑内网缓存仓库或 Dragonfly。预热可以继续做但不要让它成为唯一依赖否则每次新镜像出现时预热长尾都会很痛苦。如果集群经常批量扩缩容新节点动辄几十台上线把预热写进节点初始化脚本或 Cluster API 的 bootstrap 流程里让节点一加入就自带目标镜像能省去后续很多麻烦。没有银弹。我见过很多团队最后都是两到三个方案的组合发布前预热保证关键路径、缓存仓库打底、新节点脚本兜底。选型的关键不是追求某个方案最先进而是让它匹配你团队维护成本和业务发布节奏。7. 常见问题与排查技巧实录7.1 ImagePullBackOff 的几类原因与排查路径预热任务最常见的问题就是 Pod 一直ImagePullBackOff。排查路径按下面顺序来能解决绝大多数情况看事件kubectl describe pod pod -n namespace事件里会直接给出拉取失败的原因比如401 Unauthorized、403 Forbidden、manifest unknown、x509等。看镜像是私有仓库但 Secret 没生效确认imagePullSecrets是否配置正确Secret 里的用户名密码是否有权限拉取目标镜像。看证书拉取失败报x509: certificate signed by unknown authority说明节点的容器运行时没有信任私有 Registry 的 CA需要分发证书并重启 containerd。看仓库路径和 tag很多团队自建的 Harbor 项目是二级路径比如registry.internal/library/nginx写漏一层就会 404。看磁盘节点磁盘已满会导致镜像层写入失败报错里会有no space left on device。这几个原因里我踩得最多的是证书和 Secret 的问题因为预热经常要跨 namespace 拉镜像Secret 的权限边界很容易搞错。建议预热任务统一放在一个独立的prewarmnamespace用一个权限足够的专用 robot account避免权限分散管理。7.2 并发预热打满带宽业务跟着遭殃怎么办很多团队第一次做全量预热时会一次性把所有大镜像刷到所有节点结果把机架带宽和 Registry 出口全部打满正常业务流量跟着受牵连。这个问题的本质是“预热没有做并发控制”。解决办法有几个方向分批预热把节点按批次划分比如每次只预热 20 个节点完成后再跑下一批。可以用流水线控制也可以在 DaemonSet 里通过 nodeSelector 圈定节点子集。控制单节点并发如果预热 Job 里有多个镜像要拉不要让它们同时拉按顺序逐个拉即可。限制预热时间通过activeDeadlineSeconds给预热 Job 设超时避免某个镜像卡住拖垮整个流水线。用 P2P 或缓存仓库分流预热流量走内网缓存或 P2P 网络后对远端 Registry 出口的压力会小很多。如果我们预热的是一个 5GB 镜像100 个节点同时拉总流量就是 500GB。即使内网带宽能扛Registry 和缓存仓库的出口也不一定扛得住。所以在预热任务上线前最好先压一下内网缓存或镜像仓库的吞吐心里有个数。7.3 预热完的镜像很快被 GC 清理掉镜像预热完成后如果节点上没有 Pod 真正在引用这些镜像而节点磁盘使用率又比较高kubelet 的镜像 GC 可能会把刚预热好的镜像清理掉。kubelet 默认有两个阈值imageGCHighThresholdPercent默认 85%imageGCLowThresholdPercent默认 80%。当磁盘使用率超过 85% 时kubelet 会开始删除未使用的镜像直到降到 80% 以下。如果你的预热镜像经常被清掉优先调整 kubelet 的这两个参数给镜像留出足够的缓冲空间。比如磁盘比较大的节点可以设 High 为 90Low 为 85。但要注意这只是推迟清理不等于不清理。如果镜像长期不被 Pod 引用GC 始终可能把它当成“垃圾”处理。另一个思路是让预热完成的节点上始终有一个挂住该镜像的 Pod 存在。DaemonSet 预热方案天然满足这一点预热 DaemonSet 的 Pod 一直在运行镜像始终处于“使用中”状态GC 不会清它。如果是发布前 Job 定向预热Job 跑完就结束了可以额外保留一个长期运行的占位 DeploymentPod 引用同一个镜像把镜像“钉”在节点上。但这样做会增加资源占用一般只对核心镜像这样做。7.4 部分节点没被预热覆盖到全量预热方案里最容易被忽视的问题是“你以为覆盖了全部节点实际上漏了一部分”。常见原因有这几类节点上有污点DaemonSet 没有配置对应的 tolerationsPod 调度不上去。节点上有自定义 labelDaemonSet 的 nodeSelector 把某些节点排除在外了。新节点是后来加入的DaemonSet 的滚动更新或 Pod 创建存在延迟。集群中有部分节点处于NotReady状态DaemonSet 不会在上面创建 Pod。排查方法很简单预热任务执行后用kubectl get pods -n prewarm -o wide对比一下节点列表看哪些节点上没有预热 Pod。如果只是个别节点没有重点检查污点和NotReady状态。如果是大批节点没有回去检查 nodeSelector 和 DaemonSet 的调度配置。对于新节点加入的场景建议在节点池扩容流程里加一步“校验节点上是否有预热镜像”没有就在线补一个绑定节点的 Job。别指望 DaemonSet 一定及时特别是节点批量唤醒的时候控制器创建 Pod 的速度可能跟不上。7.5 预热时间估算与超时控制提前估算预热时间能帮你判断发布窗口够不够用。一个粗粒度的公式是预热耗时 ≈ 镜像大小 / 有效带宽 镜像解压写入耗时比如镜像 5GB节点到 Registry 的有效带宽 100MBps理论传输 50 秒加上解压和写入实际大概 60 到 90 秒。但如果 20 个节点并发拉同一个镜像Registry 出口带宽有限每个节点分到的带宽会下降实际耗时可能是单节点的两三倍。所以我做预热流水线时会先根据镜像总量和目标节点数算一个预估时间然后设置一个保守的超时时间通常按预估时间的 1.5 到 2 倍算。比如预估 5 分钟超时就设 10 分钟。超时后流水线要能报警并暂停发布而不是无限等下去。这里分享一个经验预热任务不要只盯着“是否成功”要同时记录“每个节点拉取耗时分布”。如果个别节点耗时远超平均水平大概率是网络链路问题或节点磁盘 IO 问题。尽早发现这类节点避免正式发布时踩坑。结尾做了几年 Kubernetes 集群运维我最大的体会是镜像分发问题从来不是靠一个方案解决的。如果你只是偶尔被大镜像坑一把先用 DaemonSet 预热方案撑住成本最低如果发布频繁到每天都要等镜像就把预热做成发布流水线的一个前置步骤让它变成“发布准备”的一部分如果节点多、镜像又大认真考虑缓存仓库和 P2P别硬扛。我个人习惯的做法是预热 Pod 的命名带上业务名和镜像版本方便排查时一眼看出任务来源预热完成后删除临时 Pod但保留镜像在节点上监控上重点关注节点磁盘使用率和 Registry 出口带宽这两个指标一出问题预热系统多半也跟着出问题。最后再分享一个小技巧预热不一定非要用sleep挂住容器绑定节点的 Job 配合“镜像已存在于节点即可Job 状态异常不用太在意”的心态能让预热流程简单很多。镜像分发这个事做得越简单越不容易出错。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门