【编者的话】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 partnerships各种组织机构服务,评论issue管理,Search webhook等工具的集合。Hub和Registry的关系也是类似的,大家在 DockerHub上看到的界面和各种功能就是Hub的一部分,而且处于商业的考量Hub是不开源的。
2. V1 Python Registry
下面来说一下V1的Registry以及为啥Docker官方不给他饭吃了。V1的项目地址在 https://github.com/docker/docker-registry 已经小半年没有更新了,很早就是废弃态。但说实话还是比较稳定的,API设计也比健全,使用和扩展也还算方便。至少在我使用的过程中没有碰到太多的坑,反而是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实现的与Golang的理念不符,尽管Golang写的也不咋地。
另一个比较重要的原因是tag的问题。Docker很多方面都学Git 也学得很成功,偏偏版本控制的东西学残了,最明显的就是tag。tag 的问题在于Docker images的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的热度完全不是一个量级,很多基本的功能都很缺失。
他的存储目录树十分复杂,我就简单列一下,可以看到最顶层还是两个,只不过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,但是读的时候一定要仔细,并根据具体情况进行调整,直接不看拿过来就是坑。
我们搭好的一个网络拓扑大概是这样的,之所以把网络拓扑放出来是希望大家想一下,这种大文件传输的服务在网络拓扑上要考虑什么?最主要的就是超时,你需要考虑每条链路的超时设置。其次是body size ,buffer size之类的参数保证链路的通畅。
然后再讲一下V1和V2之间的同步,由于镜像是不兼容的,肯定要涉及到同步迁移。大思路上有两个方案,第一读懂两种镜像的格式,直接做文件级别的更新,把V1的文件翻译成V2,这个华为的马道长在做大家有兴趣可以找他。另一种是利用Docker 1.6之后的版本可以和两个Registry进行通信,我们从一个registry pull再push到另一个Registry才用Docker Engin 的Runtime来解决,我们当时合计了一下觉得第二个省劲就用了第二个方案。
之后我们发现官方也给给出了个迁移工具,大家也可以看一下: https://github.com/docker/migrator,思路基本也是一样的,但问题这个工具有很多缺陷。首先他是一个单一的shell脚本,只能做离线同步,我们想要的是实时,因为我们每天量还是很大,离线很可能不收敛,而且用户体验也不好.其次它只能做V1到V2的单向同步,扩展性,和性能也不好,并且也没有相关的统计监控功能,只是个玩具产品。
接下来看一下我们做的:
思路还是用1.6之后的特性。在两个Registry上分别加hook实时获取tag更新信息发送到消息队列,消息队列再把消息发送到分布式的worker集群上进行一个同步,这个过程中每一步都落数据库,方便我们之后的监控和错误恢复。
把上面的数据流反过来再画一遍,就是我们线上的工作流了。这里有许多细节的问题大家可以之后想一下。首先是如果我做双向同步,那么我的同步也是一个push事件,这样会再触发一个同步这样一直循环下去该怎么办?还有就是一个埋的比较深的问题,也是latest最容易引起,两次间隔很近的latest push,很有可能后一个latest同步先完成,第一个同步后完成,这样就会同步的结果就是一个旧的版本,如何避免这种情况?其实就是一个事务的问题,如何确定那些操作可以并行,那些必须串行,这两个问题大家可以想一想。
5. 同步海外镜像实践
我们有了这套同步工具其实可以干很多别的事情。理论上任意一个Registry只要我能爬到它的更新就可以同步过来。我们同步官方library和一些其他库现在也是基于这套工具,但是这里会碰到一个更头疼的问题,就是网络。墙的问题大家都是中国人。而且我们这种同步都是最新的镜像,mirror也帮不了什么忙,用VPN的话这种走流量的很快就会被封IP。最后我们发现七牛有个海外上传加速可以在海外很快的上传文件到国内,我们就写了一个七牛的driver来进行这种同步。架构图就变成这个样子了,相当于部署了两套同步节点,一套在海外负责Dockerhub同步到qiniu,一套在国内负责qiniu同步到我们自己。这种方案解决了很多问题,但是问题依然很多。首先是只有上传文件走加速节点,所有的控制流比如mv rename等对象存储操作还是走国内,这样这一段依然高延时容易被墙,失败频率依然比较高。另一反面我们也好几次碰到了七牛服务不稳定的情况。所以我们虽说是按照一个实时同步进行设计的,现在的结果是大部分情况是分钟级别同步,故障时可能会到天级别,不过我们认为大部分情况下还OK。如果当初设计就是天同步或者周同步很可能最后就同步不完了,所以给大家的启发就是不妨把目标设置高点,反正目标再低也是完不成的。
额外想说的就是,尽管这套东西我做出来了,但我觉得这个东西是不该存在的。不知道大家有没有听过有个博士的论文写得是如何在奶粉中检测三氯氰胺,不能说这件事情没意义,但是这件事情有意义很可悲,我觉得我现在做的也是类似的事情。我们都把太多时间和精力花在了毫无意义的和网络作斗争上,我希望有一天可以把这套海外同步的机制干掉,或者只是做个简单的国内镜像站而已,而不是大费周折的可以画一个很花哨的图来讲。
Q&A
Q:想问下,那你们的layer数据是不是要存两份,V1、V2各一份?Q:为啥tar变为gzip会耗费CPU和网络,不就是不同的压缩格式么?
A:是要分开存两份的,因为他们的格式其实都是不一样的一个是tar包一个是gzip包,但内容一样。
Q:V1如果做些优化,一次获取Ancestry,然后并行下载layer,是不是也可以提高吞吐量么?
A:网络其实是节省的,但是压缩是很耗CPU的tar其实并不太消耗。
Q:请问,您提到的利用Registry的hook,来获取image更新的信息,指的是利用Registry的notification API?
A:理论上是这样的,我看1.8的代码在pull v1也一次会拿到所有的Image ID但是并没有去并行下载,估计Docker自己把这块放弃了吧。
Q:请问关于镜像删除的问题,V2的删除感觉坑很多,如何删除,还有,如果同一个镜像名称及版本但是内容并不同的镜像重复push,有没有办法检测,以及同步?
A:V2是这样,V1是自己在Registry那里做了个hook。
Q:V2这么不成熟,眼下上还是不上,push到V2 registry的image能不能查询?
A:我们用的AWS对象存储,存储还比较便宜,所以没太关注,GitHub上有一个v2 gc的项目可以删除无用镜像,官方叫着做停机gc,叫了好久了,目前还没实现,只能自己造轮子了;重复push和刚才提到的乱序类似,我们会保证这种情况是串行的。
Q:V2.1后,Registry提供一个叫catalog API,具有一定image搜索的功能,但还不够完美?
A:我们当初的想法是照着Docker这么任性的态度,没准1.8就不支持V1了,所以就赶紧调研用上了。查询没有直接的API,我们很多tar没有的API都是自己造轮子造出来的。
Q:请问,灵雀云的registry backend storage是什么类型,文件系统么,理由是什么?
A:catalog会遍历整个存储消耗还是蛮大的,可以通过catalog做离线,然后notify做实时更新来实现search的一个索引。
Q:针对V2的auth方式,有没有什么好的建议,对于平台类的开发?
A:直接AWS在中国的s3,目前官方支持的最好的,不用自己造轮子,就酱紫。
Q:有没有类似docker_auth的项目?
A:我的建议是使用token auth的方式,虽然复杂一步到位,可以做一些复杂的权限认证。类似的项目还是:https://github.com/SUSE/Portus ,不过建议每次Docker版本更新都跟着测一遍。
Q:由V1升级到V2,为什么非得把旧仓库上的镜像迁移到新的V2这么折腾,直接两个版本并存一段时间不行吗,新上传用新的V2的url,如果要回退旧版本旧库上镜像url也还有吧,一段时间后旧库就能退役了?
A:https://github.com/SUSE/Portus 一个开源的 auth server,但是比较坑的是Docker Engine老变,一升级可能就不一样,我们自己的auth server也改了好几次。
Q:alauda云push的时候443端口拒绝连接怎么办?
A:因为我们有用户的push而用户很多还在用旧版本,也有用户发现新版本不合适回滚的,如果只顾一头用户一变就发现镜像没了。
Q:V2好像仍然没有解决Registry最大的痛:单点,你们怎么对待这个问题的?
A:这个应该不会吧……,可以先下再联系复现一下,我们的两个版本Registry都是走HTTPS的。
Q:企业私有云场景下用多个Registry实现HA该如何选择后端存储,京东的Speedy是否合适?
A:Registry一直都是可以水平扩展的,只是一个HTTP的服务器是无状态的不存在单点问题。
Q:V1已经被官方deprecated,V2仍然缺少一些基本的管理API,请问现在私有Registry升级到V2是否还为时过早?
A:Registry有Swift的driver私有云可以考虑,或者根据已有的情况选择自己的存储自己写个driver也是可以的,写的难度其实不大,七牛那个一个下午就能写出来。要求不高的话还是不难的。京东的不是太了解,我觉得主要看现有的技术框架和产品选一个易上手的就行。把Registry水平扩展挂载lb后面就好了。
Q:用七牛海外加速之前用哪种方案的?
A:看需求了吧,我觉得要是稳定考虑deprecated也没啥影响,V2的很多好处确实在私有云表现不出来,反而会有一些表现不如V1的地方。
===========================
A:我先答一下这个吧,这个挺有意思的,我们发现一个tcp的拥塞算法是用于卫星通信的,卫星这种高延迟高丢包的拥塞算法貌似还蛮合适国外往国内传数据。
以上内容根据2015年10月16日晚微信群分享内容整理。分享人刘梦馨,灵雀云软件工程师,前阿里系统保障部系统工程师,容器技术爱好者,个人博客:oilbeater.com。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesx,进群参与,您有想听的话题可以给我们留言。
有疑问加站长微信联系(非本文作者)