Help us learn about your current experience with the documentation. Take the survey.

控制组

您可以在 Linux 中使用控制组(cgroups)来限制特定进程可以消耗的内存和 CPU 量。Cgroups 有助于保护系统免受因内存和 CPU 过度消耗而导致的意外资源耗尽。Cgroups 已被广泛使用,并通常作为容器化的基础机制。

Cgroups 通过一个伪文件系统进行配置,通常挂载在 /sys/fs/cgroup,并以分层方式分配资源。在 Gitaly 中,该挂载点是可配置的。其结构因所使用的 cgroups 版本而异:

  • Cgroups v1 遵循面向资源的层次结构。父目录是 cpumemory 等资源。
  • Cgroups v2 采用面向进程的方法。父目录是进程组,其内部的文件代表每个受控的资源。

有关更深入的介绍,请参阅 cgroups Linux man page

当 Gitaly 运行时:

  • 在虚拟机上,同时支持 cgroups v1 和 v2。Gitaly 会根据挂载点自动检测要使用的 cgroup 版本。
  • 在 Kubernetes 集群上,仅支持 cgroups v2,因为使用 cgroups v1 无法将 cgroup 层次结构的读写权限委派给容器。

当 Gitaly 与 cgroups v2 一起运行时,可能会有额外的功能和改进,例如能够使用 clone 系统调用直接在 cgroup 下启动进程。

开始之前

在您的环境中启用限制时应谨慎行事,并且仅应在特定情况下进行,例如为了防范意外的流量高峰。当达到限制时,确实会导致断开连接,从而对用户产生负面影响。为了获得一致且稳定的性能,您应该首先探索其他选项,例如调整节点规格,以及审查大型仓库或工作负载。

为内存启用 cgroups 时,您应确保 Gitaly 节点上未配置任何交换空间(swap),因为进程可能会转而使用交换空间,而不是被终止。内核将可用的交换内存视为 cgroup 所施加限制之外的额外内存。这种情况可能导致性能显著下降。

Gitaly 如何从 cgroups 中受益

在某些情况下,一些 Git 操作可能会消耗过多资源,直至耗尽,例如:

  • 意外的流量高峰。
  • 针对不遵循最佳实践的大型仓库运行的操作。

消耗这些资源的特定仓库活动被称为“吵闹的邻居”(noisy neighbors),并可能导致 Gitaly 服务器上托管的其它仓库的 Git 性能下降。

作为一种硬性保护措施,Gitaly 可以使用 cgroups 来告知内核在这些操作占用所有系统资源并导致不稳定之前将其终止。Gitaly 根据 Git 命令所操作的仓库,将 Git 进程分配给一个 cgroup。这些 cgroup 被称为仓库 cgroup。每个仓库 cgroup:

  • 具有内存和 CPU 限制。
  • 包含一个或多个仓库的 Git 进程。cgroup 的总数是可配置的。每个 cgroup 使用一致的循环哈希算法,以确保给定仓库的 Git 进程始终被分配到同一个 cgroup 中。

当仓库 cgroup 达到其:

  • 内存限制时,内核会遍历进程以寻找要终止的候选进程,这可能导致客户端请求中止。
  • CPU 限制时,进程不会被终止,但会被阻止消耗超过允许量的 CPU,这意味着客户端请求可能会受到限制,但不会中止。

当达到这些限制时,性能可能会降低,用户可能会被断开连接。

下图说明了 cgroup 的结构:

  • 父 cgroup 管理所有 Git 进程的限制。
  • 每个仓库 cgroup(命名为 repos-1repos-3)在仓库级别强制执行限制。

如果 Gitaly 存储服务:

  • 仅服务于三个仓库,则每个仓库直接分配到一个 cgroup 中。
  • 服务的仓库数量多于仓库 cgroup 的数量,则多个仓库会以一致的方式分配到同一个组中。
flowchart TB
 parent
 repos-1
 repos-2
 repos-3

 parent-->repos-1
 parent-->repos-2
 parent-->repos-3

配置超额订阅

仓库 cgroup 的数量应该足够高,以便在服务于数千个仓库的存储上仍然可以实现隔离。仓库数量的一个良好起点是该存储上活跃仓库数量的两倍。

由于仓库 cgroup 在父 cgroup 之上强制执行额外的限制,如果我们通过将父限制除以组数来配置它们,最终得到的限制会过于严格。例如:

  • 我们的父内存限制是 32 GiB。
  • 我们大约有 100 个活跃仓库。
  • 我们已配置 cgroups.repositories.count = 100

如果我们将 32 GiB 除以 100,我们每个仓库 cgroup 只能分配到区区 0.32 GiB。此设置将导致极其糟糕的性能和显著的资源利用不足。

您可以使用超额订阅来在正常操作期间保持基线性能水平,同时允许少数高工作负载的仓库在必要时“突发”使用资源,而不会影响不相关的请求。超额订阅指的是分配超过系统技术上可用资源的量。

使用前面的例子,我们可以通过为每个仓库 cgroup 分配 10 GiB 内存来实现超额订阅,尽管系统并没有 10 GiB * 100 的系统内存。这些值假定 10 GiB 足以应对针对任何一个仓库的正常操作,但同时也允许两个仓库突发到各 10 GiB,同时留下第三个资源桶以维持基线性能。

类似的规则适用于 CPU 时间。我们特意为仓库 cgroup 分配比整个系统可用数量更多的 CPU 核心。例如,我们可能决定为每个仓库 cgroup 分配 4 个核心,即使系统总共没有 400 个核心。

两个主要值控制着超额订阅:

-cpu_quota_us -memory_bytes

父 cgroup 与仓库 cgroup 之间每个值的差异决定了超额订阅的量。

测量与调优

要建立和调优超额订阅的正确基线资源需求,您必须观察 Gitaly 服务器上的生产工作负载。默认暴露的 Prometheus 指标对此已足够。您可以使用以下查询作为指南,来测量特定 Gitaly 服务器的 CPU 和内存使用情况:

查询 资源
quantile_over_time(0.99, instance:node_cpu_utilization:ratio{type="gitaly", fqdn="gitaly.internal"}[5m]) 具有指定 fqdn 的 Gitaly 节点的 p99 CPU 利用率
quantile_over_time(0.99, instance:node_memory_utilization:ratio{type="gitaly", fqdn="gitaly.internal"}[5m]) 具有指定 fqdn 的 Gitaly 节点的 p99 内存利用率

根据您在代表性时间段(例如,一个典型的工作周)内观察到的利用率,您可以确定正常操作的基线资源需求。为了得出前面示例中的配置,我们会在工作周内观察到持续 10 GiB 的内存使用量,以及 CPU 上 4 核的负载。

随着您的工作负载变化,您应该重新审视这些指标并对 cgroups 配置进行调整。如果您在启用 cgroups 后注意到性能显著下降,也应该调优配置,因为这可能是限制过于严格的指标。

可用的配置设置

要在 Gitaly 中配置仓库 cgroup,请在 /etc/gitlab/gitlab.rb 中为 gitaly['configuration'][:cgroups] 使用以下设置:

  • mountpoint 是父 cgroup 目录的挂载位置。默认为 /sys/fs/cgroup
  • hierarchy_root 是 Gitaly 在其下创建组的父 cgroup,并且该目录应归属于 Gitaly 运行时所使用的用户和组。Linux 软件包安装会在 Gitaly 启动时创建 mountpoint/<cpu|memory>/hierarchy_root 目录集。
  • memory_bytes 是对 Gitaly 生成的所有 Git 进程集体施加的总内存限制。0 表示无限制。
  • cpu_shares 是对 Gitaly 生成的所有 Git 进程集体施加的 CPU 限制。0 表示无限制。最大值为 1024 shares,代表 100% 的 CPU。
  • cpu_quota_us 是如果 cgroup 的进程超过此配额值,则用于限制它们的 cfs_quota_us。我们将 cfs_period_us 设置为 100ms,因此 1 核心为 100000。0 表示无限制。
  • repositories.count 是 cgroup 池中的 cgroup 数量。每次生成新的 Git 命令时,Gitaly 会根据该命令针对的仓库将其分配到这些 cgroup 之一。使用循环哈希算法将 Git 命令分配到这些 cgroup,因此针对某个仓库的 Git 命令总是被分配到同一个 cgroup。
  • repositories.memory_bytes 是对仓库 cgroup 中包含的所有 Git 进程施加的总内存限制。0 表示无限制。此值不能超过顶层 memory_bytes 的值。
  • repositories.cpu_shares 是对仓库 cgroup 中包含的所有 Git 进程施加的 CPU 限制。0 表示无限制。最大值为 1024 shares,代表 100% 的 CPU。此值不能超过顶层 cpu_shares 的值。
  • repositories.cpu_quota_us 是对仓库 cgroup 中包含的所有 Git 进程施加的 cfs_quota_us。Git 进程不能使用超过给定配额的量。我们将 cfs_period_us 设置为 100ms,因此 1 核心为 100000。0 表示无限制。
  • repositories.max_cgroups_per_repo 是针对特定仓库的 Git 进程可以分布到的仓库 cgroup 数量。这允许为仓库 cgroup 配置更保守的 CPU 和内存限制,同时仍能应对突发工作负载。例如,当 max_cgroups_per_repo2memory_bytes 限制为 10 GB 时,针对特定仓库的独立 Git 操作最多可以消耗 20 GB 的内存。

例如(不一定是推荐的设置):

# in /etc/gitlab/gitlab.rb
gitaly['configuration'] = {
  # ...
  cgroups: {
    mountpoint: '/sys/fs/cgroup',
    hierarchy_root: 'gitaly',
    memory_bytes: 64424509440, # 60 GB
    cpu_shares: 1024,
    cpu_quota_us: 400000 # 4 cores
    repositories: {
      count: 1000,
      memory_bytes: 32212254720, # 20 GB
      cpu_shares: 512,
      cpu_quota_us: 200000, # 2 cores
      max_cgroups_per_repo: 2
    },
  },
}

监控 cgroups

有关监控 cgroups 的信息,请参阅监控 Gitaly cgroups