Cgroups
GitLab v19.4요약
Linux의 컨트롤 그룹(cgroups)을 사용하면 특정 프로세스가 사용할 수 있는 메모리와 CPU에 제한을 둘 수 있습니다. cgroups는 보통 /sys/fs/cgroup에 마운트되는 의사 파일 시스템으로 구성하며 리소스를 계층적으로 할당합니다.
Linux의 컨트롤 그룹(cgroups)을 사용하면 특정 프로세스가 사용할 수 있는 메모리와 CPU에 제한을 둘 수 있습니다. cgroups는 메모리와 CPU 과다 사용으로 인한 예기치 않은 리소스 고갈로부터 시스템을 보호하는 데 도움이 됩니다. cgroups는 널리 사용할 수 있으며 컨테이너화의 기반 메커니즘으로 흔히 쓰입니다.
cgroups는 보통 /sys/fs/cgroup에 마운트되는 의사 파일 시스템으로 구성하며 리소스를 계층적으로
할당합니다. 마운트 지점은 Gitaly에서 구성할 수 있습니다. 구조는 사용하는 cgroups 버전에 따라
달라집니다.
- cgroups v1은 리소스 중심 계층을 따릅니다. 상위 디렉터리가
cpu,memory같은 리소스입니다. - cgroups v2는 프로세스 중심 방식을 채택합니다. 상위 디렉터리가 프로세스 그룹이고, 그 안의 파일이 제어 대상 리소스를 각각 나타냅니다.
더 자세한 소개는 cgroups Linux man 페이지를 참고합니다.
Gitaly 실행 환경별 지원 범위는 다음과 같습니다.
- 가상 머신에서는 cgroups v1과 v2를 모두 지원합니다. Gitaly는 마운트 지점을 기준으로 사용할 cgroup 버전을 자동으로 감지합니다.
- Kubernetes 클러스터에서는 cgroups v2만 지원합니다. cgroups v1에서는 cgroup 계층에 대한 읽기·쓰기 권한을 컨테이너에 위임할 수 없기 때문입니다.
Gitaly를 cgroups v2로 실행하면 clone 시스템 호출로 cgroup 아래에서 프로세스를 직접 시작하는 기능 등 추가 기능과 개선 사항을 사용할 수 있습니다.
시작하기 전에#
환경에 제한을 적용하는 작업은 예기치 않은 트래픽에 대비하는 경우처럼 선별된 상황에서만 신중하게 수행해야 합니다. 제한에 도달하면 연결이 끊어져 사용자 경험에 부정적인 영향을 줍니다. 일관되고 안정적인 성능을 위해서는 노드 사양 조정, 대용량 리포지터리 또는 워크로드 점검 같은 다른 방안을 먼저 검토해야 합니다.
메모리에 대해 cgroups를 활성화할 때는 Gitaly 노드에 스왑이 구성되어 있지 않은지 확인해야 합니다. 프로세스가
종료되는 대신 스왑을 사용하도록 전환될 수 있기 때문입니다. 커널은 사용 가능한 스왑 메모리를 cgroup이 정한
제한에 더해지는 추가 자원으로 간주합니다. 이 경우 성능이 크게 저하될 수 있습니다.
Gitaly에서 cgroups를 활성화하려면 repositories 필드의 count를 0보다 크게 구성해야 합니다.
Gitaly가 cgroups에서 얻는 이점#
일부 Git operation은 다음과 같은 상황에서 리소스를 고갈시킬 정도로 과도하게 소비할 수 있습니다.
- 예기치 않게 높은 트래픽.
- 모범 사례를 따르지 않는 대용량 리포지터리를 대상으로 하는 operation.
이러한 리소스를 소비하는 특정 리포지터리의 활동을 "시끄러운 이웃"이라고 하며, 같은 Gitaly 서버에 호스팅된 다른 리포지터리의 Git 성능을 떨어뜨릴 수 있습니다.
강력한 보호 수단으로 Gitaly는 cgroups를 사용해, 이러한 operation이 시스템 리소스를 모두 점유해 불안정을 일으키기 전에 커널이 종료하도록 지시할 수 있습니다. Gitaly는 Git 명령이 대상으로 하는 리포지터리를 기준으로 Git 프로세스를 cgroup에 배정합니다. 이 cgroup을 리포지터리 cgroup이라고 합니다. 각 리포지터리 cgroup은 다음 특성을 가집니다.
- 메모리와 CPU 제한을 가집니다.
- 하나 이상의 리포지터리에 대한 Git 프로세스를 포함합니다. 전체 cgroup 개수는 구성할 수 있습니다. 각 cgroup은 일관된 순환 해시를 사용해 특정 리포지터리의 Git 프로세스가 항상 같은 cgroup에 배정되도록 합니다.
리포지터리 cgroup이 제한에 도달하면 다음과 같이 동작합니다.
- 메모리 제한에 도달하면 커널이 종료 대상 프로세스를 찾으므로 클라이언트 요청이 중단될 수 있습니다.
- CPU 제한에 도달하면 프로세스가 종료되지는 않지만 허용치를 넘는 CPU를 사용하지 못하게 되므로, 클라이언트 요청이 지연될 수는 있어도 중단되지는 않습니다.
이러한 제한에 도달하면 성능이 떨어지고 사용자 연결이 끊어질 수 있습니다.
다음 다이어그램은 cgroup 구조를 나타냅니다.
- 상위 cgroup은 모든 Git 프로세스에 대한 제한을 관장합니다.
- 각 리포지터리 cgroup(
repos-1부터repos-3까지)은 리포지터리 단위로 제한을 적용합니다.
Gitaly 스토리지가 제공하는 리포지터리 수에 따라 다음과 같이 동작합니다.
- 리포지터리가 세 개뿐이면 각 리포지터리가 cgroup 하나씩에 그대로 배정됩니다.
- 리포지터리 cgroup 수보다 많으면 여러 리포지터리가 일관된 방식으로 같은 그룹에 배정됩니다.
소스 코드 보기
%%{init: { "fontFamily": "GitLab Sans" }}%%
flowchart TB
accTitle: Gitaly cgroups structure
accDescr: Hierarchical cgroup structure with three repository cgroups under a parent cgroup.
parent
repos-1
repos-2
repos-3
parent-->repos-1
parent-->repos-2
parent-->repos-3
초과 구독 구성#
리포지터리 cgroup 수는 수천 개의 리포지터리를 제공하는 스토리지에서도 격리가 이루어질 수 있도록 충분히 크게 잡아야 합니다. 리포지터리 개수의 좋은 출발점은 해당 스토리지의 활성 리포지터리 수의 두 배입니다.
리포지터리 cgroup은 상위 cgroup 위에 추가 제한을 적용하므로, 상위 제한을 그룹 수로 나누어 구성하면 지나치게 제한적인 값이 됩니다. 예를 들면 다음과 같습니다.
- 상위 메모리 제한이 32GiB입니다.
- 활성 리포지터리가 대략 100개입니다.
cgroups.repositories.count = 100으로 구성했습니다.
32GiB를 100으로 나누면 리포지터리 cgroup당 0.32GiB만 할당됩니다. 이 설정은 성능을 크게 떨어뜨리고 자원을 심각하게 낭비합니다.
초과 구독을 사용하면 평상시에는 기본 수준의 성능을 유지하면서, 워크로드가 큰 소수의 리포지터리가 필요할 때 무관한 요청에 영향을 주지 않고 "버스트"할 수 있습니다. 초과 구독이란 시스템에 실제로 존재하는 것보다 많은 리소스를 할당하는 방식을 말합니다.
앞의 예시에서 시스템에 10GiB × 100의 메모리가 없더라도 리포지터리 cgroup마다 10GiB의 메모리를 할당해 초과 구독할 수 있습니다. 이 값은 어느 한 리포지터리에 대한 평상시 operation에는 10GiB로 충분하다는 전제 아래, 두 리포지터리가 각각 10GiB까지 버스트하더라도 세 번째 몫의 리소스가 남아 기본 성능을 유지할 수 있게 합니다.
CPU 시간에도 비슷한 규칙이 적용됩니다. 리포지터리 cgroup에는 시스템 전체가 가진 것보다 많은 CPU 코어를 의도적으로 할당합니다. 예를 들어 시스템에 코어가 총 400개 없더라도 리포지터리 cgroup당 4코어를 할당하도록 결정할 수 있습니다.
초과 구독을 제어하는 주요 값은 두 가지입니다.
-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 메모리 사용률 |
대표성 있는 기간(예: 평범한 업무 주간) 동안 관찰한 사용률을 바탕으로 평상시 운영에 필요한 기준 리소스 요건을 정할 수 있습니다. 앞의 예시와 같은 구성을 도출하려면 업무 주간 내내 메모리 사용량이 10GiB로 일정하고 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이며, 이는 CPU의 100%를 나타냅니다.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이며, 이는 CPU의 100%를 나타냅니다. 이 값은 최상위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_repo가2이고memory_bytes제한이 10GB이면, 특정 리포지터리에 대한 독립적인 Git operation이 최대 20GB의 메모리를 사용할 수 있습니다.
예시는 다음과 같습니다(권장 설정은 아닙니다).
# 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 모니터링을 참고합니다.