题图摄于温哥华港
注:微信公众号不按照时间排序,请关注“亨利笔记”,并加星标置顶,以免错过更新。
本文介绍 Harbor 2.1 的非阻塞镜像垃圾回收功能,可以一边进行正常的镜像管理任务,一边默默地执行垃圾回收任务,如同飞机的空中加油,无需中断飞行。部分内容节选自最新出版的《Harbor权威指南》(详情参见文末),相关作者为 Harbor 开源项目维护者王岩,欢迎扫码或点击“阅读原文”购买。
垃圾回收(Garbage Collection)是计算机系统常见的一种资源管理方式,由John McCarthy 在 1959 年发明并在 Lisp 语言中应用。对,就是那位大名鼎鼎的、提出了人工智能概念并被誉为“人工智能之父”的 John McCarthy。
顾名思义,垃圾回收是指把系统中不再使用的资源(即垃圾)释放并且使其可被重新使用。程序员在 Java、Python、Go 等现代编程语言中可能都了解过垃圾回收的概念,即释放程序中不再使用的数据所占用的内存,从而腾出空间存放其他数据。垃圾回收由语言的运行时(runtime)在后台来处理,对程序员一般是透明的,程序员写代码的时候无需(或者无法)干预垃圾回收的过程。这样的设计避免了程序员去关注复杂的内存管理机制。
当然,垃圾回收也有个很大的不足之处:一般需要有阻塞的时间,就是不定期地暂停前台程序的执行,来处理后台的垃圾回收工作。取决于需要回收的垃圾数量,暂停程序的执行可能会持续比较长的时间,因此,对响应时间敏感的应用这个矛盾尤为突出。为此,人们设计了多种垃圾回收的执行方式,如并发回收、分代回收等,目的是减少程序执行“骤停”的时间或频率。
Harbor 作为镜像等云原生制品的仓库,为了释放不再使用的镜像所占的存储空间,也需要定期进行垃圾回收工作。 在 Harbor 2.0 及之前的版本中,垃圾回收一直是阻塞式的。也就是说,在 Harbor 系统执行垃圾回收任务时,系统处于只读状态,只能拉取而不能推送镜像。在部分用户的生产环境下,阻塞式的垃圾回收是不能被接受的,这会造成系统从几分钟到几十小时的阻塞状态。虽然建议用户定制周期垃圾回收任务在非工作日的夜间执行,但是并不能从根本上解决问题。
造成垃圾回收任务阻塞和执行时间较长的主要原因有如下两个。
在阻塞式的垃圾回收任务中使用的是Docker Distribution(下简称Distribution)自带的垃圾回收功能,实现流程大致如下。
(1)遍历文件系统,得到每一个共享层文件的引用数量。当一个层文件的引用数量为0时,即为待删除层文件。
(2)在得到所有待删除的层文件后,调用存储系统的删除接口,依次删除层文件。
在计算层文件引用计数的过程中,如果此时用户正在上传镜像,则垃圾回收可能会删除正在上传的层文件,从而破坏镜像。因此,在垃圾回收任务执行时需要阻塞镜像的推送。
同时,因为 Distribution 并没有使用数据库记录层文件的引用关系,所以需要遍历整个存储系统的路径来获取每一个层文件的引用计数。这种遍历方式造成了很大的时间开销,并且所需时间随着层文件数量的增加而线性增加。
在层文件引用关系的遍历和层文件的删除过程中,需要调用存储系统的接口来实现。如果用户使用云存储(如S3)作为存储系统,则存储系统接口调用的时间开销会比本地存储增加很多。
基于以上情况,Harbor 2.1 实现了非阻塞式的垃圾回收功能。该功能的目的是去除垃圾回收任务执行时的系统阻塞,同时提高垃圾回收任务的运行效率,使得 Harbor 可以一边进行正常的镜像管理任务,一边默默地执行垃圾回收任务,如同飞机的空中加油,无需中断飞行。本文将简要介绍非阻塞式的垃圾回收方案的基本思想。
在 Harbor 2.0 中,在用户成功推送一个镜像后,Harbor系统会完整记录这个镜像的信息,如下图所示。
从上图可以看出,一个镜像的层文件和其引用关系都被记录在 Artifact 数据库中。同时,在一个镜像被删除后,其层文件的引用关系也被删除。这样一来非阻塞式垃圾回收任务可以通过数据库计算出存储系统中所有层文件的引用计数。当任何一个层文件的引用计数为都0时,该层文件即待删除层文件。相比存储系统的遍历,数据库的计算可以节省大量时间开销。
通过数据库得到待删除层文件后,下一步就是将其删除。Distribution 并没有提供删除层文件和清单(manifest)文件的 API,而是暴露公有函数供其自身的垃圾回收任务使用。在非阻塞垃圾回收任务实现中,需要引用 Distribution 的代码来实现层文件和清单文件的删除 API,而删除 API 仅供非阻塞垃圾回收任务使用,不暴露给用户,如下图所示。
非阻塞式垃圾回收的核心是在垃圾回收任务运行时,不阻塞用户的镜像等 Artifact的推送。为了达到此目的,这里引入了状态控制和时间窗口机制,下面以镜像为例加以说明。
1)状态控制
在层文件的数据库表中加入了版本和状态列,层文件的每一次状态改变都会增加版本,这样可以通过版本来实现乐观锁。当非阻塞垃圾回收任务执行删除时,会尝试将待删除的层文件标记为“deleting”状态。如果该待标记的层文件刚好被Docker 客户端正在推送的镜像引用,则非阻塞垃圾回收任务的“deleting”标记将会失败。原因是 Docker 客户端在推送过程中发起的 HEAD Blob 请求被 Harbor 中间件拦截,中间件会增加层文件的版本。而非阻塞垃圾回收任务在更新层文件状态为“deleting”时,层文件的版本已经不符合数据库里的最新版本信息,导致更新失败,如下图所示。
2)时间窗口
在推送 Docker 客户端的过程中,Docker 客户端首先会推送层文件,而此时的层文件在系统中的引用计数为0,只有当清单文件推送成功后,Harbor 才会建立引用关系,使得这些层文件的引用计数非0。为保证在非阻塞垃圾回收任务执行中,用户正在推送的层文件不被删除,需要引入时间窗口概念。在层文件的数据库表中加入更新时间列,非阻塞垃圾回收仅作用于更新时间早于非阻塞垃圾回收起始时间两小时的层文件。在时间窗口内推送的层文件都会被保留,如下图所示。
在 Harbor 2.1 推出了非阻塞垃圾回收之后,解决了镜像运维的一个痛点,得到许多用户的点赞,有用户发出了“期待已久”的感叹。正如上期文章所说,这又是运维工程师一个保护”发际线“的功能,快来试试吧,欢迎留言告诉我们你使用的情况如何。
要想了解云原生、区块链和人工智能等技术原理,请立即长按以下二维码,关注本公众号亨利笔记 ( henglibiji ),以免错过更新。