TT Lab
블로그 시작하기
배우기 러닝패스 코스

리눅스 커널과 eBPF

CPU 를 나눠 쓰는 규칙을 손으로 바꿔 본다

TT Lab 에서 이어서 보기

이 실습은 VM 에서 돕니다

우분투 24.04 VM 한 대입니다(vCPU 2개). cgroup 파일에 직접 쓰고 프로세스를 옮기는 일은 커널 권한이 필요해서 실습 파드에서는 할 수 없습니다.

목표

같은 CPU 를 두 프로세스가 다툴 때 스케줄러가 무엇을 기록하는지 읽습니다. nice 로 비율을 바꾸고, cgroup v2 의 cpu.weight 로 그룹 단위 비율을 바꾸고, cpu.max 로 상한을 걸어 스로틀링이 어떻게 숫자로 남는지 봅니다. 마지막으로 그 숫자가 쿠버네티스의 requests·limits 와 어떻게 이어지는지 씁니다.

왜 중요한가

"CPU 사용률은 낮은데 응답이 느리다" 는 신고의 상당수가 스케줄러 이야기입니다. 프로세스가 실행되지 못하고 런큐에서 기다린 시간은 사용률 그래프에 나오지 않습니다. 그리고 컨테이너에 CPU limit 을 걸면 커널은 cpu.max 로 그 그룹을 주기마다 멈추는데, 이 멈춤도 사용률이 아니라 nr_throttled 라는 다른 숫자에 남습니다. 무엇을 봐야 하는지 알면 "CPU 를 더 주세요" 와 "limit 을 풀어 주세요" 를 구별할 수 있습니다.

단계

  1. kx-spin 3 을 CPU 0 에 묶어 혼자 돌리고 결과를 /root/kx/sched/01-alone.txt 에 저장하세요.
  2. 같은 CPU 에 kx-spin 3 둘을 동시에 돌리고 두 줄을 /root/kx/sched/02-contend.txt 에 저장하세요.
  3. 하나는 nice -n 10, 하나는 그대로 같은 CPU 에서 동시에 돌리고 /root/kx/sched/03-nice.txt 에 저장하세요.
  4. cgroup /sys/fs/cgroup/kx-sched 를 만들고 그 아래 gold(cpu.weight 800)와 bronze(100)를 만드세요.
  5. gold 와 bronze 에 kx-spin 6 을 하나씩 넣어 CPU 0 에서 동시에 돌리세요.
  6. kx-sched/capped 에 cpu.max 를 20000 100000 으로 걸고 kx-spin 5 를 돌려 스로틀링을 일으키세요.
  7. capped 그룹의 CPU 압력(PSI)을 /root/kx/sched/07-psi.txt 에 저장하세요.
  8. 쿠버네티스와의 연결을 /root/kx/sched/08-k8s.md 에 정리하세요.

참고

혼자 돌 때의 기록

taskset -c 0 kx-spin 3 을 실행해 나오는 한 줄을 /root/kx/sched/01-alone.txt 에 저장하세요. run_ns 는 CPU 에서 실제로 돈 시간, wait_ns 는 런큐에서 기다린 시간입니다.

kx-spin 은 끝날 때 /proc/<pid>/schedstat 의 세 숫자를 풀어서 찍습니다. 혼자 돌면 기다릴 일이 거의 없어야 합니다. 다른 일을 함께 돌리고 있지 않은지 확인하세요.

같은 CPU 를 둘이 다툰다

taskset -c 0 kx-spin 3 두 개를 동시에 돌리고, 나온 두 줄을 /root/kx/sched/02-contend.txt 에 저장하세요. 두 줄 모두 wait_ns 가 run_ns 의 절반 이상이어야 합니다.

하나를 & 로 띄우고 바로 다른 하나를 실행한 뒤 wait 하면 됩니다. 둘 다 같은 CPU 에 묶어야 합니다. 서로 다른 CPU 에 놓으면 다툼이 없어 기다림도 없습니다.

nice 가 바꾸는 것은 비율이다

같은 CPU 0 에서 nice -n 10 kx-spin 3 과 kx-spin 3 을 동시에 돌리고 두 줄을 /root/kx/sched/03-nice.txt 에 저장하세요. nice=0 쪽의 run_ns 가 nice=10 쪽의 네 배 이상이어야 합니다.

nice 값은 CFS(EEVDF) 가중치로 바뀝니다. 0 은 1024, 10 은 110 이라 같은 시간 동안 대략 9 대 1 로 나눕니다. taskset 과 nice 는 앞뒤로 겹쳐 쓸 수 있습니다.

그룹 단위로 비율을 정한다

/sys/fs/cgroup/kx-sched 를 만들고 자식 그룹에서 CPU 컨트롤러를 쓸 수 있게 한 뒤, 그 아래 gold(cpu.weight 800)와 bronze(cpu.weight 100)를 만드세요.

cgroup v2 는 디렉터리를 만들면 그룹이 생깁니다. 그런데 자식 그룹에 cpu.weight 파일이 생기려면 부모의 cgroup.subtree_control 에 컨트롤러를 켜야 합니다. 켜기 전에 만든 자식에도 켜는 순간 파일이 나타납니다.

가중치대로 나뉘는지 본다

gold 와 bronze 에 kx-spin 6 을 하나씩 넣어 CPU 0 에서 동시에 돌리세요. 끝난 뒤 두 그룹의 cpu.stat 에서 usage_usec 가 gold 쪽이 1초 이상이고 bronze 의 네 배 이상이어야 합니다.

그룹에 넣는 것은 그 그룹의 cgroup.procs 에 PID 를 쓰는 것입니다. 서브셸에서 $BASHPID 를 쓰고 exec 로 명령을 이어 받으면 그 명령이 처음부터 그룹 안에서 돕니다. 따로따로 돌리면 다툼이 없어 비율이 1 대 1 로 나옵니다.

상한을 걸면 멈춘다

/sys/fs/cgroup/kx-sched/capped 를 만들고 cpu.max 를 20000 100000 으로 건 뒤, 그 그룹 안에서 kx-spin 5 를 돌리세요. 끝난 뒤 그 그룹의 cpu.stat 에 nr_throttled 가 10 이상이어야 합니다.

cpu.max 의 두 숫자는 '한 주기(마이크로초) 동안 쓸 수 있는 시간' 과 '주기' 입니다. 20000/100000 은 CPU 0.2 개입니다. 다 쓰면 그 주기가 끝날 때까지 그룹 전체가 멈추고, 멈춘 횟수가 nr_throttled 에 쌓입니다.

멈춘 시간은 압력으로 남는다

/sys/fs/cgroup/kx-sched/capped/cpu.pressure 를 /root/kx/sched/07-psi.txt 에 저장하세요. total= 값이 0 보다 커야 합니다.

PSI(Pressure Stall Information)는 '할 일이 있는데 CPU 를 못 받은 시간' 을 마이크로초로 쌓습니다. 시스템 전체 값은 /proc/pressure/cpu 에 있지만 여기서는 그룹 하나의 값을 봅니다. 그룹마다 파일이 따로 있습니다.

쿠버네티스의 말로 옮긴다

/root/kx/sched/08-k8s.md 에 ## requests 와 cpu.weight·## limits 와 cpu.max·## 무엇을 관측할까 세 절을 쓰세요. 본문에 cpu.weight·cpu.max·nr_throttled 가 나오고, 6단계 capped 그룹의 nr_throttled 값을 그대로 인용해야 합니다.

kubelet 은 파드의 CPU requests 를 cgroup 의 가중치로, limits 를 cpu.max 의 할당량으로 바꿉니다. 그래서 limit 이 걸린 파드는 노드가 한가해도 멈출 수 있습니다. 6·7단계의 숫자가 그 장면입니다.