k3s笔记-03-镜像缓存
Overview
1. 前言
镜像缓存是一个在边缘部署集群时必须解决的问题。在中心云内网环境下,每次拉取几GB的镜像都不是太大的问题,而边缘机房通过公网访问中心云的镜像仓库时,哪怕拉取几百MB的镜像,也会有很大的不确定性。
我们都知道容器镜像使用分层存储的结构,以拉取 nginx:1.20 镜像为例,首先是获取镜像的manifest,得到关于该镜像的分层信息,然后根据这些信息依次下载对应的blob,对于体积较大的镜像来说,能减少镜像拉取耗时的主要是blob缓存。
因此利用缓存加速拉取的关键在于:
- 统一基础镜像:假如一台宿主机上的所有容器化应用都使用同一个基础镜像,更新镜像时能利用缓存跳过拉取基础镜像的步骤
- 减少镜像内容变动:理想的情况下,更新应用时只拉取包含新程序的镜像分层
上面两点都可以通过优化Dockerfile来解决,在完成优化的前提下,我们可以在单机上得到最佳的镜像拉取速度,而如果要为多个宿主机提供缓存,则需要一个外部服务。
笔者在上周折腾一番后,制作了两个镜像用于提供集群镜像缓存和用户镜像缓存,并部署到了几个灰度集群中,目前看来效果符合预期,就在这里稍作记录。
测试环境中,宿主机系统为CentOS 7,每台宿主机使用systemd运行两个containerd服务供k3s集群和容器实例服务使用,分别通过配置containerd的mirror和HTTP/HTTPS代理实现镜像缓存。
2. 集群镜像缓存
k3s集群镜像缓存使用开源的registry程序来实现,部署后registry对外提供一个http地址,将docker或containerd的registry mirror配置为该地址即可,registry相关资源如下:
这里是为内部应用缓存镜像,因此除了registry外,还有很多优化手段,例如固定使用的镜像仓库、统一镜像拉取密钥、统一基础镜像等,下面是registry程序的适用场景和优缺点:
- 适用场景:加速私有镜像或公共镜像
- 优点:非侵入式,containerd可按需配置多个要加速的镜像仓库,访问registry mirror失败时,会直接从目标仓库拉取镜像
- 缺点:
- 只支持配置单一镜像仓库,假设分别需要拉取docker hub、私有镜像仓库、k8s镜像仓库的镜像,则需要运行三个registry实例来加速
- 只支持配置单一用户凭证,例如为docker hub的用户A配置凭证后,无法再配置另一个用户B的凭证
- 只支持在registry上配置凭证,忽略containerd请求中的凭证
关于缺点可以举一个例子,docker hub是一个多租户的镜像仓库,而其他镜像仓库也参考了docker hub的设计,允许一个用户创建多个仓库,例如用户A可创建多个私有镜像仓库:docker.io/repo-a1, docker.io/repo-a2。
但registry程序的凭证配置是针对 docker.io 的,此时如果需要缓存用户B的 docker.io/repo-b1 和 docker.io/repo-b2,registry程序是无法支持的,只能将用户B的镜像先同步到用户A的镜像仓库中再拉取。
这一点让registry只适合加速单用户的私有镜像或者公共镜像,无法作为公共服务提供给多个租户来加速私有镜像。
下面是registry服务及containerd使用registry服务的配置示例:
2.1 registry服务
这里创建了一个Deployment和Service来部署registry程序
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 namespace: default
5 name: registry
6 labels:
7 app: registry
8spec:
9 selector:
10 matchLabels:
11 app: registry
12 template:
13 metadata:
14 labels:
15 app: registry
16 spec:
17 containers:
18 - name: registry
19 image: 'registry:2.8.1'
20 env:
21 - name: REGISTRY_PROXY_REMOTEURL
22 value: 'https://uhub.service.ucloud.cn'
23 - name: REGISTRY_HTTP_ADDR
24 value: ':5000'
25---
26apiVersion: v1
27kind: Service
28metadata:
29 namespace: default
30 name: registry
31spec:
32 ports:
33 - name: registry
34 port: 5000
35 targetPort: 5000
36 selector:
37 app: registry
38 type: ClusterIP
2.2 containerd配置文件
假设已创建的Service对应的集群IP为 10.42.0.32,修改containerd配置文件中关于 plugins."io.containerd.grpc.v1.cri".registry.mirrors 的配置,如下:
1...
2 [plugins."io.containerd.grpc.v1.cri".registry]
3 [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
4 [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
5 endpoint = ["https://registry-1.docker.io"]
6 [plugins."io.containerd.grpc.v1.cri".registry.mirrors."uhub.service.ucloud.cn"]
7 endpoint = ["http://10.42.0.32:5000"]
8...
在集群中的所有containerd更新配置后,即可获取机房级别的镜像缓存。
3. 用户镜像缓存
我们开发的边缘容器实例服务是面向用户的,因此面临registry程序无法解决的一个问题:缓存不同用户的私有镜像。
最初调研时考虑过改造registry程序,但研究源码后却发现registry服务几乎就是针对单用户设计的,也就是上一小节中提到的几个缺点,难点主要在于按镜像仓库授权,这需要重构所有使用凭证的模块。
重构不如重新开发,笔者的同事按照registry接口开发了一个中间层来支持多用户凭证,底层仍旧使用containerd拉取和存储镜像,这一套服务包含三个程序,运行在边缘机房的一台宿主机上,再修改机房内的所有containerd配置,使用这台机器作为镜像缓存。
不过由于开发时间紧张且缺少充分的测试,使用过程中逐步发现一些问题,例如重复拉取tag无法获得最新manifest、镜像缓存缺少垃圾回收等,而在开发人员离职后,这些问题也就没有机会修复了。
在边缘机房部署集群时,笔者再次研究了镜像缓存方案,找到了另一个解决方法:docker-registry-proxy,相关的资源如下:
这个程序使用了另外一个思路:
- 通过环境变量为containerd配置HTTP/HTTPS代理,让containerd的所有请求经过docker-registry-proxy
- 自动生成根证书和服务端证书,根证书需要提前安装到containerd宿主机
- 拦截与解析containerd的HTTP请求,将其中关于拉取镜像的部分按需缓存
本质上它就是一个中间人,一个基于nginx的拦截代理,针对HTTP请求的缓存可以实线对多用户、多仓库的支持。
下面是docker-registry-proxy服务、根证书以及systemd配置文件示例:
3.1 docker-registry-proxy
这里同registry服务一样创建了一个Deployment和Service,在Deployment环境变量中禁用manifest缓存,设置需要缓存的域名。
注意:docker-registry-proxy程序启动时会自动创建根证书和服务端证书,这里建议提前生成证书后一起打包到镜像中,避免容器组重启后导致证书变化。
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 namespace: default
5 name: registry-proxy
6 labels:
7 app: registry-proxy
8spec:
9 selector:
10 matchLabels:
11 app: registry-proxy
12 template:
13 metadata:
14 labels:
15 app: registry-proxy
16 spec:
17 containers:
18 - name: registry-proxy
19 image: 'rpardini/docker-registry-proxy:0.6.4'
20 env:
21 - name: ENABLE_MANIFEST_CACHE
22 value: 'false'
23 - name: REGISTRIES
24 value: 'uhub.service.ucloud.cn'
25---
26apiVersion: v1
27kind: Service
28metadata:
29 namespace: default
30 name: registry-proxy
31spec:
32 ports:
33 - name: registry-proxy
34 port: 3128
35 targetPort: 3128
36 selector:
37 app: registry-proxy
38 type: ClusterIP
3.2 安装根证书
将docker-registry-proxy的根证书 ca.crt 拷贝到containerd宿主机的 /etc/pki/ca-trust/source/anchors/docker_registry_proxy.crt,执行 update-ca-trust 更新受信赖的根证书列表。
3.3 systemd配置文件
假设已创建的Service对应的集群IP为 10.42.0.64,修改 /lib/systemd/system/containerd.service 文件,在 [Service] 下添加两行HTTP代理配置,如下:
1...
2[Service]
3Environment="HTTP_PROXY=http://10.42.0.64:3128"
4Environment="HTTPS_PROXY=http://10.42.0.64:3128"
5ExecStart=/usr/local/bin/containerd -c /etc/containerd/config.toml
6...
执行 systemctl daemon-reload && systemctl restart containerd 重启服务。