转载

DockOne 技术分享:Docker Registry V1 to V2

【编者的话】Docker registry 2.0 版本在安全性和性能上做了诸多优化,并重新设计了镜像的存储的格式。我们将详细介绍Docker Registry V1 与 V2 的区别,并在此基础上分享了灵雀云的实时同步迁移实践。

1. 相关概念

首先讲一下 registry 相关的概念。大家对 docker 应该比较了解了,就是容器技术使用了 cgroups, namespaces, union fs 等一系列机制来保证隔离。但我们实际使用中可能并不会直接接触这些技术,更直接使用的是 pull image, run image,再进阶一点会使用 build image 和 push image。我们日常的使用主要还是围绕 image 展开的。

而 image 和 registry 的关系可以想象成自己机器上的源码和远端 svn 或者 git 服务的关系。registry 是一个几种存放 image 并对外提供上传下载以及一系列 api 的服务。可以很容易和本地源代码以及远端 git 服务的关系相对应。

然后再说一下 hub,很多人不太清楚 docker registry 和 docker hub 到底有什么区别。其实依然可以利用 git 的概念来类比,registry 和 hub 的关系可以类比 git 和 github 的关系。git 是一个服务端程序,github 是全球最大的同性社交网站,那么 github 比 git 多了些什么?首先是 UI,然后就是用户鉴权,public private organization 各种组织机构服务,评论 issue 管理,search webhook 等工具的集合。hub 和 registry 的关系也是类似的,大家在 dockerhub 上看到的界面和各种功能就是 hub 的一部分,而且处于商业的考量 hub 是不开源的。

2. V1 Python Registry

下面来说一下 v1 的registry 以及为啥 docker 官方不给他饭吃了。v1 的项目地址在 https://github.com/docker/docker-registry 已经小半年没有更新了,很早就是废弃态。但说实话还是比较稳定的,api 设计也比健全,使用和扩展也还算方便。至少在我使用的过程中没有碰到太多的坑,反而是 v2 把我坑的够呛。

DockOne 技术分享:Docker Registry V1 to V2

这是一张 v1 registry 存储镜像的目录树。可以看到最上面是两层结构 images 和 repositories,关注一下 images 里面的内容,最叶子节点有一个 layer 和 ancestry。layer 就是这一层文件系统的 tar 包,ancestry 中存储的是它父亲层 id,可以看到 layer 之间在 v1 是通过一个链表的形式进行关系组织的。大家学过数据结构应该知道,链表的特点是插入删除方便,随机读取性能差,而 layer 之间显然是没有插入删除这个需求的,所以这个设计在我看来是有些问题的。这个组织结构的另一个缺点就是 pull layer 只能单线程,下完一层才能知道父亲是谁,就只能按序下载,没有发挥多核的优势。

当然这只是一个很小的问题,最主要的问题出现在那一长串 image id 上。不知道大家有没有想过这个 id 是如何生成的。这个 id 是在本地 build 时 随机生成的,随机生成的,随机生成的。 随机生成的意思就是 id 和内容完全没有关系,同样的 layer 再次 build 生成的 id 就是不同的。

这会造成很大的安全隐患。docker pull 和 push 判断 image layer 存在都是根据这个 id 所以在双向都存在造假的可能。恶意用户可以伪造 id 直接 push 上去,这样以后再有别人同样的 id 就上传不上去了,因为 registry 会认为 layer 已经存在了,但内容其实是不一样的。这个倒是还好,因为碰撞概率没那么高。

另一方面恶意的 registry 也可以根据 id 伪造内容,反正你只校验 id 也不知道是不是这个内容和 id 是关联的。类似于前一阵 xcode 的木马,下下来一看名字是 xcode 就以为是真的。安全性的因素也是 docker 官方想要重新设计 registry 的主要理由。

同样这种随机 id 也会造成 push 性能下降,因为可能重复内容被多次 push。其他原因还有 v1 是 python 实现的与 docker golang 的理念不符,尽管 docker golang 写的也不咋地。

另一个比较重要的原因是 tag 的问题。docker 很多方面都学 git 也学得很成功,偏偏版本控制的东西学残了,最明显的就是 tag。tag 的问题在于 docker image 的 tag 是可变的,你没有办法通过 tag 来确定唯一的版本,这一点在 latest 上尤为明显。因为 latest 是可以自己定义的,如果你在 dockerfile 里 from latest 很可能你过一阵 build 出来的镜像和之前不一样了,这个问题还很难发现。其他 tag 也存在同样的问题,我们现在的做法是用代码 commit id 做 tag,每次都是按照 commit id 作部署,尽量远离 latest。

3. V2 Golang Distribution

上面说了 v1 的一堆不好,下面来看一下 v2。

项目地址是 https://github.com/docker/distribution , 实话说目前的开发进度很缓慢,和 docker engine 的热度完全不是一个量级,很多基本的功能都很缺失。

DockOne 技术分享:Docker Registry V1 to V2

他的存储目录树十分复杂,我就简单列一下,可以看到最顶层还是两个,只不过 images 改名为 blobs,最叶子节点变为了 data,他的目录是一串长 id,需要注意的是这个 id 和 image id 是没有关系的。由于一些兼容性的历史问题,docker 并没有取消 image id 的概念,这里的一长串 id 是把 data 的内容经过 sha256 做 hash 得出的结果。

这样就很好的解决了之前 v1 所存在的随机 id 的大问题。

通过这种方式id 和 内容是一一对应的了,同样的内容总会生成同样的 id,而且这种 key->value 的形式也很利于缓存,为未来的优化也埋下伏笔。这样就可以变原来的链表顺序查找为数组的随机读取,这也是 v2 pull 可以并行的一个基础,并行 pull 大概也是客户端唯一能感受到的比较大的和 v1 的区别。大家可能发现这里没有 ancestry 这种链表结构了,v2 中会有一个新的文件,叫 manifest,它会记录改镜像所有layer 的信息。manifest 中还有大量其他信息,感兴趣的可以看一下 spec https://github.com/docker/dist ... -1.md

这一长串id 在docker叫 digest, 从 docker 的想法来看他是希望通过 digest 来取代 tag 达到可以指定唯一版本的目的。但是由于image id 历史遗留的问题太长远了,现在看改起来十分艰辛。还需要注意的是 这一串 id 是服务器端计算得出的,这样也杜绝了客户端造假 id 的行为,当然这个做法也带来其他的问题。

其他的新的地方还包括新的 auth 方式,notification 机制,以及全新的 api。这里全新的api的意思是和之前完全不兼容,如果现有系统想迁移需要考虑一下这点。然后他是 go 实现的,我自己的感觉吞吐量有两三倍的提升。

再说一下 v2 存在的问题。

首先是 api 的缺失,delete,search 这种基本功能都没有,而且 tag 和 digest 的关系很难找,而所有 api 又都是基于 digest 的。然后 push 和 pull 的速度有了新瓶颈。之前 v1 是把文件系统做 tar 包 v2 变成了 gzip 包,只要一 push 镜像 cpu 就会打满,而且压缩解压都是单核的,如果是内网话很有可能 push 和 pull 都变慢了。最后一点就是 v1 和 v2 镜像格式不兼容,不兼容,不兼容。

4. V1 V2 共存及同步实践

我们做的就是让 v1 和 v2 两个版本共存,并且镜像在两个 registry 中都存在。docker 目前 1.6 之后支持 v2,但我们的用户从 0.9 到 1.8 都有使用, docker 自己给我们挖了坑然后提提裤子就跑了,我们需要让用户无感知。而且用户可能会升级降级版本,不能发现镜像不在了。

先说一下共存,这一点相对容易,需要一个域名支持两套 registry,由于 v1 和 v2 的后接url是不一样的,所以可以很容易的通过 nginx 做一个转发。官方也给出了一个示例 https://github.com/docker/dist ... nx.md

但是读的时候一定要仔细,并根据具体情况进行调整,直接不看拿过来就是坑。

DockOne 技术分享:Docker Registry V1 to V2

我们搭好的一个网络拓扑大概是这样的,之所以把网络拓扑放出来是希望大家想一下,这种大文件传输的服务在网络拓扑上要考虑什么?最主要的就是超时,你需要考虑每条链路的超时设置。其次是 body size ,buffer size 之类的参数保证链路的通畅。

然后再讲一下 v1 v2 之间的同步,由于镜像是不兼容的,肯定要涉及到同步迁移。大思路上有两个方案,第一读懂两种镜像的格式,直接做文件级别的更新,把 v1 的文件翻译成 v2,这个华为的马道长在做大家有兴趣可以找他。另一种是利用 docker 1.6 之后的版本可以和 两个 registry 进行通信,我们从一个 registry pull 再 push 到另一个 registry 才用 docker engine 的 runtime 来解决,我们当时合计了一下觉得第二个省劲就用了第二个方案。

之后我们发现官方也给给出了个迁移工具,大家也可以看一下 https://github.com/docker/migrator 思路基本也是一样的,但问题这个工具有很多缺陷。首先他是一个单一的 shell 脚本,只能做离线同步,我们想要的是实时,因为我们每天量还是很大,离线很可能不收敛,而且用户体验也不好.其次它只能做v1 到 v2 的单向同步,扩展性,和性能也不好,并且也没有相关的统计监控功能,只是个玩具产品。

接下来看一下我们做的:

DockOne 技术分享:Docker Registry V1 to V2

思路还是用 1.6 之后的特性。在 两个registry 上分别加 hook 实时获取tag 更新信息发送到消息队列,消息队列再把消息发送到分布式的 worker 集群上进行一个同步,这个过程中每一步都落数据库,方便我们之后的监控和错误恢复。

把上面的数据流反过来再画一遍,就是我们线上的工作流了。这里有许多细节的问题大家可以之后想一下。首先是如果我做双向同步,那么我的同步也是一个 push 事件,这样会再触发一个同步这样一直循环下去该怎么办?还有就是一个埋的比较深的问题,也是 latest 最容易引起,两次间隔很近的 latest push,很有可能后一个 latest 同步先完成,第一个同步 后完成,这样就会同步的结果就是一个旧的版本,如何避免这种情况?其实就是一个事务的问题,如何确定那些操作可以并行,那些必须串行,这两个问题大家可以想一想。

5. 同步海外镜像实践

我们有了这套同步工具其实可以干很多别的事情。理论上任意一个 registry 只要我能爬到它的更新就可以同步过来。我们同步官方 library和一些其他库现在也是基于这套工具,但是这里会碰到一个更头疼的问题,就是网络。墙的问题大家都是中国人。而且我们这种同步都是最新的镜像,mirror也帮不了什么忙,用 vpn 的话这种走流量的很快就会被封ip。最后我们发现七牛有个海外上传加速可以在海外很快的上传文件到国内,我们就写了一个 七牛的 driver 来进行这种同步。架构图就变成这个样子了,相当于部署了两套同步节点,一套在海外负责dockerhub同步到qiniu,一套在国内负责qiniu同步到我们自己。

DockOne 技术分享:Docker Registry V1 to V2

这种方案解决了很多问题,但是问题依然很多。首先是只有上传文件走加速节点,所有的控制流比如 mv rename 等对象存储操作还是走国内,这样这一段依然高延时容易被墙,失败频率依然比较高。另一反面我们也好几次碰到了七牛服务不稳定的情况。所以我们虽说是按照一个实时同步进行设计的,现在的结果是大部分情况是分钟级别同步,故障时可能会到天级别,不过我们认为大部分情况下还 ok。如果当初设计就是天同步或者周同步很可能最后就同步不完了,所以给大家的启发就是不妨把目标设置高点,反正目标再低也是完不成的。

额外想说的就是,尽管这套东西我做出来了,但我觉得这个东西是不该存在的。不知道大家有没有听过有个博士的论文写得是如何在奶粉中检测三氯氰胺,不能说这件事情没意义,但是这件事情有意义很可悲,我觉得我现在做的也是类似的事情。我们都把太多时间和精力花在了毫无意义的和网络作斗争上,我希望有一天可以把这套海外同步的机制干掉,或者只是做个简单的国内镜像站而已,而不是大费周折的可以画一个很花哨的图来讲。

Q&A

Q: 想问下, 那你们的layer数据是不是要存两份?v1v2各一份?

A:是要分开存两份的,因为他们的格式其实都是不一样的一个是 tar 包一个是 gzip包,但内容一样

Q: 为啥tar变为gzip会耗费CPU和网络 不就是不同的压缩格式么?

A:网络其实是节省的,但是压缩是很耗 cpu 的 tar 其实并不太消耗

Q:v1如果做些优化,一次获取ancestry,然后并行下载layer,是不是也可以提高吞吐量么?

A: 理论上是这样的,我看 1.8 的代码在 pull v1 也一次会拿到所有的 image id 但是并没有去并行下载,估计 docker

自己把这块放弃了吧

Q:请问,您提到的利用registry的hook,来获取image更新的信息,指的是利用registry的notification API?

A: v2 是这样,v1 是自己在 registry 那里做了个 hook

Q:请问关于镜像删除的问题,v2的删除感觉坑很多,如何删除?还有,如果同一个镜像名称及版本但是内容并不同的镜像重复push,有没有办法检测,以及同步?

A: 我们用的aws对象存储,存储还比较便宜,所以没太关注,github 上有一个 v2 gc 的项目可以删除无用镜像,官方叫着做停机 gc

叫了好久了,目前还没实现,只能自己造轮子了;重复 push 和刚才提到的乱序类似,我们会保证这种情况是串行的。

Q:v2这么不成熟,眼下上还是不上…,push 到v2 registry的image能不能查询?

A: 我们当初的想法是照着 docker 这么任性的态度,没准 1.8 就不支持v1 了所以就赶紧调研用上了。查询没有直接的 api

,我们很多他没有的 api 都是自己造轮子造出来的

Q:v2.1后,registry提供一个叫catalog API,具有一定image搜索的功能,但还不够完美

A:catalog 会遍历整个存储消耗还是蛮大的,可以通过 catalog 做离线,然后 notify 做实时更新来实现 search

的一个索引

Q:请问,灵雀云的registry backend storage是什么类型?文件系统么?理由是什么?

A:直接 aws 在中国的 s3 ,目前官方支持的最好的,不用自己造轮子,就酱紫

Q,针对V2的auth方式,有没有什么好的建议。对于平台类的开发

A: 我的建议是使用 token auth 的方式,虽然复杂一步到位,可以做一些复杂的权限认证。类似的项目还是

https://github.com/SUSE/Portus ,不过建议每次 docker 版本更新都跟着测一遍

Q:有没有类似docker_auth的项目

A: https://github.com/SUSE/Portus 一个开源的 auth server,但是比较坑的是 docker

engine 老变,一升级可能就不一样,我们自己的auth server 也改了好几次

Q:由v1升级到v2,为什么非得把旧仓库上的镜像迁移到新的v2这么折腾,直接两个版本并存一段时间不行吗?新上传用新的v2的url,如果要回退旧版本旧库上镜像url也还有吧,一段时间后旧库就能退役了?

A: 因为我们有用户的 push 而用户很多还在用旧版本,也有用户发现新版本不合适回滚的,如果只顾一头用户一变就发现镜像没了

Q:alauda云 push的时候443端口拒绝连接怎么办?

A: 这个应该不会吧……,可以先下再联系复现一下,我们的两个版本registry 都是 走 https 的

Q:V2好像仍然没有解决registry最大的痛:单点。你们怎么对待这个问题的?

A:registry 一直都是可以水平扩展的,只是一个 http 的服务器是无状态的不存在单点问题

Q:企业私有云场景下用多个registry 实现HA 该如何选择后端存储?京东的speedy是否合适?

A:registry 有 swift

的driver私有云可以考虑,或者根据已有的情况选择自己的存储自己写个driver也是可以的,写的难度其实不大,七牛哪个一个下午就能写出来。要求不高的话还是不难的。京东的不是太了解,我觉得主要看现有的技术框架和产品选一个易上手的就行。ha

把 registry 水平扩展 挂载 lb 后面就好了

Q:V1已经被官方deprecated,v2仍然缺少一些基本的管理API,请问现在私有registry升级到v2是否还为时过早?

A: 看需求了吧,我觉得要是稳定考虑 deprecated 也没啥影响,v2 的很多好处确实在私有云表现不出来,反而会有一些表现不如 v1

的地方

Q:用七牛海外加速之前用哪种方案的?

A: 我先答一下这个吧,这个挺有意思的,我们发现一个 tcp

的拥塞算法是用于卫星通信的,卫星这种高延迟高丢包的拥塞算法貌似还蛮合适国外往国内传数据

正文到此结束
Loading...