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

Cgroups 通过使用伪文件系统进行配置,通常挂载在 /sys/fs/cgroup,并以层次结构分配资源。挂载点在极狐 GitLab 中是可配置的。结构因使用的 cgroups 版本而异:

  • Cgroups v1 遵循资源导向的层次结构。父目录是诸如 cpumemory 这样的资源。
  • Cgroups v2 采用进程导向的方法。父目录是进程组,内部文件代表每个被控制的资源。

当极狐 GitLab 运行时:

  • 在虚拟机上,支持 cgroups v1 和 cgroups v2。极狐 GitLab 会根据挂载点自动检测使用哪个 cgroup 版本。
  • 在 Kubernetes 集群上,仅支持 cgroups v2,因为无法将读写权限委托给使用 cgroups v1 的容器。

当极狐 GitLab 运行 cgroups v2 时可能会有额外的功能和改进,例如可以使用 clone 系统调用直接在 cgroup 下启动进程。

开始之前

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

当为内存启用 cgroups 时,你应该确保极狐 GitLab 节点上没有配置交换,因为进程可能会切换到使用交换而不是被终止。内核认为可用交换内存是 cgroup 施加的限制之外的额外内存。这种情况可能导致性能显著下降。

极狐 GitLab 如何受益于 cgroups

某些 Git 操作可能会消耗过多资源,直到耗尽资源,例如:

  • 意外的高流量。
  • 对不遵循最佳实践的大型存储库进行操作。

消耗这些资源的特定存储库上的活动被称为 “noisy neighbors”,可能导致托管在极狐 GitLab 服务器上的其他存储库的 Git 性能下降。

作为硬性保护,极狐 GitLab 可以使用 cgroups 告诉内核在这些操作占用所有系统资源并导致不稳定之前终止这些操作。极狐 GitLab 根据 Git 命令操作的存储库将 Git 进程分配到一个 cgroup。这些 cgroups 被称为存储库 cgroups。每个存储库 cgroup:

  • 具有内存和 CPU 限制。
  • 包含一个或多个存储库的 Git 进程。总 cgroups 数量是可配置的。每个 cgroup 使用一致的循环哈希来确保给定存储库的 Git 进程始终在同一 cgroup 中。

当存储库 cgroup 达到其:

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

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

以下图表说明了 cgroup 结构:

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

如果极狐 GitLab 存储服务:

  • 只有三个存储库,每个存储库直接插入一个 cgroup。
  • 超过存储库 cgroups 数量,多个存储库以一致的方式分配到同一个组。
flowchart TB parent repos-1 repos-2 repos-3 parent-->repos-1 parent-->repos-2 parent-->repos-3

配置过度订阅

存储库 cgroups 的数量应该合理地高,以便在服务数千个存储库的存储上仍然能够进行隔离。存储库数量的一个好的起点是存储上活动存储库数量的两倍。

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

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

如果我们将 32GiB 除以 100,我们将为每个存储库 cgroup 分配仅仅 0.32GiB。这种设置将导致极差的性能和显著的资源未充分利用。

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

使用上述示例,我们可以通过分配每个存储库 cgroup 10GiB 内存来进行过度订阅,尽管系统没有 10GiB * 100 的系统内存。这些值假设 10GiB 足以支持对任何单个存储库的正常操作,但也允许两个存储库突发到各自的 10GiB,同时保留第三个资源桶以保持基准性能。

CPU 时间适用类似规则。我们故意分配存储库 cgroups 比整个系统可用的 CPU 核心更多。例如,我们可能决定分配每个存储库 cgroup 4 个核心,即使系统没有 400 个总核心。

两个主要值控制过度订阅:

  • cpu_quota_us
  • memory_bytes

父 cgroups 与存储库 cgroups 的每个值之间的差异决定了过度订阅的数量。

测量和调整

要建立和调整过度订阅的正确基准资源需求,你必须观察极狐 GitLab 服务器上的生产工作负载。默认情况下公开的 Prometheus 指标 足以满足这一需求。你可以使用以下查询作为指南来测量特定极狐 GitLab 服务器的 CPU 和内存使用情况:

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

根据你在一个有代表性的时间段(例如,一个典型的工作周)观察到的使用情况,你可以确定正常操作的基准资源需求。为了得出前面的配置示例,我们会观察到整个工作周内一致的 10GiB 内存使用和 4 核 CPU 负载。

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

可用的配置设置

{{< history >}}

  • 配置存储库 cgroups 的这种方法在极狐 GitLab 15.1 中引入。
  • cpu_quota_us 在极狐 GitLab 15.10 中引入。
  • max_cgroups_per_repo 在极狐 GitLab 16.7 中引入。
  • 文档中关于遗留方法的内容在极狐 GitLab 17.8 中被移除。

{{< /history >}}

要在极狐 GitLab 中配置存储库 cgroups,请使用 /etc/gitlab/gitlab.rbgitaly['configuration'][:cgroups] 的以下设置:

  • mountpoint 是父 cgroup 目录的挂载位置。默认为 /sys/fs/cgroup
  • hierarchy_root 是极狐 GitLab 创建组的父 cgroup,且预计由极狐 GitLab 运行的用户和群组拥有。Linux 软件包安装会在极狐 GitLab 启动时创建 mountpoint/<cpu|memory>/hierarchy_root 目录集。
  • memory_bytes 是对极狐 GitLab 生成的所有 Git 进程集体施加的总内存限制。0 表示没有限制。
  • cpu_shares 是对极狐 GitLab 生成的所有 Git 进程集体施加的 CPU 限制。0 表示没有限制。最大值为 1024 份,代表 100% 的 CPU。
  • cpu_quota_uscfs_quota_us,用于在进程超过此配额值时限制 cgroups 的进程。我们将 cfs_period_us 设置为 100ms,所以 1 个核心为 100000。0 表示没有限制。
  • repositories.count 是 cgroups 池中的 cgroups 数量。每次生成新的 Git 命令时,极狐 GitLab 根据命令的存储库将其分配到这些 cgroups 中。循环哈希算法将 Git 命令分配给这些 cgroups,因此存储库的 Git 命令始终分配到同一个 cgroup。
  • repositories.memory_bytes 是对存储库 cgroup 中包含的所有 Git 进程施加的总内存限制。0 表示没有限制。此值不能超过顶级 memory_bytes 的限制。
  • repositories.cpu_shares 是对存储库 cgroup 中包含的所有 Git 进程施加的 CPU 限制。0 表示没有限制。最大值为 1024 份,代表 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 进程可以分布到的存储库 cgroups 的数量。这允许为存储库 cgroups 配置更保守的 CPU 和内存限制,同时仍然允许突发工作负载。例如,使用 max_cgroups_per_repo2memory_bytes 限制为 10 GB,针对特定存储库的独立 Git 操作可以消耗最多 20 GB 的内存。

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

# 在 /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 核心
    repositories: {
      count: 1000,
      memory_bytes: 32212254720, # 20 GB
      cpu_shares: 512,
      cpu_quota_us: 200000, # 2 核心
      max_cgroups_per_repo: 2
    },
  },
}

监控 cgroups

有关监控 cgroups 的信息,请参见 监控极狐 GitLab cgroups