Systemd Cgroups를 활용한 리소스 제한 및 격리 종합 가이드

systemd cgroups, 슬라이스 및 유닛 속성을 사용하여 원시 cgroup 파일을 편집하지 않고 CPU, 메모리 및 I/O를 제한합니다.

Systemd Cgroups를 활용한 리소스 제한 및 격리 종합 가이드

Systemd는 이미 서비스를 Linux 제어 그룹에 배치합니다. 배치 작업자가 전체 시스템을 잡아먹는 것을 방지하기 위해 수동으로 원시 cgroup 디렉토리를 생성할 필요가 없습니다. 대부분의 경우 서비스나 슬라이스에 몇 가지 속성을 추가하고 systemd를 다시 로드하면 재부팅 후에도 유지되고 일반 systemctl 도구에서 확인할 수 있는 CPU, 메모리, 작업 및 I/O 제어를 얻을 수 있습니다.

핵심은 적절한 종류의 제한을 선택하는 것입니다. 하드 메모리 상한은 호스트를 보호할 수 있지만 너무 낮게 설정하면 서비스가 중단될 수 있습니다. CPU 가중치는 시스템이 바쁠 때까지 부드럽게 작동합니다. CPU 할당량은 엄격하지만 지연 시간을 추가할 수 있습니다. I/O 제한은 스토리지 스택과 cgroup 버전에 따라 다릅니다. 리소스 제어는 단순한 체크박스가 아니라 운영상의 절충점입니다.

제어 그룹(cgroups) 이해하기

Systemd 구현을 살펴보기 전에 cgroups의 기본 개념을 이해하는 것이 필수적입니다. Cgroups는 Linux 커널의 계층적 메커니즘으로, 프로세스를 그룹화하고 이러한 그룹에 리소스 관리 정책을 할당할 수 있게 합니다. 이러한 정책에는 다음이 포함됩니다:

  • CPU: CPU 시간 제한, CPU 액세스 우선순위 지정.
  • 메모리: 메모리 사용량 제한 설정, OOM(Out-Of-Memory) 조건 방지.
  • I/O: 디스크 읽기/쓰기 작업 제한.
  • 네트워크: 네트워크 제어는 Linux 트래픽 제어 및 관련 도구를 통해 가능하지만, systemd의 내장 유닛 속성은 주로 CPU, 메모리, 프로세스 수, 장치 액세스 및 블록 I/O에 중점을 둡니다.
  • 장치 액세스: 특정 장치에 대한 액세스 제어.

커널은 일반적으로 /sys/fs/cgroup에 마운트된 가상 파일 시스템을 통해 cgroup 구성을 노출합니다. 각 컨트롤러(예: cpu, memory)는 자체 디렉토리를 가지며, 이 디렉토리 내에서 디렉토리 계층 구조는 그룹과 관련 리소스 제한을 나타냅니다.

Systemd의 Cgroup 관리 아키텍처

Systemd는 직접적인 cgroup 조작의 복잡성을 추상화하여 구조화된 유닛 관리 시스템을 제공합니다. 프로세스를 유닛 계층 구조로 구성한 다음 cgroup 계층 구조에 매핑합니다. 리소스 관리와 관련된 주요 유닛 유형은 다음과 같습니다:

  • 슬라이스(Slices): 서비스 유닛을 위한 추상 컨테이너입니다. 슬라이스는 계층 구조를 형성하여 리소스 위임을 허용합니다. 예를 들어, 사용자 세션용 슬라이스에는 개별 애플리케이션용 슬라이스가 포함될 수 있습니다. Systemd는 시스템 서비스, 사용자 세션 및 가상 머신/컨테이너에 대해 자동으로 슬라이스를 생성합니다.
  • 스코프(Scopes): 일반적으로 전체 서비스 유닛으로 관리되지 않는 사용자 세션이나 시스템 서비스와 관련된 임시 또는 동적으로 생성된 프로세스 그룹에 사용됩니다. 이들은 일시적이며 내부 프로세스가 실행되는 동안 존재합니다.
  • 서비스(Services): 데몬 및 애플리케이션 관리를 위한 기본 유닛입니다. 서비스 유닛이 시작되면 systemd는 해당 프로세스를 일반적으로 슬라이스 내의 cgroup 계층 구조에 배치합니다. 리소스 제한은 서비스 유닛에 직접 적용할 수 있습니다.

Systemd의 기본 계층 구조는 종종 다음과 같습니다:

-.slice (루트 슬라이스)
  |- system.slice
  |  |- <service_name>.service
  |  |- another-service.service
  |  ... 
  |- user.slice
  |  |- user-1000.slice
  |  |  |- session-c1.scope
  |  |  |  |- <application>.service (사용자가 시작한 경우)
  |  |  |  ...
  |  |  ...
  |  ... 
  |- machine.slice (VM/컨테이너용)
  ... 

Systemd 유닛 파일로 리소스 제한 적용하기

Systemd를 사용하면 .service, .slice 또는 .scope 유닛 파일 내에서 직접 cgroup 리소스 제한을 지정할 수 있습니다. 이러한 지시문은 각각 [Service], [Slice] 또는 [Scope] 섹션 아래에 배치됩니다.

CPU 제한

CPU 리소스 제어를 위한 주요 지시문은 다음과 같습니다:

  • CPUQuota=: 유닛이 사용할 수 있는 총 CPU 시간을 제한합니다. 백분율(예: CPU 코어 절반의 경우 50%) 또는 CPU 코어의 분수(예: 0.5)로 지정됩니다. 기간당 마이크로초 단위로 값을 지정할 수도 있습니다. 기본 기간은 100ms입니다.
  • CPUWeight=: cgroup v2 시스템에서 CPU 시간에 대한 상대적 가중치를 설정합니다. 가중치가 높은 유닛은 경합이 있을 때 더 많은 몫을 얻지만, 시스템이 유휴 상태일 때 CPU를 예약하지는 않습니다.
  • CPUShares=: 이전 cgroup v1 시대의 가중치입니다. v1 호환성이 필요하지 않은 한 최신 배포판에서는 CPUWeight=를 선호하십시오.
  • CPUQuotaPeriodSec=: CPUQuota의 기간을 설정합니다. 기본값은 100ms입니다.

예: 웹 서버를 CPU 코어 하나의 75%로 제한:

서비스 파일을 생성하거나 편집합니다(예: /etc/systemd/system/mywebapp.service):

[Unit]
Description=내 웹 애플리케이션

[Service]
ExecStart=/usr/bin/mywebapp
User=webappuser
Group=webappgroup

# CPU 코어 하나의 75%로 제한
CPUQuota=75%

[Install]
WantedBy=multi-user.target

서비스 파일을 생성하거나 수정한 후 systemd 데몬을 다시 로드하고 서비스를 다시 시작합니다:

sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service

메모리 제한

메모리 제한은 다음과 같은 지시문에 의해 제어됩니다:

  • MemoryMax=: 유닛의 프로세스가 소비할 수 있는 메모리 양에 대한 하드 제한을 설정합니다. 바이트 단위 또는 K, M, G, T와 같은 접미사(예: 512M)로 지정할 수 있습니다.
  • MemoryLimit=: 일부 시스템에서 호환성을 위해 유지되는 이전 철자입니다. 최신 systemd 릴리스에서는 MemoryMax=를 선호하십시오.
  • MemoryHigh=: 소프트 제한을 설정합니다. 이 제한에 가까워지면 메모리 회수(스와핑)가 더 적극적으로 트리거되지만 하드 제한은 아직 적용되지 않습니다.
  • MemorySwapMax=: 유닛이 사용할 수 있는 스왑 공간의 양을 제한합니다.

예: 데이터베이스를 2GB RAM으로 제한:

서비스 파일을 생성하거나 편집합니다(예: /etc/systemd/system/mydb.service):

[Unit]
Description=내 데이터베이스 서비스

[Service]
ExecStart=/usr/bin/mydb
User=dbuser
Group=dbgroup

# 메모리를 2GB로 제한
MemoryMax=2G

[Install]
WantedBy=multi-user.target

다시 로드하고 다시 시작합니다:

sudo systemctl daemon-reload
sudo systemctl restart mydb.service

I/O 제한

I/O 제한은 다음과 같은 지시문을 사용하여 제어할 수 있습니다:

  • IOWeight=: I/O 작업에 대한 상대적 가중치를 설정합니다. 값이 높을수록 I/O 우선순위가 높아집니다. 범위는 1에서 1000까지입니다(기본값 500).
  • IOReadBandwidthMax=: 읽기 I/O 대역폭을 제한합니다. [<device>] <bytes_per_second> 형식으로 지정됩니다. 예를 들어, IOReadBandwidthMax=/dev/sda 100M/dev/sda에 대한 읽기 작업을 100MB/s로 제한합니다.
  • IOWriteBandwidthMax=: 쓰기 I/O 대역폭을 제한합니다. IOReadBandwidthMax와 유사한 형식입니다.

예: 백그라운드 처리 서비스를 특정 디스크에서 50MB/s로 제한:

서비스 파일을 생성하거나 편집합니다(예: /etc/systemd/system/batchproc.service):

[Unit]
Description=배치 처리 서비스

[Service]
ExecStart=/usr/bin/batchproc
User=batchuser
Group=batchgroup

# /dev/sdb에 대한 쓰기 작업을 50MB/s로 제한
IOWriteBandwidthMax=/dev/sdb 50M

# 적당한 읽기 우선순위 부여
IOWeight=200

[Install]
WantedBy=multi-user.target

다시 로드하고 다시 시작합니다:

sudo systemctl daemon-reload
sudo systemctl restart batchproc.service

Cgroups 관리 및 모니터링

Systemd는 유닛과 연결된 cgroups를 검사하고 관리하는 도구를 제공합니다.

Cgroup 상태 검사

systemctl status 명령은 유닛의 cgroup 멤버십 및 리소스 사용량에 대한 정보를 제공합니다.

systemctl status mywebapp.service

cgroup 경로를 나타내는 줄을 찾으십시오. 예:

● mywebapp.service - 내 웹 애플리케이션
     Loaded: loaded (/etc/systemd/system/mywebapp.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-10-27 10:00:00 UTC; 1 day ago
       Docs: man:mywebapp(8)
   Main PID: 12345 (mywebapp)
      Tasks: 5 (limit: 4915)
     Memory: 15.5M
        CPU: 2h 30m 15s
      CGroup: /system.slice/mywebapp.service
              └─12345 /usr/bin/mywebapp

cgroup 파일 시스템을 직접 검사할 수도 있습니다:

systemd-cgls # systemd가 관리하는 cgroup 계층 구조 표시
systemd-cgtop # top과 유사하지만 cgroups용

서비스의 cgroup에 적용된 특정 제한을 보려면:

# 일반적인 cgroup v2 호스트의 메모리 제한 확인
cat /sys/fs/cgroup/system.slice/mywebapp.service/memory.max

# CPU 제한 확인
cat /sys/fs/cgroup/system.slice/mywebapp.service/cpu.max

정확한 경로와 파일 이름은 cgroup 버전 및 배포판에 따라 다릅니다. cgroup v1 시스템에서는 /sys/fs/cgroup/memory/...와 같은 컨트롤러별 경로가 여전히 존재할 수 있습니다. cgroup v2 시스템에서는 /sys/fs/cgroup/... 아래의 통합 계층 구조가 일반적인 보기입니다.

즉시 Cgroup 제한 수정

유닛 파일에 제한을 설정하는 것이 모범 사례이지만, systemctl set-property를 사용하여 일시적으로 조정할 수 있습니다:

sudo systemctl set-property mywebapp.service CPUQuota=50%

systemd 버전 및 플래그에 따라 set-property는 영구 속성을 위해 /etc/systemd/system.control/ 아래에 드롭인을 작성할 수 있습니다. systemctl cat mywebapp.servicesystemctl show mywebapp.service -p CPUQuota -p MemoryMax를 사용하여 어떤 일이 발생했는지 확인하십시오. 인프라를 코드로 관리하고 동료 검토를 위해서는 명시적인 유닛 드롭인이 일반적으로 더 명확합니다.

리소스 위임을 위한 슬라이스

슬라이스는 서비스 또는 애플리케이션 그룹을 관리하는 데 강력합니다. 슬라이스에 리소스 제한을 정의할 수 있으며, 해당 슬라이스 내의 모든 서비스 또는 스코프는 해당 제한을 상속받거나 제한됩니다.

예: 리소스 집약적인 배치 작업을 위한 전용 슬라이스 생성:

슬라이스 파일을 생성합니다(예: /etc/systemd/system/batch.slice):

[Unit]
Description=배치 처리 슬라이스

[Slice]
# 이 슬라이스의 모든 작업에 대한 총 CPU를 1코어로 제한
CPUQuota=100%
# 총 메모리를 4GB로 제한
MemoryMax=4G

이제 .service 파일에서 Slice= 지시문을 사용하여 이 슬라이스 내에서 실행되도록 서비스를 구성할 수 있습니다:

[Unit]
Description=특정 배치 작업

[Service]
ExecStart=/usr/bin/mybatchjob

# 이 서비스를 batch.slice에 배치
Slice=batch.slice

[Install]
WantedBy=multi-user.target

Systemd를 다시 로드하고, 필요한 경우 슬라이스를 활성화/시작하고(종종 암시적으로 활성화됨), 서비스를 시작합니다.

sudo systemctl daemon-reload
sudo systemctl start mybatchjob.service

이 접근 방식을 사용하면 관련 프로세스를 그룹화하고 집계 리소스 소비를 관리할 수 있습니다.

모범 사례 및 고려 사항

  • 점진적 제한부터 시작: 제한을 설정할 때 보수적인 값으로 시작하여 필요에 따라 점진적으로 늘리십시오. 공격적인 제한은 애플리케이션을 불안정하게 만들 수 있습니다.
  • 모니터링: 시스템의 리소스 사용량과 cgroup 설정의 영향을 정기적으로 모니터링하십시오. systemd-cgtop, htop, topiotop과 같은 도구는 매우 유용합니다.
  • Cgroup v1과 v2 이해: Systemd는 cgroup v1과 v2를 모두 지원합니다. 많은 지시문이 유사하지만 v2는 통합 계층 구조와 몇 가지 동작 차이를 제공합니다. 복잡한 문제가 발생할 경우 시스템이 어떤 버전을 사용하는지 알고 있어야 합니다.
  • 우선순위 지정과 하드 제한: 리소스가 부족할 때 우선순위 지정에는 CPUWeight를 사용하고, 엄격한 상한에는 CPUQuota를 사용하십시오. 유사하게, MemoryHigh는 하드 제한 전의 압력을 위한 것이고, MemoryMax는 하드 제한입니다.
  • 서비스 대 슬라이스: 개별 애플리케이션에는 서비스 유닛을 사용하고, 관련 애플리케이션 그룹 또는 리소스 풀을 관리하려면 슬라이스를 사용하십시오.
  • 문서화: 특히 프로덕션 환경에서 중요한 서비스에 적용된 리소스 제한을 명확하게 문서화하십시오.
  • OOM 킬러: 프로세스가 MemoryMax 제한을 초과하면 커널의 OOM(Out-Of-Memory) 킬러가 cgroup 내에 있더라도 프로세스를 종료할 수 있습니다. Systemd는 OOMPolicy=와 같은 지시문을 사용하여 특정 cgroup에 대해 OOM 킬러가 동작하는 방식을 관리할 수 있습니다.

제한을 안전하게 롤아웃하는 방법

관찰부터 시작하십시오. 제한을 추가하기 전에 정상 부하 및 예상 최대 부하 동안 서비스가 어떻게 동작하는지 살펴보십시오:

systemctl status mywebapp.service
systemd-cgtop
systemctl show mywebapp.service -p MemoryCurrent -p CPUUsageNSec -p TasksCurrent

메모리의 경우 MemoryMax=보다 MemoryHigh=가 첫 번째 좋은 선택인 경우가 많습니다:

[Service]
MemoryHigh=1G
MemoryMax=1536M

MemoryHigh=는 서비스가 하드 상한에 도달하기 전에 커널에 압력을 가하도록 지시합니다. MemoryMax=는 벽입니다. 프로세스가 이를 초과하고 메모리를 회수할 수 없으면 커널이 cgroup 내의 프로세스를 종료할 수 있습니다. 이는 실행이 중단된 작업자에게는 정확히 원하는 것일 수 있지만, 계획하지 않았다면 데이터베이스에는 좋지 않은 놀라움입니다.

CPU의 경우 공정성(fairness)을 원하는지 하드 상한을 원하는지 결정하십시오:

[Service]
CPUWeight=50

이것은 경합 시 우선순위를 낮추지만 서비스가 유휴 CPU를 계속 사용할 수 있도록 합니다. 백그라운드 작업의 경우 할당량보다 종종 더 좋습니다.

[Service]
CPUQuota=200%

이것은 서비스를 대략 두 개의 CPU 코어 시간으로 제한합니다. 이는 시끄러운 배치 프로세서에 유용하지만, 트래픽 급증 중에 작업자 스레드가 제한되면 지연 시간에 민감한 애플리케이션에 해로울 수 있습니다.

프로세스 폭발을 방지하려면 작업 제한을 추가하십시오:

[Service]
TasksMax=200

이것은 우발적인 포크 폭풍으로부터 호스트를 보호합니다. 정상적인 스레드 수에 충분히 높게 설정하십시오. Java, 데이터베이스 및 브라우저와 같은 워크로드는 예상보다 더 많은 작업을 사용할 수 있습니다.

공급업체 유닛 편집 대신 드롭인 사용

/usr/lib/systemd/system/ 또는 /lib/systemd/system/ 아래 패키지에서 제공하는 유닛 파일 편집을 피하십시오. 드롭인을 사용하십시오:

sudo systemctl edit mywebapp.service

그런 다음 추가:

[Service]
MemoryHigh=1G
MemoryMax=1536M
CPUWeight=80

저장 후:

sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service
systemctl cat mywebapp.service

systemctl cat은 공급업체 유닛과 사용자 재정의를 함께 표시합니다. 이렇게 하면 활성 구성이 하나의 명령으로 표시되므로 향후 디버깅이 훨씬 쉬워집니다.

팀, 테넌트 및 워크로드 클래스를 위한 슬라이스

슬라이스는 한 번에 하나의 서비스만 생각하는 것을 멈출 때 유용해집니다. 호스트가 API, 보고서 생성기 및 여러 가져오기 작업자를 실행한다고 가정해 보겠습니다. 어떤 가져오기 작업자가 CPU를 사용하는지는 중요하지 않을 수 있지만, 모든 가져오기 작업이 함께 API를 굶주리게 할 수 없다는 것은 중요합니다.

슬라이스 생성:

# /etc/systemd/system/import.slice
[Unit]
Description=가져오기 및 백필 워크로드

[Slice]
CPUWeight=30
MemoryHigh=4G
MemoryMax=5G

그 안에 가져오기 서비스를 배치:

[Service]
Slice=import.slice
ExecStart=/usr/local/bin/import-worker

이제 그룹에 공유 압력이 있습니다. 이는 모든 작업자에 대해 별도의 하드 상한을 설정하고 누군가 새 작업자를 추가한 후에도 수학이 여전히 맞기를 바라는 것보다 더 깔끔합니다.

사람들을 혼란스럽게 하는 한 가지 명명 세부 사항이 있습니다: 슬라이스 이름은 계층 구조를 인코딩합니다. customer-a.slice는 최상위 슬라이스입니다. customer-a-batch.slicecustomer-a.slice의 하위가 아닙니다. 단지 다른 최상위 이름일 뿐입니다. 계층적 슬라이스는 특정 방식으로 대시를 구분 기호로 사용하므로 큰 슬라이스 트리를 설계하기 전에 systemd.slice(5)를 읽으십시오.

리소스 제한으로 해결할 수 없는 것

Cgroups는 하나의 워크로드가 호스트를 압도하는 것을 막을 수 있지만, 부족한 시스템을 빠르게 만들 수는 없습니다. 데이터베이스가 허용하는 것보다 작업 세트에 더 많은 메모리가 필요한 경우 메모리 회수에 더 많은 시간을 소비하거나 부하가 걸리면 실패할 수 있습니다. API가 짧은 응답 시간을 필요로 하는 경우 엄격한 CPU 할당량은 임의의 지연 시간처럼 보이는 제한 지연을 생성할 수 있습니다. 스토리지 장치가 이미 포화 상태인 경우 I/O 가중치가 공정성을 향상시킬 수는 있지만 처리량을 생성하지는 않습니다.

제한을 가드레일로 취급하십시오. 애플리케이션 수준 설정과 함께 사용하십시오: 데이터베이스 버퍼 크기, 작업자 수, 대기열 동시성, JVM 힙 제한, Go GOMEMLIMIT, Node 메모리 플래그 또는 런타임이 제공하는 모든 것. 최상의 설정은 일반적으로 둘 다입니다: 애플리케이션은 자체 메모리 및 동시성 모델을 알고 있으며, systemd는 해당 모델이 손상될 경우 나머지 시스템을 보호합니다.

유지해야 할 정신 모델

하나의 데몬에는 서비스 수준 제한을 사용하십시오. 관련 워크로드 그룹에는 슬라이스 수준 제한을 사용하십시오. 경합 시 우선순위를 원할 때는 가중치를 사용하십시오. 확실한 경계가 필요하고 그 결과를 준비했을 때는 할당량과 하드 메모리 상한을 사용하십시오. systemctl show로 유효 속성을 확인하고, systemd-cgtop으로 동작을 관찰하며, 팀이 검토할 수 있는 드롭인 또는 유닛 파일에 구성을 유지하십시오.