前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >构建远程缓存系统

构建远程缓存系统

作者头像
树华子
发布2018-01-19 09:38:37
1.8K0
发布2018-01-19 09:38:37

上个月,我们的工程团队发布了一个大的更新,关于在使用我们的Docker平台Jet时Docker镜像是如何被缓存和存储的。在本文中,我们将讨论更新的动机,特性的设计和实现,以及我们面临的一些棘手的工程问题。

什么是镜像缓存?

使用Docker构建服务通常非常快,因为它使用分层文件系统。在你的Dockerfile中,每条指令都是作为一个单独的层来执行和存储的。重建镜像时,只要层内容不变,Docker将只使用缓存层而不是重建它。

更好的是,层可以在多个镜像上共享。拥有高度优化的基本镜像和优化的Dockerfiles可以为您带来难以置信的性能优势。

在Codeship构建期间,使用缓存镜像的能力是至关重要的。在大多数情况下,Docker镜像基于很少更改的层 - 即FROM镜像,软件包安装,甚至可能是多少固定的应用程序目录的拷贝,如config。通常情况下,这些也是需要花费最长时间来建立的层,而且同样的工作做两次没有任何意义。

Codeship面临的一个独特的情况是,我们的构建机器是短暂的,这意味着每次运行构建时都会得到一个新机器。没有镜像缓存可以重新使用,因为这些镜像以前从未建立过,在构建过程中,只需要构建一次。

为了解决这个问题,我们必须依靠远程缓存源来存储镜像信息。除了远程,这个缓存存储也需要被限定到每个客户,因此客户A不能访问客户B的构建缓存,并且它也需要速度很快。

依靠缓存注册表

在很长一段时间里,直到Docker 1.10,Docker注册表都提供了远程缓存的最佳解决方案。使用客户提供的注册表,缓存的镜像可以以独一无二的标签来存储。

在每个构建开始时,Jet从注册表中取出缓存的镜像,然后使用docker build命令重建镜像。由于是分层文件系统,Docker只需要重建与刚取出的缓存镜像相比已经改变的层。这显着加快了镜像生成时间,因为大部分镜像可能会回到缓存中。

对于一名工程师来说,这个方法是理想的。它不仅是“Docker原生”的行为,而且还是对Docker API的额外调用。我们已经需要为注册表推送步骤解析加密的凭据,因此实现这个远程缓存系统的工程量很小。

这种方法曾一度完美,直到事情发生变化。缓存对我们的用户来说是一个非常重要且关键的功能。有人可能会争辩说,即使第三方是Docker本身,我们也不应该依赖第三方来实施它。

一切都被打破

今年早些时候,Docker发布了1.10版引擎,其中包括与镜像层命名和存储方式有关的重大更改。这个改变实现了内容可寻址存储

以前,层ID是依赖于构建上下文的随机UUID。在两台机器上构建相同的镜像会导致两组层ID; 这不是所有原因中最优的那一个。内容可寻址存储更新了命名约定,以便具有完全相同内容的层也具有相同的ID。这提供了对所有存,取,保存和加载步骤的数据完整性的深入了解。现在可以依靠镜像ID和摘要,而不是依靠Dockerfile指令来判断内容是否相同。

但是1.10也更新了图像从注册表分发的方式。

虽然执行一个docker pull my-image命令之前会返回双亲层链,包括所有双亲层的ID,但在1.10中您将只得到带有ID的单个层。这不会像1.10之前的那样再生本地镜像缓存。

如果你想亲自看看,请启动一个没有镜像的Docker主机,然后跟着这个例子。我们将构建这个Dockerfile作为一个叫做rheinwein/cache-test的镜像。

FROM busybox RUN echo hello RUN echo let’s RUN echo make RUN echo some RUN echo layers

$ docker build -t rheinwein/cache-test .

我们可以在本地查看层指示和ID docker history rheinwein/cache-test

IMAGE CREATED CREATED BY SIZE COMMENT

be5223baa801 40 seconds ago /bin/sh -c echo layers 0 B

5855fcd4e55d 40 seconds ago /bin/sh -c echo some 0 B

46d873a211d7 40 seconds ago /bin/sh -c echo make 0 B

cec59edb74ae 41 seconds ago /bin/sh -c echo let’s 0 B

8aaa0dcf27b2 41 seconds ago /bin/sh -c echo hello 0 B

2b8fd9751c4c 5 weeks ago /bin/sh -c #(nop) CMD ["sh"] 0 B

<missing> 5 weeks ago /bin/sh -c #(nop) ADD file:9ca60502d646bdd815 1.093 MB

如果再次重建镜像,在构建输出中会看到“使用缓存”的好消息。现在用docker rmi $(docker images -q)清除你的镜像,确保您的主机上没有镜像。

要查看行为的差异,请尝试从Docker Hub中下载镜像,然后查看层。

$ docker pull rheinwein/cache-test

$ docker history rheinwein/cache-test

IMAGE CREATED CREATED BY SIZE COMMENT

be5223baa801 3 minutes ago /bin/sh -c echo layers 0 B

<missing> 3 minutes ago /bin/sh -c echo some 0 B

<missing> 3 minutes ago /bin/sh -c echo make 0 B

<missing> 3 minutes ago /bin/sh -c echo let’s 0 B

<missing> 3 minutes ago /bin/sh -c echo let’s 0 B

<missing> 5 weeks ago /bin/sh -c #(nop) CMD ["sh"] 0 B

<missing> 5 weeks ago /bin/sh -c #(nop) ADD file:9ca60502d646bdd815 1.093 MB) ADD file:9ca60502d646bdd815 1.093 MB

优点:我知道这个镜像正是我所期望的,因为镜像ID与我们以前构建的相匹配。

缺点:尝试重建这个镜像。与之前不同,并没有可用的缓存。

鉴于这种新的行为,我们的远程缓存系统被彻底打破。因为构建性能对我们的客户非常重要,所以我们回滚并停留在Docker 1.9.2上以保留缓存系统。此时,除了使用持久化构建机器之外,显然没有其他办法重新构建镜像缓存,这将需要对构建系统的功能进行大规模和根本性的改变。

经过与Docker社区中其他人的热烈讨论,Docker Engine确实恢复了Docker 1.11中的一些双亲层链功能,但是只保存和加载事件。

所以我们开始工作了。

!新的号召

新的缓存系统

鉴于1.11中的更新,显然我们需要设计一个远程缓存系统来依赖保存和加载事件。那些保存的Docker镜像可以远程存储,并在构建开始之前下拉。本质上,流程与之前的实现完全相同,只不过存储组件从注册表切换到S3。

Caching-S3.png
Caching-S3.png

而不是使用注册表作为我们的远程存储位置,我们可以使用像S3这样的对象存储服务。

证书和安全

由于Codeship管理远程缓存S3 buckets,而我们控制凭据和访问。我们不仅负责在构建过程中发布对buckets的访问权限,还负责在静止时以合理安全的方式存储对象。在以前的缓存系统中,这些函数是用户的责任,因为用户需要定义哪个注册表用于缓存,并且他们完全控制了对象。

幸运的是,AWS的安全令牌服务(STS)为我们做了大部分工作。在构建过程中,我们生成一个临时的证书集,以便构建能够获取和放置对象。这些证书的范围是项目本身,而构建只能访问它所属项目的对象。

透明度

更新我们的缓存系统有很多好处,例如客户不再需要在Codeship构建期间为了缓存而设置私有注册表。然而从另一个角度看这个优点其实是一个很大的缺点:客户不能再看到缓存的对象之外的生成运行,因为它们存储在一个Codeship拥有的bucket。

为了让我们的用户更容易看到他们的缓存,每个导出命令都包含了导出镜像中包含的所有镜像层和标记的日志。由于具有相同内容的层具有相同的ID,因此可以更轻松地检查您在本地使用的镜像是否与构建基础架构期间Jet使用的镜像相匹配。

如果我们的用户由于任何原因需要使其缓存失效,我们还在我们的构建系统中添加了缓存冲洗器。这是一个新功能。以前,用户可以从他们自己的存储库中手动删除缓存的镜像。

缓存刷新按钮位于Jet项目中的“常规项目设置”下。它将删除项目的所有缓存镜像,并在后续的构建运行期间,您将能够看到正在填充新的缓存。在服务日志中查找“没有$ service的缓存镜像”,以知道缓存已被清除。

性能瓶颈和优化

自从上个月推出新的缓存系统以来,我们的Jet平台一直运行良好。不过,我们故意发布了此功能的MVP版本,然后再计划优化。有几个系统需要进行微调,但不幸的是,并不是所有这些都在我们的控制之内。

双亲元数据使用新的Docker保存/加载系统持久化的方式有一个很大的问题:整个双亲链需要在docker save命令执行期间保存。

简单地使用docker save cache-test说是不够的。您必须包含您希望保存在链中的双亲镜像的ID。如果保存的层不存在其父级,则不会保存元数据,并且该层位于链的末尾。这意味着我们也不能分别保存客户镜像(如cache-test)和基础镜像(像busybox),并上传/下载它们。在这种情况下,cache-test不知道它应该在寻找一个busybox层,因为这个元数据没有被保存。

这有时会导致巨大的镜像。我们可以假设最好的情况,因为S3 bucket和build机器都在同一个AZ中,但是并不理想。我们很想找到一种方法来分割缓存的镜像,以便我们可以并行上传和下载,避免缓存重复的基本镜像。

未来

我们构建新的缓存系统的主要动力是我们可以升级Docker 1.10版本,而不必放弃缓存。编译时间对于我们的用户来说是最重要的,因此为了构建新的缓存系统,我们停留在1.9.2版本上的时间比正常情况要长一些。

现在我们已经可以毫无问题地升级到1.10以上了,我们期待着不断改进我们的构建平台。升级到1.10允许我们支持Compose V2语法,相关工作正在进行中。

相关的参考资料:

Docker入门

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是镜像缓存?
  • 依靠缓存注册表
    • 一切都被打破
    • 新的缓存系统
    • 证书和安全
      • 透明度
        • 性能瓶颈和优化
        • 未来
        相关产品与服务
        容器镜像服务
        容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档