콘텐츠로 이동

CPU Scheduling

운영체제가 여러 프로세스 중 어떤 프로세스에게 CPU를 얼마나 줄지 결정하는 정책으로, 시스템 처리량·응답 시간·공정성을 동시에 최적화하는 핵심 메커니즘이다.


현대 컴퓨터는 수십~수백 개의 프로세스가 동시에 실행을 원한다. 그러나 CPU 코어는 한 번에 하나의 명령만 처리할 수 있다. 이 불일치를 해결하지 않으면:

  • 사용자 체감 응답 시간이 폭발적으로 증가한다 — 한 프로세스가 CPU를 독점하면 다른 프로세스는 무한정 대기해야 한다.
  • 시스템 처리량(throughput)이 낭비된다 — I/O를 기다리는 프로세스가 CPU를 잡고 있으면 CPU가 유휴 상태가 된다.
  • 공정성(fairness)이 무너진다 — 특정 프로세스만 CPU를 계속 차지하면 나머지 프로세스는 기아(starvation) 상태에 빠진다.

BackOps 엔지니어 관점에서 CPU 스케줄링을 이해해야 하는 이유:

  1. Linux 서버 성능 튜닝: nice, renice, chrt 명령으로 프로세스 우선순위를 조정할 때 내부 원리를 알아야 효과적으로 사용할 수 있다.
  2. Kubernetes Pod 스케줄링: K8s의 Pod Priority와 Preemption은 CPU 스케줄링 개념과 동일한 원리로 동작한다.
  3. Node.js Event Loop 이해: microtask > macrotask 순서도 일종의 우선순위 스케줄링이다.
  4. 장애 대응: CPU 사용률 100% 상황에서 어떤 프로세스를 먼저 처리할지 결정하는 판단 근거가 된다.

3.1 선점(Preemptive) vs 비선점(Non-preemptive)

섹션 제목: “3.1 선점(Preemptive) vs 비선점(Non-preemptive)”

놀이공원에서 롤러코스터(CPU)를 기다리는 사람들(프로세스)이 줄을 서고 있다.

  • 비선점 방식: 한 번 탑승하면 내릴 때까지 기다려야 한다. 앞 사람이 2시간짜리 프리패스를 갖고 있으면 뒷사람은 무조건 2시간을 기다린다.
  • 선점 방식: 긴급 환자(높은 우선순위 프로세스)가 오면 현재 탑승 중인 사람을 잠시 내리게 하고 그 자리를 줄 수 있다.
구분선점 (Preemptive)비선점 (Non-preemptive)
정의OS가 실행 중인 프로세스를 강제로 CPU에서 내릴 수 있음프로세스가 스스로 CPU를 반납할 때까지 대기
응답 시간짧음 (인터럽트 가능)길어질 수 있음
오버헤드Context Switching 비용 발생상대적으로 낮음
예시Round Robin, Priority (선점형)FCFS, SJF (비선점형)

Context Switching: CPU를 빼앗을 때 현재 프로세스의 레지스터 값, 프로그램 카운터 등을 PCB(Process Control Block)에 저장하고, 다음 프로세스의 상태를 복원하는 작업. 너무 자주 발생하면 오버헤드가 증가한다.

Context Switch 비용 정량화: 컨텍스트 스위치 자체는 수 μs 수준이지만, 전환 후 L1/L2 캐시 콜드 미스 복구까지 포함하면 실제 비용은 10~100μs 범위로 측정된다. Linux에서는 두 종류를 구분한다.

종류발생 조건vmstat/pidstat 컬럼
Voluntary (자발적)프로세스가 I/O 대기·lock 등으로 스스로 CPU 반납pidstat -wcswch/s
Involuntary (비자발적)Time Quantum 소진 → OS가 강제 선점pidstat -wnvcswch/s
Terminal window
# 프로세스별 Context Switch 비율 실시간 확인
pidstat -w 1 # 1초마다 갱신
# cswch/s (voluntary) 높으면 I/O 대기 과다
# nvcswch/s (involuntary) 높으면 CPU 포화 → 더 잦은 선점
# 시스템 전체 cs/s (vmstat의 cs 컬럼)
vmstat 1
# cs 컬럼: 초당 전체 context switch 수
# 단일 8코어 서버 정상 범위: ~5,000~20,000/s
# 100,000/s 초과 시 스케줄링 오버헤드 의심

📖 더 보기: pidstat(1) — man7.org, vmstat(8) — man7.org


3.2 FCFS (First Come First Served, 선입선출)

섹션 제목: “3.2 FCFS (First Come First Served, 선입선출)”

은행 창구에서 번호표를 뽑고 순서대로 처리한다. 먼저 온 사람이 먼저 서비스를 받는다.

  • 도착 순서대로 Ready Queue에 삽입
  • 앞 프로세스가 완료될 때까지 CPU를 양보하지 않는다 (비선점)
  • Convoy Effect (호위 효과): 실행 시간이 긴 프로세스가 앞에 있으면 짧은 프로세스들이 모두 뒤에서 오래 기다려야 하는 현상
프로세스 도착 순서: P1(30ms), P2(3ms), P3(3ms)
Gantt Chart:
| P1 (0~30ms) | P2 (30~33ms) | P3 (33~36ms) |
P1 대기: 0ms
P2 대기: 30ms ← 3ms짜리인데 30ms를 기다림!
P3 대기: 33ms
평균 대기 시간: (0 + 30 + 33) / 3 = 21ms ← 매우 비효율

만약 순서가 P2 → P3 → P1 이었다면:

| P2 (0~3ms) | P3 (3~6ms) | P1 (6~36ms) |
P2 대기: 0ms
P3 대기: 3ms
P1 대기: 6ms
평균 대기 시간: (0 + 3 + 6) / 3 = 3ms ← 7배 차이!

특성 요약:

  • 구현이 가장 단순하다
  • Convoy Effect로 평균 대기 시간이 매우 길어질 수 있다
  • 실시간 시스템이나 대화형 시스템에 부적합하다
  • 배치 처리 시스템에서 주로 사용되었다

3.3 SJF (Shortest Job First, 최단 작업 우선)

섹션 제목: “3.3 SJF (Shortest Job First, 최단 작업 우선)”

슈퍼마켓 계산대에서 계산할 물건이 적은 사람을 먼저 처리해주는 “빠른 계산대(Express Lane)”. 짐이 많은 사람은 뒤로 밀린다.

  • 실행 시간이 짧은 프로세스를 먼저 실행한다
  • 이론적으로 평균 대기 시간이 가장 짧다 (최적 알고리즘)
  • 핵심 문제: 미래의 CPU 실행 시간을 알 수 없다 → 실제 구현에서는 과거 실행 시간의 지수 평균(Exponential Moving Average)으로 예측

예측 공식:

τ(n+1) = α × t(n) + (1 - α) × τ(n)
τ(n+1) : 다음 CPU 버스트 예측값
t(n) : 실제 n번째 CPU 버스트 시간
α : 0 ≤ α ≤ 1 (보통 0.5)
프로세스: P1(6ms), P2(8ms), P3(7ms), P4(3ms)
SJF 순서: P4(3) → P1(6) → P3(7) → P2(8)
Gantt Chart:
| P4 (0~3) | P1 (3~9) | P3 (9~16) | P2 (16~24) |
대기 시간:
P1: 3ms
P2: 16ms
P3: 9ms
P4: 0ms
평균 대기 시간: (3 + 16 + 9 + 0) / 4 = 7ms

SRTF (Shortest Remaining Time First): SJF의 선점 버전. 새로운 프로세스가 도착했을 때 현재 실행 중인 프로세스보다 남은 시간이 짧으면 CPU를 빼앗는다.

Starvation (기아 현상): 짧은 프로세스가 계속 들어오면 긴 프로세스는 영원히 실행되지 못할 수 있다.


회의에서 발언권을 돌아가며 준다. 각자 1분씩 발언하고, 더 하고 싶으면 다음 차례를 기다린다. 긴급한 내용도 다음 차례까지 기다려야 한다.

  • 각 프로세스에게 Time Quantum(시간 할당량, q) 만큼만 CPU를 준다
  • 할당 시간이 끝나면 Ready Queue의 맨 뒤로 이동 (선점)
  • 시분할 시스템(Time-sharing System)의 기본 알고리즘
  • n개의 프로세스가 있을 때 각 프로세스는 (n-1) × q 이하의 시간을 기다린다
Time Quantum이 너무 작으면:
- Context Switching이 매우 빈번하게 발생
- 오버헤드 증가 → 실제 작업보다 전환 비용이 더 커질 수 있음
Time Quantum이 너무 크면:
- FCFS와 동일하게 동작
- 응답 시간이 증가
일반적으로 Time Quantum = 10~100ms 사이가 적절
CPU 버스트 시간의 80%가 Time Quantum보다 짧아야 효율적
프로세스: P1(24ms), P2(3ms), P3(3ms)
실행 순서:
| P1(0-4) | P2(4-7) | P3(7-10) | P1(10-14) | P1(14-18) | P1(18-22) | P1(22-26) | P1(26-30) |
P1: 4ms × 1 → P2, P3 처리 → 다시 P1...
P2: 3ms만 필요, 7ms에 완료
P3: 3ms만 필요, 10ms에 완료
대기 시간:
P1: 6ms (처음 4ms 실행 후 6ms 대기)
P2: 4ms
P3: 7ms
평균 대기 시간: (6 + 4 + 7) / 3 = 5.67ms

3.5 Priority Scheduling (우선순위 스케줄링)

섹션 제목: “3.5 Priority Scheduling (우선순위 스케줄링)”

응급실 분류(triage) 시스템. 심각한 환자(높은 우선순위)는 먼저 진료하고, 가벼운 상처(낮은 우선순위)는 대기한다. 하지만 기다리는 시간이 길어질수록 우선순위가 올라간다(Aging).

  • 각 프로세스에 우선순위 번호를 부여 (보통 낮은 숫자 = 높은 우선순위)
  • 선점/비선점 모두 구현 가능
  • Starvation 문제: 높은 우선순위 프로세스가 계속 들어오면 낮은 우선순위 프로세스는 실행되지 못한다
  • Aging 해결책: 대기 시간이 늘어날수록 우선순위를 점차 높여준다 (예: 15분마다 우선순위 +1)
# Aging 개념 시뮬레이션 (의사 코드)
def update_priority_with_aging(processes, elapsed_time):
for process in processes:
if process.state == "WAITING":
# 15분(900초) 대기마다 우선순위 1단계 상승
aging_boost = elapsed_time // 900
process.effective_priority = process.base_priority - aging_boost
process.effective_priority = max(process.effective_priority, 0) # 최고 우선순위 상한

공항의 탑승 우선순서. 비즈니스 클래스 → 프리미엄 이코노미 → 일반 이코노미 순으로 탑승. 각 그룹은 별도 줄에 서고, 앞 그룹이 모두 탑승해야 다음 그룹이 시작한다.

  • Ready Queue를 여러 개로 분리 (예: 시스템 프로세스, 인터랙티브, 배치)
  • 각 Queue마다 다른 스케줄링 알고리즘 적용
  • Queue 간 우선순위가 고정되어 있다
Queue 구조:
┌─────────────────────────────────────┐
│ Queue 1: 시스템 프로세스 (최고 우선순위) │ ← Round Robin (q=8ms)
├─────────────────────────────────────┤
│ Queue 2: 인터랙티브 프로세스 │ ← Round Robin (q=16ms)
├─────────────────────────────────────┤
│ Queue 3: 배치 프로세스 (최저 우선순위) │ ← FCFS
└─────────────────────────────────────┘
CPU는 Queue 1이 비어야 Queue 2를 처리

Multilevel Feedback Queue: 프로세스가 Queue 간을 이동할 수 있는 확장 버전. CPU를 많이 사용하면 낮은 Queue로 이동, I/O를 많이 하면 높은 Queue로 이동. 현재 대부분의 OS가 이 방식을 채택.

왜 단일 알고리즘으로 충분하지 않은가? 실제 시스템에는 특성이 전혀 다른 작업들이 공존한다. 텍스트 편집기(인터랙티브, I/O 위주)는 짧은 응답 시간이 중요하고, 비디오 인코딩(CPU 집약)은 처리량이 중요하다. 하나의 알고리즘으로 두 요구를 동시에 만족시키기 어렵기 때문에, 여러 큐에 다른 정책을 적용하고 큐 간 이동을 허용하는 Multilevel Feedback Queue가 탄생했다. CFS/EEVDF도 이 원리를 수학적으로 일반화한 것이다.


3.7 Nest.js / Node.js에서의 CPU 스케줄링 연결

섹션 제목: “3.7 Nest.js / Node.js에서의 CPU 스케줄링 연결”

비유: Node.js의 이벤트 루프는 OS의 CPU 스케줄러와 같다. 이벤트 루프가 “어떤 콜백을 언제 실행할지” 결정하는 것이 OS가 “어떤 프로세스에 CPU를 줄지” 결정하는 것과 동일한 원리다.

2차 보강 — Event Loop Lag 측정과 프로덕션 대응:

프로덕션에서 Event Loop Lag이 급증하면 모든 요청이 늦어진다. OS 스케줄러 관점에서 이것은 “하나의 긴 Job이 CPU를 독점하는 Convoy Effect”와 동일하다. 실제로 측정하고 대응하는 방법을 알아야 한다.

// NestJS에서 Event Loop Lag 실시간 측정 (perf_hooks 기반)
import { performance } from "perf_hooks";
@Injectable()
export class EventLoopMonitorService implements OnModuleInit {
private readonly logger = new Logger(EventLoopMonitorService.name);
onModuleInit() {
setInterval(() => {
const start = performance.now();
setImmediate(() => {
const lag = performance.now() - start;
if (lag > 50) {
// 50ms 이상이면 경고
this.logger.warn(`Event Loop Lag: ${lag.toFixed(2)}ms`);
}
});
}, 1000);
}
}
// @nestjs/terminus의 under-pressure를 활용한 자동 503 반환
// npm install @nestjs/throttler @godaddy/terminus
// 이벤트 루프가 막히면 자동으로 503 상태를 반환해 로드밸런서가 다른 인스턴스로 라우팅

배치 처리로 이벤트 루프 양보 (Convoy Effect 방지):

// ❌ Convoy Effect: 10만 개 레코드 동기 처리 → 2초 동안 다른 요청 불가
@Get('export')
async exportAll() {
const records = await this.repo.findAll(); // 10만 건
return records.map(r => transform(r)); // 동기 변환 → 이벤트 루프 2초 블로킹
}
// ✅ Round Robin처럼 양보: 1000개 단위로 나눠 처리하며 이벤트 루프 해제
@Get('export')
async exportAll() {
const records = await this.repo.findAll();
const results = [];
const BATCH = 1000;
for (let i = 0; i < records.length; i += BATCH) {
const batch = records.slice(i, i + BATCH).map(r => transform(r));
results.push(...batch);
// 매 1000건마다 이벤트 루프에 제어권 반환 → 다른 요청 처리 가능
await new Promise(resolve => setImmediate(resolve));
}
return results;
}

예상 효과: 동기 처리 시 p99 2000ms이던 다른 API 응답이, setImmediate 배치 처리 후 p99 50ms 이하로 회복된다.

원리: Node.js 이벤트 루프는 내부적으로 우선순위 기반 스케줄링을 사용한다.

Node.js 이벤트 루프 우선순위 (높음 → 낮음):
1. process.nextTick() ← microtask 큐 (가장 먼저)
2. Promise.resolve() ← microtask 큐
3. setImmediate() ← check 페이즈
4. setTimeout() ← timers 페이즈
5. I/O 콜백 ← poll 페이즈
→ 이것은 Priority Scheduling + Round Robin의 조합이다.
microtask는 항상 macrotask보다 높은 우선순위를 가진다.

코드 예시 (NestJS에서 스케줄링 우선순위가 실무에 미치는 영향):

// NestJS Interceptor - 응답 처리 타이밍 이해
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
return next.handle().pipe(
// pipe 내부는 Promise 체인 → microtask로 처리
// → 다른 macrotask(setTimeout, I/O 콜백)보다 먼저 실행됨
map((data) => {
const duration = Date.now() - start;
return { data, duration };
}),
);
}
}

예상 출력:

{ "data": { "userId": 1 }, "duration": 12 }

NestJS에서 CPU 집약 작업이 문제가 되는 이유: NestJS의 요청 처리는 이벤트 루프 위에서 동작한다. 만약 Interceptor나 Service에서 동기 CPU 집약 작업(대용량 JSON 변환, 복잡한 정규식)을 실행하면, 그 시간 동안 다른 모든 요청의 콜백이 대기한다. OS의 비선점 스케줄링(FCFS)처럼 앞 작업이 끝날 때까지 뒤가 기다리는 것이다.

// ❌ 위험: NestJS에서 이벤트 루프 블로킹
@Get('report')
async generateReport() {
// 이 작업이 2초 걸리면 다른 모든 요청이 2초 대기
const result = this.processHugeData(data); // 동기 CPU 집약 작업
return result;
}
// ✅ 안전: Worker Thread로 CPU 작업 분리 (선점 스케줄링처럼 이벤트 루프 보호)
@Get('report')
async generateReport() {
const result = await this.workerService.runInThread(data);
return result;
}

📖 더 보기: Node.js Event Loop Phases — nodejs.org — nextTick, setImmediate, setTimeout 실행 순서를 페이즈별로 설명

EEVDF 스케줄러와 Node.js 서버의 관계: Linux 6.6+에서 도입된 EEVDF는 동일 우선순위 태스크들 중 가상 데드라인(virtual deadline)이 가장 빠른 것을 선택한다. 이전 CFS가 vruntime만으로 선택했다면, EEVDF는 “이 태스크가 얼마나 오래 기다렸는가(lag)“까지 고려한다. Node.js 프로세스 여러 개가 동시에 CPU를 요청할 때(PM2 cluster 환경), EEVDF는 CFS보다 더 일관된 레이턴시를 제공한다. 특히 고동시성 API 서버에서 p99 응답 시간이 줄어드는 효과가 보고되었다.

Terminal window
# 현재 서버의 Linux 커널 버전 확인 (EEVDF 여부)
uname -r
# 6.6 이상: EEVDF 적용, 이하: CFS 적용
# AWS EC2에서 커널 업그레이드 여부 확인
# Amazon Linux 2023: 커널 6.1 (CFS)
# Ubuntu 24.04 LTS: 커널 6.8 (EEVDF 적용)

Linux 2.6.23부터 도입된 현재 리눅스의 기본 스케줄러다. 이름 그대로 “완전히 공정한” 스케줄링을 목표로 한다.

핵심 개념: vruntime (Virtual Runtime)

CFS의 핵심은 vruntime이다. 모든 프로세스가 CPU를 공평하게 나눠 쓰는 “이상적인 멀티태스킹 CPU”를 가상으로 모델링하고, 각 프로세스가 이 이상적 CPU에서 실행한 시간을 vruntime으로 추적한다.

vruntime 계산:
- 프로세스가 실제로 CPU를 사용할수록 vruntime 증가
- 우선순위(nice값)에 따라 vruntime 증가 속도가 다름:
- nice=-20 (최고 우선순위): vruntime이 느리게 증가 → CPU 더 많이 받음
- nice=+19 (최저 우선순위): vruntime이 빠르게 증가 → CPU 적게 받음
CFS는 항상 vruntime이 가장 작은 프로세스를 선택
(= 지금까지 CPU를 가장 적게 쓴 프로세스)

Red-Black Tree 자료구조: CFS는 모든 실행 가능한 프로세스를 vruntime을 키로 하는 Red-Black Tree에 저장한다. 가장 왼쪽 노드(vruntime 최솟값)가 다음 실행 프로세스. O(log n) 시간에 다음 프로세스 선택.

왜 CFS는 단순 큐가 아닌 Red-Black Tree를 사용하는가? 정렬된 배열을 쓰면 삽입/삭제 시 O(n) 비용이 든다. Heap을 쓰면 최솟값 추출은 O(log n)이지만 임의 원소 삭제는 비효율적이다. Red-Black Tree는 삽입, 삭제, 최솟값 조회 모두 O(log n)이면서 최악의 경우에도 균형이 보장되어, 수천 개의 프로세스가 동시에 실행 가능한 서버 환경에서 최적의 자료구조다.

CFS 후속: EEVDF 스케줄러 (Linux 6.6+): Linux 6.6(2023년)부터 CFS를 대체하는 EEVDF(Earliest Eligible Virtual Deadline First) 스케줄러가 도입되었고, Linux 6.12(2024년 11월)에서 CFS 코드가 완전히 제거되어 EEVDF가 유일한 공정 스케줄러가 되었다.

EEVDF는 CFS와 마찬가지로 vruntime 기반이지만, 각 태스크에 가상 데드라인(Virtual Deadline) 을 추가로 계산한다. lag(현재 vruntime과 이상적 vruntime의 차이)가 0 이상인 “eligible” 태스크 중 가상 데드라인이 가장 빠른 태스크를 선택한다. CFS가 16년간 축적한 다양한 휴리스틱(sleeper fairness, vruntime normalization 등)을 더 깔끔한 수학적 모델로 대체하여, 레이턴시가 더 낮고 예측 가능한 동작을 보장한다.

Terminal window
# 현재 커널의 스케줄러 확인 (Linux 6.6+ 여부)
uname -r
# 6.6 이상이면 EEVDF, 이전이면 CFS

📖 더 보기: EEVDF Scheduler — The Linux Kernel documentation — Linux 커널 공식 EEVDF 스케줄러 설계 문서

Terminal window
# 현재 프로세스의 스케줄링 정보 확인
cat /proc/$(pidof nginx)/schedstat
# 출력: [CPU 사용 시간] [대기 시간] [스케줄링 횟수]
# CFS 관련 커널 파라미터 확인
cat /proc/sys/kernel/sched_min_granularity_ns # 최소 실행 시간 (기본 4ms)
cat /proc/sys/kernel/sched_latency_ns # 스케줄링 주기 (기본 8ms)

K8s 스케줄러는 CPU 스케줄링 개념을 클러스터 레벨로 확장한 것이다.

CPU 스케줄링 개념K8s 대응 개념
프로세스Pod
CPUNode (노드의 CPU/Memory)
Ready QueuePending Pod Queue
우선순위PriorityClass
선점 (Preemption)Pod Preemption (낮은 우선순위 Pod 퇴출)
자원 요청requests/limits
# PriorityClass 예시 - 높은 우선순위 정의
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: critical-workload
value: 1000000 # 높을수록 우선순위 높음
globalDefault: false
preemptionPolicy: PreemptLowerPriority # 낮은 우선순위 Pod 선점 허용
description: "mission-critical 워크로드"
---
# Pod에 우선순위 적용
apiVersion: v1
kind: Pod
metadata:
name: api-server
spec:
priorityClassName: critical-workload
containers:
- name: api
image: my-api:latest
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"

선점 동작 시나리오:

  1. critical-workload Pod가 스케줄링 요청
  2. 모든 노드의 리소스가 부족한 상태
  3. K8s 스케줄러가 낮은 우선순위 Pod를 찾아 Evict (퇴출)
  4. 기본 30초 Graceful Termination 후 리소스 회수
  5. critical-workload Pod 스케줄링 완료

AWS ECS는 컨테이너 배치 전략으로 CPU 스케줄링 개념의 일부를 구현한다:

{
"placementStrategy": [
{
"type": "binpack",
"field": "cpu"
}
]
}
전략설명CPU 스케줄링 유사 개념
binpackCPU/메모리를 최대한 빽빽하게 채움처리량(throughput) 최적화
spread인스턴스/AZ에 균등 분산공정성(fairness)
random무작위 배치FCFS와 유사

Node.js는 단일 스레드이지만 내부적으로 우선순위 기반 스케줄링을 사용한다:

우선순위 (높음 → 낮음):
1. process.nextTick() 콜백 ← 가장 먼저 실행 (microtask 큐 앞)
2. Promise.resolve() 콜백 ← microtask 큐
3. setImmediate() 콜백 ← check 페이즈
4. setTimeout() / setInterval() ← timers 페이즈
5. I/O 콜백 ← I/O 콜백 페이즈
// 실행 순서 시연
console.log("1. 동기 코드");
setTimeout(() => console.log("4. setTimeout"), 0);
Promise.resolve().then(() => console.log("3. Promise"));
process.nextTick(() => console.log("2. nextTick"));
console.log("1b. 동기 코드 끝");
// 예상 출력:
// 1. 동기 코드
// 1b. 동기 코드 끝
// 2. nextTick ← microtask (nextTick 큐)
// 3. Promise ← microtask (Promise 큐)
// 4. setTimeout ← macrotask

이것은 Round Robin + Priority Scheduling의 조합이다: 이벤트 루프의 각 페이즈가 라운드 로빈처럼 순환하고, microtask는 항상 macrotask보다 높은 우선순위를 갖는다.

3.8 CPU 집약 작업 처리 선택 매트릭스

섹션 제목: “3.8 CPU 집약 작업 처리 선택 매트릭스”

Node.js/NestJS에서 CPU 집약 작업을 만났을 때 어떤 방법을 선택할지 기준이 없으면 잘못된 도구를 쓰게 된다. 아래 매트릭스가 첫 번째 판단 기준이 된다.

조건권장 방법이유
이벤트 루프 차단 < 50ms, 마이크로 배치 분할 가능setImmediate 양보스레드 생성 오버헤드 없이 이벤트 루프 점유 시간만 쪼갬
CPU 집약 작업 > 100ms, 단일 서버 내 처리Worker Thread별도 V8 인스턴스에서 CPU 병렬 실행, 이벤트 루프 완전 분리
작업 지속성 필요 (서버 재시작 후 복구)외부 큐 (BullMQ/RabbitMQ)작업을 Redis에 영속화, 재시도 정책 적용 가능
우선순위 큐잉 · Dead Letter Queue 필요외부 큐BullMQ priority 옵션, failed job 재처리 내장
멀티 서버 분산 처리외부 큐워커를 수평 확장하면 처리량 선형 증가

언제 외부 큐가 필수인가?

  • 서버 재시작 후 복구: Worker Thread는 프로세스 종료 시 작업이 소실된다. BullMQ는 Redis에 작업을 영속화하므로 재시작 후 자동 재개된다.
  • 재시도 정책: attempts: 3, backoff: { type: 'exponential' } 한 줄로 지수 백오프 재시도 설정 가능.
  • Dead Letter Queue: 최대 재시도 초과 작업을 failed 큐로 이동해 별도 모니터링·수동 처리.
// 선택 기준 요약 (의사 코드)
if (blockingTime < 50ms && 분할가능) {
// setImmediate로 청크 분할 → 이벤트 루프 양보
} else if (blockingTime >= 100ms && 단일서버) {
// Worker Thread (worker_threads 모듈) → 멀티코어 병렬 실행
} else {
// 외부 큐 (BullMQ + Redis)
// 지속성 필요 OR 재시도 필요 OR 멀티서버 분산
}

📖 더 보기: Node.js Don’t Block the Event Loop — nodejs.org, BullMQ Parallelism and Concurrency — docs.bullmq.io


시나리오 1: NestJS API 서버 성능 저하 대응

섹션 제목: “시나리오 1: NestJS API 서버 성능 저하 대응”
상황: NestJS 서버 CPU 사용률 지속적으로 90% 이상
→ 특정 무거운 작업(대용량 파일 파싱)이 Event Loop를 블로킹
→ CPU 스케줄링 관점: 하나의 "긴 Job"이 다른 "짧은 Job"을 기다리게 하는 Convoy Effect
해결책:
1. 무거운 작업을 Worker Thread로 분리 (별도 프로세스로 CPU를 나눔)
2. 또는 작업을 청크로 쪼개 setImmediate()로 양보하며 처리
// Bad: Event Loop 블로킹
function parseHugeFile(data) {
return data.split("\n").map((line) => processLine(line)); // 동기 처리, 블로킹
}
// Good: setImmediate로 CPU 양보하며 처리
async function parseHugeFileAsync(data) {
const lines = data.split("\n");
const results = [];
for (let i = 0; i < lines.length; i++) {
results.push(processLine(lines[i]));
// 1000줄마다 다른 콜백에게 CPU 양보
if (i % 1000 === 0) {
await new Promise((resolve) => setImmediate(resolve));
}
}
return results;
}

시나리오 2: AWS EC2 서버에서 특정 서비스 우선순위 조정

섹션 제목: “시나리오 2: AWS EC2 서버에서 특정 서비스 우선순위 조정”
Terminal window
# 배치 처리 작업의 nice 값을 높여(우선순위 낮춤) API 서버에 CPU 양보
renice +10 -p $(pidof batch-processor)
# 결과: CPU 경합 상황에서 batch-processor가 API 서버에게 CPU를 양보

시나리오 3: K8s에서 크리티컬 서비스 보호

섹션 제목: “시나리오 3: K8s에서 크리티컬 서비스 보호”
# API 서버 - 높은 우선순위 보장
spec:
priorityClassName: critical-workload # 값: 1,000,000
# 배치 Job - 낮은 우선순위
spec:
priorityClassName: batch-workload # 값: 1,000
# 리소스 부족 시 이 Pod가 먼저 퇴출됨

알고리즘선점 여부평균 대기 시간Starvation구현 복잡도적합한 환경
FCFS비선점높음없음매우 낮음배치 처리
SJF비선점최적있음중간예측 가능한 작업
SRTF선점최적있음높음이론적 최적
Round Robin선점중간없음낮음시분할 시스템
Priority선/비선점낮음있음중간실시간 시스템
Multilevel Queue선점낮음있음높음범용 OS
CFS (Linux)선점낮음거의 없음매우 높음현대 Linux
OS스케줄러특징
LinuxCFS (기본), SCHED_RT (실시간)vruntime 기반, Red-Black Tree
WindowsMultilevel Feedback Queue (32단계)우선순위 부스팅
macOSMach SchedulerThread 우선순위 5단계
FreeBSDULE SchedulerCFS와 유사

CPU 스케줄링 vs 디스크 스케줄링

섹션 제목: “CPU 스케줄링 vs 디스크 스케줄링”

CPU 스케줄링과 유사한 개념이 I/O에도 존재한다:

  • FCFS: 디스크 요청 순서대로 처리
  • SSTF (Shortest Seek Time First): SJF 대응, 헤드에서 가장 가까운 요청 먼저
  • SCAN (엘리베이터 알고리즘): Round Robin 대응, 헤드가 한 방향으로 쭉 이동 후 반전

트러블슈팅 1: CPU 사용률 100% — 특정 프로세스 우선순위 강제 조정

섹션 제목: “트러블슈팅 1: CPU 사용률 100% — 특정 프로세스 우선순위 강제 조정”

증상:

Terminal window
$ top
%Cpu(s): 99.8 us, 0.1 sy, 0.0 ni, 0.0 id
PID USER PR NI %CPU COMMAND
12345 app 20 0 95.0 batch-job 배치 작업이 CPU를 독점
23456 app 20 0 3.2 api-server API 서버가 CPU를 거의 받음

원인: 배치 작업과 API 서버가 동일한 우선순위(nice=0)로 CPU 경합. CFS가 공평하게 나누려 하지만 배치 작업의 CPU 버스트가 훨씬 길어 API 서버 응답 지연 발생.

해결 방법:

Terminal window
# 방법 1: 실행 중인 프로세스 우선순위 변경 (renice)
sudo renice +15 -p 12345 # batch-job의 nice 값을 +15로 올림 (우선순위 낮춤)
# 방법 2: 프로세스 시작 시 우선순위 지정
nice -n 15 ./batch-job # nice 15로 시작 (범위: -20~+19)
# 방법 3: 실시간 스케줄링 정책으로 API 서버 우선 처리
sudo chrt -f -p 50 23456 # SCHED_FIFO 정책, 우선순위 50으로 설정
# 결과 확인
$ top
%Cpu(s): 60.0 us, ...
PID USER PR NI %CPU COMMAND
23456 app 20 0 55.0 api-server API 서버가 충분한 CPU 확보
12345 app 35 15 40.0 batch-job 배치는 남는 CPU만 사용

트러블슈팅 2: K8s Pod Pending 상태 지속 — 우선순위 충돌

섹션 제목: “트러블슈팅 2: K8s Pod Pending 상태 지속 — 우선순위 충돌”

증상:

Terminal window
$ kubectl get pods
NAME READY STATUS RESTARTS
critical-api-v2-xyz 0/1 Pending 0 계속 Pending
batch-worker-abc 1/1 Running 0
batch-worker-def 1/1 Running 0
$ kubectl describe pod critical-api-v2-xyz
Events:
Warning FailedScheduling ... 0/3 nodes are available:
3 Insufficient cpu.
preemption: 0/3 nodes are available:
3 No preemption victims found for incoming pod.

원인: critical-api-v2 Pod의 priorityClassName이 설정되어 있지 않거나, 배치 워커와 동일한 우선순위여서 선점이 발생하지 않음.

해결 방법:

Terminal window
# 1. PriorityClass 생성
kubectl apply -f - <<EOF
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
preemptionPolicy: PreemptLowerPriority
globalDefault: false
EOF
# 2. Pod에 PriorityClass 적용
kubectl patch deployment critical-api \
--patch '{"spec":{"template":{"spec":{"priorityClassName":"high-priority"}}}}'
# 3. 재확인 - 배치 워커가 Evict되고 critical-api가 Running으로 변경
$ kubectl get pods
NAME READY STATUS
critical-api-v2-xyz 1/1 Running 스케줄링 성공
batch-worker-abc 0/1 Terminating 선점되어 퇴출

트러블슈팅 3: Node.js 서버 — Event Loop Lag 급증

섹션 제목: “트러블슈팅 3: Node.js 서버 — Event Loop Lag 급증”

증상:

# 모니터링 대시보드에서 확인:
Event Loop Lag: 2000ms (정상: < 10ms)
API 응답 시간: p99 5000ms (정상: < 200ms)
CPU 사용률: 100% (단일 코어)
# 로그:
[WARN] Event loop blocked for 1500ms
[ERROR] Request timeout after 5000ms

원인: 동기 처리 코드(JSON.parse 대용량, 복잡한 정규식, 동기 파일 읽기 등)가 Event Loop를 블로킹. Node.js는 단일 스레드이므로 CPU를 독점하면 다른 요청을 처리할 수 없다.

해결 방법:

Terminal window
# 1. Event Loop Lag 측정 코드 추가
node --inspect app.js
# 2. clinic.js로 병목 지점 분석
npx clinic doctor -- node app.js
// 문제 코드 예시
app.get("/process", (req, res) => {
// 이 작업이 2초 걸리면 그 2초 동안 다른 모든 요청이 블로킹됨
const result = heavyComputation(req.body.data); // 동기 CPU 집약 작업
res.json(result);
});
// 해결책 1: Worker Threads로 분리
const { Worker, isMainThread, parentPort } = require("worker_threads");
app.get("/process", async (req, res) => {
const result = await runInWorker(req.body.data); // 별도 스레드에서 실행
res.json(result);
});
function runInWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker("./heavy-computation.js", { workerData: data });
worker.on("message", resolve);
worker.on("error", reject);
});
}
# 적용 후 결과:
Event Loop Lag: 3ms (정상 수준으로 회복)
API 응답 시간: p99 150ms
CPU 사용률: 멀티코어로 분산

트러블슈팅 5: NestJS — 특정 요청에서 p99 레이턴시 급증 (Event Loop Scheduling)

섹션 제목: “트러블슈팅 5: NestJS — 특정 요청에서 p99 레이턴시 급증 (Event Loop Scheduling)”

증상:

# 평상시 API p99: 30ms
# 특정 대용량 요청 이후 다른 API들도 p99 500ms~2000ms로 급증
# CPU 사용률: 90%+ (단일 코어 집중)
# 에러 로그는 없음

원인: NestJS 서비스에서 동기 CPU 집약 작업(대용량 JSON.stringify, 복잡한 정규식, 동기 파일 읽기)이 이벤트 루프를 블로킹. OS 스케줄러 관점에서 하나의 긴 Job이 다른 짧은 Job들을 뒤에서 기다리게 하는 Convoy Effect.

해결 방법:

3000/api/users
# 1. clinic.js로 Event Loop 블로킹 지점 분석
npx clinic doctor -- node dist/main.js
# .clinic 폴더의 HTML 리포트에서 블로킹 지점 확인
# 2. under-pressure로 Event Loop Lag 자동 모니터링 + 503 반환
# npm install @fastify/under-pressure (또는 직접 구현)
// 3. CPU 집약 작업을 Worker Thread로 분리
import { Worker } from "worker_threads";
import * as path from "path";
@Injectable()
export class ReportService {
// ❌ 이벤트 루프 블로킹
generateReportSync(data: any[]): string {
return JSON.stringify(data.map((item) => complexTransform(item))); // 3초 소요
}
// ✅ Worker Thread로 오프로드
generateReportAsync(data: any[]): Promise<string> {
return new Promise((resolve, reject) => {
const worker = new Worker(path.join(__dirname, "report.worker.js"), {
workerData: data,
});
worker.on("message", resolve);
worker.on("error", reject);
});
}
}

적용 후 결과:

# Before: 대용량 리포트 요청 시 다른 API p99 2000ms
# After: Worker Thread 분리 후 다른 API p99 30ms 유지
# CPU 사용률: 멀티코어로 분산 (단일 코어 집중 해소)

트러블슈팅 4: Linux 서버 — 높은 Load Average, 낮은 CPU Usage

섹션 제목: “트러블슈팅 4: Linux 서버 — 높은 Load Average, 낮은 CPU Usage”

증상:

Terminal window
$ uptime
15:00:00 up 10 days, load average: 15.00, 12.00, 10.00
# CPU 코어: 4개인데 Load Average가 15? → CPU만의 문제가 아님
$ top
%Cpu(s): 20.0 us, 2.0 sy, 0.0 ni, 20.0 id, 55.0 wa
# wa(iowait)가 55% → CPU가 I/O를 기다리며 idle

원인: Load Average는 CPU 대기 + I/O 대기 + 언인터럽터블 슬립 상태의 프로세스 수를 합산. I/O 집약 작업들이 CPU 스케줄러 큐가 아닌 I/O 대기 큐에 쌓여 있는 상태.

해결 방법:

Terminal window
# I/O 병목 프로세스 확인
$ iotop -o
TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND
12345 be/4 app 50.00 M/s 10.00 M/s 0.00% 95% db-backup
# 해결: 백업 작업의 I/O 우선순위 낮춤 (ionice)
sudo ionice -c 3 -p 12345 # Idle I/O 클래스로 변경 (다른 프로세스 I/O 먼저)
# 또는 새로 시작할 때 I/O와 CPU 우선순위 동시 낮춤
nice -n 19 ionice -c 3 ./db-backup.sh

  • FCFS의 Convoy Effect가 왜 발생하는지 설명할 수 있다
  • SJF가 최적 알고리즘인데 왜 실제 구현이 어려운지 설명할 수 있다
  • Round Robin에서 Time Quantum 크기가 성능에 미치는 영향을 설명할 수 있다
  • Starvation과 Aging의 관계를 설명할 수 있다
  • 선점/비선점 스케줄링의 차이와 Context Switching을 설명할 수 있다
  • vruntime이 무엇이고 왜 Red-Black Tree를 사용하는지 설명할 수 있다
  • nice 값이 vruntime 증가 속도에 어떤 영향을 주는지 설명할 수 있다
  • top 명령에서 PR, NI 컬럼의 의미를 안다
  • renice 명령으로 실행 중인 프로세스 우선순위를 변경할 수 있다
  • K8s PriorityClass를 생성하고 Pod에 적용할 수 있다
  • K8s Pod Preemption 동작 시나리오를 설명할 수 있다
  • Node.js에서 microtask와 macrotask 실행 순서를 예측할 수 있다
  • Event Loop Lag 문제를 Worker Thread로 해결할 수 있다
  • CPU 100% 상황에서 nice/renice로 우선순위를 조정할 수 있다

키워드설명
CPU 스케줄링어떤 프로세스에게 CPU를 줄지 결정하는 OS 메커니즘
선점 (Preemptive)OS가 실행 중인 프로세스를 강제로 중단시킬 수 있는 방식
비선점 (Non-preemptive)프로세스가 스스로 CPU를 반납할 때까지 기다리는 방식
FCFSFirst Come First Served, 가장 단순한 스케줄링
Convoy Effect긴 작업 뒤에 짧은 작업이 오래 기다리는 현상
SJFShortest Job First, 이론적 최적이나 Starvation 발생
SRTFSJF의 선점 버전, 남은 시간이 더 짧은 프로세스가 오면 선점
Round Robin시간 할당량(Time Quantum)씩 순환하는 시분할 방식
Time QuantumRound Robin에서 각 프로세스에게 주어지는 최대 실행 시간
Priority Scheduling우선순위 번호 기반으로 CPU를 할당하는 방식
Starvation낮은 우선순위 프로세스가 영원히 실행되지 못하는 현상
Aging오래 기다린 프로세스의 우선순위를 점진적으로 높여 Starvation 방지
Multilevel QueueReady Queue를 여러 개로 분리해 각각 다른 알고리즘 적용
Context Switching프로세스 전환 시 CPU 상태(레지스터, PC 등)를 저장/복원하는 작업
CFSCompletely Fair Scheduler, 현재 Linux의 기본 스케줄러
vruntimeCFS가 각 프로세스의 CPU 사용 시간을 추적하는 가상 실행 시간
nice / reniceLinux에서 프로세스 우선순위 설정 명령 (범위: -20~+19)
PriorityClassK8s에서 Pod 스케줄링 우선순위를 정의하는 객체
Pod PreemptionK8s에서 높은 우선순위 Pod를 위해 낮은 우선순위 Pod를 퇴출하는 메커니즘
Event Loop LagNode.js Event Loop 처리 지연 시간, 10ms 이하가 정상

기본적으로 Linux 스케줄러는 어떤 CPU 코어에서도 프로세스를 실행할 수 있다. 하지만 프로세스를 특정 코어에 고정하면 캐시 온도(cache warmth) 를 유지할 수 있어 성능이 향상된다. 프로세스가 항상 같은 코어에서 실행되면 L1/L2 캐시에 그 프로세스의 데이터가 남아 있을 가능성이 높다.

Terminal window
# taskset으로 CPU 친화성 설정
# 예: Node.js 프로세스를 코어 0, 1에만 고정
taskset -c 0,1 node dist/main.js
# 실행 중인 프로세스의 CPU 친화성 변경
taskset -cp 0,1 $(pgrep node)
# 현재 CPU 친화성 확인
taskset -p $(pgrep node)
# 출력: pid 12345's current affinity mask: 3 (0b0011 = 코어 0,1)

언제 유용한가?

  • 지연 시간 민감 서비스: 특정 코어를 API 서버 전용으로 예약
  • NUMA 최적화: 메모리와 CPU가 가까운 NUMA 노드에 고정
  • 배치 작업 분리: 배치 프로세스를 별도 코어에 고정해 API 서버와 CPU 경쟁 방지

멀티소켓 서버에서는 각 CPU 소켓이 자신과 물리적으로 가까운 RAM에 더 빠르게 접근한다. 프로세스가 CPU 0에서 실행되는데 CPU 1의 RAM에 접근하면 원격 메모리 접근이 발생하여 레이턴시가 2~3배 증가한다.

Terminal window
# NUMA 토폴로지 확인
numactl --hardware
# Node 0: CPU 0-7, 메모리 64GB
# Node 1: CPU 8-15, 메모리 64GB
# Node 간 거리: 10 (로컬) vs 21 (원격)
# 특정 NUMA 노드에 프로세스 바인딩
numactl --cpunodebind=0 --membind=0 node dist/main.js
# → CPU 0-7에서 실행, 메모리도 Node 0에서만 할당

AWS에서의 NUMA: c5.18xlarge (72 vCPU)처럼 큰 인스턴스에서는 내부적으로 NUMA 토폴로지가 존재한다. Kubernetes에서 Topology Manager를 활성화하면 Pod의 CPU와 메모리를 같은 NUMA 노드에 할당하여 성능을 최적화할 수 있다.

실시간 스케줄링 정책 (SCHED_FIFO, SCHED_RR)

섹션 제목: “실시간 스케줄링 정책 (SCHED_FIFO, SCHED_RR)”

CFS/EEVDF는 일반 프로세스용이고, 레이턴시가 0에 가까워야 하는 실시간 프로세스를 위한 별도 정책이 있다.

Terminal window
# 스케줄링 정책 변경
chrt -f -p 50 <PID> # SCHED_FIFO, 우선순위 50 (1~99)
chrt -r -p 50 <PID> # SCHED_RR (Round Robin 실시간)
chrt -o -p 0 <PID> # SCHED_OTHER (일반 CFS로 복귀)
# 현재 스케줄링 정책 확인
chrt -p <PID>
# scheduling policy: SCHED_FIFO
# scheduling priority: 50
정책설명사용 사례
SCHED_OTHERCFS/EEVDF (기본)일반 서버 프로세스
SCHED_FIFO선점형 실시간, 타임슬라이스 없음오디오 드라이버, 로봇 제어
SCHED_RR실시간 + 타임슬라이스 (Round Robin)실시간이지만 공정성도 필요한 경우
SCHED_BATCHCPU 집약 배치 작업 (nice보다 강함)야간 배치 처리
SCHED_IDLE시스템이 완전히 idle할 때만 실행매우 낮은 우선순위 백그라운드 작업

오용 패턴 / Silent Failure — 설정 실수가 증상 없이 시스템 전체에 영향을 줄 수 있다.

오용 패턴증상원인대응
SCHED_FIFO를 일반 서버 프로세스에 적용RT 프로세스가 CPU를 반납하지 않아 시스템 전체 hang 또는 sshd·watchdog 응답 불가SCHED_FIFO는 타임슬라이스가 없어 자발적 양보 없이 CPU를 무한 점유chrt -o -p 0 <PID>로 SCHED_OTHER로 복귀; 커널이 rt_bandwidth(기본 95%)로 제한하지만 5% 남은 CPU는 응급 복구에만 충분
nice -20 남용해당 프로세스만 빨라지는 게 아니라 다른 프로세스 Starvation 유발CFS 가중치 구조상 nice -20(가중치 88761)과 nice 0(가중치 1024)은 약 87:1 CPU 점유 비율 — 경합 시 나머지 프로세스가 CPU를 거의 못 받음nice -20은 오디오·실시간 제어 등 검증된 특수 용도만 사용; 일반 서비스 우선순위는 nice -5~-10이면 충분
setImmediate 재귀 호출Event Loop는 block되지 않지만 다른 I/O 이벤트·타이머 지연setImmediate 루프는 check 페이즈를 계속 점유; I/O 콜백(poll 페이즈)이 밀림재귀 대신 일정 횟수 후 setTimeout(fn, 0)으로 교체하거나 Worker Thread로 분리
Terminal window
# SCHED_FIFO 오용 감지: RT 정책 프로세스 확인
ps -eo pid,policy,rtprio,comm | grep -v SCHED_OTHER | grep -v "^ PID"
# policy=FF(FIFO) 또는 RR(Round Robin)이 예상치 못한 프로세스에 설정된 경우 즉시 확인
# nice -20 남용 감지
ps -eo pid,ni,comm | awk '$2 <= -15'
# nice -15 이하 프로세스 목록 — 의도적 설정인지 검토

📖 더 보기: sched(7) — man7.org, SCHED_FIFO and realtime throttling — LWN.net, Scheduler Nice Design — kernel.org

📖 더 보기: CPU Affinity and NUMA: The Hidden Performance Multiplier — Brendan Gregg — NUMA와 CPU 친화성이 실제 서버 성능에 미치는 영향 분석 (중급)



9.1 현재 시스템의 스케줄링 정보 확인

섹션 제목: “9.1 현재 시스템의 스케줄링 정보 확인”
Terminal window
# 1. 프로세스별 우선순위 확인 (PR=우선순위, NI=nice값)
top -b -n 1 | head -20

예상 출력:

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 169156 9432 7164 S 0.0 0.1 0:03.21 systemd
892 root 20 0 274424 9856 8288 S 0.0 0.1 0:00.89 sshd
1234 app 20 0 1234567 98765 12345 S 45.0 2.5 5:12.34 node ← NestJS
5678 app 35 15 456789 23456 7890 S 2.0 0.5 0:30.00 batch ← nice +15
Terminal window
# 1. 현재 실행 중인 Node.js 프로세스 PID 확인
pidof node
# 2. 배치 작업 우선순위 낮추기
sudo renice +10 -p $(pidof batch-worker)

예상 출력:

12345 (process ID) old priority 0, new priority 10
Terminal window
# 3. 변경 확인
ps -o pid,ni,comm -p 12345

예상 출력:

PID NI COMMAND
12345 10 batch-worker

9.3 Linux 스케줄러 파라미터 확인

섹션 제목: “9.3 Linux 스케줄러 파라미터 확인”
Terminal window
# CFS 스케줄링 레이턴시 설정 확인
cat /proc/sys/kernel/sched_latency_ns

예상 출력:

8000000 # 8ms (기본값) - 이 시간 내에 모든 프로세스가 최소 한 번 실행됨
Terminal window
cat /proc/sys/kernel/sched_min_granularity_ns

예상 출력:

4000000 # 4ms (기본값) - 한 프로세스가 최소로 실행될 수 있는 시간

9.4 특정 프로세스 스케줄링 통계

섹션 제목: “9.4 특정 프로세스 스케줄링 통계”
Terminal window
# /proc/[PID]/schedstat: [CPU 사용 나노초] [대기 나노초] [스케줄링 횟수]
cat /proc/1/schedstat

예상 출력:

1234567890 9876543210 5432
# CPU 사용: 1.23초, 대기: 9.88초, 5432번 스케줄링됨
Terminal window
# 현재 클러스터의 PriorityClass 목록
kubectl get priorityclasses

예상 출력:

NAME VALUE GLOBAL-DEFAULT AGE
system-cluster-critical 2000000000 false 10d
system-node-critical 2000001000 false 10d
high-priority 1000000 false 1d
Terminal window
# Pod의 우선순위 확인
kubectl get pod <pod-name> -o jsonpath='{.spec.priority}'

예상 출력:

1000000
// event-loop-lag.js - Event Loop 지연 측정
const start = process.hrtime.bigint();
setImmediate(() => {
const lag = Number(process.hrtime.bigint() - start) / 1_000_000;
console.log(`Event Loop Lag: ${lag.toFixed(2)}ms`);
});
Terminal window
node event-loop-lag.js

예상 출력:

Event Loop Lag: 0.15ms # 정상 상태
# 만약 수십 ms 이상이면 Event Loop 블로킹 의심

CPU 스케줄링은 “누구에게, 얼마나, 언제 CPU를 줄 것인가”를 결정하는 OS의 핵심 정책이며, FCFS·SJF·Round Robin·Priority의 원리는 Linux CFS, Kubernetes Pod 스케줄링, Node.js Event Loop에 이르기까지 모든 계층의 자원 관리에 동일하게 적용된다.