乐趣区

关于cncf:克服清理容器镜像的挑战

客座文章作者:Alexey Igrychevm,Flant 的软件工程师。最后在 Flant 博客发表。

解决交付到 Kubernetes 的古代云原生应用程序 CI/CD 流水线时,在容器注册表(container registry)驻留大量镜像会成为一个显著的问题。如果你不想让注册表超载(并为无用的占用空间买单),那么你须要理解哪些镜像将不再应用。

找到它们的规范是什么?为什么注册表基本不能意识到它们?这里是咱们了解和以一个广泛的形式解决这个问题的旅程。尽管咱们的解决方案的实际局部是在一个特定的开放源码工具(werf)中展现,然而同样的办法也能够利用到你本人的案例和工具中。

介绍

随着工夫的推移,容器注册表中的镜像数量会大幅增长,这会占用越来越多的空间,并让你付出微小的代价。为了标准、限度或维持注册表空间可承受的增长速度,你最有可能能够:

  • 对镜像应用固定数量的标记(tag);
  • 以某种形式清理镜像。

第一种办法只实用于小团队。如果永恒标记(如 latest、main、test、boris 等)满足开发人员的需要,注册核心的规模就不会增长,你能够在很长一段时间内遗记清理它。这是因为所有过期的镜像最终都将被重写,并且你不用手动清理任何内容(惯例的垃圾收集器会解决任何残留物)。

然而,这种办法重大限度了开发过程,很少在古代应用程序的 CI/CD 中应用。自动化曾经成为开发过程中不可分割的一部分。它容许你更快地测试、部署和交付令人兴奋的新个性给用户。例如,在每次提交之后,CI 流水线会在所有我的项目中主动创立。作为 CI 流水线的一部分,咱们构建和测试镜像,将其部署到各种 Kubernetes 环境(层)中进行调试和其余测试。如果一切正常,那么这些更改将达到最终用户。这不再是一个火箭迷信 – 毕竟,如果你正在浏览这篇文章,你可能也会这样做。

因为调试和实现新性能是同时进行的,而且每天可能有多个版本,因而开发过程波及大量提交,这会导致注册表中呈现大量镜像。因而,咱们须要找到一种办法来革除注册表中未应用的(不再相干的)镜像。

然而咱们如何晓得镜像是否相干呢?

镜像相关性的规范

在大多数状况下,相关性的规范如下:

  1. 咱们须要的第一种(最显著也是最重要的)镜像类型是目前在 Kubernetes 中应用的那些。删除这些镜像可能会导致生产停机(例如,可能须要应用这些镜像来创立新的正本),或者在某些环境中打消团队的所有调试工作。(这就是为什么咱们创立了这个 Prometheus exporter 来监控 Kubernetes 集群中失落的镜像)。
  2. 第二种类型(不太显著,但依然很要害且与操作相干)是在以后版本呈现重大问题时执行回滚所需的镜像。例如,在 Helm 的状况下,这些是在已保留的版本中应用的镜像。(作为一个边注,Helm 默认放弃 256 个版本;这是一个很大的数字,而且你很可能不须要所有这些版本)。毕竟,咱们在开发过程中保留了所有这些不同的版本,因为咱们心愿应用它们进行回滚。
  3. 另一种类型是对于开发人员的需要:咱们须要所有以某种形式与他们正在进行的流动相干的镜像。例如,如果咱们思考 PR,那么放弃链接到上一个提交的镜像是有意义的。通过这种形式,开发人员能够疾速回滚更改并对其进行剖析。
  4. 最初一种类型包含匹配应用程序特定版本的镜像,即示意最终产品:v1.0.0、20.04.01、sierra 等等。

注:以上规范是依据咱们与来自不同公司的数十个开发团队单干的教训设计进去的。然而,这些规范可能会因开发过程和应用的基础设施的个性而有所不同(例如,没有应用 Kubernetes)。

现有的注册核心以及它们如何满足这些规范

风行的容器注册解决方案都有用于清理镜像的内置策略。它们容许你设置从注册表中删除标记的条件。然而,这些规定通常仅限于指定名称、创立工夫和标记的数量 *。

* 取决于容器注册表的具体实现。咱们剖析了以下解决方案:Azure CR、Docker Hub、ECR、GCR、GitHub package、GitLab Container Registry、Harbor Registry、JFrog Artifactory、Quay.io(2020 年 9 月)。

这组参数足以抉择合乎第四个条件的镜像,即链接到应用程序特定版本的镜像。然而,你必须做出斗争,以某种形式满足剩下的三个规范 – 以更严格或更宽松的形式,这取决于你的冀望和经济能力。

例如,你能够批改开发团队中的工作流,以匹配第三种与开发人员相干的镜像类型:引入定制的镜像命名,保护特定的容许列表,或者安顿团队成员之间的外部协定。但最终你必须让它自动化。如果可用的解决方案不够灵便,你将不得不本人设计!

其余两个条件(#1 和 #2)也会呈现相似的状况:如果不从部署应用程序的内部零碎(在咱们的例子中是 Kubernetes)获取数据,就无奈满足这些条件。

Git 工作流程阐明

设想一下,这是你残缺的 Git 工作流程:


在一个通用的开发工作流中提交、分支和公布

在下面的图片中,咱们应用了一个男人的头部图标来标记目前 Kubernetes 中为任何类型的用户(最终用户、测试人员、管理人员等)部署的所有镜像,或者开发人员正在应用的所有镜像(用于调试和其余起因)。

如果你的清理策略容许你仅通过特定标记的名称来保留镜像,将会产生什么?


应用特定的标记保留镜像

这相对不是咱们想要的。

然而,如果你的策略容许你在特定的工夫框架 / 最初 N 次提交前保留镜像,会产生什么呢?


应用特定的工夫框架保留镜像

这个看起来好多了。然而,它远非完满!例如,依然有一些开发人员须要其余可用的镜像(甚至部署到 K8s)来进行调试。

总结一下以后的市场状况:在清理镜像时,现有的容器注册解决方案没有提供足够的灵活性。造成这种状况的次要起因是他们无奈与内部世界进行交换。因而,须要这种灵活性的团队被迫“从内部”实现镜像删除,应用 Docker Registry API(或特定注册表实现的 API)的变通方法。

这就是为什么咱们试图找到一个通用的解决方案,能够为所有团队和所有类型的容器注册自动化镜像清理过程……

咱们的通用镜像清理办法

但咱们到底为什么须要它呢?问题是,咱们并不是一个特定的开发团队,而是一个业务团队,反对各种类型的团队,帮忙他们全面而理论地解决 CI/CD 问题。而 werf 开源工具是这一过程的次要驱动因素。werf 的显著个性是它监督 CD 过程贯通所有阶段:从构建到部署。

将镜像推送到注册表 *(在它们被构建之后)是这样一个工具的显著性能之一。因为所有的镜像都必须被存储,因而天然须要以某种形式清理它们。这就是为什么咱们提出的解决这个问题的办法合乎下面列出的所有四个规范。

* 注册表用户面临同样的问题,即便注册表自身能够是不同的品种(Docker Registry、GitLab Container Registry,Harbor 等)。在咱们的例子中,通用解决方案没有绑定到特定的注册表实现,因为它运行在注册表之外,并且它的行为在所有实现中是雷同的。

尽管咱们在示例中应用了 werf,但咱们心愿其余有相似艰难的团队会发现咱们的办法是有用的,并能提供信息。

因而,咱们转向清理机制的内部实现,而不是构建在容器注册表中的实现。咱们的第一步是应用 Docker Registry API 依据标记的数量和它们的创立日期(下面探讨过)从新实现雷同的根本策略。它们扩大为基于部署在 Kubernetes 中的镜像的非凡容许列表。为了实现它,咱们应用 Kubernetes API 循环了所有已部署的资源,并取得了一个关联镜像列表。当咱们有这个列表时,咱们永远不会从注册表中革除这些镜像。

这个相当琐碎的解决方案解决了最要害的问题(规范 1),但这仅仅是咱们改良清理机制之旅的开始。下一步 – 也是更乏味的一步 – 是咱们决定将公布的镜像链接到 Git 历史记录中。

标记计划

首先,咱们抉择了一种办法,在最终的镜像中蕴含荡涤所需的所有信息,并引入标记计划。公布镜像时,用户抉择首选的标记选项(git-branch、git-commit 或 git-tag)并应用相应的值。在 CI 零碎中,这些值是依据环境变量主动调配的。基本上,最终的镜像链接到一个特定的 Git 对象(branch/commit/tag),并将清理所需的数据存储在标记中。

这种办法产生了一组策略,容许咱们应用 Git 作为假相的繁多起源:

  • 当删除 Git branch/tag 时,注册表中相干的镜像会主动删除。
  • 咱们能够通过更改标记计划中的标记数量,和设置自创立关联提交以来的最大天数,来管制链接到 Git 标记 / 提交的镜像数量。

总的来说,这个实现合乎咱们的须要,但很快咱们就面临了一个新的挑战。应用基于 Git 的标记计划给咱们带来了几个毛病 – 足以重新考虑这种办法。(对这些缺点的详细描述超出了本文的范畴;你能够在这里理解更多信息)。因而,转向一种更无效的标记策略,基于内容的标记,导致咱们扭转了清理容器镜像的形式。

新算法

技术上的起因是什么?当应用基于内容的标记策略时,每个标记都能够在 Git 中链接到屡次提交。因而,当你在清理过程中抉择镜像时,你不能再独自依赖提交。

在咱们新的清理算法中,咱们决定齐全放弃标记计划,而将整个过程建设在元镜像根底上。每个 meta-image 蕴含:

  • 公布镜像里的提交(也就是说,镜像是否在容器注册表中增加、更改或放弃不变并不重要);
  • 对应于所构建的镜像的外部标识符。

换句话说,咱们将公布的标记链接到 Git 中的提交。

最终的配置和个别算法

在配置清理时,用户当初能够应用各种策略来抉择相干镜像。每个策略定义如下:

  • 一组用于查找所需镜像的参考,即 Git 标记或 Git 分支;
  • 对每个参考集相干镜像为的限度。

上面是默认策略配置的样子:

cleanup:
 keepPolicies:
 - references:
 tag: /.*/
 limit:
 last: 10
 - references:
 branch: /.*/
 limit:
 last: 10
 in: 168h
 operator: And
 imagesPerReference:
 last: 2
 in: 168h
 operator: And
 - references: 
 branch: /^(main|staging|production)$/
 imagesPerReference:
 last: 10

下面的配置定义了三个策略,它们对应以下规定:

  • 保留 10 个最新的 Git 标记的镜像(依据创立日期)。
  • 放弃上周公布的镜像不超过两个,并且上周的分支不超过 10 个。
  • 为主分支、登台分支和生产分支保留 10 个镜像。

最终算法包含以下步骤:

  • 从容器注册表获取清单。
  • 不包含在 Kubernetes 中应用的镜像(咱们通过 Kubernetes API 取得已部署的资源)。
  • 扫描 Git 历史记录,并依据指定的策略排除镜像。
  • 删除其余的镜像。

回到咱们的演示,上面是咱们当初能够应用 werf 做的事件:


用 werf 保留所有你真正须要的容器镜像

即便你不应用 werf,你能够或多或少(取决于标记镜像的办法)应用与其余零碎 / 应用其余工具中的高级镜像清理相似的办法。只有记住潜在的问题,并尝试在你的堆栈中找到最好的办法来顺利地解决这些问题。

咱们心愿咱们的经验教训将帮忙你从新扫视你的具体案例,带来新的想法和见解。

总结

  • 迟早,大多数团队都会面临容器注册表变得十分大且无奈治理的问题。
  • 在寻找解决方案时,首先必须定义镜像相关性的规范。
  • 个别容器注册核心的性能受到根本清理算法的限度,这些算法没有思考到“内部世界”,比方 Kubernetes 中应用的镜像以及团队工作流的个性。
  • 灵便和高效的算法必须理解 CI/CD 过程,不应该限度本人的 Docker 镜像数据。

如果你对本文中探讨有任何想法或问题,请通过 Twitter 与咱们的开发人员分割。要理解更多对于 werf 作为 CI/CD 工具的信息,请拜访其网站。

点击浏览网站原文。


CNCF (Cloud Native Computing Foundation) 成立于 2015 年 12 月,隶属于 Linux  Foundation,是非营利性组织。
CNCF(云原生计算基金会)致力于培养和保护一个厂商中立的开源生态系统,来推广云原生技术。咱们通过将最前沿的模式民主化,让这些翻新为公众所用。扫描二维码关注 CNCF 微信公众号。

退出移动版