cgroups v2 CPU 컨트롤러로 코어 리소스 제한하기

같은 서버에서 여러 서비스를 돌리다 보면 배치 작업 하나가 CPU를 독차지해 다른 서비스 응답 속도가 같이 떨어지는 경우가 생긴다. nice는 우선순위만 조정할 뿐 상한을 강제하지 못한다. cgroups v2의 cpu 컨트롤러는 프로세스 그룹 단위로 CPU 사용량에 절대 상한(cpu.max)을 걸거나 상대적 비중(cpu.weight)을 나눌 수 있다. 이 글에서는 CPU 바운드 프로세스로 이 두 방식의 효과를 실측한다.

cpu.max — 절대 쿼터 제한

$ cat /sys/fs/cgroup/init.scope/cpu.max
max 100000   ← 기본값. "max"는 무제한이라는 뜻(쿼터 자체가 없음)

$ sudo mkdir /sys/fs/cgroup/cputest
$ echo "50000 100000" | sudo tee /sys/fs/cgroup/cputest/cpu.max
$ echo $PID | sudo tee /sys/fs/cgroup/cputest/cgroup.procs

cpu.max는 “쿼터 기간”(usec) 두 값이다. 50000 100000이면 100ms 주기마다 최대 50ms만 실행을 허용한다는 뜻 — 코어 하나 기준 50% 상한이다. 한 코어에 고정(taskset -c 0)한 무한 루프 프로세스를 넣고 cpu.statusage_usec 증가량으로 실제 사용률을 계산했다.

cpu.max 설정실측 사용률cpu.stat 비고
50000 100000 (50%)약 42.5%nr_periods 212, nr_throttled 172(81% 주기에서 쓰로틀링 발생)
20000 100000 (20%)약 9~12%대부분의 주기에서 쓰로틀링

이 VM은 하이퍼바이저 레벨 변동이 섞여 설정값과 정확히 일치하진 않지만, 쿼터를 낮추면 실측치도 비례해서 낮아지는 경향은 명확하다. nr_throttlednr_periods에 가까울수록 쿼터에 거의 항상 막히고 있다는 뜻이다.

같은 quota, 코어 여러 개에 걸치면?

quota=”100000 100000″(1코어 몫)인 cgroup에, 서로 다른 코어(0번·1번)에 고정한 프로세스 2개를 동시에 넣고 5초간 측정했다.

cgroup 전체 CPU 사용 시간: 6.66s / 5s wall = 133.2%
nr_periods 116
nr_throttled 114   ← 116번 중 114번 쓰로틀됨

1코어 몫 쿼터인데도 133%까지 나왔다. 두 코어에서 동시에 도는 순간은 CPU-시간이 2배속으로 소모돼 매 주기 예산(100ms)을 절반도 안 돼서 다 쓰고, 남은 시간은 쓰로틀된다. 그 결과 이론상 상한(200%)보다는 훨씬 낮지만 순수 1코어 몫(100%)보다는 높은 값에서 요동친다 — nr_throttled 비율(114/116)이 이 쿼터가 실제로 얼마나 빡빡하게 걸리고 있는지 보여준다. 정확히 1코어로 제한하려면 cpuset.cpus로 코어 자체도 하나로 묶어야 한다.

cpu.weight — 상대적 비중 분배

상한 대신 경쟁 시 비율만 정하고 싶다면 cpu.weight(기본 100, 범위 1~10000)를 쓴다. 같은 코어에 고정한 무한 루프 프로세스 2개를 각각 weight 100, 900인 cgroup에 넣고 경쟁시켰다.

$ echo 100 | sudo tee /sys/fs/cgroup/weight_low/cpu.weight
$ echo 900 | sudo tee /sys/fs/cgroup/weight_high/cpu.weight
# 둘 다 taskset -c 1로 같은 코어에 고정, 각자 cgroup에 배정 후 6초간 측정

low_weight(100)  cpu=0.60s
high_weight(900) cpu=5.41s
ratio=9.04

weight 비율 900:100 = 9:1이 실제 CPU 점유 시간 비율(9.04)과 거의 정확히 일치했다. 300:100(3:1)으로도 반복했다.

weight100=1.62s  weight300=4.89s  ratio=3.01

3.01로 이번에도 설정값(3.0)과 거의 정확히 맞아떨어졌다. cpu.max와 달리 cpu.weight는 다른 그룹이 놀고 있으면 그 몫까지 가져다 쓸 수 있어(work-conserving), 상한이 아니라 “경쟁이 붙었을 때의 우선순위”로 이해하는 게 맞다.

cpu.weight.nice — 익숙한 nice 값으로 설정

1~10000 스케일이 낯설면 전통적인 nice 범위(-20~19)로도 같은 값을 설정할 수 있다. 커널이 내부적으로 cpu.weight로 변환한다.

$ echo -10 > cpu.weight.nice
$ cat cpu.weight
932

$ echo 10 > cpu.weight.nice
$ cat cpu.weight
11

nice 값이 낮을수록(우선순위 높을수록) weight가 커진다. 두 파일은 같은 값을 다른 스케일로 보여줄 뿐이라 아무 쪽이나 편한 걸 쓰면 된다.

주의사항

항목내용
cpu.max는 유휴 상태에서도 상한다른 프로세스가 전혀 없어도 설정한 쿼터 이상은 못 쓴다. 순간 버스트가 필요한 워크로드엔 부적합할 수 있다
cpu.weight는 경쟁 없으면 의미 없음혼자 코어를 쓰고 있다면 weight 값과 무관하게 100% 다 쓸 수 있다. 상한이 필요하면 cpu.max와 병행할 것
멀티코어 quota는 코어 수만큼 단순 비례하지 않는다위 실측처럼 100~200% 사이 어딘가에서 요동친다. 정확한 코어 수 제한이 필요하면 cpuset.cpus를 함께 쓸 것
systemd 서비스는 유닛 파일에서 직접 설정 가능CPUQuota=50%, CPUWeight=900 지시자로 수동 cgroup 조작 없이 서비스 시작 시 자동 적용된다

마무리

확실히 못 쓰게 막아야 한다면 cpu.max, 경쟁 시 우선순위만 정하고 남는 자원은 유동적으로 나누고 싶다면 cpu.weight다. 배치 작업이 실시간 서비스의 CPU를 잠식하는 문제라면 배치 쪽에 cpu.max로 상한을 걸어두는 게 가장 확실하다.

참고

Kernel.org – cgroup v2 CPU Interface Files

답글 남기기