用于改进Docker容器管理和性能的指标

248 阅读9分钟

在运行云服务时,客户是第一个注意到问题的人,这绝不是好事。在几个月的时间里,这种情况发生在我们的客户身上,我们开始积累了一系列关于Docker作业启动时间不可预测的报告。起初,这些报告很少,但频率开始增加。在这些报告中,具有高度并行性的作业所占比例过大。

虽然我们对启动的哪些部分有怀疑,但我们想确定一下。首先,我们查看了报告中出现问题的具体工作。为了帮助解决这个问题,我们改进了旋转步骤中的日志,使其显示每个阶段所需的时间。日志显示,在问题案例中,下载、提取和创建阶段花费的时间最多。

这就是原因。当Docker作业启动时,我们会查看用户在其配置中指定的Docker镜像列表,并提取它们,以确保它们在主机虚拟机的镜像缓存中。拉取一个镜像是由每个镜像层的若干阶段组成的。

  • 在下载过程中,我们从Docker资源库中获取压缩的层
  • 在提取过程中,该层被解压
  • 然后我们在配置中为每个镜像创建一个容器。
  • 然后在主容器中运行工作步骤

这个过程要重复n次,其中n是作业的并行性。这些作业的每个实例都被称为 "任务"。使用这些定义,就可以清楚地知道为什么具有高并行度的作业可能会受到更多的影响。这项工作只是在 "减速骰子 "上掷了更多次。

使问题更加严重的是,我们自己不能迅速地轻易识别问题。我们决心解决客户的报告,但我们也想确保我们自己在未来更容易发现问题。在我们创建解决方案时,这两个想法都得到了考虑。

我们的方法

最初,我们需要回答以下关于下载、提取和创建阶段的问题。

  • 典型的性能是什么样的?
  • 什么是 "慢 "的意思?
  • 事情 "慢 "的频率如何?

衡量性能

我们开始研究如何为下载、提取和创建这三个阶段中的每一个产生指标。追踪下载层和提取层的性能被证明是有点困难的。

  • Docker的pull API并不直接提供关于层下载或提取所需时间的详细汇总信息。
  • 拉动API只提供了一个JSON消息流,旨在产生进度条。如果你曾经在命令行上运行过docker pull,你可能对这个消息流很熟悉。

经过对消息流的反复思考,我们发现,通过汇总各层和下载/提取阶段的时间和数据传输,我们可以产生相当准确的性能指标。

幸运的是,测量容器创建的性能就像添加一个定时器指标一样简单。

除了将这些数据送入我们的指标处理系统外,我们还将其纳入旋转步骤的日志。

...
Starting container cimg/go:1.16
  image cache not found on this host, downloading cimg/go:1.16
1.16: Pulling from cimg/go
...
Digest: sha256:6621f92d57703a89f82d20660e8947aa00eb07baba90be16e64666a3c726077c73MB/1.873MBB
Status: Downloaded newer image for cimg/go:1.16
  pull stats: download 124.3MiB in 1.283s (96.84MiB/s), extract 124.3MiB in 2.806s (44.28MiB/s)
  time to create container: 3.201s
  using image cimg/go@sha256:6621f92d57703a89f82d20660e8947aa00eb07baba90be16e64666a3c726077c
Starting container circleci/postgres:12.4-ram
  image cache not found on this host, downloading circleci/postgres:12.4-ram
12.4-ram: Pulling from circleci/postgres
...
Digest: sha256:368f896494f6cc56cbd1993b1f789548e78a1348e88e94a79aeca7c6ca8f8ac328kB/8.228kBB
Status: Downloaded newer image for circleci/postgres:12.4-ram
  pull stats: download 108.1MiB in 1.099s (98.33MiB/s), extract 108MiB in 3.853s (28.03MiB/s)
  time to create container: 2.973s</b>
  using image circleci/postgres@sha256:368f896494f6cc56cbd1993b1f789548e78a1348e88e94a79aeca7c6ca8f8ac3
Time to upload agent and config: 1.256190772s
Time to start containers: 1.347100754s

决定什么是 "慢拉"?

CircleCI并不控制下载速度;它依赖于外部托管的Docker仓库的性能。在可能的情况下,我们确保我们有快速连接到流行的存储库,包括Docker Hub和亚马逊的ECR。谷歌似乎在GCR的速度方面做得很好,尽管我们的连接取决于公共互联网。根据我们的衡量标准得出的数据,我们把20 MiB/s作为一个好指标。

提取量在我们的控制之下,完全取决于运行Docker的虚拟机的可用资源。我们发现10 MiB/s是一个很好的指标,表明提取性能已经下降到足以对用户造成明显的影响。由于小数值的四舍五入问题,我们发现我们不得不排除那些花了不到5秒就完成的提取。

对于创建,我们决定(有点武断),容器的创建时间不应超过10秒。我知道我不希望等待的时间超过这个时间。

那么,问题有多严重呢?

不幸的是,它比我们预期的要严重得多。这是我们2020年11月的仪表板显示的情况。

  • 3.5%的下载速度很慢。
  • 6.7%的提取速度慢。
  • 12%的创建速度很慢--哎呀!

这些数字(尤其是创建)是令人震惊的!然而,团队有一些想法来解决这个问题。然而,团队有一些想法,认为原因可能是什么,以及如何改善这些数字。

我们做了什么来改善我们的发现

我们所做的第一件事,也是最简单的一件事,就是简单地升级我们所运行的Docker基础设施。我们在AUFS文件系统驱动程序中发现了各种补丁,这些补丁看起来与容器创建性能有关。结果是。

  • 3.5%的下载速度很慢。
  • 7.8%的提取速度慢。
  • 0.77%的创建速度很慢--好多了

这为容器的创建时间提供了显著的即时改进,尽管对提取时间来说代价很小。这是在正确方向上的一个有希望的转折,但它本身是不够的。

那层的下载和提取性能呢?

既然我们对容器的创建性能感到满意,我们就把注意力转移到另外两个指标上:层下载和提取。在这里,找到一个解决方案并不那么简单。我们花了相当多的时间对Docker守护程序进行检测,以确定Docker层子系统的哪些部分进展缓慢。我们希望能找出一个像锁争夺这样的问题。这种搜索产生了不确定的结果,也许是因为在个人Linux/Mac机器上运行Docker容器的兼容性问题。AUFS将被废弃的事实也可能是一个促成因素。

**另外值得注意的是:**作为切换到overlay2文件系统驱动的一个副作用,我们看到磁盘性能有了明显的改善。

我们也一直在研究从我们一直在运行的AUFS驱动转移到overlay2文件系统驱动。我们决定在overlay2驱动上试用一小部分Docker舰队,这样我们就可以与AUFS进行性能比较,并解决客户可能面临的任何问题。

比较指标看起来很有希望。虽然在创建指标上有轻微的退步,但提取性能有明显的改善,下载也有轻微的改善。

提取速度慢的改进

Slow extracts

创建速度慢的改进

Slow creates

这个过程的下一部分是慢慢开始向我们所有的用户推广这一变化。我们预计在这个过程中会遇到一些小的兼容性问题,并与我们的客户一起努力工作,帮助他们克服任何问题。虽然统计数字看起来好多了,但我们觉得仍有改进的余地,特别是随着推广的进展,缓慢创建的统计数字又爬升到了1.7%。

改进CircleCI的Docker垃圾收集器

我们有一个叫做docker-gc的组件,负责维护我们的Docker实例的健康。它的两个职责是确保实例的存储空间不被耗尽,以及删除任何现在未使用的Docker对象,如容器、图像、卷和网络。

目前的实现是一个简单的BASH脚本,由一个cron-job调度,定期运行。这有几个问题。

  • 它是不可观察的,只产生简单的日志。
  • 如果一个GC循环花了很长时间,cron调度程序会尝试同时运行两个循环。
  • 它不容易测试,因为BASH本身并不是特别容易测试的。
  • 变化的推出需要很长的时间,因为它们被包含在实例的虚拟机镜像中。这需要更换实例来更新。

Go是一种很好的语言,可以编写像这样的低资源占用率的代理。我们决定将脚本移植到Go中,并将其作为Docker容器本身进行部署。除了立即解决我们遇到的所有问题外,我们还深入了解了docker-gc对我们的Docker实例产生的消极和积极影响。

我们看到的情况非常清楚。当docker-gc运行其垃圾收集周期时,对启动阶段(下载、提取和创建)有很大影响。由于我们的舰队相当大,这对整体数字的影响百分比并不大。但对于那些不幸在docker-gc收集垃圾时开始运行的任务来说,它有负面的影响。

我们花了一个月的时间,在新的可观察性的支持下,对事情进行了调整。

  • Docker/CircleCI build-agent只泄露了网络。
  • 修剪卷和容器在Docker内部需要很长时间,修剪结果显示没有任何东西被删除。由于这些新的见解,我们改变了docker-gc,只修剪网络。
  • 迅速删除大量的图像与旋转速度的降低密切相关。这似乎与Docker守护进程中的锁争夺有关,但事实证明无法解决这个问题。

我们想更频繁地运行垃圾收集周期,但我们担心这会使问题 "不那么严重,但更频繁"。经过一番思考,我们决定在实例上的任何任务处于 "旋转 "状态时暂停垃圾收集。

项目的结果--成功!

在所有这些优化之后,我们能够实现以下改进。

  • 下载速度慢。3.6%降至3.2%,改善了0.4%。
  • 缓慢的提取。6.7%到0.84%,改善了5.8%。
  • 创建速度慢。12%到1.7%,提高了10.3%。

因此,取得了进展。但是,团队认为仍有可能进一步改进。我们一定会与你分享。