参考自:张晨,邓玉辉.基于镜像层关联的Docker注册表缓存预取策略[J].计算机科学与探索,2021,15(02):249-260.
研究意义
一、用户拉取镜像的请求流程
以 IBM Cloud Container Registry 的架构为例
- 用户输入 docker pull 指令,Docker Client 解析完请求后发送 HTTP 给 Docker Server,Docker Daemon 根据请求请求方法类型选择对应 Handler,该 Handler 在结合请求路由创建名为“pull”的 job,进而向注册表服务器发出方法名为 GET 的 http 请求: https://<registry URL>/v2/<username>/<repository name>/manifests/<image tag>。
- 若注册表服务器中存在 manifest,则将 manifest 发给 Docker Daemon,否则返回 404错误代码。
- Docker Daemon 解析 manifest,并查询本地存储,得出缺失镜像层,并发出方法名为HEAD 的 http 请求:https://<registry URL>/v2/<username>/<repository name>/blobs/<digest>。
- 注册表服务器根据请求信息检索对象存储服务器中是否存在对应镜像层。
- 若该镜像层存在,则对象存储服务器返回 200 给注册表服务器。
- 进而注册服务器给用户返回 200。
- 用 户 通 过 Docker Daemon 发 出 方 法 名 为 GET 的 http 请 求 : https://<registry URL>/v2/<username>/<repository name>/blobs/<digest>。
- 注册表服务器给用户返回一个重定向 url。
- 通过新 url 找到存放镜像层的实际存储服务器.
- 镜像层被取回客户端进行校验,所有镜像层被拉取完成时合并成镜像。
其中 username 是用户名,repository name 是镜像仓库名,v2 是注册表应用程序的版本,digest 是镜像层的 256SHA 值。
二、内存去冗余的镜像缓存策略 L-Cache
1.拉取延迟计算方法分析
根据实际的docker镜像请求流程,确定如下拉取延迟公式
即一个镜像层的拉取延迟 = 非拉取镜像的网络请求传输和跳跃产生的延迟开销 + 镜像通过网络传输产生的延迟开销 + 从对象存储服务器中将镜像层从硬盘传入内存产生的延迟开销
具体计算方式如下:
是除拉取镜像请求之外其他请求在三个组件中传输和跳跃产生的延迟开销。公式包含两个部分:一般请求在网络中传输的延迟开销和镜像元文件在网络中传输的延迟开销, 是广域网中下载速度的倒数,是镜像元文件的数据大小,是普通请求的数据大小, 表示在完整的请求流程中除了拉取镜像层请求的其他所有请求。
是镜像层在对象存储服务器中通过网络端口传输到用户本机的延迟开销,等于单个镜像层数据大小除以广域网中下载速度。是广域网中下载速度的倒数,是镜像层的数据大小。
是对象存储服务器接收到用户拉取镜像层的请求后,将数据从硬盘读入内存所需的时间开销,等于镜像层数据大小除以服务器硬盘的读速度。 是服务器硬盘的读速度的倒数, 是镜像层的数据大小。
由上得出:
由于磁盘提取速度比网络传输速度快得多,所以优化网络传输阶段能够获得较好的延迟减少
镜像层和镜像元数据的大小无法改变,只能通过减少发送请求数量,通过分析拉取镜像的完成请求流程,我们提出将部分热点镜像缓存至注册表服务器的内存中,例如包含系统文件、依赖库、常用中间件的镜像层即为热点镜像层,当请求命中缓存时即可拉取镜像,节省了后续请求的延迟开销,尤其对于镜像层数据较小的请求流中,降低延迟的效果更加明显。同时,由于磁盘 I/O 的限制,镜像层数据从磁盘拷贝到内存需要的时间也开销不容忽视,于是我们对注册表服务器和对象存储服务器的内存镜像层数据去冗余,提高存储资源利用率,使请求尽可能命中缓存,从而隐藏了磁盘 I/O 的延迟开销。
2.观察分析镜像特点
(1)镜像层与镜像元数据的大小
大部分镜像层体积都很小,其中 1K~10K 的镜像数量最多,并且小于 1M 的约占 65%,小于 10MB 的约占 80%。虽然超过 1G 的巨型镜像层不足 5%,但拉取该类镜像的时间开销非常巨大,缓存方案不予考虑这类镜像。——尽可能缓存较小的镜像层,提高缓存的镜像数量从而减少磁盘 I/O的延迟开销,最终节省用户拉取镜像的延迟。
(2)镜像流行度
所有注册表中最流行的 layer 都占据较高的比重,这证明了在不同工作负载中总是有热点镜像,结合上一小节中得出 65%的镜像层小于 1M——将小的热点镜像层缓存在注册表服务器内存的方案具有良好的可行性。
(3)注册表服务器的资源情况
发现第 3 类服务器架构是最接近实际生产中所使用的注册表服务器。如图 3.6 所示,此类
服务器在长期处理业务时内存使用率在 31.51%~39.11%的范围内,平均内存使用率为34.28%,同时其他服务器的平均内存使用率不会超过 50%。——现在 Docker Registry 架构中,负载均衡器智能地调节不同注册表服务器上的负载数量,同时注册表服务器在运行中有较为充裕的存储资源。
3.L-cache缓存策略设计
实验结果
L-cache(带内存去冗余功能)和R-cache(不带内存去冗余功能)的缓存命中率随缓存大小变化对比图
L-cache中registry端预取缓存和后端存储端预取缓存随缓存大小变化对比图
延迟节省率计算方法为
其中为使用 L-cache 或 R-Cache 策略时从注册表服务器中拉取镜像的延迟, 为使用传统的拉取流程所获得镜像的延迟开销
L-Cache 方案比 Docker 原生镜像拉取延迟减少了 7.7%~19.9%,这为加快容器启动提供了一个新的研究方向。但该方案的不足在于注册表服务器内存的缓存命中率较低,下一章提出新的预取策略以进一步优化缓存命中率,降低拉取镜像的延迟开销。
三、基于关联度的镜像层预取策略 LCPA
预取触发点问题:
过去的研究中一般在收到GET manifest请求后就直接开始预取。这会存在以下两个问题:
1.用户只会pull未拥有的layer,而registry端无法知道用户已经拥有哪些layer,全部预取的话就会导致预取的和实际需要的偏差较大,造成网络资源浪费和命中率的降低。
2.观察发现部分用户会因为网络拥塞或中断操作,只发送GET manifest请求而没有后续的GET layer请求,如果收到GET manifest就开始预取的话会导致全部预取都浪费掉
解决方法
本文选择第二次miss的pull layer请求作为触发点,首先使用pull layer作为触发点解决了问题2。选择第二个是因为注册表服务器第一次接收到某个镜像仓库内的未命中请求时,没有前驱请求可以建立关联,无法准确判断是否还有后续请求以及与其他镜像层的相关性。
LCPA策略关联性分为三种情况:
为每一个镜像仓库设置独立预取窗口,将拉取请求处理模块发送来的未命中请求保存在相应的预取窗口里,以便记录和查询请求之间对应的镜像层是否产生关联。
1.强关联:客户端pull一个layer一定会pull另一个layer,即所有包含其中一个layer的镜像一定也包含另一个。如镜像存储库1中的2,4,6。
2.弱关联:pull 一个layer可能会pull另一个layer,即一个镜像I_A包含layer_A和layer_B,另一个镜像I_B包含layer_A和layer_C,则layer A和B就是弱关联的。如镜像存储库2中的1和5
3.无关联:两个layer没有任何关系,没有同时包含它们两个的镜像存在,就像镜像存储库2的10和15。
预取策略:
新请求与历史请求的关系:
1.如果是强关联,预取集合就是新层的所有强关联层
2.如果是弱关联,则计算关联程度
3.如果是无关联,清除该预取窗口中所有历史请求,仅保留最新请求,为后继请求提供关联性参考。
获得了预取集合后先检查内存中是否已经缓存了其中的层,然后再进行缓存。
实验评测
请求命中率:
延迟节省率: