스케줄러와 부하 지표 - 실행 큐와 선점, 부하 평균과 CPU 제한 해석 (CS 기초 6장)
이 장에서 배우는 것
5장에서는 프로세스와 스레드가 공유하는 자원과 실행 전환을 구분했다. 이번에는 실행할 준비가 된 흐름 중 누가 CPU를 받는지 살핀다. 선수지식은 준비, 실행, 기다림이라는 상태의 차이다. 작업자 개수와 실제로 사용할 수 있는 계산 능력은 같지 않다는 점이 이 장의 출발점이다.
- 선점과 실행 큐가 여러 작업을 번갈아 진행시키는 과정을 설명한다.
- Linux 부하 평균과 CPU 사용률이 다른 질문에 답함을 구분한다.
- 코어 수와 CPU 시간 제한을 함께 읽어 작업자 증설의 효과를 판단한다.
개념
서비스의 요청이 밀리는데 호스트 CPU 사용률은 낮다고 가정한다. 작업자를 더 만들었지만 응답은 오히려 늦어진다. 작업이 저장장치를 기다리거나 컨테이너가 자신의 CPU 시간 예산을 이미 소진했다면 이런 현상이 가능하다. CPU가 놀고 있다는 화면 한 장으로 응용 프로그램도 즉시 실행할 수 있다고 판단하면 원인과 처방이 어긋난다.
기다림의 종류부터 나눈다
실행 큐에는 실행할 수 있으나 CPU 배정을 기다리는 작업이 있다. 반면 입력이나 자원 조건을 기다리는 작업은 그 조건이 충족되기 전까지 명령을 진행할 수 없다. 선점은 실행 중인 작업을 멈추고 다른 준비된 작업에 기회를 주는 동작이다. 여러 작업이 조금씩 진행된다는 동시성과 같은 순간 서로 다른 CPU에서 실행된다는 병렬성은 이 지점에서 갈린다.
실행 큐의 A와 B가 CPU를 번갈아 사용하고, I/O를 기다리는 C는 준비가 끝난 뒤 실행 큐로 돌아간다. 그림의 경계는 소유 범위를, 화살표는 실행 또는 참조 방향을 뜻한다.
| 상태 | CPU를 주면 진행 가능한가 | 지연 원인의 예 |
|---|---|---|
| 실행 중 | 이미 진행 중 | 연산량·메모리 접근 |
| 실행 가능 대기 | 가능 | 경쟁 작업·허용 CPU 범위 |
| 조건을 기다림 | 조건 충족 전에는 불가 | I/O 완료·공유 자원 |
| 예산 제한으로 지연 | 실행 권한 회복 필요 | CPU 시간 할당량 소진 |
입문 그림은 같은 길이의 시간 조각으로 번갈아 실행시킨다. 이 모형은 선점의 의미를 드러내지만 Linux의 모든 스케줄링 정책이 고정된 시간 조각을 순환 배정한다는 뜻은 아니다. 실제 선택에는 정책, 우선순위, 실행 이력, CPU 배치가 관여한다. 정책의 이름이나 한 버전의 구현을 외우기보다 실행 가능 시간이 어떻게 지연으로 바뀌는지 관찰하는 것이 목적이다.
부하 평균은 사용률이 아니다
Linux의 전역 부하 평균은 실행 중이거나 실행 가능한 작업과 중단 불가능한 대기 상태의 작업 수를 바탕으로 계산한다. 통상 보이는 1분, 5분, 15분 값은 시간에 따른 변화를 부드럽게 반영하는 지표다. 최근 정확히 그 분량의 기록을 산술 평균한 값으로 해석하지 않는다. 부하 평균의 단위는 퍼센트가 아니다.
중단 불가능한 대기는 흔히 I/O와 관련되지만 모든 경우를 디스크라고 단정할 수 없다. 또한 모든 대기 스레드가 부하에 들어가는 것도 아니다. 값이 높으면 실행 가능 작업이 밀렸는지 해당 대기 상태가 쌓였는지 나누어야 한다. 한 시점의 상태 개수를 더한 값과 시간에 걸쳐 계산된 부하 평균이 바로 같지 않은 것도 이 때문이다.
논리 CPU가 여러 개라면 작업 여럿이 같은 순간 실행될 수 있다. 그러나 그 수로 부하 평균을 나눈 결과만으로 병목을 확정할 수는 없다. 작업의 CPU 배치 제한과 컨테이너의 시간 제한이 유효한 용량을 줄일 수 있고 I/O 대기 항목도 섞이기 때문이다. CPU 사용률, 작업 상태, 제한 정보를 같은 구간에서 수집하면 서로 다른 가능성을 차례로 배제할 수 있다.
CPU 시간 예산은 코어 전용 배정과 다르다
cgroup v2의 cpu.max는 기간과 그 기간에 허용하는 CPU 시간으로 제한을 표현한다. 아래 실습의 50000과 100000은 설명을 위해 고른 값이며 단위는 마이크로초다. 기간당 0.5 CPU에 해당하는 평균 예산이지만 특정 코어 절반을 전용으로 떼어 준다는 뜻은 아니다. 작업자가 여러 CPU에서 동시에 실행하면 합산 예산을 더 빨리 소진할 수 있다.
예산을 모두 쓴 실행 그룹은 다음에 실행이 허용될 때까지 지연될 수 있다. 호스트의 다른 CPU가 유휴 상태라도 그룹의 제한은 남아 있다. 반면 CPU weight는 경쟁 시의 상대적 배분을 조절하는 개념이므로 시간의 상한과 같은 말이 아니다. 상위 그룹의 제한도 영향을 주므로 현재 그룹의 파일 하나에 제한이 없다는 이유만으로 무제한이라고 단정하지 않는다.
모형 입력: cpu.max = 50000 100000
허용 평균: 50000 / 100000 = 0.5 CPU
뜻: 기간별 합산 CPU 시간 예산이며 전용 코어가 아니다
직접 확인하기
실행 큐를 작은 모형으로 만든다
다음 Python 3 코드는 한 단위마다 선점하는 단순 모형이다. A는 세 단위, B는 두 단위가 필요하며 두 작업은 처음부터 준비되어 있다. 운영체제의 실제 스케줄러를 측정하는 코드가 아니므로 커널 버전과 무관하게 결과를 해석할 수 있다. 큐의 끝으로 돌아가는 동작이 작업의 완료와 다르다는 점을 확인한다.
from collections import deque
queue = deque([("A", 3), ("B", 2)])
timeline = []
while queue:
name, left = queue.popleft()
timeline.append(name)
if left > 1:
queue.append((name, left - 1))
print("slots", " ".join(timeline))
print("units", len(timeline))결과
----
slots A B A B A
units 5
A와 B가 번갈아 등장하지만 총 다섯 단위라는 계산량은 줄지 않았다. 동시에 실행할 CPU가 하나라는 모형의 가정 때문이다. 짧은 작업을 중간에 진행시키는 효과와 전체 처리량을 늘리는 효과를 구분해야 한다. 시간 조각을 더 짧게 나눈다고 계산 자체가 사라지지는 않고 실제 시스템에는 전환 비용까지 더해진다.
지표의 분자와 관찰 구간을 계산한다
다음은 상태 표본을 분류하는 교육용 코드다. R은 실행 가능, D는 중단 불가능한 대기, S는 여기서 세지 않는 대기를 뜻하도록 정했다. 합계는 모형의 즉시 입력이며 부하 평균 출력이 아니다. 숫자의 이름을 정확히 붙여야 정상적인 계산을 잘못된 지표로 보고하지 않는다.
states = ["R", "R", "D", "S", "S", "S"]
runnable = states.count("R")
uninterruptible = states.count("D")
print("runnable", runnable)
print("uninterruptible", uninterruptible)
print("active model", runnable + uninterruptible)결과
----
runnable 2
uninterruptible 1
active model 3
시간 예산의 계산도 독립된 코드로 확인한다. 두 작업자가 같은 속도로 동시에 실행된다고 단순화하면 각자 사용할 수 있는 시간의 몫은 줄어든다. 실제 실행 순서와 몫이 항상 똑같다는 보장은 없다. 이 계산은 요청 지연의 예측기가 아니라 합산 제한의 의미를 보여 준다.
quota, period = 50000, 100000
print("CPU equivalent", quota / period)
for workers in (1, 2):
print("workers", workers, "budget per worker", quota // workers)
print("unit", "microseconds per period")결과
----
CPU equivalent 0.5
workers 1 budget per worker 50000
workers 2 budget per worker 25000
unit microseconds per period
누적 스로틀링 횟수는 관찰 시작과 끝의 차이로 읽는다. 다음 숫자는 직접 만든 문제용 입력이며 특정 서버의 측정값이 아니다. 증가한 제한 기간 비율을 계산한 뒤에도 제한 때문에 잃은 요청 수나 지연 시간을 바로 알 수는 없다. 그 판단에는 같은 시간창의 요청 분포와 CPU 사용 정보를 함께 놓아야 한다.
before = {"nr_periods": 100, "nr_throttled": 10}
after = {"nr_periods": 120, "nr_throttled": 18}
periods = after["nr_periods"] - before["nr_periods"]
throttled = after["nr_throttled"] - before["nr_throttled"]
print("periods", periods)
print("throttled periods", throttled)
print("fraction", throttled / periods)결과
----
periods 20
throttled periods 8
fraction 0.4
환경별 차이
| 환경 | 읽을 대상 | 동일하게 취급하면 안 되는 것 |
|---|---|---|
| Linux 호스트 | CPU 사용률·작업 상태·부하 추이 | 부하 평균과 사용률 |
| macOS | 플랫폼별 부하 정의와 CPU 상태 | Linux의 대기 상태 계산을 그대로 적용 |
| cgroup v2 컨테이너 | cpu.max·cpu.stat·상위 제한 | 보이는 CPU 수와 허용 CPU 시간 |
| cgroup v1 컨테이너 | 해당 버전의 CPU 제어 파일 | v2의 파일명·필드와 동일하다는 가정 |
이 장의 실행 실습은 어디서나 재현할 수 있는 모형이며 운영 설정을 읽거나 바꾸지 않는다. 실제 Linux 진단에서는 관찰 대상이 호스트인지 컨테이너인지부터 기록한다. 도구가 컨테이너 내부에서도 호스트 전체 부하를 보여 주는 상황이 있기 때문이다. 상태 파일의 경로와 이름 공간이 확인되지 않은 숫자를 애플리케이션 전용 값처럼 붙이지 않는다.
cgroup v2의 cpu.stat에는 누적 사용 정보가 있으며 CPU 제어가 활성화된 조건에서 제한 관련 항목을 볼 수 있다. nr_periods와 nr_throttled의 차이는 제한을 만난 기간의 빈도를 이해하는 데 도움이 된다. throttled_usec를 단순히 관찰 벽시계 시간으로 나누어 모든 환경에서 정확한 손실률로 쓰는 것은 피한다. 집계 범위와 여러 실행 주체의 영향부터 확인해야 한다.
동일 서비스의 전후 비교에서는 관찰 간격과 요청량을 맞춘다. 배포 직후의 순간 값과 장시간 누적 값을 비교하면 개선 여부가 왜곡된다. 작업자 수 변경은 대기열의 위치를 바꿀 뿐 처리 용량을 늘리지 못할 수 있다. 제한을 높이는 결정도 호스트의 다른 작업과 여유를 확인한 뒤에 판단할 일이다.
실무에서 자주 틀리는 것
1. 부하 평균 8을 사용률 800%로 읽는다
부하 평균이 8로 올라간 증상에서 CPU가 반드시 여덟 개 꽉 찼다는 오진이 나온다. 실제 값은 Linux에서 정한 활성 작업의 시간에 따른 평균이며 대기 항목을 포함할 수 있다. 사용률과 작업 상태의 변화를 함께 확인해야 계산 포화와 I/O 관련 정체를 나눈다. 단일 숫자가 아니라 같은 시간창의 변화 방향을 비교한다.
2. 호스트 CPU가 남으니 제한은 없다고 본다
호스트 사용률이 낮은데 컨테이너 응답이 일정 구간마다 늦어지는 증상이다. 흔한 오진은 외부 통신만 느리다는 것이다. 실제 원인에는 컨테이너의 시간 예산 소진과 상위 그룹 제한이 포함된다. 해당 그룹과 상위 제한, 제한 횟수의 증분을 확인하고 요청 지연과 시점을 대조한다.
3. 대기 중인 스레드를 모두 부하로 센다
스레드가 수백 개인데 부하 평균은 작게 나오는 증상을 측정 오류라고 판단한다. 하지만 작업의 존재와 부하 계산의 대상은 같지 않다. 입력을 기다리는 많은 스레드는 즉시 CPU를 요구하지 않을 수 있다. 전체 개수 대신 실행 가능 상태와 중단 불가능한 대기의 개수를 나누고 CPU를 받을 수 있는 범위도 확인한다.
지표마다 범위와 시간을 붙인다. 예를 들어 호스트 전체의 장기 부하와 특정 그룹의 짧은 제한 증분은 직접 대소 비교할 값이 아니다. 지표가 답할 수 있는 질문을 먼저 적으면 불필요한 작업자 증설을 줄일 수 있다. 진단 기록에는 추정 원인과 이를 반박할 다음 관찰을 함께 남긴다.
스스로 확인하기
- 논리 CPU가 8개 보이지만 cpu.max가 50000 100000이다. 이 그룹이 항상 8 CPU만큼 계산할 수 있다는 주장을 판단하라.
- nr_periods가 100에서 120, nr_throttled가 10에서 18로 늘었다. 관찰 구간의 제한 기간 비율과 이 값만으로 알 수 없는 것을 설명하라.
- 부하 평균은 오르고 CPU 사용률은 낮다. 확인할 작업 상태와 추가 제한 정보를 설명하라.
첫 문제는 거짓이며 예시의 기간당 평균 예산은 0.5 CPU다. 두 번째는 20기간 중 8기간, 즉 40%지만 요청 지연의 40%가 제한 때문이라는 뜻은 아니다. 세 번째는 중단 불가능한 대기와 실행 가능한 작업의 변화를 나누고, 그룹의 제한과 CPU 배치 범위를 확인한다. 어떤 답에서도 부하 평균만으로 디스크 고장이나 CPU 부족을 확정하지 않는다.
참고와 출처: Linux 커널 부하 평균 구현, Linux cgroup v2 문서, Linux 스케줄러 설계 문서를 사실 확인에 사용했다. 설명과 모형 코드는 독립 작성했으며 커널 소스 코드나 문장을 복제하지 않았다.
다음 7장에서는 CPU를 받을 수 있어도 메모리 때문에 진행이 늦어지거나 프로세스가 종료되는 상황을 살핀다. 가상 주소, 실제 상주 페이지, 메모리 제한을 분리하면 메모리가 남는다는 말의 기준부터 다시 정할 수 있다.