CPU Scheduling
CPU 스케줄링 (CPU Scheduling)
섹션 제목: “CPU 스케줄링 (CPU Scheduling)”1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”운영체제가 여러 프로세스 중 어떤 프로세스에게 CPU를 얼마나 줄지 결정하는 정책으로, 시스템 처리량·응답 시간·공정성을 동시에 최적화하는 핵심 메커니즘이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”현대 컴퓨터는 수십~수백 개의 프로세스가 동시에 실행을 원한다. 그러나 CPU 코어는 한 번에 하나의 명령만 처리할 수 있다. 이 불일치를 해결하지 않으면:
- 사용자 체감 응답 시간이 폭발적으로 증가한다 — 한 프로세스가 CPU를 독점하면 다른 프로세스는 무한정 대기해야 한다.
- 시스템 처리량(throughput)이 낭비된다 — I/O를 기다리는 프로세스가 CPU를 잡고 있으면 CPU가 유휴 상태가 된다.
- 공정성(fairness)이 무너진다 — 특정 프로세스만 CPU를 계속 차지하면 나머지 프로세스는 기아(starvation) 상태에 빠진다.
BackOps 엔지니어 관점에서 CPU 스케줄링을 이해해야 하는 이유:
- Linux 서버 성능 튜닝:
nice,renice,chrt명령으로 프로세스 우선순위를 조정할 때 내부 원리를 알아야 효과적으로 사용할 수 있다. - Kubernetes Pod 스케줄링: K8s의 Pod Priority와 Preemption은 CPU 스케줄링 개념과 동일한 원리로 동작한다.
- Node.js Event Loop 이해: microtask > macrotask 순서도 일종의 우선순위 스케줄링이다.
- 장애 대응: CPU 사용률 100% 상황에서 어떤 프로세스를 먼저 처리할지 결정하는 판단 근거가 된다.
3. 핵심 개념
섹션 제목: “3. 핵심 개념”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 -w의 cswch/s |
| Involuntary (비자발적) | Time Quantum 소진 → OS가 강제 선점 | pidstat -w의 nvcswch/s |
# 프로세스별 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 초과 시 스케줄링 오버헤드 의심3.2 FCFS (First Come First Served, 선입선출)
섹션 제목: “3.2 FCFS (First Come First Served, 선입선출)”은행 창구에서 번호표를 뽑고 순서대로 처리한다. 먼저 온 사람이 먼저 서비스를 받는다.
- 도착 순서대로 Ready Queue에 삽입
- 앞 프로세스가 완료될 때까지 CPU를 양보하지 않는다 (비선점)
- Convoy Effect (호위 효과): 실행 시간이 긴 프로세스가 앞에 있으면 짧은 프로세스들이 모두 뒤에서 오래 기다려야 하는 현상
예시: Convoy Effect 시각화
섹션 제목: “예시: Convoy Effect 시각화”프로세스 도착 순서: P1(30ms), P2(3ms), P3(3ms)
Gantt Chart:| P1 (0~30ms) | P2 (30~33ms) | P3 (33~36ms) |
P1 대기: 0msP2 대기: 30ms ← 3ms짜리인데 30ms를 기다림!P3 대기: 33ms
평균 대기 시간: (0 + 30 + 33) / 3 = 21ms ← 매우 비효율만약 순서가 P2 → P3 → P1 이었다면:
| P2 (0~3ms) | P3 (3~6ms) | P1 (6~36ms) |
P2 대기: 0msP3 대기: 3msP1 대기: 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)SJF 예시
섹션 제목: “SJF 예시”프로세스: 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: 3msP2: 16msP3: 9msP4: 0ms
평균 대기 시간: (3 + 16 + 9 + 0) / 4 = 7msSRTF (Shortest Remaining Time First): SJF의 선점 버전. 새로운 프로세스가 도착했을 때 현재 실행 중인 프로세스보다 남은 시간이 짧으면 CPU를 빼앗는다.
Starvation (기아 현상): 짧은 프로세스가 계속 들어오면 긴 프로세스는 영원히 실행되지 못할 수 있다.
3.4 Round Robin (라운드 로빈)
섹션 제목: “3.4 Round Robin (라운드 로빈)”회의에서 발언권을 돌아가며 준다. 각자 1분씩 발언하고, 더 하고 싶으면 다음 차례를 기다린다. 긴급한 내용도 다음 차례까지 기다려야 한다.
- 각 프로세스에게 Time Quantum(시간 할당량, q) 만큼만 CPU를 준다
- 할당 시간이 끝나면 Ready Queue의 맨 뒤로 이동 (선점)
- 시분할 시스템(Time-sharing System)의 기본 알고리즘
n개의 프로세스가 있을 때 각 프로세스는(n-1) × q이하의 시간을 기다린다
Time Quantum 선택의 중요성
섹션 제목: “Time Quantum 선택의 중요성”Time Quantum이 너무 작으면: - Context Switching이 매우 빈번하게 발생 - 오버헤드 증가 → 실제 작업보다 전환 비용이 더 커질 수 있음
Time Quantum이 너무 크면: - FCFS와 동일하게 동작 - 응답 시간이 증가
일반적으로 Time Quantum = 10~100ms 사이가 적절CPU 버스트 시간의 80%가 Time Quantum보다 짧아야 효율적Round Robin 예시 (q = 4ms)
섹션 제목: “Round Robin 예시 (q = 4ms)”프로세스: 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: 4msP3: 7ms
평균 대기 시간: (6 + 4 + 7) / 3 = 5.67ms3.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) # 최고 우선순위 상한3.6 Multilevel Queue (다단계 큐)
섹션 제목: “3.6 Multilevel Queue (다단계 큐)”공항의 탑승 우선순서. 비즈니스 클래스 → 프리미엄 이코노미 → 일반 이코노미 순으로 탑승. 각 그룹은 별도 줄에 서고, 앞 그룹이 모두 탑승해야 다음 그룹이 시작한다.
- 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 응답 시간이 줄어드는 효과가 보고되었다.
# 현재 서버의 Linux 커널 버전 확인 (EEVDF 여부)uname -r# 6.6 이상: EEVDF 적용, 이하: CFS 적용
# AWS EC2에서 커널 업그레이드 여부 확인# Amazon Linux 2023: 커널 6.1 (CFS)# Ubuntu 24.04 LTS: 커널 6.8 (EEVDF 적용)4. 실무에서 어떻게 쓰이나
섹션 제목: “4. 실무에서 어떻게 쓰이나”4.1 Linux CFS (Completely Fair Scheduler)
섹션 제목: “4.1 Linux CFS (Completely Fair Scheduler)”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 등)을 더 깔끔한 수학적 모델로 대체하여, 레이턴시가 더 낮고 예측 가능한 동작을 보장한다.
# 현재 커널의 스케줄러 확인 (Linux 6.6+ 여부)uname -r# 6.6 이상이면 EEVDF, 이전이면 CFS📖 더 보기: EEVDF Scheduler — The Linux Kernel documentation — Linux 커널 공식 EEVDF 스케줄러 설계 문서
# 현재 프로세스의 스케줄링 정보 확인cat /proc/$(pidof nginx)/schedstat# 출력: [CPU 사용 시간] [대기 시간] [스케줄링 횟수]
# CFS 관련 커널 파라미터 확인cat /proc/sys/kernel/sched_min_granularity_ns # 최소 실행 시간 (기본 4ms)cat /proc/sys/kernel/sched_latency_ns # 스케줄링 주기 (기본 8ms)4.2 Kubernetes Pod 스케줄링
섹션 제목: “4.2 Kubernetes Pod 스케줄링”K8s 스케줄러는 CPU 스케줄링 개념을 클러스터 레벨로 확장한 것이다.
| CPU 스케줄링 개념 | K8s 대응 개념 |
|---|---|
| 프로세스 | Pod |
| CPU | Node (노드의 CPU/Memory) |
| Ready Queue | Pending Pod Queue |
| 우선순위 | PriorityClass |
| 선점 (Preemption) | Pod Preemption (낮은 우선순위 Pod 퇴출) |
| 자원 요청 | requests/limits |
# PriorityClass 예시 - 높은 우선순위 정의apiVersion: scheduling.k8s.io/v1kind: PriorityClassmetadata: name: critical-workloadvalue: 1000000 # 높을수록 우선순위 높음globalDefault: falsepreemptionPolicy: PreemptLowerPriority # 낮은 우선순위 Pod 선점 허용description: "mission-critical 워크로드"
---# Pod에 우선순위 적용apiVersion: v1kind: Podmetadata: name: api-serverspec: priorityClassName: critical-workload containers: - name: api image: my-api:latest resources: requests: cpu: "500m" memory: "256Mi" limits: cpu: "1000m" memory: "512Mi"선점 동작 시나리오:
critical-workloadPod가 스케줄링 요청- 모든 노드의 리소스가 부족한 상태
- K8s 스케줄러가 낮은 우선순위 Pod를 찾아 Evict (퇴출)
- 기본 30초 Graceful Termination 후 리소스 회수
critical-workloadPod 스케줄링 완료
4.3 ECS Task Placement 전략
섹션 제목: “4.3 ECS Task Placement 전략”AWS ECS는 컨테이너 배치 전략으로 CPU 스케줄링 개념의 일부를 구현한다:
{ "placementStrategy": [ { "type": "binpack", "field": "cpu" } ]}| 전략 | 설명 | CPU 스케줄링 유사 개념 |
|---|---|---|
binpack | CPU/메모리를 최대한 빽빽하게 채움 | 처리량(throughput) 최적화 |
spread | 인스턴스/AZ에 균등 분산 | 공정성(fairness) |
random | 무작위 배치 | FCFS와 유사 |
4.4 Node.js Event Loop의 스케줄링
섹션 제목: “4.4 Node.js Event Loop의 스케줄링”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
5. 내 업무와의 연결고리
섹션 제목: “5. 내 업무와의 연결고리”시나리오 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 서버에서 특정 서비스 우선순위 조정”# 배치 처리 작업의 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가 먼저 퇴출됨6. 비슷한 개념과 비교
섹션 제목: “6. 비슷한 개념과 비교”스케줄링 알고리즘 종합 비교
섹션 제목: “스케줄링 알고리즘 종합 비교”| 알고리즘 | 선점 여부 | 평균 대기 시간 | Starvation | 구현 복잡도 | 적합한 환경 |
|---|---|---|---|---|---|
| FCFS | 비선점 | 높음 | 없음 | 매우 낮음 | 배치 처리 |
| SJF | 비선점 | 최적 | 있음 | 중간 | 예측 가능한 작업 |
| SRTF | 선점 | 최적 | 있음 | 높음 | 이론적 최적 |
| Round Robin | 선점 | 중간 | 없음 | 낮음 | 시분할 시스템 |
| Priority | 선/비선점 | 낮음 | 있음 | 중간 | 실시간 시스템 |
| Multilevel Queue | 선점 | 낮음 | 있음 | 높음 | 범용 OS |
| CFS (Linux) | 선점 | 낮음 | 거의 없음 | 매우 높음 | 현대 Linux |
OS별 스케줄러 비교
섹션 제목: “OS별 스케줄러 비교”| OS | 스케줄러 | 특징 |
|---|---|---|
| Linux | CFS (기본), SCHED_RT (실시간) | vruntime 기반, Red-Black Tree |
| Windows | Multilevel Feedback Queue (32단계) | 우선순위 부스팅 |
| macOS | Mach Scheduler | Thread 우선순위 5단계 |
| FreeBSD | ULE Scheduler | CFS와 유사 |
CPU 스케줄링 vs 디스크 스케줄링
섹션 제목: “CPU 스케줄링 vs 디스크 스케줄링”CPU 스케줄링과 유사한 개념이 I/O에도 존재한다:
- FCFS: 디스크 요청 순서대로 처리
- SSTF (Shortest Seek Time First): SJF 대응, 헤드에서 가장 가까운 요청 먼저
- SCAN (엘리베이터 알고리즘): Round Robin 대응, 헤드가 한 방향으로 쭉 이동 후 반전
6.5. 트러블슈팅
섹션 제목: “6.5. 트러블슈팅”트러블슈팅 1: CPU 사용률 100% — 특정 프로세스 우선순위 강제 조정
섹션 제목: “트러블슈팅 1: CPU 사용률 100% — 특정 프로세스 우선순위 강제 조정”증상:
$ top%Cpu(s): 99.8 us, 0.1 sy, 0.0 ni, 0.0 id
PID USER PR NI %CPU COMMAND12345 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 서버 응답 지연 발생.
해결 방법:
# 방법 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 COMMAND23456 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 상태 지속 — 우선순위 충돌”증상:
$ kubectl get podsNAME READY STATUS RESTARTScritical-api-v2-xyz 0/1 Pending 0 ← 계속 Pendingbatch-worker-abc 1/1 Running 0batch-worker-def 1/1 Running 0
$ kubectl describe pod critical-api-v2-xyzEvents: 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이 설정되어 있지 않거나, 배치 워커와 동일한 우선순위여서 선점이 발생하지 않음.
해결 방법:
# 1. PriorityClass 생성kubectl apply -f - <<EOFapiVersion: scheduling.k8s.io/v1kind: PriorityClassmetadata: name: high-priorityvalue: 1000000preemptionPolicy: PreemptLowerPriorityglobalDefault: falseEOF
# 2. Pod에 PriorityClass 적용kubectl patch deployment critical-api \ --patch '{"spec":{"template":{"spec":{"priorityClassName":"high-priority"}}}}'
# 3. 재확인 - 배치 워커가 Evict되고 critical-api가 Running으로 변경$ kubectl get podsNAME READY STATUScritical-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를 독점하면 다른 요청을 처리할 수 없다.
해결 방법:
# 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 150msCPU 사용률: 멀티코어로 분산트러블슈팅 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.
해결 방법:
# 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”증상:
$ 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 대기 큐에 쌓여 있는 상태.
해결 방법:
# I/O 병목 프로세스 확인$ iotop -o TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND12345 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.sh7. 체크리스트
섹션 제목: “7. 체크리스트”이론 이해
섹션 제목: “이론 이해”- FCFS의 Convoy Effect가 왜 발생하는지 설명할 수 있다
- SJF가 최적 알고리즘인데 왜 실제 구현이 어려운지 설명할 수 있다
- Round Robin에서 Time Quantum 크기가 성능에 미치는 영향을 설명할 수 있다
- Starvation과 Aging의 관계를 설명할 수 있다
- 선점/비선점 스케줄링의 차이와 Context Switching을 설명할 수 있다
Linux CFS 이해
섹션 제목: “Linux CFS 이해”- 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로 우선순위를 조정할 수 있다
8. 핵심 키워드
섹션 제목: “8. 핵심 키워드”| 키워드 | 설명 |
|---|---|
| CPU 스케줄링 | 어떤 프로세스에게 CPU를 줄지 결정하는 OS 메커니즘 |
| 선점 (Preemptive) | OS가 실행 중인 프로세스를 강제로 중단시킬 수 있는 방식 |
| 비선점 (Non-preemptive) | 프로세스가 스스로 CPU를 반납할 때까지 기다리는 방식 |
| FCFS | First Come First Served, 가장 단순한 스케줄링 |
| Convoy Effect | 긴 작업 뒤에 짧은 작업이 오래 기다리는 현상 |
| SJF | Shortest Job First, 이론적 최적이나 Starvation 발생 |
| SRTF | SJF의 선점 버전, 남은 시간이 더 짧은 프로세스가 오면 선점 |
| Round Robin | 시간 할당량(Time Quantum)씩 순환하는 시분할 방식 |
| Time Quantum | Round Robin에서 각 프로세스에게 주어지는 최대 실행 시간 |
| Priority Scheduling | 우선순위 번호 기반으로 CPU를 할당하는 방식 |
| Starvation | 낮은 우선순위 프로세스가 영원히 실행되지 못하는 현상 |
| Aging | 오래 기다린 프로세스의 우선순위를 점진적으로 높여 Starvation 방지 |
| Multilevel Queue | Ready Queue를 여러 개로 분리해 각각 다른 알고리즘 적용 |
| Context Switching | 프로세스 전환 시 CPU 상태(레지스터, PC 등)를 저장/복원하는 작업 |
| CFS | Completely Fair Scheduler, 현재 Linux의 기본 스케줄러 |
| vruntime | CFS가 각 프로세스의 CPU 사용 시간을 추적하는 가상 실행 시간 |
| nice / renice | Linux에서 프로세스 우선순위 설정 명령 (범위: -20~+19) |
| PriorityClass | K8s에서 Pod 스케줄링 우선순위를 정의하는 객체 |
| Pod Preemption | K8s에서 높은 우선순위 Pod를 위해 낮은 우선순위 Pod를 퇴출하는 메커니즘 |
| Event Loop Lag | Node.js Event Loop 처리 지연 시간, 10ms 이하가 정상 |
8.3. 멀티코어 스케줄링 심화
섹션 제목: “8.3. 멀티코어 스케줄링 심화”CPU Affinity (CPU 친화성)
섹션 제목: “CPU Affinity (CPU 친화성)”기본적으로 Linux 스케줄러는 어떤 CPU 코어에서도 프로세스를 실행할 수 있다. 하지만 프로세스를 특정 코어에 고정하면 캐시 온도(cache warmth) 를 유지할 수 있어 성능이 향상된다. 프로세스가 항상 같은 코어에서 실행되면 L1/L2 캐시에 그 프로세스의 데이터가 남아 있을 가능성이 높다.
# 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 경쟁 방지
NUMA (Non-Uniform Memory Access)
섹션 제목: “NUMA (Non-Uniform Memory Access)”멀티소켓 서버에서는 각 CPU 소켓이 자신과 물리적으로 가까운 RAM에 더 빠르게 접근한다. 프로세스가 CPU 0에서 실행되는데 CPU 1의 RAM에 접근하면 원격 메모리 접근이 발생하여 레이턴시가 2~3배 증가한다.
# 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에 가까워야 하는 실시간 프로세스를 위한 별도 정책이 있다.
# 스케줄링 정책 변경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_OTHER | CFS/EEVDF (기본) | 일반 서버 프로세스 |
SCHED_FIFO | 선점형 실시간, 타임슬라이스 없음 | 오디오 드라이버, 로봇 제어 |
SCHED_RR | 실시간 + 타임슬라이스 (Round Robin) | 실시간이지만 공정성도 필요한 경우 |
SCHED_BATCH | CPU 집약 배치 작업 (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로 분리 |
# 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 친화성이 실제 서버 성능에 미치는 영향 분석 (중급)
8.5. 추천 리소스
섹션 제목: “8.5. 추천 리소스”📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 Linux Kernel CFS 공식 문서 — CFS 설계자(Ingo Molnar)가 직접 작성한 vruntime 철학과 구현 원리 설명 (중급)
- 📖 EEVDF Scheduler — The Linux Kernel documentation — Linux 6.6+에서 CFS를 대체한 EEVDF 스케줄러의 공식 설계 문서 (중급)
- 📖 Kubernetes Pod Priority and Preemption — kubernetes.io — K8s PriorityClass 생성부터 Pod Preemption 동작까지 공식 가이드 (중급)
- 📖 Node.js Event Loop Phases — nodejs.org — nextTick, setImmediate, setTimeout 실행 순서를 페이즈별로 설명 (입문)
- 📖 Inside the Node.js Event Loop: What Actually Blocks Your Production System — NodeSource — 프로덕션에서 실제로 이벤트 루프를 블로킹하는 패턴과 clinic.js 진단 방법 (중급)
9. 직접 확인해보기
섹션 제목: “9. 직접 확인해보기”9.1 현재 시스템의 스케줄링 정보 확인
섹션 제목: “9.1 현재 시스템의 스케줄링 정보 확인”# 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 +159.2 프로세스 우선순위 변경
섹션 제목: “9.2 프로세스 우선순위 변경”# 1. 현재 실행 중인 Node.js 프로세스 PID 확인pidof node
# 2. 배치 작업 우선순위 낮추기sudo renice +10 -p $(pidof batch-worker)예상 출력:
12345 (process ID) old priority 0, new priority 10# 3. 변경 확인ps -o pid,ni,comm -p 12345예상 출력:
PID NI COMMAND12345 10 batch-worker9.3 Linux 스케줄러 파라미터 확인
섹션 제목: “9.3 Linux 스케줄러 파라미터 확인”# CFS 스케줄링 레이턴시 설정 확인cat /proc/sys/kernel/sched_latency_ns예상 출력:
8000000 # 8ms (기본값) - 이 시간 내에 모든 프로세스가 최소 한 번 실행됨cat /proc/sys/kernel/sched_min_granularity_ns예상 출력:
4000000 # 4ms (기본값) - 한 프로세스가 최소로 실행될 수 있는 시간9.4 특정 프로세스 스케줄링 통계
섹션 제목: “9.4 특정 프로세스 스케줄링 통계”# /proc/[PID]/schedstat: [CPU 사용 나노초] [대기 나노초] [스케줄링 횟수]cat /proc/1/schedstat예상 출력:
1234567890 9876543210 5432# CPU 사용: 1.23초, 대기: 9.88초, 5432번 스케줄링됨9.5 K8s PriorityClass 확인
섹션 제목: “9.5 K8s PriorityClass 확인”# 현재 클러스터의 PriorityClass 목록kubectl get priorityclasses예상 출력:
NAME VALUE GLOBAL-DEFAULT AGEsystem-cluster-critical 2000000000 false 10dsystem-node-critical 2000001000 false 10dhigh-priority 1000000 false 1d# Pod의 우선순위 확인kubectl get pod <pod-name> -o jsonpath='{.spec.priority}'예상 출력:
10000009.6 Node.js Event Loop Lag 측정
섹션 제목: “9.6 Node.js Event Loop Lag 측정”// 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`);});node event-loop-lag.js예상 출력:
Event Loop Lag: 0.15ms # 정상 상태# 만약 수십 ms 이상이면 Event Loop 블로킹 의심10. 한 줄 요약
섹션 제목: “10. 한 줄 요약”CPU 스케줄링은 “누구에게, 얼마나, 언제 CPU를 줄 것인가”를 결정하는 OS의 핵심 정책이며, FCFS·SJF·Round Robin·Priority의 원리는 Linux CFS, Kubernetes Pod 스케줄링, Node.js Event Loop에 이르기까지 모든 계층의 자원 관리에 동일하게 적용된다.