课程目录(第 25 章 / 共 33 章)
镜像仓库与加速:让拉取又快又稳
用 Harbor 把镜像拉取收进内网,用 imagePullSecret 打通鉴权,再用 P2P 与节点缓存把「扩容时全员等镜像」变成几秒钟的事。
学完这一章,你将能够
- ✓说得出生产集群自建镜像仓库的四个理由
- ✓用 Helm 装好 Harbor,完成一次登录、推送与拉取
- ✓在 Pod 与 ServiceAccount 上正确引用 imagePullSecret
- ✓知道 Dragonfly 与节点预拉取分别在什么场景下有用
为什么生产集群要有自己的镜像仓库
刚开始学习时,image: nginx:1.27 就够了:kubelet 会自己去 Docker Hub 拉。到了生产环境,这条路径会变成一连串问题。
- 外网依赖:节点出不了公网、或者上游 registry 抖动,整个发布流程就停摆。集群越大,这个依赖越致命。
- 速率限制:公共仓库对匿名拉取有频率限制,一次扩容几十个节点、或者一批 Pod 同时启动,很容易被限流,表现为
toomanyrequests或ImagePullBackOff。 - 安全审计:你不知道镜像里到底有什么、有没有被替换过。自建仓库才有扫描、签名、来源控制的抓手。
- 离线与内网环境:很多生产集群(尤其是金融、制造、科研)根本不允许访问公网。
一句话:生产集群的镜像来源应该只有一个,就是你自己控制的仓库。
自建仓库不是「多一台服务器」那么简单,它同时承担了分发入口、安全闸门和跨机房同步三个角色:
开发机构建 ──push──▶ Harbor(内网唯一入口)
│
├── 漏洞扫描(Trivy)→ 扫描报告
├── 签名(cosign)→ 供准入策略校验
└── 仓库复制 ──▶ 灾备机房 / 另一套集群
▲
│ pull
节点 containerd
▲
└── Dragonfly / Spegel:节点之间 P2P 互相分发Harbor 能给你什么
自己用 registry 镜像起一个仓库也能用,但它只有一个账号池,没有项目隔离、没有扫描。Harbor 是在 registry 之上加了管理能力的发行版,生产上更常用。
| 能力 | 说明 |
|---|---|
| 项目与用户 | 项目级隔离,用户、组、机器人账号的 RBAC,可对接 LDAP 或 OIDC |
| 镜像复制 | 仓库之间复制,用于跨机房、跨云同步 |
| 漏洞扫描 | 内置 Trivy,可按项目配置自动扫描 |
| 镜像签名 | 支持 cosign 等签名方式,配合准入策略校验来源 |
| 代理缓存 | 把上游 registry 配成 proxy cache,替公网镜像做一层缓存 |
| 保留策略与配额 | 自动清理旧 tag,限制项目容量 |
| 制品类型 | OCI 镜像、Helm chart 以及其它 OCI 制品 |
代价是组件多:core、jobservice、portal、registry 加 PostgreSQL 与 Redis。所以生产环境建议外接数据库与对象存储,而不是全靠内置组件加 PVC。
动手:用 Helm 装 Harbor 并推拉一个镜像
动手练习:装 Harbor 并推一个镜像进去
chart 名是 harbor/harbor,仓库地址 https://helm.goharbor.io:
helm repo add harbor https://helm.goharbor.io
helm repo update
helm search repo harbor/harbor --versions | head -5把关键参数写进 harbor-values.yaml(<你的 StorageClass> 换成集群里真实存在的 StorageClass):
expose:
type: ingress
tls:
enabled: true
ingress:
className: nginx
hosts:
core: harbor.example.com
externalURL: https://harbor.example.com
persistence:
enabled: true
persistentVolumeClaim:
registry:
storageClass: <你的 StorageClass>
size: 200Gi
database:
storageClass: <你的 StorageClass>
size: 20Gi
redis:
storageClass: <你的 StorageClass>
size: 10Gi
harborAdminPassword: "<改成强密码>"
trivy:
enabled: truehelm install harbor harbor/harbor \
--namespace harbor --create-namespace \
-f harbor-values.yaml
kubectl -n harbor get pods
kubectl -n harbor get svcexpose.type 有四个取值:ingress、loadBalancer、nodePort、clusterIP。本地实验没有 Ingress 时用 nodePort,再加 expose.nodePort.ports.http.nodePort: 30002;externalURL 必须和客户端实际访问地址一致,否则登录回调会失败。
等所有 Pod Running 之后,登录并推一个镜像(library 是 Harbor 自带的默认项目):
echo '<adminPassword>' | docker login harbor.example.com -u admin --password-stdin
docker pull nginx:1.27
docker tag nginx:1.27 harbor.example.com/library/nginx:1.27
docker push harbor.example.com/library/nginx:1.27
docker rmi harbor.example.com/library/nginx:1.27
docker pull harbor.example.com/library/nginx:1.27推送成功会看到一串 digest: sha256:... 与 Pushed;到 Harbor 的 Web 界面进入 library 项目,能在 Repositories 里看到 nginx。能推、能删本地镜像后再拉回来,才算仓库真的可用。
imagePullSecret:让 Pod 有权拉私有镜像
仓库通了,但 Pod 还不知道怎么登录。私有仓库需要一个 docker-registry 类型的 Secret,创建时必须把仓库地址写对:
kubectl -n demo create secret docker-registry harbor-cred \
--docker-server=harbor.example.com \
--docker-username='robot$kube101' \
--docker-password='<机器人账号的 token>' \
--docker-email=dev@example.com在 Harbor 里给应用建一个机器人账号,而不是用 admin。然后在 Pod 里引用它:
apiVersion: v1
kind: Pod
metadata:
name: app
namespace: demo
spec:
imagePullSecrets:
- name: harbor-cred
containers:
- name: app
image: harbor.example.com/library/nginx:1.27如果每个 Pod 都写一遍太啰嗦,就把它挂到命名空间的 ServiceAccount 上,所有使用该 SA 的 Pod 自动继承:
kubectl -n demo patch serviceaccount default \
-p '{"imagePullSecrets":[{"name":"harbor-cred"}]}'
kubectl -n demo get serviceaccount default -o yaml | grep -A3 imagePullSecrets验证方式是拉取失败时的报错能不能自证:kubectl -n demo describe pod app 的 Events 里出现 401 Unauthorized 或 pull access denied,说明鉴权没通;出现 Pulling 之后 Successfully pulled,说明通了。
Harbor 的生产配置:复制、扫描与配额
装起来只是第一步,「谁能推、推上去要不要拦、拦下来谁来清」这条链才是生产价值所在。三个旋钮最容易被忽略。
镜像复制解决跨机房、跨集群分发:在「仓库管理 → 新建目标」登记对端仓库,再用复制规则让 Harbor 自己同步,别靠人往两边各推一次。两个讲究:规则按项目建而不是全库复制,避免把开发镜像同步到灾备机房;事件驱动加定时兜底,事件让新 tag 尽快到位、定时修补中断的同步。公网镜像不必手工同步,直接把项目做成 proxy cache 项目:
harbor login https://harbor.example.com -u admin -p '<adminPassword>'
for r in docker.io registry.k8s.io quay.io nvcr.io; do
url="https://$r"; [ "$r" = "docker.io" ] && url="https://mirror.gcr.io"
harbor registry create --name "$r" --type docker-registry --url "$url"
done
harbor registry list -o json | jq -r '.Payload[] | "\(.id) \(.name)"' | while read id name; do
harbor project create "$name" --proxy-cache --public --registry-id "$id"
done漏洞扫描的价值不在 trivy.enabled: true,而在什么时候扫、扫出来怎么办:项目里打开「推送时自动扫描」,并勾选阻止有高危漏洞的镜像被拉取,再让 CI 在推完镜像后查一次扫描结果——高危未修复就不进入部署流程。内网还要单独规划 Trivy 漏洞库的更新源,否则扫描结论越来越旧,看着「全绿」却没有意义。
配额与保留策略必须一起配:配额(项目级存储上限)防止一个团队写满整个仓库,保留策略按 tag 数量或天数清理历史镜像。只配配额,写满时整个项目都推不上去;只配保留策略,则可能在清理前就被写满。为什么生产上非做不可:仓库是全集群唯一的镜像入口,它一满或一慢,表现就是所有人 ImagePullBackOff——这两个配置不是省磁盘,而是把故障半径限制在单个项目内。
参考:reference/k8s-in-action/repo/harbor/README.mdDragonfly:P2P 分发加速
镜像仓库解决了「从哪拉」,但没解决「拉多快」。一次扩容 50 个 Pod,50 个节点同时去仓库拉同一个 2GB 镜像,仓库出口带宽立刻被打满,镜像拉取变成扩容的瓶颈。
Dragonfly 的思路是 P2P:把镜像切成块,节点之间互相交换已经下载的块,只让少数节点(seed peer)回源仓库。
┌──────────────┐
│ Harbor 仓库 │
└──────┬───────┘
│ 只有 seed peer 回源
▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 节点 A │◀─▶│ 节点 B │◀─▶│ 节点 C │
│ (seed peer)│ │ │ │ │
└────────────┘ └────────────┘ └────────────┘
P2P 网络:块在节点之间直接交换组件构成是 manager、scheduler、seed peer 加每个节点上的客户端,另带 MySQL 与 Redis。用 Helm 安装,chart 名 dragonfly/dragonfly:
helm repo add dragonfly https://dragonflyoss.github.io/helm-charts/
helm repo update
helm install dragonfly dragonfly/dragonfly \
--namespace dragonfly-system --create-namespace
kubectl -n dragonfly-system get pods和 containerd 集成靠 client.dfinit:
client:
dfinit:
enable: true
config:
containerRuntime:
containerd:
configPath: /etc/containerd/config.toml
registries:
- {hostNamespace: docker.io, serverAddr: https://mirror.gcr.io, capabilities: [pull, resolve]}和 containerd 集成靠 client.dfinit:它改写节点上的 containerd 配置,把镜像拉取指向本机的 dfdaemon 代理,再由 P2P 网络决定从哪里取块。早期版本里节点侧组件叫 dfget-proxy 与 dfdaemon,v2 之后统一为 dragonfly-client,集成方式也从手改配置变成由 dfinit 自动完成。
验证方式是在节点上直接拉一个镜像,再看客户端日志:
crictl pull harbor.example.com/library/nginx:1.27
kubectl -n dragonfly-system logs -l app=dragonfly-client --tail=50
kubectl -n dragonfly-system port-forward svc/dragonfly-manager 8080:8080日志里出现 download task started 就说明流量走了 P2P;manager 的控制台能看任务与流量统计。
Dragonfly 与 Spegel 怎么选
两者都做 P2P,取向不同:Spegel 是单个 DaemonSet,让节点互为镜像源、直接复用各节点 containerd 里已经拉取过的镜像层,没有中心节点,但只支持 containerd;Dragonfly 是一套独立的 P2P 网络,由 seed client 统一回源,能力更多、组件也更多。
| 维度 | Spegel | Dragonfly |
|---|---|---|
| 组件 | 单个 DaemonSet | manager / scheduler / seed client / client + MySQL + Redis |
| 原理 | 节点互为镜像源,复用本地已有层 | 独立 P2P 网络,seed client 回源 |
| 预热 | 不支持,首次拉取仍要回源 | 支持,首次拉取也能加速 |
| 分发对象 | 容器镜像 | 镜像,以及模型权重、数据集等任意文件 |
| 资源开销 | 低 | 高,生产建议外接 MySQL 与 Redis |
Spegel 有个节点侧前提最容易漏:containerd 不能丢弃已解包的层,否则节点之间没有可复用的内容,P2P 直接失效。滚动改完再重启,然后看它的自检页面:
# 在 /etc/containerd/config.toml 的 overlayfs 段设 discard_unpacked_layers = false 后滚动重启:
systemctl restart containerd
kubectl -n spegel port-forward svc/spegel 9090 # 打开 http://localhost:9090/debug/web判断标准很直接:UI 里 Last Mirror Success 显示时间而不是 Pending,说明镜像源发现正常。选型记一句话:只是「同一个镜像在多个节点重复拉」,选 Spegel;要预热新镜像、要分发模型权重这类非镜像文件、或节点运行时不是 containerd,才上 Dragonfly。
参考:reference/k8s-in-action/base/spegel/README.md、reference/k8s-in-action/repo/dragonfly/README.md
节点级镜像缓存:预拉取与 kube-fledged
P2P 解决的是「多个节点同时拉」。还有一类场景是「镜像已经确定,但不想等它拉」:比如大版本发布前,先把镜像预热到所有节点上。
最简单的办法是用一个 DaemonSet 在每台节点上把镜像拉一遍:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: preload-nginx
namespace: kube-system
spec:
selector: {matchLabels: {app: preload-nginx}}
template:
metadata: {labels: {app: preload-nginx}}
spec:
initContainers:
- name: preload
image: harbor.example.com/library/nginx:1.27
command: ["/bin/true"]
containers:
- name: pause
image: registry.k8s.io/pause:3.10
resources: {requests: {cpu: 1m, memory: 8Mi}}initContainer 的作用只是「让 kubelet 在每台节点上拉一次这个镜像」,拉完立刻退出。如果镜像很多、或者想按标签批量管理缓存,可以用 kube-fledged:它用一个 ImageCache 资源声明要缓存的镜像列表,再由控制器负责在节点上预拉并保持。
预拉取会把镜像钉在节点上
预拉取的镜像会一直占用节点磁盘。镜像很大、节点磁盘很小时,预拉取可能直接把节点写满(kubelet 会触发镜像回收,把没在用的镜像删掉,预拉取就白做了)。先算清磁盘容量,再决定预拉多少。
tag、digest 与多架构镜像
- tag 是可变的,
app:1.2.3可以被重新推送,所以生产上更推荐image@sha256:...固定 digest,这一点在 安全加固 里讲过。 - 多架构镜像用 manifest list 承载:同一个 tag 下面挂着 amd64、arm64 等多个架构的镜像,节点拉取时自动选择匹配的那一份。
docker buildx build --platform linux/amd64,linux/arm64 \
-t harbor.example.com/library/app:1.2.3 --push .
docker buildx imagetools inspect harbor.example.com/library/app:1.2.3imagetools inspect 会列出所有平台与各自的 digest。只在 arm64 笔记本上构建、直接推给 amd64 节点用,会遇到 exec format error 或 no matching manifest——这就是多架构没做对的表现。
离线依赖源:PyPI 与 Conda 镜像
镜像只是一半。构建训练镜像时要 pip install、运行时要用 conda 环境,这些依赖同样出网,集群内网化之后它们是最先断掉的一环。做法是在内网跑按需缓存的镜像源:第一次请求回源并落盘,之后命中缓存。
Python 用 devpi 建一个 mirror index(服务端执行 devpi index -c root/pypi type=mirror mirror_url=https://pypi.org/simple/,上游建议指向 pypi.org,部分公共镜像源在 devpi 下有兼容问题;索引设成 volatile=False,已缓存的版本才不会被顶掉),客户端改一行就切过去:
pip config set global.index-url https://pypi.example.com/root/pypi/+simple/Conda 用 quetz 建 proxy 模式的 channel,客户端写 ~/.condarc:
curl --request POST --url https://conda.example.com/api/channels \
--header 'Content-Type: application/json' --header 'X-API-Key: <API Key>' \
--data '{"name":"main","private":false,"mirror_mode":"proxy","mirror_channel_url":"https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main"}'# ~/.condarc
channels:
- defaults
default_channels:
- https://conda.example.com/get/main
- https://conda.example.com/get/r改完执行 conda clean -i 清掉索引缓存,否则客户端还在用旧 channel。存储只随「实际拉取过的包」增长(PyPI 全量在 20Ti 量级,按需缓存远小于此)。适用场景:PyPI 镜像给 Python 应用与训练镜像构建用,收益是断网可用加构建可复现;Conda 镜像给数据科学与 GPU 环境用,包体积大、缓存收益更明显。它们替代不了制品库——内部私有包仍要发到同一套 devpi / quetz 的私有索引里。容器内注意 pip config 只对当前用户生效,Dockerfile 里要用 pip install --index-url https://pypi.example.com/root/pypi/+simple/,或者往镜像里写全局 /etc/pip.conf。
参考:reference/k8s-in-action/repo/pypi/README.md、reference/k8s-in-action/repo/conda/README.md
常见坑与速查表
镜像拉取最常踩的五个坑
- HTTP 仓库没配 insecure-registries:Docker 报
http: server gave HTTP response to HTTPS client,containerd 报类似的 TLS 错误。生产上请签证书,实验环境才用insecure-registries或insecure_skip_verify。 - 证书 SAN 不匹配:用 IP 访问但证书只签了域名,报
x509: certificate is valid for ..., not ...。把访问用的地址加进证书 SAN,或者改用域名访问。 - imagePullSecret 在错误的命名空间:Secret 是命名空间级的,Pod 只认自己所在命名空间的 Secret。跨命名空间引用会直接报
secret not found。 - Harbor 的容量规划:镜像层放对象存储比放 PVC 更稳,数据库生产上外接 PostgreSQL;另外默认单层上传上限是 128GiB,超大镜像要调
core.extraEnvVars里的BEEGO_MAX_UPLOAD_SIZE_BYTES。 - P2P 加速跨网段失效:P2P 依赖节点之间互通且带宽充足。跨可用区、跨网段时,节点间传输可能比直接回源还慢,反而拖慢拉取。
| 现象 | 原因 | 怎么确认 |
|---|---|---|
ImagePullBackOff,Events 里 401 | imagePullSecret 缺失、错误或过期 | kubectl describe pod 看 Events,核对 Secret 所在命名空间与 --docker-server |
no such host | 节点 DNS 解析不到仓库域名 | 在节点上 nslookup harbor.example.com |
x509: certificate signed by unknown authority | 仓库用了自签证书,节点不信任 | 把 CA 装到节点信任库,或配 containerd 的 CA 配置 |
| 拉取很慢但仓库带宽没满 | 节点到仓库链路或 P2P 跨网段问题 | 对比单节点 crictl pull 耗时与仓库出口带宽曲线 |
扩容时大量 Pod 卡在 ContainerCreating | 镜像太大、仓库限流或节点磁盘不足 | kubectl describe pod 看 Pulling image 与磁盘告警 |
镜像在开发机能跑、节点报 exec format error | 架构不匹配,镜像不是多架构的 | docker buildx imagetools inspect <镜像> 看平台列表 |
自测题
自测:为什么说「镜像来源应该只有一个」,多配几个 mirror 不是更稳吗?(点击展开答案)
多配 mirror 看起来提升了可用性,但会破坏三件事:来源可控(你不知道 Pod 最终从哪个镜像源拉到了什么)、审计可追溯(扫描和签名只在某一个源上做过)、一致性(同一个 tag 在不同 mirror 上可能是不同内容)。正确的做法是保持「唯一入口」:所有镜像先进内网仓库,再由仓库做复制、代理缓存和多副本,把对外网的依赖收敛到仓库这一层,而不是分散到每个节点。
自测:`imagePullSecrets` 为什么有时明明配了还是拉不到?(点击展开答案)
最常见的原因是位置不对:Secret 是命名空间级的,写在 demo 命名空间却让 prod 命名空间的 Pod 引用,必然失败。其次要检查 --docker-server 是否和镜像地址的域名完全一致——harbor.example.com 与 harbor.example.com:443 在凭据匹配上会被当成不同的地址。
还有一个容易忽略的点:Secret 里的 token 过期或被删除后,Pod 新建时才会重新鉴权,已经在跑的 Pod 不受影响,所以「突然拉不到」往往发生在扩容或重建时。
自测:什么时候 P2P 加速反而会拖慢拉取?(点击展开答案)
P2P 的收益来自「多个节点同时需要同一个镜像」。只有一个节点拉、或者每个节点拉的镜像都不一样时,P2P 反而增加了调度开销。更明显的是网络拓扑:如果节点分散在多个网段或可用区,节点之间的带宽和延迟比直接回源更差,块交换就成了瓶颈,所以部署前要先确认节点间的网络质量,必要时把 P2P 限定在同一网段内,跨网段走回源。
小结
- 生产集群自建镜像仓库的理由是外网依赖、速率限制、安全审计与离线环境,镜像来源应当收敛到唯一入口。
- Harbor 在 registry 之上提供项目 RBAC、复制、扫描、签名、代理缓存与保留策略;生产上要配好按项目的复制规则、推送时自动扫描与项目配额,代价是组件多、建议外接数据库与对象存储。
- 私有镜像要在 Pod 或 ServiceAccount 上引用
imagePullSecret,Secret 必须和目标 Pod 在同一个命名空间;固定 digest 比固定 tag 更可靠,多架构镜像用 buildx 构建 manifest list。 - P2P 解决「多节点同时拉」:Spegel 用一个 DaemonSet 复用节点已有层,Dragonfly 支持预热与非镜像文件但组件多;节点预拉取适合发布前预热,注意磁盘容量。
- 镜像只是一半,内网还要有 PyPI / Conda 的按需缓存镜像源(devpi / quetz),否则内网化之后依赖是最先断掉的一环。
练习
- 给 Harbor 配一个上游
docker.io的 proxy cache 项目,从它拉一个公网镜像,观察缓存命中前后的耗时差异。 - 在
demo命名空间删掉harbor-credSecret,再重建一个 Pod,用kubectl describe pod记录完整的鉴权失败报错。 - 给一个常用镜像构建多架构版本并推到 Harbor,然后在 amd64 节点上确认它跑得起来。
到这里,「进阶:生产实践」模块就讲完了:集群怎么装、网络怎么通、数据怎么落、状态怎么看、安全怎么加固、交付怎么自动化、数据怎么恢复、镜像怎么分发——这八件事撑起了在生产环境里管集群的底气。