k3s笔记-03-镜像缓存

文章目录

1. 前言

镜像缓存是一个在边缘部署集群时必须解决的问题。在中心云内网环境下,每次拉取几GB的镜像都不是太大的问题,而边缘机房通过公网访问中心云的镜像仓库时,哪怕拉取几百MB的镜像,也会有很大的不确定性。

我们都知道容器镜像使用分层存储的结构,以拉取 nginx:1.20 镜像为例,首先是获取镜像的manifest,得到关于该镜像的分层信息,然后根据这些信息依次下载对应的blob,对于体积较大的镜像来说,能减少镜像拉取耗时的主要是blob缓存。

因此利用缓存加速拉取的关键在于:

  1. 统一基础镜像:假如一台宿主机上的所有容器化应用都使用同一个基础镜像,更新镜像时能利用缓存跳过拉取基础镜像的步骤
  2. 减少镜像内容变动:理想的情况下,更新应用时只拉取包含新程序的镜像分层

上面两点都可以通过优化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-b1docker.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,相关的资源如下:

这个程序使用了另外一个思路:

  1. 通过环境变量为containerd配置HTTP/HTTPS代理,让containerd的所有请求经过docker-registry-proxy
  2. 自动生成根证书和服务端证书,根证书需要提前安装到containerd宿主机
  3. 拦截与解析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 重启服务。