사용률 그래프에 안 나오는 시간
한 줄 요약
CPU 가 모자랄 때 프로세스는 런큐에서 기다리고, CPU 상한에 걸리면 그룹째 멈춥니다. 두 시간 모두 사용률 그래프에는 나오지 않고 schedstat·cpu.stat·PSI 같은 다른 숫자에 남습니다.
왜 이것이 문제가 되나
"CPU 사용률은 40% 인데 응답이 느리다" 는 신고는 흔합니다. 사용률은 CPU 가 일한 시간의 비율일 뿐이고, 일하고 싶었는데 차례를 못 받은 시간은 그 안에 없습니다. 짧게 몰리는 요청은 평균 사용률을 거의 올리지 않으면서 런큐를 길게 만듭니다. 그리고 쿠버네티스에서 CPU limit 을 건 파드는 노드가 한가해도 할당량을 다 쓰면 주기 끝까지 멈춥니다. 이 멈춤도 사용률이 아니라 스로틀링 횟수로만 남습니다. 무엇을 봐야 하는지 모르면 "노드를 늘리자" 와 "limit 을 풀자" 를 구별할 수 없습니다.
어떻게 동작하나
리눅스 6.6 부터 일반 프로세스의 스케줄러는 CFS 를 이은 EEVDF 입니다. 핵심 생각은 같습니다. 각 프로세스는 가중치를 갖고, 스케줄러는 가중치에 비례해 CPU 시간을 나눕니다. nice 값은 이 가중치로 바뀝니다. nice 0 은 1024, nice 10 은 110 이라 두 프로세스가 한 CPU 를 다투면 대략 9 대 1 로 나뉩니다. nice 는 "먼저" 가 아니라 "얼마나" 를 정합니다.
커널은 프로세스마다 /proc/<pid>/schedstat 에 세 숫자를 남깁니다. CPU 에서 실제로 돈 시간, 런큐에서 기다린 시간, 받은 타임슬라이스 수입니다. 혼자 돌면 기다린 시간이 거의 0 이고, 같은 CPU 를 둘이 다투면 각자 돈 만큼 기다립니다.
cgroup v2 는 같은 생각을 프로세스 묶음으로 넓힙니다. cpu.weight(기본 100, 1 이상 10000 이하)는 그룹 사이의 비율을 정합니다. 다툼이 없으면 아무 일도 하지 않고, 다툴 때만 몫을 가릅니다. cpu.max 는 전혀 다른 장치입니다. 20000 100000 은 "100ms 주기마다 20ms 까지" 라는 상한이고, 그룹이 주기 안에 할당량을 다 쓰면 다음 주기까지 그룹 전체가 멈춥니다. 그 횟수와 시간이 cpu.stat 의 nr_throttled·throttled_usec 에 쌓입니다.
PSI(Pressure Stall Information)는 "할 일이 있는데 자원을 못 받은 시간" 을 그룹마다 cpu.pressure 에 남깁니다. some 줄은 그룹 안의 누군가가 기다린 시간이라, 스로틀링으로 멈춘 시간도 여기에 쌓입니다.
현장에서 만나는 모습
kubelet 은 파드의 CPU requests 를 cgroup 의 cpu.weight 로, limits 를 cpu.max 의 할당량으로 바꿉니다. 그래서 requests 는 다툴 때의 몫이고, limits 는 한가할 때도 넘을 수 없는 벽입니다. 응답 지연이 주기적으로 튀는 파드가 있다면 그 파드 cgroup 의 nr_throttled 가 늘고 있는지 먼저 봅니다. 멀티스레드 런타임(JVM, Go)은 짧은 시간에 여러 코어를 한꺼번에 쓰기 때문에 평균 사용률이 낮아도 할당량을 주기 초반에 다 써 버리기 쉽습니다.
숫자도 한 번 따져 봅시다. kubelet 은 requests 를 옛 cgroup v1 의 shares(1 CPU = 1024)로 먼저 바꾼 뒤 v2 의 가중치로 환산합니다. 공식은 1 + (shares - 2) × 9999 / 262142 라서 requests 1 CPU 인 파드의 cpu.weight 는 39 가 됩니다. 기본값 100 보다 작다고 놀랄 필요는 없습니다. 가중치는 절대값이 아니라 같은 부모 아래 형제끼리의 비율이기 때문입니다.
배치 작업이 API 서버를 굶긴다면 nice 를 높이는 것보다 두 일을 다른 cgroup 으로 나눠 가중치를 주는 편이 확실합니다. nice 는 같은 그룹 안에서만 비교되기 때문입니다.
English corner
- throttling: 상한 때문에 강제로 멈추는 것입니다. "The container is being throttled even though the node is idle."
- starvation: 차례를 영영 못 받는 상태입니다. "This change could starve low-priority tasks."
- I can reproduce this on ...: 버그 보고의 첫 문장으로 자주 씁니다. 재현한 커널 판과 환경을 바로 뒤에 붙입니다. "I can reproduce this on v6.8 with the attached script."
- bisected to: 어느 커밋에서 문제가 시작됐는지 이분 탐색으로 찾았다는 뜻입니다. "Bisected to commit 1234abcd (
sched: ...)."
다음 실습에서 할 것
VM 의 CPU 0 에 프로세스를 묶어 혼자 돌 때와 둘이 다툴 때의 schedstat 을 비교하고, nice 로 비율을 바꿉니다. 그다음 cgroup 두 개에 가중치 800 과 100 을 주어 사용 시간이 8 대 1 로 갈리는 것을 보고, cpu.max 로 상한을 걸어 nr_throttled 와 PSI 가 쌓이는 것을 확인합니다.