같은 서버에서 여러 서비스를 돌리다 보면 배치 작업 하나가 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.stat의 usage_usec 증가량으로 실제 사용률을 계산했다.
| cpu.max 설정 | 실측 사용률 | cpu.stat 비고 |
|---|---|---|
| 50000 100000 (50%) | 약 42.5% | nr_periods 212, nr_throttled 172(81% 주기에서 쓰로틀링 발생) |
| 20000 100000 (20%) | 약 9~12% | 대부분의 주기에서 쓰로틀링 |
이 VM은 하이퍼바이저 레벨 변동이 섞여 설정값과 정확히 일치하진 않지만, 쿼터를 낮추면 실측치도 비례해서 낮아지는 경향은 명확하다. nr_throttled가 nr_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로 상한을 걸어두는 게 가장 확실하다.