一、背景
我们知道,容器运行起来的时间是非常快的,但是如果节点上容器的镜像不存在,那么在运行容器时要先拉取镜像,拉取镜像在容器启动的过程中占用的时间比较长,这个过程要将容器所有的镜像层都拉取到本地磁盘中。据统计,拉镜像操作要占用容器启动时间的76%。这在容器数量少的情况下问题不大,但容器数量比较多并且都是冷启动的时候会非常的慢。
如何解决容器冷启动过程中拉取镜像慢这个问题?有这样的一种解决思路:在容器启动过程中,容器要用的镜像通过高速网络按需从镜像仓库中读取,而不是将镜像所有的层都拉下来。Stargz-snapshotter是containerd下面的一个子项目,以Proxy Plugin的方式扩展containerd的功能,是Containerd的一个remote-snapshotter实现。
版本变迁
二、使用
Stargz-snapshotter在kuberentes中使用比较简单:使用Containerd作为kuberentes的CRI运行时。在本地起一个stargz-snapshotter的服务,作为containerd的一个remote snapshotter。
镜像转换
在使用前需要将我们的普通的镜像转换成stargz-snapshotter可以识别的镜像,使用ctr-remote工具进行转换,下面示例是将本地一个centos镜像进行转换,转换完成后推送到镜像仓库中:
$ ctr-remote image optimize --plain-http --entrypoint='[ "sleep" ]' --args='[ "3000" ]' centos:7 centos:7-eg复制代码
对比
使用crictl工具在本地拉取转换前和转换后的镜像,做一下对比,通过lazy的方式拉取镜像的速度更快:
# 正常拉取镜像 $ time crictl pull centos:7 Image is up to date for sha256:ef8f4eaacef5da519df36f0462e280c77f8125870dbb77e85c898c87fdbbea27real 0m5.967suser 0m0.009ssys 0m0.012s # 拉取优化后的镜像 $ time crictl pull centos:7-eg Image is up to date for sha256:36edf0c0bb4daca572ba284057960f1441b14e0d21d5e497cb47646a22f653d6real 0m0.624suser 0m0.012ssys 0m0.010s复制代码
查看镜像层
使用crictl创建一个pod,进入容器中,查看/.stargz-snapshotter/目录下各个镜像层在本地缓存的情况:
$ cat /.stargz-snapshotter/*{"digest":"sha256:857949cb596c96cc9e91156bf7587a105a2e1bc1e6db1b9507596a24a80f351a","size":80005845,"fetchedSize":3055845,"fetchedPercent":3.819527185794988}{"digest":"sha256:8c57b1a6bef1480562bc27d145f6d371955e1f1901ebdea590f38bfedd6e17d0","size":33614550,"fetchedSize":64550,"fetchedPercent":0.19202993941611593}复制代码
三、原理
上图是stargz-snapshotter的实现概览,通常的我们在拉取镜像时,要将镜像的每一层拉取下来,而使用stargz-snapshotter后containerd不再是拉取镜像的层,而是为存储在镜像仓库中镜像的每一层在容器运行节点上创建一个目录,通过远程挂载的方式挂到各个目录上。容器启动前再将各个目录做overlay挂载,为容器提供一个rootfs。当需要读取某个文件时,通过网络读取镜像仓库中镜像层中的文件。
下面再看一下镜像层是怎么远程挂载和如何从镜像层中按需读取文件的。
用户态文件系统
Stargz-snapshotter使用FUSE实现了用户态的文件系统。FUSE(Filesystem in userspace)框架是一个内核模块,能够让用户在用户空间实现文件系统并且挂载到某个目录,就像在内核实现文件系统一样。
如上图所示,stargz-snapshotter是一个实现了用户态文件系统的程序(golang语言,使用go-fuse作为实现的依赖)。当有拉取镜像的操作发生时,stargz-snapshotter会为镜像的每一层在${stargz-root}/snapshotter/snapshots/下创建一个目录,执行实现一个文件系统的逻辑,并将这个文件系统挂载到刚创建的目录上,例如图中的/dcos/snapshotter/snapshots/1。当有用户读取目录下的文件时,请求的流向是这样的:
① 操作请求经VFS到FUSE
② FUSE内核模块根据请求类型,调用stargz-snapshotter的逻辑,stargz-snapshotter从镜像仓库中读取该层中的文件
③ Stargz-snapshotter将文件的内容通过VFS返回给系统调用
(e)stargz格式
a. stargz格式
通常存放在镜像仓库中的镜像层都是使用gzip压缩过的,我们不能从这个压缩后的文件中提取单个文件。那stargz-snapshotter是怎么做到从单个镜像层中读取单个文件的呢?
Stargz-snapshotter使用了另一种压缩镜像层的格式,它也是gzip包,一种可seekable的gzip包,图3是targz和stargz的对比。压缩包里的文件可以被检索和抽取,但仍是zip格式的文件;镜像层中的每个文件都会被打成一个zip包,最后再组成一个大的zip包;整个zip包中有一个TOC文件,它记录了包中每个文件的偏移量;Footer占最后47个字节,记录了TOC在整个zip包中的偏移量。
这样就可以通过镜像层最后47个字节的Footer,找到TOC的偏移量,然后读取TOC的内容就能得到整个镜像层中有哪些文件,每个文件的偏移量是多少。Stargz-snapshotter就是通过这个TOC文件去按需检索整个镜像层中文件的。
b. estartgz格式
默认情况下,将镜像的某一层远程挂载到目标主机后,stargz-snapshotter默认会创建一个后台任务去缓存镜像层。在容器启动过程中,如果容器启动需要的文件没有在本地缓存那么stargz-snapshotter就需要通过网络去镜像仓库中读取,这会导致容器启动速度比较慢。
estargz是对stargz格式进行了优化,如上图所示。它多了一个landmark文件,这个文件将镜像层中的文件分成了两类:一类是容器运行时最有可能用到的,另一类是可能用不到的。这样后台任务会优先去缓存那些容器运行时需要的文件,这样会增加本地缓存的命中率,加快容器的启动速度。
下图是三种镜像层的对比情况,legacy是普通镜像层,stargz是stargz格式的镜像层,estargz是优化后stargz格式的镜像层。
c. 分层拉取镜像
镜像层使用estargz格式可以做到从压缩包中检索文件,那stargz是如何从镜像仓库中按照分片获取文件全部或者部分数据的?
在OCI规范中有关于如何从仓库中获取部分数据的描述,而docker registry也有对应接口实现。
Registry中获取镜像层部署数据的接口如下:
其中,name就是目标repository的名称,digest就是镜像层blob的digest的值,Host就是镜像仓库的地址,Range描述的就是要获取的blob分片。
返回的响应如下:
其中,start是开始的字节,end是结束的字节,size是层大小,length是本次请求的层分片。
lazy-pulling流程
Containerd使用stargz-snapshotter拉取镜像的流程如下:
① 根据镜像名称和tag解析出镜像manifest的digest的值
② 根据镜像manifest的digest的值,从镜像仓库中下载manifest,保存在content store中
③ 根据manifest的内容获取镜像config的digest的值,从镜像仓库中下载config,保存在content store中
④ 解析镜像的每一层,创建snapshot,如果containerd使用的stargz-snapshotter,它会返回一个snapshot已经存在的错误。Stargz-snapshotter PrepareSnapshot的逻辑就是为当前层准备文件系统并挂载到本地的一个过程。
⑤ 所有镜像层解析完成后会保存镜像的元数据
四、小结
创建容器时,拉取镜像过程在容器启动时间的占比高,通常我们会使用多种方法去制作尽量小一点的镜像,或者通过P2P网络去分发镜像。镜像lazy pull是另一种提高镜像分发速度的方式。
使用stargz-snapshotter在镜像拉取时,仅将镜像的manifest和config下载下来,并镜像每一层通过远程挂载的方式挂到当前主机上,容器运行时达到按需读取文件的效果。而传统方式是将镜像的每一层都下载到本地进行解压。相比而言前者能加快镜像的拉取速度,加快容器冷启动的速度。但需要注意,文件是按需加载的,它依赖于一个比较好的网络环境。