콘텐츠로 이동

CPU Scheduling

CPU 스케줄링은 운영체제가 실행 가능한 작업 중 누구에게 CPU 코어를 얼마나 줄지 결정하는 정책과 메커니즘이다.

여기서 작업은 보통 프로세스나 스레드다. process-thread.mdx에서 본 것처럼 Linux 커널은 프로세스와 스레드를 모두 스케줄 가능한 task로 다룬다. 이 문서에서는 이해를 쉽게 하기 위해 “프로세스”라고 부르되, 실제 CPU를 받는 단위는 스레드 또는 커널 task일 수 있다고 기억하면 된다.

CPU 스케줄러가 풀어야 하는 문제는 단순하지 않다.

  • 짧은 요청은 빨리 응답해야 한다.
  • 긴 배치 작업도 언젠가는 끝나야 한다.
  • CPU 코어는 한 순간에 제한된 수의 실행 흐름만 실행할 수 있다.
  • 실행 흐름을 너무 자주 바꾸면 실제 일을 하지 않고 전환 비용만 낸다.
  • 컨테이너나 서버리스 환경에서는 “CPU가 부족한 것”과 “CPU limit 때문에 throttling되는 것”을 구분해야 한다.

그래서 CPU 스케줄링은 “CPU 사용률을 높인다”가 아니라 응답 시간, 처리량, 공정성, 전환 비용 사이의 균형을 잡는다로 읽어야 한다.

첫 회독에서는 아래 순서만 따라간다.

  1. 1~5절에서 runnable/waiting, run queue, 선점, context switch와 고전 알고리즘의 trade-off를 잡는다.
  2. 6절 도입과 6.5절에서 cgroup quota가 만드는 throttling의 원리만 읽는다. CFS/EEVDF 내부 정책인 6.1~6.4절은 건너뛴다.
  3. 7절과 9절에서 Node.js event loop 지연과 OS 스케줄링 신호의 경계를 본다.
  4. 10.1절과 13절에서 worker 수, I/O 대기와 CPU 대기를 구분한다.
  5. 14절의 첫 회독 체크로 종료한다.

종료 조건은 다음과 같다.

  • runnable, waiting, run queue의 차이를 설명할 수 있다.
  • FCFS의 Convoy Effect를 손으로 계산할 수 있다.
  • SJF가 평균 대기 시간에 강하지만 starvation 위험이 있는 이유를 설명할 수 있다.
  • Round Robin에서 time quantum이 너무 작거나 클 때의 trade-off를 설명할 수 있다.
  • preemption과 context switch 비용이 왜 함께 등장하는지 설명할 수 있다.
  • CPU throttling과 CPU 100%를 구분할 수 있다.
  • Node.js Event Loop lag가 OS 스케줄링 문제인지 JS thread 블로킹 문제인지 나눠 볼 수 있다.
  • load average가 높을 때 CPU run queue와 iowait를 구분해 볼 수 있다.

선택 심화 절: 6.16.4절(CFS/EEVDF·nice 내부 모델), 8절(플랫폼 연결), 10.210.7절(cgroup tuning·우선순위·실시간 정책·affinity/NUMA·scheduler tuning), 11절(관찰 명령), 12절(런북 부록), 14절의 선택 심화 체크다. 특히 cgroup 값이나 커널 scheduler parameter는 원리를 읽은 뒤 격리된 실험에서만 조정한다.


이 문서는 OS 내부 용어가 많다. 처음 읽을 때는 아래 단어를 먼저 잡아두면 뒤의 알고리즘이 훨씬 덜 낯설다.

용어첫 의미왜 중요한가
프로세스(process)독립 주소 공간을 가진 실행 중인 프로그램OS가 자원과 권한을 붙여 관리하는 대표 실행 단위다.
스레드(thread)프로세스 안에서 CPU가 실제로 실행하는 흐름같은 프로세스 안의 여러 스레드는 메모리를 공유하지만 각각 CPU 스케줄링 대상이 된다.
runnableCPU만 받으면 바로 실행할 수 있는 상태I/O를 기다리는 작업과 CPU를 기다리는 작업을 구분하는 핵심 상태다.
waiting/sleepingI/O, lock, timer, 이벤트 등을 기다리는 상태CPU를 줘도 할 일이 없으므로 run queue에서 빠진다.
ready queue/run queuerunnable task들이 CPU 차례를 기다리는 큐run queue가 길어지면 “CPU를 쓰고 싶은데 못 쓰는” 대기가 늘어난다.
선점(preemption)OS가 실행 중인 task를 강제로 CPU에서 내리는 것긴 작업 하나가 CPU를 독점하지 못하게 한다.
time quantum/time slice한 task가 연속으로 CPU를 받을 수 있는 시간 조각너무 작으면 전환 비용이 커지고, 너무 크면 응답성이 나빠진다.
context switch현재 task 상태를 저장하고 다음 task 상태를 복원하는 전환필요한 비용이지만 너무 많으면 처리량을 갉아먹는다.
throughput단위 시간당 끝낸 작업량배치 처리, 백그라운드 작업, 전체 서버 처리량에서 중요하다.
latency하나의 작업이 기다리고 끝나는 데 걸린 시간API p99, CLI 반응성, UI 체감 속도와 연결된다.
fairness여러 task가 CPU를 납득 가능한 비율로 나눠 쓰는 성질특정 task가 굶는 starvation을 막는다.
starvation어떤 task가 계속 밀려 실행 기회를 거의 못 받는 상태평균 지표는 괜찮아도 일부 요청이나 낮은 우선순위 작업이 망가진다.
CFSCompletely Fair Scheduler, Linux의 기존 공정 스케줄러vruntime으로 “얼마나 덜 받았는가”를 추적했다.
EEVDFEarliest Eligible Virtual Deadline First, Linux 6.6+ 공정 스케줄러fairness에 latency 요구를 더 명시적으로 반영한다.
niceLinux 일반 task의 CPU 가중치를 조정하는 값낮은 nice는 CPU를 더 받고, 높은 nice는 덜 받는다.
CPU throttlingcgroup quota 때문에 runnable이어도 CPU 실행이 막히는 상태CPU 사용률만 보면 원인을 놓치기 쉽다.

중요한 경계: runnable은 “CPU를 받으면 당장 실행 가능”하다는 뜻이고, waiting은 “CPU를 받아도 지금은 진행할 수 없음”이라는 뜻이다. 서버가 느릴 때 이 둘을 구분하지 못하면 CPU 문제와 I/O 문제를 계속 섞어 보게 된다.


2.5 선행 기술의 한계 - CPU 스케줄러가 등장한 이유

섹션 제목: “2.5 선행 기술의 한계 - CPU 스케줄러가 등장한 이유”

초기 컴퓨터는 한 번에 하나의 프로그램을 끝까지 실행하는 방식에 가까웠다. 작업이 하나뿐이면 “누구에게 CPU를 줄 것인가”라는 질문이 거의 없다. 그러나 여러 사용자가 같은 머신을 쓰고, 터미널이 대화형으로 바뀌고, 백그라운드 작업과 짧은 명령이 동시에 들어오면서 이 단순 모델은 바로 깨졌다.

가장 쉬운 실패는 FCFS(First Come First Served) 모델이다. 먼저 온 작업을 끝까지 실행하면 구현은 단순하지만, 긴 작업 하나가 앞에 있다는 이유만으로 뒤의 짧은 작업이 오래 기다린다.

도착 순서:
P1 = 30ms CPU 작업
P2 = 3ms CPU 작업
P3 = 3ms CPU 작업
FCFS 실행:
| P1 0~30 | P2 30~33 | P3 33~36 |
대기 시간:
P1 = 0ms
P2 = 30ms
P3 = 33ms
평균 대기 시간 = (0 + 30 + 33) / 3 = 21ms

같은 작업이라도 짧은 작업을 먼저 처리하면 결과가 완전히 달라진다.

순서만 바꾼 실행:
| P2 0~3 | P3 3~6 | P1 6~36 |
대기 시간:
P2 = 0ms
P3 = 3ms
P1 = 6ms
평균 대기 시간 = (0 + 3 + 6) / 3 = 3ms

이 차이가 CPU 스케줄러의 출발점이다. OS는 “작업을 받은 순서대로 끝까지 돌린다”로는 대화형 응답성과 전체 처리량을 동시에 만족시킬 수 없다. 그래서 CPU를 작은 시간 조각으로 나누고, 실행 가능한 task들의 상태와 우선순위를 보면서 다음 실행 대상을 계속 고른다.

Linux 스케줄러의 역사도 이 문제를 반복해서 다시 푼 과정이다.

Linux 스케줄러가 반복해서 푼 한계

  1. ~2.4: O(n)

    다음 task를 고르기 위해 run queue를 훑는 비용이 task 수에 비례했다.

  2. 2.6.0: O(1)

    선택 비용은 줄였지만 interactive task를 휴리스틱으로 추정해 mixed workload에서 오분류가 생겼다.

  3. 2.6.23: CFS

    vruntime이 가장 작은 task를 고르는 방식으로 공정성을 중심에 놓았다.

  4. 6.6+: EEVDF

    eligible task와 virtual deadline을 이용해 공정성뿐 아니라 latency 요구를 더 직접 표현한다.

이 흐름에서 중요한 것은 연도 암기가 아니다. 스케줄러는 늘 같은 질문으로 돌아온다.

  1. 지금 CPU를 받을 수 있는 task는 무엇인가?
  2. 누가 너무 오래 기다렸는가?
  3. 누가 짧은 응답 시간을 요구하는가?
  4. 우선순위를 반영하되 starvation을 만들지는 않는가?
  5. 전환 비용보다 전환으로 얻는 응답성 이득이 큰가?

3. CPU 스케줄러가 최적화하는 것

섹션 제목: “3. CPU 스케줄러가 최적화하는 것”

CPU 스케줄러는 하나의 숫자만 최적화하지 않는다. 아래 목표들은 서로 자주 충돌한다.

목표의미좋아지는 선택나빠질 수 있는 것
CPU utilizationCPU가 놀지 않는 비율waiting task가 생기면 바로 다른 runnable task를 실행전환이 너무 잦으면 오버헤드 증가
throughput단위 시간당 완료 작업 수긴 배치를 방해하지 않고 몰아서 처리짧은 요청 latency 악화
latency한 요청이 기다리고 끝나는 시간선점, 작은 time slice, interactive task 우대cache locality와 throughput 악화
fairnessCPU를 납득 가능한 비율로 나눔vruntime, aging, weight 기반 배분특정 high-priority 작업의 절대 latency 악화
predictabilityp99/p999가 튀지 않음격리, quota, affinity, RT 정책 일부자원 활용률과 운영 복잡도 악화

CPU 스케줄링은 평균보다 tail latency에서 자주 문제를 드러낸다. 평균 응답이 낮아도, 소수 요청이 2초짜리 CPU 작업 뒤에 줄 서면 p99/p999나 max에서 incident가 드러난다.

요청 100개 중 99개는 20ms에 끝남
1개는 CPU 집약 작업 뒤에 밀려 2000ms에 끝남
평균 = (99 * 20 + 1 * 2000) / 100 = 39.8ms
p99는 표본/정렬 방식에 따라 20ms처럼 보일 수 있지만,
p100/max와 일부 사용자 체감 incident는 2000ms 쪽에서 발생한다.

그래서 운영에서는 평균 CPU 사용률보다 다음 질문이 더 중요할 때가 많다.

  • runnable task가 코어 수보다 계속 많은가?
  • p99 latency가 CPU burst와 같이 튀는가?
  • event loop lag가 CPU 집약 코드 배포 이후 증가했는가?
  • context switch가 급증했지만 throughput은 오히려 떨어졌는가?
  • cgroup quota 때문에 throttling되는 시간이 늘었는가?

3.2 CPU 사용률 100%가 항상 나쁜 것은 아니다

섹션 제목: “3.2 CPU 사용률 100%가 항상 나쁜 것은 아니다”

CPU 100%는 두 가지로 나뉜다.

상황해석먼저 볼 것
배치 worker가 CPU 100%로 꾸준히 처리하고 backlog가 줄어든다정상적인 CPU 포화일 수 있다throughput, backlog 감소 속도
API p99가 튀고 run queue가 길며 throttling도 증가한다사용자 요청이 CPU를 기다리는 장애 신호다p99, run queue, cgroup throttle
CPU는 20%인데 load average가 높고 iowait가 크다CPU 부족이 아니라 I/O 대기일 수 있다iowait, disk latency, uninterruptible sleep
CPU는 낮고 event loop lag가 높다단일 스레드 블로킹 또는 GC/동기 코드일 수 있다Node event loop lag, CPU profile

CPU 스케줄링 문서를 읽는 목표는 “CPU 100%면 worker를 늘린다” 같은 반사 행동을 피하는 것이다. 어떤 대기열이 길어졌는지 먼저 구분해야 한다.


CPU 스케줄러가 보는 핵심 상태는 단순하다.

New
-> Ready/Runnable
-> Running
-> Waiting/Sleeping
-> Ready/Runnable
-> Terminated

각 상태는 다음처럼 읽는다.

상태의미CPU 스케줄러 관점
Ready/RunnableCPU만 받으면 실행 가능run queue에서 순서를 기다린다.
Running지금 CPU 코어에서 실행 중time slice가 끝나거나 더 높은 우선순위 task가 오면 내려갈 수 있다.
Waiting/SleepingI/O, lock, timer, 이벤트를 기다림CPU를 줘도 진행할 수 없으므로 보통 run queue 밖에 있다.
Terminated종료됨스케줄링 대상이 아니다.

Node.js의 await fs.promises.readFile()을 떠올려도 된다. 파일 I/O가 끝나기 전에는 해당 continuation이 기다리는 상태이고, CPU를 계속 붙잡을 필요가 없다. 반대로 큰 배열을 동기적으로 정렬하는 코드는 CPU를 받는 동안 계속 runnable/running 상태로 남아 다른 콜백을 밀어낸다.

교과서에서는 ready queue라고 부르고, Linux에서는 CPU별 run queue라는 표현을 자주 쓴다. 둘 다 “CPU를 받을 준비가 된 실행 흐름들이 줄 서 있는 곳”으로 이해하면 된다.

작은 2코어 서버를 생각해보자.

CPU 코어 수 = 2
runnable task = A, B, C, D, E
동시에 실행 가능: 2개
나머지 runnable task: 3개는 CPU를 기다림

이 상태가 짧은 burst라면 정상이다. 그러나 runnable task가 2코어 서버에서 계속 8개, 12개로 유지되면 CPU 대기열이 쌓이고 응답 시간이 길어진다.

정량 감각:

지표대략적 해석
runnable task 수 ≈ 코어 수CPU가 꽉 차지만 대기는 제한적이다.
runnable task 수가 코어 수의 2배 이상 지속CPU 대기 시간이 사용자 latency로 드러날 수 있다.
load average가 코어 수보다 크고 iowait는 낮음CPU run queue가 길 가능성이 높다.
load average가 큰데 iowait가 높음CPU보다 I/O 대기나 D-state task를 먼저 봐야 한다.

**선점(preemption)**은 OS가 실행 중인 task를 중간에 멈추고 다른 task에게 CPU를 주는 능력이다. 선점이 없으면 긴 CPU burst가 끝날 때까지 뒤의 짧은 작업은 기다린다.

time quantum/time slice는 한 task가 연속으로 CPU를 받을 수 있는 시간 조각이다.

time quantum이 너무 작다:
+ 짧은 요청이 빨리 차례를 받는다.
- context switch가 너무 많아진다.
time quantum이 너무 크다:
+ cache locality와 throughput이 좋아질 수 있다.
- FCFS처럼 긴 작업이 뒤 요청을 밀어낸다.

context switch는 CPU가 현재 실행 흐름의 레지스터, 프로그램 카운터, 스택 포인터 같은 상태를 저장하고 다음 실행 흐름의 상태를 복원하는 작업이다. 프로세스 전환이라면 주소 공간과 TLB/cache 영향까지 따라올 수 있다.

비용은 환경마다 다르지만 학습용 감각은 이렇게 잡으면 된다.

비용 원천설명체감
레지스터 저장/복원CPU 실행 상태를 바꾼다아주 작지만 빈번하면 누적된다.
커널 진입/스케줄러 실행다음 task를 고른다runnable task가 많고 lock 경합이 있으면 커진다.
cache/TLB 영향방금 쓰던 데이터가 캐시에서 밀릴 수 있다CPU 집약 작업에서는 실제 비용이 커진다.
lock handofflock을 기다리던 thread가 깨어나며 전환이 이어진다context switch가 throughput을 갉아먹는다.

수치 감각: context switch 자체는 마이크로초 단위로 끝날 수 있지만, cache miss와 lock 경합까지 포함하면 “전환이 많다”는 현상은 쉽게 밀리초 단위 tail latency로 번진다. 단일 서버에서 초당 context switch가 평소 10,000 수준이던 것이 100,000 이상으로 뛰고 throughput이 떨어지면 worker 수, lock, thread pool 크기를 의심할 만하다.


5. 고전 스케줄링 알고리즘으로 직관 만들기

섹션 제목: “5. 고전 스케줄링 알고리즘으로 직관 만들기”

현대 Linux의 CFS/EEVDF를 바로 읽으면 추상적이다. 먼저 FCFS, SJF, Round Robin, Priority Scheduling을 손으로 계산해보면 스케줄러가 왜 복잡해졌는지 보인다.

5.1 FCFS - 단순하지만 convoy effect가 생긴다

섹션 제목: “5.1 FCFS - 단순하지만 convoy effect가 생긴다”

FCFS(First Come First Served)는 먼저 온 작업을 먼저 실행한다. 비선점 방식으로 설명하는 경우가 많다.

장점:

  • 구현이 쉽다.
  • 도착 순서가 명확하다.
  • starvation은 잘 생기지 않는다.

문제:

  • 긴 작업이 앞에 있으면 짧은 작업이 뒤에서 오래 기다린다.
  • 평균 대기 시간과 tail latency가 입력 순서에 크게 흔들린다.
  • 대화형 시스템에는 부적합하다.

worked example: convoy effect

프로세스:
P1 = 30ms CPU burst
P2 = 3ms CPU burst
P3 = 3ms CPU burst
FCFS:
| P1 0~30 | P2 30~33 | P3 33~36 |
P2는 자기 실행 시간의 10배를 기다린다.
P3는 자기 실행 시간의 11배를 기다린다.

반례: “FCFS는 항상 나쁘다”는 말도 틀렸다. 모든 작업 길이가 거의 같고, 처리 순서가 외부 계약이라면 FCFS는 단순하고 예측 가능하다. 문제는 길이가 크게 다른 작업이 섞일 때다.

5.2 SJF/SRTF - 평균 대기 시간은 좋지만 미래를 알아야 한다

섹션 제목: “5.2 SJF/SRTF - 평균 대기 시간은 좋지만 미래를 알아야 한다”

SJF(Shortest Job First)는 CPU burst가 짧은 작업을 먼저 실행한다. SRTF(Shortest Remaining Time First)는 남은 시간이 더 짧은 새 작업이 들어오면 현재 작업을 선점하는 SJF의 선점 버전이다.

worked example: 평균 대기 시간 개선

프로세스:
P1 = 6ms
P2 = 8ms
P3 = 7ms
P4 = 3ms
SJF 실행:
| P4 0~3 | P1 3~9 | P3 9~16 | P2 16~24 |
대기 시간:
P4 = 0ms
P1 = 3ms
P3 = 9ms
P2 = 16ms
평균 대기 시간 = (0 + 3 + 9 + 16) / 4 = 7ms

SJF의 큰 한계는 미래의 CPU burst를 정확히 모른다는 점이다. 실제 시스템은 과거 실행 시간을 보고 예측할 수밖에 없다.

다음 burst 예측:
tau(n+1) = alpha * 실제 burst(n) + (1 - alpha) * tau(n)
alpha가 크다:
최근 변화에 빨리 반응한다.
잡음에도 쉽게 흔들린다.
alpha가 작다:
안정적이다.
갑자기 길어진 작업이나 짧아진 작업을 늦게 따라간다.

worked example: EMA 예측이 조용히 틀리는 경우

alpha = 0.5
이전 예측 tau = 10ms
실제 burst = 100ms
다음 예측 = 0.5 * 100 + 0.5 * 10 = 55ms

한 번 긴 CPU burst가 나오면 예측값이 55ms로 올라간다. 그 뒤 실제 작업이 다시 3ms짜리로 짧아져도 스케줄러는 잠시 동안 그 작업을 “길 것”으로 본다. 반대로 alpha를 너무 작게 두면 100ms burst를 늦게 따라가고, 너무 크게 두면 잡음 하나에 순서가 흔들린다. 그래서 SJF 계열은 평균 대기 시간 직관을 주지만, 실제 시스템에서는 예측 오차가 latency의 silent failure가 될 수 있다.

counterexample: SJF의 starvation

긴 작업 L = 100ms가 대기 중이다.
매 2ms마다 짧은 작업 S = 1ms가 계속 도착한다.
SJF는 매번 S를 먼저 고른다.
L은 이론상 계속 뒤로 밀릴 수 있다.

SJF는 평균 대기 시간을 줄이는 데 강하지만, 긴 작업을 보호하는 aging이나 fairness 장치가 없으면 starvation이 생긴다.

5.3 Round Robin - 응답성을 위해 시간을 조각낸다

섹션 제목: “5.3 Round Robin - 응답성을 위해 시간을 조각낸다”

Round Robin은 각 task에게 time quantum만큼 CPU를 주고, 시간이 끝나면 큐 뒤로 보낸다.

worked example: q = 4ms

프로세스:
P1 = 24ms
P2 = 3ms
P3 = 3ms
Round Robin q=4ms:
| P1 0~4 | P2 4~7 | P3 7~10 | P1 10~14 | P1 14~18 | P1 18~22 | P1 22~26 | P1 26~30 |
짧은 P2, P3는 P1 전체 24ms가 끝나기 전에 완료된다.

quantum trade-off

quantum결과
너무 작음context switch가 많아져 CPU를 전환에 쓴다.
적당함대화형 응답성과 처리량의 균형을 잡는다.
너무 큼사실상 FCFS처럼 동작한다.

수치 감각:

context switch 비용을 50us로 가정한다.
quantum = 1ms이면 전환 비용만 약 5%가 될 수 있다.
quantum = 10ms이면 전환 비용은 약 0.5%로 줄지만, 짧은 요청 대기 시간이 늘 수 있다.

실제 Linux의 일반 스케줄러는 단순 Round Robin은 아니지만, “시간을 너무 잘게 쪼개면 전환 비용이 커지고, 너무 크게 쪼개면 응답성이 나빠진다”는 직관은 그대로 남는다.

5.4 Priority Scheduling - 중요한 일을 먼저 하되 starvation을 막아야 한다

섹션 제목: “5.4 Priority Scheduling - 중요한 일을 먼저 하되 starvation을 막아야 한다”

Priority Scheduling은 각 task에 우선순위를 붙이고 높은 우선순위를 먼저 실행한다.

장점:

  • 중요한 작업을 더 빨리 처리할 수 있다.
  • batch와 interactive workload를 구분하기 쉽다.
  • 실시간 시스템의 일부 요구를 표현할 수 있다.

문제:

  • 높은 우선순위 작업이 계속 들어오면 낮은 우선순위 작업이 굶는다.
  • priority 값을 크게 벌려도 “시간” 기준의 보장이 자동으로 생기지는 않는다.
  • 잘못된 우선순위는 시스템 전체를 더 불공정하게 만든다.

aging은 오래 기다린 task의 effective priority를 점점 올리는 방법이다.

base priority gap = 20단계
aging interval = 10초마다 1단계 상승
낮은 우선순위 task가 높은 우선순위 task와 같은 수준까지 올라오는 데:
20 * 10초 = 200초

반례: “aging이 있으니 starvation이 없다”는 말은 충분하지 않다. SLA가 1초인데 aging으로 200초 뒤에야 우선순위가 따라잡힌다면 운영 관점에서는 이미 starvation이다. 이 경우 스케줄러는 결국 실행해주지만, 사용자가 보는 계약은 이미 깨졌다. 그래서 starvation은 “영원히 실행되지 않음”뿐 아니라 “필요한 시간 안에 실행되지 않음”으로도 읽어야 한다.

5.5 Multilevel Feedback Queue - 작업 성격을 실행 중에 추정한다

섹션 제목: “5.5 Multilevel Feedback Queue - 작업 성격을 실행 중에 추정한다”

Multilevel Queue는 ready queue를 여러 단계로 나누고 각 단계에 다른 정책을 적용한다. Multilevel Feedback Queue(MLFQ)는 task가 실행 패턴에 따라 큐 사이를 이동할 수 있게 한다.

높은 큐:
interactive task
짧은 quantum
빠른 응답
낮은 큐:
CPU-bound batch task
긴 quantum 또는 FCFS
처리량 중심

CPU를 오래 쓰면 낮은 큐로 내려가고, I/O를 기다리며 자주 잠드는 interactive task는 높은 큐에 남는 식이다. 이 방식은 “실행 시간이 짧은지 긴지 미리 모른다”는 SJF의 한계를 runtime behavior로 완화한다.

오해 방지: MLFQ는 마법이 아니다. CPU-bound 작업이 interactive처럼 보이게 의도적으로 자주 sleep하면 우대를 받을 수 있고, interactive 판정 휴리스틱이 workload를 오분류하면 latency가 튄다. Linux 스케줄러가 CFS/EEVDF로 옮겨간 배경에는 이런 휴리스틱의 한계도 있다.


6. Linux의 공정 스케줄링: CFS와 EEVDF (6.1~6.4 선택 심화)

섹션 제목: “6. Linux의 공정 스케줄링: CFS와 EEVDF (6.1~6.4 선택 심화)”

CFS(Completely Fair Scheduler)는 이름처럼 모든 runnable task가 이상적인 CPU를 공정하게 나눠 쓰는 모델을 목표로 했다. 핵심 값은 vruntime(virtual runtime) 이다.

vruntime은 “이 task가 가중치를 고려했을 때 CPU를 얼마나 받은 것으로 볼 것인가”를 나타낸다.

task가 CPU를 실제로 사용한다
-> vruntime이 증가한다
nice가 낮아 weight가 크다
-> vruntime이 천천히 증가한다
-> 더 자주 선택될 수 있다
nice가 높아 weight가 작다
-> vruntime이 빨리 증가한다
-> CPU를 덜 받는다

CFS는 runnable task 중 vruntime이 가장 작은 task를 고른다. 직관적으로는 “지금까지 덜 받은 task에게 다음 CPU를 준다”에 가깝다.

6.2 nice는 우선순위라기보다 CPU 가중치다

섹션 제목: “6.2 nice는 우선순위라기보다 CPU 가중치다”

Linux 일반 task의 nice 값은 보통 -20부터 +19까지다.

nice의미CPU 배분 직관
-20매우 높은 가중치다른 일반 task보다 훨씬 많은 CPU를 받는다.
0기본값보통의 일반 프로세스다.
+10낮은 가중치CPU 경합 시 양보하는 쪽에 가깝다.
+19매우 낮은 가중치idle에 가까운 백그라운드 작업에 가깝다.

worked example: nice 0 vs nice +10

Linux의 실제 weight table은 단계마다 약 1.25배 차이를 갖도록 설계되어 있다. 학습용으로 다음 숫자만 기억해도 충분하다.

nice 0 weight ≈ 1024
nice +10 weight ≈ 110
두 task가 같은 CPU에서 계속 runnable이라면:
nice 0 몫 = 1024 / (1024 + 110) ≈ 90.3%
nice +10 몫 = 110 / (1024 + 110) ≈ 9.7%

따라서 renice +10은 “이 프로세스를 느리게 한다”가 아니라 “경합이 있을 때 CPU 지분을 크게 낮춘다”로 읽어야 한다. CPU가 남아 있으면 nice +10 프로세스도 충분히 실행된다.

반례: nice는 CPU hard limit이 아니다. 배치 작업에 nice +19를 줘도 다른 runnable task가 없으면 그 배치 작업은 CPU를 거의 100% 사용할 수 있다. CPU 사용량 상한을 강제하려면 cgroup quota나 컨테이너 limit을 봐야 한다.

CFS는 runnable task를 vruntime 기준으로 정렬해 관리한다. 핵심 연산은 세 가지다.

  1. 가장 작은 vruntime task 찾기
  2. task 삽입
  3. task 삭제 또는 재배치

정렬 배열을 쓰면 삽입/삭제가 비싸고, 단순 큐를 쓰면 vruntime 최솟값을 빨리 찾기 어렵다. 그래서 CFS는 균형 트리 계열 구조로 runnable task를 관리한다. 학습 관점에서 중요한 것은 자료구조 이름보다 “공정성을 유지하려면 단순 FIFO보다 더 많은 상태를 추적해야 한다” 는 점이다.

6.4 EEVDF의 핵심: eligible과 virtual deadline

섹션 제목: “6.4 EEVDF의 핵심: eligible과 virtual deadline”

Linux 6.6 이후 기본 공정 스케줄러는 EEVDF(Earliest Eligible Virtual Deadline First) 방향으로 이동했다. EEVDF도 공정성을 버린 것이 아니다. CFS의 vruntime 기반 사고 위에 eligible 여부virtual deadline을 더해 latency 요구를 더 잘 표현하려는 접근이다.

학습용으로는 아래처럼 잡으면 된다.

개념직관
lag이상적으로 받았어야 할 CPU와 실제 받은 CPU의 차이
eligible지금 CPU를 받을 자격이 있는 상태
virtual deadline이 task를 언제까지 실행해주는 것이 좋은지 나타내는 가상 마감
weightnice 등으로 정해지는 CPU 지분

아주 단순화하면 EEVDF는 다음 질문을 한다.

1. 공정성 관점에서 지금 CPU를 받을 자격이 있는 task인가?
2. 그중 virtual deadline이 가장 이른 task는 누구인가?
3. weight가 큰 task는 deadline이 더 가깝게 잡히므로 더 자주 선택될 수 있다.

처음 읽을 때는 아래 식을 외우기보다 각 항의 의미만 잡으면 된다.

virtual_deadline ~= virtual_eligible_time + requested_slice / weight

requested_slice가 길면 deadline이 뒤로 밀리고, weight가 크면 같은 slice라도 deadline이 더 가까워진다. 즉 EEVDF는 “덜 받은 task”만 보는 것이 아니라 “받을 자격이 있는 task 중 누가 더 급한가”를 같이 모델링한다.

작은 숫자로 보면 방향이 더 선명하다.

task A: requested_slice = 4ms, weight = 1
task B: requested_slice = 4ms, weight = 2
A의 deadline 증가분 ~= 4 / 1 = 4
B의 deadline 증가분 ~= 4 / 2 = 2

같은 실행 시간을 요청해도 weight가 큰 B의 virtual deadline이 더 가까워진다. 이것은 “무조건 B만 실행”이 아니라, 공정성 lag와 함께 보면서 B가 CPU를 더 자주 받을 수 있다는 뜻이다.

왜 CFS만으로 충분하지 않았나: CFS는 “지금까지 덜 받은 task”를 고르는 데 강했다. 하지만 interactive latency 요구를 표현하려면 여러 휴리스틱이 필요했다. EEVDF는 latency 요구를 virtual deadline이라는 더 직접적인 형태로 스케줄러 모델에 넣으려는 시도다.

처음 읽을 때 EEVDF 수식까지 외울 필요는 없다. 중요한 것은 현대 스케줄러가 단순 priority queue가 아니라 fairness와 latency를 동시에 모델링한다는 점이다.

6.5 cgroup CPU quota와 throttling (첫 회독)

섹션 제목: “6.5 cgroup CPU quota와 throttling (첫 회독)”

컨테이너 환경에서는 Linux 스케줄러가 공정하게 CPU를 나눠주더라도, cgroup quota가 별도로 CPU 사용량을 제한할 수 있다.

예를 들어 cpu.max = 100000 100000이면 100ms period마다 CPU time 100ms를 쓸 수 있다는 뜻이다. wall-clock 기준으로는 1코어 한도와 비슷하다. 2개 이상의 thread가 동시에 CPU를 요구하면 50ms wall time 동안 총 100ms CPU time을 태울 수 있고, quota를 빨리 소진한 뒤 남은 period 동안 throttling될 수 있다.

period = 100ms
quota = 100ms
worker 2개가 각각 CPU를 계속 요구
처음 50ms 동안:
worker A 50ms + worker B 50ms = quota 100ms 소진
남은 50ms:
runnable이어도 cgroup quota 때문에 실행 불가
p99 latency가 튄다.

오해 방지: CPU throttling은 nice와 다르다. nice는 경합 중 CPU 지분을 조정한다. quota는 cgroup 전체가 쓸 수 있는 CPU 시간의 상한을 만든다.


7. Node.js Event Loop와 OS 스케줄러의 경계

섹션 제목: “7. Node.js Event Loop와 OS 스케줄러의 경계”

Node.js Event Loop 문서가 이미 이벤트 루프의 내부 페이즈, microtask, macrotask, event loop lag를 다룬다. 여기서는 OS 스케줄링과의 경계만 정리한다.

7.1 같은 말처럼 보이지만 층이 다르다

섹션 제목: “7.1 같은 말처럼 보이지만 층이 다르다”
질문OS CPU 스케줄러Node.js Event Loop
무엇을 고르는가runnable task/thread 중 다음 CPU 실행 대상준비된 callback 중 다음 JS 실행 대상
누가 제어하는가Linux 커널Node.js/libuv/V8 런타임
선점 가능한가일반적으로 timer interrupt 등으로 선점 가능JS callback 실행 중에는 런타임이 임의로 중간 선점하지 않는다
긴 작업의 영향다른 task/thread가 CPU를 덜 받는다같은 event loop의 모든 JS callback이 밀린다
대표 지표run queue, context switch, CPU throttlingevent loop lag, ELU(Event Loop Utilization), callback delay

따라서 “Event Loop는 OS 스케줄러와 같다”는 비유는 조심해야 한다. 둘 다 “다음 실행 대상을 고른다”는 점은 닮았지만, OS 스케줄러는 CPU 코어를 나누고 Node Event Loop는 한 JS thread 안의 callback 순서를 정한다.

7.2 Event Loop lag를 CPU 스케줄링 관점으로 해석하기

섹션 제목: “7.2 Event Loop lag를 CPU 스케줄링 관점으로 해석하기”

Node.js에서 동기 CPU 작업이 500ms 걸리면 그 500ms 동안 같은 event loop의 다른 callback은 실행되지 못한다. OS 입장에서는 해당 Node thread가 CPU를 쓰는 runnable task일 뿐이다. 같은 서버의 다른 프로세스는 OS가 선점해서 실행할 수 있지만, 같은 Node event loop 안의 callback들은 JS 실행이 끝날 때까지 기다린다.

HTTP 요청 A:
대용량 JSON.stringify 500ms
HTTP 요청 B:
DB 결과 callback은 이미 준비됨
하지만 JS thread가 A를 실행 중이라 callback 실행 대기
OS 관점:
Node thread가 CPU를 사용 중
Node 관점:
B callback은 event loop lag만큼 밀림

의사결정 기준:

조건선택
동기 CPU 점유가 10~30ms 이하이고 작은 chunk로 나눌 수 있다setImmediate/chunking으로 event loop에 양보
CPU 작업이 100ms 이상이고 요청 경로에 있다Worker Thread 또는 별도 프로세스 검토
작업 재시도, 영속성, 분산 처리가 필요하다BullMQ/RabbitMQ 같은 외부 큐 검토
HTTP 서버 자체를 멀티코어로 확장하고 싶다cluster/PM2 cluster 또는 컨테이너 replica

이 기준은 CPU 스케줄러의 원리를 Node로 그대로 옮긴 것이 아니라, OS 실행 단위와 Node callback 대기열을 구분한 뒤 세운 운영 판단이다.


8. 선택 심화 - Kubernetes, ECS, Kafka와의 연결은 어디까지인가

섹션 제목: “8. 선택 심화 - Kubernetes, ECS, Kafka와의 연결은 어디까지인가”

CPU 스케줄링의 사고방식은 다른 계층의 리소스 정책을 읽는 데 도움이 된다. 하지만 K8s scheduler, ECS placement, Kafka consumer rebalance를 CPU 스케줄러와 같은 것으로 보면 안 된다.

계층무엇을 배치/선택하는가CPU 스케줄링과 닮은 점다른 점
Linux CPU schedulerrunnable task를 CPU 코어에 실행선점, 공정성, latency, priority마이크로초~밀리초 단위로 반복
K8s schedulerPending Pod를 Node에 배치priority, preemption, resource request한 번 배치한 뒤 실행 중 CPU 시간까지 직접 나누지는 않음
ECS placementtask를 container instance/AZ에 배치binpack/spread 같은 자원 선택OS-level time sharing이 아님
Kafka consumer grouppartition을 consumer에 할당fairness unit과 lag 개념CPU가 아니라 log offset 처리량 문제
Node Event Loopcallback 실행 순서 결정queue, priority, starvation 가능성JS thread 내부의 런타임 정책

이 문서에서 가져갈 전이 가능한 질문은 아래 정도다.

  1. lifecycle 제어 주체는 누구인가?
  2. 공정성 단위는 task, pod, partition, callback 중 무엇인가?
  3. lag는 시간, queue length, offset 차이 중 무엇으로 정의되는가?
  4. 선점이 가능한가, 아니면 새 배치부터만 반영되는가?
  5. 낮은 우선순위 작업이 starvation될 때 감지 지표가 있는가?

이 질문들은 짧은 decision echo로 충분하다. CPU 스케줄링 본문을 K8s나 Kafka 설명으로 대체하면 OS 개념이 흐려진다.


9. 실패 신호로 읽는 CPU 스케줄링

섹션 제목: “9. 실패 신호로 읽는 CPU 스케줄링”

운영에서 CPU 스케줄링 문제는 “스케줄러가 틀렸다”로 드러나는 경우보다, 응답 지연과 대기열의 조합으로 드러난다.

9.1 p99 latency 상승 + CPU run queue 증가

섹션 제목: “9.1 p99 latency 상승 + CPU run queue 증가”

증상:

  • API p99가 평소 100ms에서 800ms 이상으로 상승한다.
  • CPU 사용률이 높다.
  • load average가 코어 수보다 계속 크다.
  • iowait는 높지 않다.

의미: runnable task가 CPU를 기다리는 시간이 늘었을 가능성이 크다. worker 수가 너무 많거나, CPU 집약 코드가 요청 경로에 들어왔거나, CPU limit이 낮을 수 있다.

먼저 볼 것:

  • 최근 배포에서 동기 CPU 작업이 늘었는가?
  • worker/process 수가 코어 수를 크게 초과했는가?
  • cgroup throttling이 증가했는가?
  • context switch가 평소보다 급증했는가?

증상:

  • Node.js event loop lag p99가 100ms 이상으로 튄다.
  • CPU는 한 코어에 집중된다.
  • 특정 요청 뒤에 다른 요청도 같이 느려진다.

의미: OS가 다른 프로세스를 스케줄할 수 있더라도, 해당 Node process의 JS callback들은 긴 동기 작업 뒤에 줄 서 있다. CPU 스케줄러 문제라기보다 Node 실행 모델과 CPU-bound 코드의 경계 문제다.

먼저 볼 것:

  • 대용량 JSON parse/stringify
  • 복잡한 정규식
  • 동기 압축/암호화
  • 큰 배열 sort/map/filter
  • 너무 큰 microtask 체인

9.3 context switch 급증 + throughput 하락

섹션 제목: “9.3 context switch 급증 + throughput 하락”

증상:

  • vmstatcs가 평소보다 크게 증가한다.
  • CPU 사용률은 높지만 처리량은 오히려 줄어든다.
  • thread pool을 늘린 뒤 문제가 시작됐다.

의미: 실제 연산보다 실행 흐름 전환, lock 경합, cache miss에 CPU를 쓰고 있을 수 있다.

counterexample: context switch가 증가했다고 항상 나쁜 것은 아니다. I/O-bound 서버에서 정상적으로 많은 연결을 처리하면 voluntary context switch가 늘 수 있다. 문제는 context switch 증가와 throughput 하락, p99 상승이 함께 나타날 때다.

9.4 높은 load average + 낮은 CPU 사용률

섹션 제목: “9.4 높은 load average + 낮은 CPU 사용률”

증상:

  • load average가 코어 수보다 크다.
  • CPU idle이 남아 있다.
  • iowait가 높거나 D-state task가 많다.

의미: Linux load average에는 CPU runnable task뿐 아니라 uninterruptible sleep 상태의 task도 포함될 수 있다. CPU 스케줄링만 보고 worker를 늘리면 오히려 악화될 수 있다.

먼저 볼 것:

  • disk latency
  • network filesystem
  • database I/O
  • swap
  • D-state task

증상:

  • 컨테이너 CPU 사용률은 limit 근처다.
  • p99가 period 단위로 톱니처럼 튄다.
  • nr_throttled / nr_periods 비율이 증가한다.

의미: 프로세스가 runnable이어도 cgroup quota 때문에 실행이 막히는 시간대가 생긴다.

수치 감각:

throttled 비율해석
0~1%보통 큰 문제 아님
5~10% 지속burst 여유가 부족할 수 있음
25% 이상 지속CPU limit 또는 worker 수 조정 검토 필요

9.6 nice 조정 후 낮은 우선순위 작업이 끝나지 않음

섹션 제목: “9.6 nice 조정 후 낮은 우선순위 작업이 끝나지 않음”

증상:

  • batch/autovacuum/background worker에 높은 nice 값을 줬다.
  • API p99는 좋아졌지만 backlog나 dead tuple이 계속 쌓인다.
  • 낮은 우선순위 작업이 완료되지 않아 며칠 뒤 더 큰 장애가 생긴다.

의미: nice는 양보를 만들지만 완료 보장을 만들지는 않는다. 낮은 우선순위 작업도 언젠가 필요한 만큼 CPU를 받아야 한다.

판단 기준:

  • batch backlog가 줄고 있는가?
  • maintenance 작업의 지연이 데이터 품질이나 저장공간 문제로 번지지 않는가?
  • 낮은 priority가 “느리지만 진행”인지 “사실상 starvation”인지 구분되는가?

10.1 worker/process 수를 늘릴지 줄일지

섹션 제목: “10.1 worker/process 수를 늘릴지 줄일지”

CPU 관련 문제를 만나면 worker를 늘리는 결정을 자주 한다. 그러나 worker 수 증가는 항상 처리량 증가로 이어지지 않는다.

상황worker 증가 효과판단
코어가 남고 runnable task가 적다처리량이 늘 수 있다worker 증가 검토
코어 수보다 CPU-bound worker가 이미 많다context switch와 cache miss가 늘 수 있다worker 축소 또는 queueing
I/O 대기 시간이 대부분이다적절한 concurrency 증가는 도움 가능DB/외부 API 한도와 같이 본다
cgroup quota가 낮다worker가 quota를 더 빨리 소진한다CPU limit 조정 또는 worker 축소
Node event loop 하나가 막힌다worker process/worker thread 분리가 필요할 수 있다lag 원인 먼저 profiling

결정 규칙: worker 수는 “CPU 코어 수”, “CPU quota”, “I/O 대기 비율”, “작업별 CPU burst 길이”를 같이 보고 정한다. 코어 수만 보고 무조건 worker를 맞추면 cgroup quota 환경에서 틀릴 수 있다.

10.2 nice/renice를 쓸 때 (선택 심화)

섹션 제목: “10.2 nice/renice를 쓸 때 (선택 심화)”

nice는 같은 호스트 안에서 CPU 경합이 있을 때 상대적 지분을 조정하는 도구다.

사용하기 좋은 경우피해야 하는 경우
API 서버와 배치 작업이 같은 VM에서 경합한다CPU 사용량 상한을 만들고 싶다
낮은 우선순위 작업이 느려져도 된다작업 완료 deadline이 엄격하다
임시 완화가 필요하다root cause 없이 영구 운영 정책으로 숨긴다
task가 CPU-bound다병목이 I/O나 lock이다

worked example: 배치 작업 nice +10

API server nice 0 -> weight 1024
batch nice +10 -> weight 110
CPU 경합 중 예상 지분:
API ≈ 90%
batch ≈ 10%
효과:
API p99는 좋아질 수 있다.
batch 완료 시간은 길어진다.

실패 신호: batch backlog가 줄지 않거나 maintenance 지연이 누적되면 nice 값을 다시 낮추거나 별도 실행 창/별도 노드/별도 queue로 분리해야 한다.

극단적인 nice 조정은 작은 설정처럼 보여도 CPU 지분을 크게 흔든다.

비교대략적 weight경합 시 직관
nice -2088761거의 모든 CPU를 가져가려 한다
nice 01024일반 작업 기준점
nice +10110batch를 크게 양보시킨다

반례: latency를 줄이겠다고 API 프로세스를 nice -20으로 올리면 batch만 느려지는 데서 끝나지 않을 수 있다. logging, backup, maintenance처럼 복구와 관측에 필요한 낮은 priority 작업도 밀려 장애 대응 능력이 나빠진다.

10.3 CPU limit을 올릴지, worker를 줄일지 (선택 심화)

섹션 제목: “10.3 CPU limit을 올릴지, worker를 줄일지 (선택 심화)”

컨테이너에서 throttling이 보이면 두 선택지가 있다.

선택맞는 경우부작용
CPU limit 상향실제로 CPU가 부족하고 비용/노드 용량이 허용됨다른 workload와의 경합 증가
worker 수 축소quota를 너무 빨리 소진해서 tail이 튐처리량 감소 가능
request/limit 분리 재검토burst가 필요하지만 hard limit이 tail을 만든다노드 과commit 정책과 같이 봐야 함
CPU-bound 작업 분리API 요청 경로와 배치가 섞여 있음아키텍처 복잡도 증가

반례: CPU limit을 올리면 항상 해결되는 것은 아니다. event loop를 막는 동기 코드 하나가 원인이면 CPU를 더 줘도 같은 Node thread 안의 callback 순서는 여전히 막힌다.

10.4 실시간 스케줄링 정책은 마지막 도구다 (선택 심화)

섹션 제목: “10.4 실시간 스케줄링 정책은 마지막 도구다 (선택 심화)”

Linux에는 SCHED_FIFO, SCHED_RR 같은 실시간 정책이 있다. 일반 웹 서버의 p99를 줄이겠다고 바로 적용할 도구는 아니다.

정책의미주의
SCHED_OTHER일반 CFS/EEVDF 대상대부분의 서버 기본값
SCHED_BATCHbatch 성격 task 힌트interactive latency보다 throughput 중심
SCHED_IDLEidle 때만 실행하는 낮은 우선순위maintenance 작업에 제한적으로 고려
SCHED_FIFO선점형 실시간, time slice 없음잘못 쓰면 시스템 전체 응답 불능
SCHED_RR실시간 Round RobinRT 우선순위 설계가 필요

실시간 정책은 오디오, 로봇 제어, 통신 장비처럼 deadline miss 자체가 실패인 시스템에서 별도 격리와 모니터링을 갖춘 뒤 쓴다. 일반 API 서버에는 nice, cgroup, worker 분리, CPU isolation 같은 더 낮은 위험의 선택지가 먼저다.

10.5 CPU affinity와 NUMA는 언제 떠올릴까 (선택 심화)

섹션 제목: “10.5 CPU affinity와 NUMA는 언제 떠올릴까 (선택 심화)”

CPU affinity는 특정 프로세스나 스레드가 실행될 CPU 코어 집합을 제한하는 설정이다. 기본적으로 Linux 스케줄러는 부하 균형을 위해 task를 여러 코어 사이에서 옮길 수 있다. 이 이동은 평균 utilization에는 좋지만, 아주 latency-sensitive한 작업에서는 cache locality를 흔들 수 있다.

cache locality는 방금 접근한 데이터가 CPU cache에 남아 있어 다음 접근이 빨라지는 성질이다. 같은 task가 같은 코어에서 계속 실행되면 L1/L2 cache에 남은 데이터를 재사용할 가능성이 커진다. 반대로 task가 코어를 자주 옮기면 cache가 차가워지고, 같은 연산도 더 오래 걸릴 수 있다.

상황:
API worker A가 코어 0에서 실행되며 hot data를 cache에 올림
다음 time slice에 코어 5로 이동
결과:
코어 5의 cache에는 A의 hot data가 없음
메모리 접근 latency 증가
tail latency가 튈 수 있음

NUMA(Non-Uniform Memory Access)는 멀티소켓 또는 큰 인스턴스에서 CPU와 메모리의 물리적 거리에 따라 접근 비용이 달라지는 구조다. 어떤 CPU socket 가까이에 붙은 메모리는 빠르고, 다른 socket에 붙은 메모리는 상대적으로 느릴 수 있다.

선택지맞는 경우조심할 점
기본 스케줄러에 맡김대부분의 일반 웹 서버가장 먼저 선택할 기본값
CPU affinitycache locality가 p99에 민감하고 worker 수가 안정적부하 균형을 망칠 수 있음
cpuset/cgroup 격리batch와 latency-sensitive worker를 물리적으로 분리남는 CPU를 공유하기 어려움
NUMA binding대형 인스턴스에서 memory locality가 병목잘못 묶으면 오히려 불균형 발생

반례: affinity를 설정하면 항상 빨라지는 것은 아니다. worker를 특정 코어에 묶었는데 그 코어만 바쁘고 다른 코어가 놀면 throughput이 떨어진다. cache locality 이득보다 load imbalance 손해가 커질 수 있다.

첫 독서 기준: 처음에는 affinity와 NUMA를 튜닝 값으로 외우지 않는다. “task가 코어를 옮기면 cache locality가 흔들릴 수 있다”, “큰 서버에서는 CPU와 메모리 거리도 latency가 될 수 있다” 정도만 잡으면 된다.

10.6 autovacuum 같은 maintenance worker는 왜 민감한가 (선택 심화)

섹션 제목: “10.6 autovacuum 같은 maintenance worker는 왜 민감한가 (선택 심화)”

기존 문서의 PostgreSQL autovacuum 예시는 런북으로는 길었지만, 스케줄링 학습 자산으로는 가치가 있다. 핵심은 “낮은 우선순위 작업을 너무 밀어내면 당장은 API가 좋아져도 나중에 더 큰 장애가 온다”는 반례다.

상황:
OLTP backend task 여러 개 = 사용자 요청 처리
autovacuum worker = dead tuple 정리 maintenance
둘 다 nice 0이고 CPU-bound 구간이 겹침
-> CFS/EEVDF는 같은 일반 task로 보고 CPU를 나눔
-> API p99가 상승할 수 있음
autovacuum을 nice +19로 과하게 낮춤
-> API p99는 당장 좋아질 수 있음
-> autovacuum 진행이 너무 늦어짐
-> dead tuple, table bloat, vacuum 지연이 누적
-> 며칠 뒤 DB 전체 성능이 더 크게 악화

이 예시의 판단 기준은 “maintenance는 낮은 priority로 밀어내면 된다”가 아니다. 정확한 질문은 다음이다.

  1. 사용자 요청과 maintenance가 같은 CPU에서 경합하는가?
  2. maintenance를 늦춰도 데이터 품질과 저장공간 문제가 생기지 않는가?
  3. 낮춘 priority에서도 backlog가 줄어드는가?
  4. API p99 개선 폭이 maintenance 지연 비용보다 큰가?
  5. 별도 시간대, 별도 노드, DB 내부 throttle 같은 더 나은 분리 방법이 있는가?

수치 감각:

신호읽는 법
API p99가 80ms -> 350ms로 상승사용자 경로가 CPU 경합 영향을 받는다.
maintenance backlog가 계속 증가낮은 priority가 starvation에 가까울 수 있다.
dead tuple 비율이 계속 상승CPU 양보 정책이 장기적으로 실패 중일 수 있다.
CPU는 90%인데 TPS가 절반으로 감소CPU 부족보다 경합과 cache/lock 영향이 섞였을 수 있다.

반례의 교훈: fairness와 priority는 둘 다 필요하다. 사용자 요청을 보호해야 하지만, 시스템을 건강하게 유지하는 background 작업이 영원히 굶어서는 안 된다.

10.7 scheduler parameter를 바로 바꾸지 않는 이유 (선택 심화)

섹션 제목: “10.7 scheduler parameter를 바로 바꾸지 않는 이유 (선택 심화)”

Linux에는 sched_latency_ns, sched_min_granularity_ns처럼 스케줄링 동작에 영향을 주는 커널 파라미터가 있다. 이 값들은 학습에 유용하지만, 일반 애플리케이션 문제를 만났을 때 첫 번째 조정 대상은 아니다.

파라미터 성격직관위험
latency 관련전체 runnable task가 일정 기간 안에 CPU를 받도록 조정모든 workload에 영향을 준다.
minimum granularity너무 작은 time slice로 쪼개지지 않게 막음낮추면 context switch가 늘 수 있다.
RT bandwidth실시간 task가 일반 task를 완전히 굶기지 않게 제한잘못 풀면 복구 경로까지 굶을 수 있다.

학습용 실험에서는 값을 바꿔 latency와 context switch 변화를 관찰할 수 있다. 운영에서는 먼저 애플리케이션 CPU profile, worker 수, cgroup quota, nice, queue 분리를 확인한다. 커널 전역 파라미터는 같은 노드의 다른 서비스에도 영향을 주기 때문이다.

결정 규칙: 애플리케이션 하나의 p99 문제를 커널 전역 파라미터로 해결하려 한다면 대개 층을 잘못 고른 것이다. 문제 task를 분리하거나 quota/worker 수를 조정하는 쪽이 먼저다.


긴 런북보다 “무엇을 관찰하면 어떤 개념을 의심할지”가 학습에 더 중요하다. 아래 명령은 외울 대상이 아니라 관찰 포인트다.

11.1 프로세스별 nice와 CPU 사용 보기

섹션 제목: “11.1 프로세스별 nice와 CPU 사용 보기”
Terminal window
ps -eo pid,ni,pcpu,comm --sort=-pcpu | head

읽는 법:

  • NI가 낮을수록 일반적으로 CPU 가중치가 높다.
  • NI=+10인데 CPU를 많이 쓰는 것은 이상이 아닐 수 있다. 다른 runnable task가 없으면 CPU를 받을 수 있다.
  • 같은 시간대 p99와 backlog를 같이 봐야 한다.
Terminal window
vmstat 1
pidstat -w 1

읽는 법:

  • vmstatcs는 시스템 전체 context switch 수다.
  • pidstat -w의 voluntary switch는 I/O나 lock 등으로 스스로 양보한 경우가 많다.
  • involuntary switch가 급증하면 CPU 경합과 time slice 소진을 의심한다.
Terminal window
cat /sys/fs/cgroup/cpu.stat

읽는 법:

  • nr_periods는 CPU quota period 수다.
  • nr_throttled는 throttling이 발생한 period 수다.
  • throttled_usec는 throttling으로 막힌 누적 시간이다.
  • nr_throttled / nr_periods가 지속적으로 높으면 CPU limit과 worker 수를 같이 본다.

11.4 Node.js event loop lag와 연결하기

섹션 제목: “11.4 Node.js event loop lag와 연결하기”
const start = process.hrtime.bigint();
setImmediate(() => {
const lagMs = Number(process.hrtime.bigint() - start) / 1_000_000;
console.log(`event loop lag: ${lagMs.toFixed(2)}ms`);
});

읽는 법:

  • lag가 높다는 것은 JS callback이 제때 실행되지 못했다는 뜻이다.
  • OS run queue가 길어서 Node thread 자체가 CPU를 못 받은 경우도 있고, Node thread가 긴 JS 작업을 실행하느라 같은 event loop callback을 밀어낸 경우도 있다.
  • CPU profile과 OS 지표를 같이 봐야 층을 구분할 수 있다.

11.5 load average를 CPU 문제로 단정하지 않기

섹션 제목: “11.5 load average를 CPU 문제로 단정하지 않기”
Terminal window
uptime
top

읽는 법:

  • load average가 코어 수보다 높아도 CPU만의 문제가 아닐 수 있다.
  • wa가 높으면 iowait를 먼저 본다.
  • CPU idle이 남는데 load가 높으면 D-state task나 I/O 병목을 의심한다.

12. 선택 부록: 기존 런북을 학습용으로 줄여 읽기

섹션 제목: “12. 선택 부록: 기존 런북을 학습용으로 줄여 읽기”

아래 절은 실전 명령 모음이 아니라, 본문 개념을 관찰하는 짧은 부록이다. 실제 운영 변경은 staging에서 재현하고, 서비스별 SLO와 rollback 경로를 확인한 뒤 적용한다.

배치 작업의 CPU 지분을 낮춰보기
Terminal window
pidof batch-worker
sudo renice +10 -p <PID>
ps -o pid,ni,pcpu,comm -p <PID>

관찰 포인트:

  • API p99가 내려가는가?
  • batch backlog가 여전히 줄어드는가?
  • CPU가 남는 시간대에는 batch가 충분히 실행되는가?

실패 신호:

  • batch가 사실상 진행되지 않는다.
  • maintenance 작업 지연이 데이터 품질 문제로 번진다.
  • API p99가 그대로라면 CPU 지분 문제가 아닐 수 있다.
CPU throttling 비율 계산하기
Terminal window
cat /sys/fs/cgroup/cpu.stat

예시 해석:

nr_periods 10000
nr_throttled 2500
throttled_usec 120000000
throttled ratio = 2500 / 10000 = 25%

관찰 포인트:

  • throttling ratio가 p99 spike와 같은 시간대에 증가하는가?
  • worker 수를 줄이면 throttling은 줄지만 throughput도 줄어드는가?
  • CPU limit을 올리면 p99가 회복되는가?
실시간 정책 오용 확인하기
Terminal window
ps -eo pid,policy,rtprio,comm | grep -v SCHED_OTHER

관찰 포인트:

  • 예상하지 못한 서버 프로세스가 FF 또는 RR 정책을 쓰는가?
  • sshd, watchdog, logging agent 같은 복구 경로가 굶고 있지는 않은가?

주의:

  • SCHED_FIFO는 time slice가 없으므로 일반 서버 프로세스에 잘못 적용하면 시스템 전체가 멈춘 것처럼 보일 수 있다.
  • RT 정책은 “p99가 높다”의 일반 해법이 아니다.
Node.js CPU-bound 요청을 작은 chunk로 나누기
async function transformInChunks(items, transform) {
const out = [];
const chunkSize = 1000;
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
out.push(...chunk.map(transform));
await new Promise((resolve) => setImmediate(resolve));
}
return out;
}

관찰 포인트:

  • 다른 요청의 event loop lag가 줄어드는가?
  • 전체 작업 완료 시간이 허용 가능한가?
  • chunking만으로 부족하면 Worker Thread나 외부 queue가 필요한가?
  • setImmediate를 재귀적으로 너무 촘촘히 호출해 timer와 I/O callback이 계속 뒤로 밀리지는 않는가?

반례: chunking은 “CPU 작업을 잘게 나누는” 도구이지, 무제한 양보 장치가 아니다. chunk 하나가 여전히 크거나 다음 chunk를 즉시 계속 예약하면 event loop의 다른 phase가 충분히 숨 쉴 시간을 얻지 못한다.


둘 다 “줄 선 요청 중 무엇을 먼저 처리할지”를 다루지만 대상이 다르다.

항목CPU 스케줄링I/O 스케줄링
자원CPU core timedisk/network/device service
대기열runnable taskI/O request queue
대표 실패run queue 증가, context switch 과다iowait 증가, disk latency 증가
대표 정책FCFS, RR, CFS, EEVDFFCFS, deadline, BFQ, mq-deadline 등

CPU가 낮고 iowait가 높다면 CPU 스케줄링 튜닝보다 I/O 병목을 먼저 봐야 한다.

CPU 스케줄링은 “누가 CPU를 받을까”이고, 동시성 제어는 “동시에 접근하면 안 되는 자원을 어떻게 보호할까”다.

현상CPU 스케줄링 문제일 수 있음동시성 제어 문제일 수 있음
runnable task가 많아 CPU 대기lock 경합 때문에 runnable/blocked가 섞일 수 있음
lock wait가 길다간접 영향 가능
context switch가 많다lock handoff가 원인일 수 있음
데이터가 깨진다보통 아님

다음 문서인 concurrency-sync.mdx에서는 lock, race condition, atomicity 쪽을 다룬다. 이 문서에서는 CPU 시간을 나누는 문제에 집중한다.

Autoscaling은 더 많은 실행 단위를 만들거나 배치 위치를 바꾸는 상위 계층 결정이다. CPU 스케줄링은 이미 실행 가능한 task에게 CPU 시간을 나누는 하위 계층 결정이다.

CPU 스케줄링:
같은 노드 안의 runnable task 사이에서 CPU 시간 배분
Autoscaling:
replica/task/pod 수를 늘리거나 줄여 전체 수요를 분산

CPU-bound 작업을 autoscaling으로만 해결하면 각 replica 안의 event loop lag나 cgroup throttling을 놓칠 수 있다. 반대로 한 노드 안에서 nice만 조정해도 전체 트래픽이 노드 용량을 넘으면 해결되지 않는다.


CPU Scheduling 첫 회독 종료 체크

  • runnable, waiting, run queue의 차이를 설명할 수 있다.
  • FCFS의 Convoy Effect를 손으로 계산할 수 있다.
  • SJF가 평균 대기 시간에 강하지만 starvation 위험이 있는 이유를 설명할 수 있다.
  • Round Robin에서 time quantum이 너무 작거나 클 때의 trade-off를 설명할 수 있다.
  • preemption과 context switch 비용이 왜 함께 등장하는지 설명할 수 있다.
  • CPU throttling과 CPU 100%를 구분할 수 있다.
  • Node.js Event Loop lag가 OS 스케줄링 문제인지 JS thread 블로킹 문제인지 나눠 볼 수 있다.
  • load average가 높을 때 CPU run queue와 iowait를 구분해 볼 수 있다.

CPU Scheduling 선택 심화 체크

  • CFS의 vruntime을 '지금까지 CPU를 얼마나 받았는가'의 가중치 모델로 설명할 수 있다.
  • EEVDF가 fairness에 latency 요구를 더 직접 반영하려는 방향임을 설명할 수 있다.
  • nice가 CPU hard limit이 아니라 상대적 weight 조정임을 설명할 수 있다.
  • CPU affinity·NUMA·실시간 정책을 일반 서버의 첫 조치가 아니라 측정 후 검토할 선택지로 설명할 수 있다.
  • cgroup quota와 scheduler parameter를 바꾸기 전에 throttling, run queue, latency 기준선을 비교할 수 있다.

키워드설명
CPU Schedulingrunnable task 중 누구에게 CPU를 줄지 결정하는 OS 메커니즘
RunnableCPU만 받으면 실행할 수 있는 상태
Waiting/SleepingI/O, lock, timer, 이벤트 등을 기다리는 상태
Ready Queue/Run Queuerunnable task가 CPU 차례를 기다리는 대기열
PreemptionOS가 실행 중인 task를 강제로 CPU에서 내리는 것
Time Quantum한 task가 연속으로 CPU를 받을 수 있는 시간 조각
Context Switch현재 실행 상태를 저장하고 다음 task 상태를 복원하는 전환
FCFS먼저 온 작업을 먼저 실행하는 단순 정책
Convoy Effect긴 작업 뒤에 짧은 작업들이 줄줄이 밀리는 현상
SJF가장 짧은 CPU burst를 먼저 실행하는 정책
SRTF남은 시간이 가장 짧은 task를 우선하는 선점형 SJF
Round Robintime quantum 단위로 task를 순환 실행하는 정책
Priority Scheduling우선순위 기반으로 CPU를 배분하는 정책
Starvation어떤 task가 계속 밀려 실행 기회를 거의 못 받는 상태
Aging오래 기다린 task의 effective priority를 올려 starvation을 완화하는 기법
MLFQ실행 패턴에 따라 여러 큐 사이를 이동시키는 스케줄링 모델
CFSvruntime 기반으로 공정성을 추구한 Linux 스케줄러
vruntimeweight를 반영한 가상 실행 시간
EEVDFeligible task 중 virtual deadline이 빠른 task를 고르는 Linux 공정 스케줄러
nice일반 task의 CPU weight를 조정하는 값
CPU Throttlingcgroup quota 때문에 runnable task 실행이 제한되는 현상
Event Loop LagNode.js callback 실행이 제때 되지 못하고 밀린 시간


CPU 스케줄링은 제한된 CPU 시간을 runnable task 사이에 나누는 OS의 정책이다. 좋은 스케줄링 감각은 FCFS, SJF, Round Robin 같은 고전 예제로 trade-off를 익히고, Linux의 CFS/EEVDF, nice, cgroup throttling, Node.js event loop lag를 각각 다른 층의 대기열로 구분해 읽는 데서 생긴다.