들어가며: 4코어 서버에서 1000 프로세스가 돌아가는 법
간단한 관찰
`htop`을 실행해 보자. 수백 개의 프로세스가 보인다. 그러나 CPU는 4개, 혹은 8개뿐이다. 어떻게 이 모든 프로세스가 "동시에" 실행되는가?
답: **아무도 실제로 동시에 실행되지 않는다**. Linux 커널은 수십 ms 단위로 CPU를 프로세스들 사이에 **시간 분할**한다. 너무 빠르게 전환되어 사용자에겐 마치 **동시 실행처럼 보인다**.
이 마법의 주인공이 **Linux 스케줄러**다. 언제, 어떤 프로세스에게 CPU를 줄지를 결정하는 커널의 심장.
좋은 스케줄러의 조건
스케줄러는 여러 상충하는 목표를 동시에 만족해야 한다:
1. **공정성(Fairness)**: 모든 프로세스가 합리적 시간을 받음.
2. **응답성(Responsiveness)**: 인터랙티브 작업은 빨리 반응.
3. **처리량(Throughput)**: CPU가 놀지 않음.
4. **에너지 효율**: 배터리 오래.
5. **확장성**: 수백 코어에서 선형 성능.
6. **우선순위 존중**: 중요한 작업 우선.
7. **Real-time 지원**: 데드라인 맞추기.
이 목표들은 때로 충돌한다. 공정성과 응답성, 처리량과 에너지 효율. 스케줄러의 역사는 이 균형을 찾는 긴 여정이다.
이 글에서 다룰 것
1. **스케줄링의 기본**: Time slice, preemption, context switch.
2. **O(1) 스케줄러**: 2.4~2.6 초기.
3. **CFS**: 2007년 혁명.
4. **EEVDF**: 2023년 CFS의 후계자.
5. **Real-time 클래스**: FIFO, RR, Deadline.
6. **Nice와 가중치**: 우선순위 메커니즘.
7. **cgroup과 그룹 스케줄링**.
8. **실전 튜닝과 관찰**.
1. 스케줄링의 기본 개념
Process States
Linux 프로세스는 여러 상태를 가진다:
- **R (Running)**: 현재 CPU에서 실행 중 또는 실행 대기열에 있음.
- **S (Sleeping)**: I/O 대기 등으로 잠자는 중 (interruptible).
- **D (Uninterruptible sleep)**: 디스크 I/O 등으로 잠자는 중. 시그널도 못 깨움.
- **T (Stopped)**: SIGSTOP 등으로 중단됨.
- **Z (Zombie)**: 종료됐지만 부모가 수확 안 함.
스케줄러는 **R 상태 프로세스**들 사이에서 누구를 실행할지 결정한다.
Context Switch
한 프로세스를 중단하고 다른 프로세스로 전환하는 과정:
1. 현재 프로세스의 **레지스터, 스택 포인터 저장** (kernel stack).
2. **Address space 전환** (`mm_struct`, CR3 레지스터).
3. **TLB flush** (필요 시).
4. 새 프로세스의 **레지스터 로드**.
5. 새 프로세스 실행.
**비용**: 수 μs (대략 1~10 μs). 너무 자주 하면 오버헤드 폭발. 너무 드물면 응답성 나쁨.
Time Slice (Quantum)
프로세스가 CPU를 한 번에 잡을 수 있는 최대 시간. 이 시간이 지나면 강제로 양보:
- **너무 짧으면**: 너무 많은 context switch → 오버헤드.
- **너무 길면**: 다른 프로세스가 기다림 → 응답성 나쁨.
전형적 값: 1~10 ms.
Preemption
프로세스가 time slice를 다 쓰기 전에도 **강제로 중단**될 수 있다. 다음 경우:
1. **Time slice 만료**: 정해진 시간 다 씀.
2. **더 높은 우선순위 프로세스가 ready**: 현재 것 중단.
3. **프로세스가 block**: I/O 대기 등.
4. **자발적 양보**: `sched_yield()`.
Linux 2.6부터 **kernel preemption** (커널 코드 실행 중에도 preempt 가능) 지원.
Run Queue
각 CPU는 **run queue**를 가진다. Ready 상태 프로세스들의 리스트:
CPU 0 rq: [P1, P5, P9, P12, ...]
CPU 1 rq: [P2, P6, P10, ...]
CPU 2 rq: [P3, P7, ...]
CPU 3 rq: [P4, P8, ...]
각 CPU가 자기 큐에서 다음 프로세스를 선택. **이는 확장성 핵심**이다 (전역 락 없음).
Load Balancing
프로세스를 CPU들에 **균등 분산**하는 작업. 주기적으로 실행:
- 한 CPU의 큐가 다른 것보다 훨씬 길면 프로세스 이동.
- **NUMA 인식**: 메모리 locality 고려.
- **Cache affinity**: 프로세스를 기존 CPU에 붙이려는 경향 (캐시 히트율).
2. O(1) 스케줄러: 2.6 초기의 답
배경
Linux 2.4의 스케줄러는 O(N)이었다. 프로세스 수가 증가하면 스케줄링 오버헤드도 증가. 수천 프로세스면 병목.
O(1)의 핵심 아이디어
2002년 Ingo Molnar가 **O(1) 스케줄러**를 도입했다. 상수 시간에 다음 프로세스를 선택.
**구조**:
- 각 CPU마다 **140개의 우선순위 큐** (100 real-time + 40 일반).
- **Bitmap**으로 비어 있지 않은 큐 빠르게 추적.
- `__ffs()` (find first set) 명령으로 O(1) 탐색.
인터랙티브 휴리스틱
O(1)의 복잡한 부분: **인터랙티브 프로세스 감지**. "사용자와 상호작용하는 프로세스는 우선순위를 올려주자"는 의도.
- Sleep 시간 측정.
- Sleep이 많으면 인터랙티브로 판단 → bonus.
- 계산 집약적이면 penalty.
**문제**: 이 휴리스틱이 매우 복잡하고 때때로 틀렸다. Ingo Molnar 본인도 만족하지 못했다.
O(1)의 한계
1. **휴리스틱 블랙박스**: 왜 이 프로세스가 저 프로세스보다 우선인지 이해하기 어려움.
2. **코너 케이스**: 특정 워크로드에서 이상한 동작.
3. **Mobile/Interactive**: 데스크톱에선 응답성 문제.
Linux 커뮤니티는 더 우아한 해결책을 찾기 시작했다. 그 답이 **CFS**였다.
3. CFS: Completely Fair Scheduler
등장
2007년 Linux 2.6.23에 **CFS (Completely Fair Scheduler)** 가 도입되었다. 개발자는 **Ingo Molnar**.
철학: "이상적인 공정성"
CFS의 핵심 아이디어:
> **"이상적으로는 모든 ready 프로세스가 동시에 CPU를 N분의 1씩 쓴다."**
N개 프로세스가 있으면 각자 1/N의 CPU를 받아야 한다. 그러나 실제론 한 번에 하나만 실행 가능. 그래서 CFS는 **"가장 덜 실행된 프로세스"** 를 다음에 실행한다.
vruntime: 가상 실행 시간
각 프로세스는 **vruntime** (virtual runtime)을 가진다. 프로세스가 CPU를 사용한 "공정한" 양.
- CPU를 쓰는 동안 vruntime이 증가.
- 가중치(nice)에 따라 증가 속도가 다름.
- **가장 낮은 vruntime 프로세스가 다음에 실행**.
이는 **"모든 프로세스의 vruntime을 동기화"** 하려는 시도와 같다.
Red-Black Tree
CFS는 프로세스들을 **red-black tree**로 정렬한다. 키는 vruntime:
[P5: vr=100]
/ \
[P3: vr=50] [P8: vr=150]
/ \ / \
[P1: vr=25] [P4: vr=75] [P7: vr=125] [P9: vr=200]
- **삽입/삭제**: O(log N).
- **최솟값 찾기**: O(log N) (또는 캐시된 leftmost 노드로 O(1)).
- **다음 실행**: 가장 왼쪽 노드.
Nice와 가중치
Unix의 **nice** 값 (-20 ~ +19):
- **낮음 (-20)**: 높은 우선순위. 더 많은 CPU.
- **높음 (+19)**: 낮은 우선순위. 적은 CPU.
CFS는 nice를 **가중치**로 변환한다:
nice 0 → weight 1024 (기준)
nice -1 → weight 1277 (~1.25배)
nice +1 → weight 820 (~0.8배)
nice -20 → weight 88761 (~87배)
nice +19 → weight 15 (~0.014배)
각 단계마다 약 1.25배씩 변한다. 10단계 차이는 ~10배 차이.
가중치 적용
vruntime 증가 속도가 가중치에 반비례:
실제 CPU 시간 Δt 사용 시
vruntime 증가 = Δt × (NICE_0_LOAD / weight)
- Nice 0: `vruntime += Δt`.
- Nice -5: 가중치 높음 → vruntime 덜 증가 → 더 자주 선택됨.
- Nice +10: 가중치 낮음 → vruntime 많이 증가 → 덜 선택됨.
결과: **CPU 시간이 가중치에 비례**해서 분배된다. 이것이 "공정한" 분배다.
예시 계산
nice 0 프로세스 3개가 있다면:
- 각자 동일한 weight 1024.
- 총 weight: 3072.
- 각자 몫: 1024/3072 = **1/3**.
nice 0과 nice -5 (weight 약 3906) 프로세스가 있으면:
- 총 weight: 1024 + 3906 = 4930.
- nice 0: 1024/4930 ≈ **21%**.
- nice -5: 3906/4930 ≈ **79%**.
Target Latency와 Minimum Granularity
CFS에는 **목표 레이턴시** 개념이 있다:
- `sched_latency_ns`: 모든 프로세스가 한 번씩 실행되는 목표 주기 (기본 24 ms).
- N개 프로세스면 각자 `24ms / N` 몫.
그러나 프로세스가 많으면 time slice가 너무 작아진다. 그래서:
- `sched_min_granularity_ns`: 최소 time slice (기본 3 ms).
if N ≤ 8: 각자 24/N ms
if N > 8: 각자 3 ms, 총 사이클 3N ms
장점
1. **우아한 알고리즘**: 단일 메커니즘으로 공정성과 우선순위 처리.
2. **예측 가능**: Nice 값과 CPU 배분이 직관적 관계.
3. **휴리스틱 없음**: 인터랙티브 휴리스틱 제거, 단순화.
4. **확장성**: 프로세스 수에 O(log N).
단점과 EEVDF로의 전환
CFS는 2007년부터 2023년까지 16년간 Linux의 기본이었다. 그러나 시간이 지나며 문제가 드러났다:
1. **Latency-sensitive 작업**: 게임, 오디오 등은 평균 공정성보다 **지연 상한**이 중요.
2. **Sleep에 대한 보너스**: Sleep에서 깨어난 프로세스는 vruntime이 낮아 즉시 선점됨 → 복잡한 보정 로직.
3. **WAKE_UP_PREEMPT**: 깨어난 프로세스가 현재 프로세스를 즉시 preempt할지 결정이 까다로움.
CFS의 코드는 점점 복잡해졌고 코너 케이스가 늘어났다. 새로운 접근이 필요했다.
4. EEVDF: 2023년의 새 시대
EEVDF란
**EEVDF (Earliest Eligible Virtual Deadline First)** 는 1995년 Stoica와 Abdel-Wahab이 발표한 알고리즘이다. 2023년 Linux 6.6에서 **CFS를 대체**했다. Peter Zijlstra가 구현.
새로운 아이디어: Deadline
CFS는 "과거의 공정성"에 집중했다 (vruntime은 과거 실행량). EEVDF는 "미래의 deadline"에 집중한다.
각 프로세스는 **virtual deadline**을 가진다:
vdeadline = 최근 ready 시점 + (time_slice / weight)
스케줄러는 **가장 이른 deadline을 가진 적격 프로세스**를 선택한다.
Eligible (적격)
"적격"이라는 개념이 EEVDF의 핵심:
- 프로세스가 지금까지 공정한 몫보다 **덜** 받았으면 eligible.
- 더 받았으면 not eligible (잠시 양보).
이는 **과거 공정성**과 **미래 deadline**을 결합한다. "빚이 있는" 프로세스만 deadline 경쟁에 참여.
장점
1. **Latency 제어**: deadline 기반이라 최악 지연 제한.
2. **Time slice 유연성**: 프로세스별로 slice 요청 가능.
3. **단순성**: CFS의 복잡한 휴리스틱 제거.
4. **수학적 증명**: 공정성과 지연이 수학적으로 보장됨.
실전 영향
대부분의 워크로드에선 **사용자가 차이를 느끼지 못한다**. EEVDF는 CFS와 거의 같은 동작을 하되 코너 케이스가 개선됐다.
구체적 개선:
- **오디오/비디오 재생** 더 안정.
- **게임 프레임 시간** 일관성 향상.
- **대량 task 환경**에서 공정성 더 정확.
현재 상태
Linux 6.6+ 시스템은 기본적으로 EEVDF를 쓴다. 대부분의 문서와 도구는 아직 "CFS"라고 부르지만 내부는 EEVDF다.
5. Scheduling Classes
Linux는 **여러 스케줄링 클래스**를 계층적으로 운영한다. 우선순위 순서대로:
stop_sched_class (최고: 정지 태스크)
↓
dl_sched_class (SCHED_DEADLINE)
↓
rt_sched_class (SCHED_FIFO, SCHED_RR)
↓
fair_sched_class (CFS / EEVDF - 대부분 프로세스)
↓
idle_sched_class (최저: idle task)
상위 클래스에 runnable 태스크가 있으면 항상 먼저 실행된다.
SCHED_NORMAL (CFS/EEVDF)
일반 프로세스. `nice`로 조정.
SCHED_BATCH
CFS와 유사하지만:
- **선점을 덜 함** (응답성보다 처리량 우선).
- 긴 계산 작업에 적합.
- 에너지 효율이 약간 더 좋음.
chrt -b -p 0 <pid>
SCHED_IDLE
가장 낮은 우선순위. 다른 작업이 없을 때만 실행.
chrt -i -p 0 <pid>
백그라운드 큰 계산 (백업, 인덱싱)에 유용.
6. Real-Time Classes
Real-time 스케줄링은 **엄격한 타이밍 요구**가 있는 경우를 위한 것:
SCHED_FIFO
**"First-In-First-Out"**. 특성:
- **우선순위 레벨 1~99** (99가 가장 높음).
- 높은 우선순위가 **항상 선점**.
- **Time slice 없음**: 스스로 양보하거나 block할 때까지 실행.
- 같은 우선순위끼리는 FIFO 순서.
struct sched_param param = { .sched_priority = 50 };
sched_setscheduler(pid, SCHED_FIFO, ¶m);
**주의**: 무한 루프 RT 태스크는 **시스템 동결 가능**. RT 태스크는 반드시 I/O로 block하거나 sleep해야 한다.
SCHED_RR (Round Robin)
SCHED_FIFO와 비슷하지만 **time slice 있음**:
- 같은 우선순위끼리 **시간 분할**.
- 기본 time slice: 100 ms.
- 더 안전.
SCHED_DEADLINE
가장 엄격한 real-time. **"이 시점까지 이만큼 CPU 필요"** 를 명시:
struct sched_attr attr = {
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 10 * 1000 * 1000, // 10 ms
.sched_deadline = 30 * 1000 * 1000, // 30 ms
.sched_period = 30 * 1000 * 1000, // 30 ms
};
sched_setattr(0, &attr, 0);
의미: "매 30 ms 주기마다 10 ms의 CPU를 30 ms 이내에 끝내줘".
EDF (Earliest Deadline First) + CBS (Constant Bandwidth Server) 기반. 수학적 보장 제공.
**사용처**: 오디오 처리, 로봇 제어, 산업 자동화.
RT Throttling
RT 태스크가 **영원히 CPU를 잡을 위험** 방지:
/proc/sys/kernel/
sched_rt_runtime_us = 950000 # 950 ms
sched_rt_period_us = 1000000 # 1000 ms
기본적으로 RT는 **매 1초 중 950 ms**만 쓸 수 있음. 나머지 50 ms는 일반 태스크에 양보. 이 덕분에 RT 태스크 버그로 시스템이 멈추지 않는다.
PREEMPT_RT Patch
표준 Linux 커널의 real-time 지원은 제한적이다. **PREEMPT_RT** 패치는 거의 모든 커널 섹션을 preemptable로 만들어 **실시간 성능**을 제공한다.
Linux 6.12 (2024)부터 PREEMPT_RT가 **메인라인에 병합**되었다. 산업용 Linux의 큰 이정표.
용도: 자동차 제어, 공장 자동화, 전자 증권 거래, 로봇, 우주선.
7. Nice와 ionice
nice 명령어
프로세스 CPU 우선순위 조정:
nice 10으로 실행 (낮은 우선순위)
nice -n 10 ./heavy_script.sh
실행 중 프로세스의 nice 변경
renice 15 -p 1234
사용자의 모든 프로세스
renice 10 -u alice
ionice 명령어
**디스크 I/O 우선순위** 조정 (스케줄러가 CPU뿐 아니라 I/O 스케줄링도 관리):
Idle class (최저)
ionice -c 3 -p 1234
Best-effort with priority 7 (낮음)
ionice -c 2 -n 7 -p 1234
- **Class 1**: Real-time.
- **Class 2**: Best-effort (기본, priority 0-7).
- **Class 3**: Idle (다른 I/O 없을 때만).
백업 스크립트에서 흔히 사용:
ionice -c 3 nice -n 19 ./backup.sh
"CPU와 디스크 모두 idle 우선순위로". 다른 작업을 방해하지 않음.
8. cgroup과 그룹 스케줄링
문제
`nice` 값은 **프로세스 단위**. 하지만 복잡한 시나리오:
- 사용자 Alice가 10개 프로세스 실행.
- 사용자 Bob이 1000개 프로세스 실행.
- Bob이 총 CPU의 99%를 가져간다 (많은 프로세스 덕분).
이는 공정하지 않다. **Alice와 Bob에게 50%씩** 주고 싶다.
cgroup의 그룹 스케줄링
cgroup v2의 **cpu controller**가 이를 해결한다. 프로세스를 그룹으로 묶어 **그룹 단위 공정성**:
mkdir /sys/fs/cgroup/alice
echo "500" > /sys/fs/cgroup/alice/cpu.weight
mkdir /sys/fs/cgroup/bob
echo "500" > /sys/fs/cgroup/bob/cpu.weight
Alice와 Bob 프로세스를 각 그룹에 할당
echo <pid_alice_1> > /sys/fs/cgroup/alice/cgroup.procs
echo <pid_bob_1> > /sys/fs/cgroup/bob/cgroup.procs
이제 **그룹 수준에서 CFS**가 적용되고, 그 안에서 다시 프로세스 수준에서 CFS. 이중 공정성.
cpu.max: 절대 제한
최대 50% CPU
echo "50000 100000" > /sys/fs/cgroup/alice/cpu.max
"100 ms 주기 중 50 ms 사용 가능"
cpu.weight.nice: Nice 스타일
일반 nice 값과 매핑
echo "10" > /sys/fs/cgroup/alice/cpu.weight.nice
실전: Kubernetes
Kubernetes의 CPU `requests`와 `limits`가 바로 cgroup으로 구현된다:
resources:
requests:
cpu: "500m" # 최소 500 milli-cores (공정 몫)
limits:
cpu: "1000m" # 최대 1 core
kubelet이 이를 cgroup 파일로 변환:
- `requests.cpu: 500m` → `cpu.weight = ...`
- `limits.cpu: 1000m` → `cpu.max = "100000 100000"`
9. 멀티 CPU 스케줄링
Per-CPU Run Queue
앞서 설명했듯 각 CPU가 자기 run queue를 가진다. 이는 **확장성의 기반**이다. 수백 코어에서도 한 CPU의 스케줄링이 다른 CPU에 영향 없음.
Load Balancing
그러나 때때로 **균형이 필요**하다:
- 한 CPU에 3개, 다른 CPU에 0개면 불공평.
- 주기적으로 커널이 **load balance** 실행.
Load balance 트리거:
- 주기적 (수 ms 단위).
- CPU가 idle이 되면 (`nohz_idle_balance`).
- Exec/fork 시 (새 프로세스 CPU 선택).
- Wake up 시 (깨운 프로세스 CPU 선택).
Scheduling Domain
모든 CPU가 같은 "비용"으로 접근되지 않는다:
- **같은 코어의 하이퍼스레드**: 매우 저렴 (캐시 공유).
- **같은 소켓의 코어**: 저렴 (L3 공유).
- **다른 소켓 (NUMA)**: 비쌈 (원격 메모리 접근).
Linux는 **scheduling domain**을 계층적으로 구성:
NUMA Node 0
├── Socket 0
│ ├── Core 0
│ │ ├── CPU 0 (HT 0)
│ │ └── CPU 1 (HT 1)
│ └── Core 1
│ ├── CPU 2
│ └── CPU 3
└── Socket 1
├── Core 2
│ ...
NUMA Node 1
...
Load balance는 **작은 도메인부터** 먼저 시도. NUMA 노드 간 이동은 최후의 수단.
CPU Affinity
특정 프로세스를 특정 CPU에 **고정**:
PID 1234를 CPU 2, 3에만 실행
taskset -pc 2,3 1234
새 명령어 실행
taskset -c 0-3 ./my_app
효과:
- **캐시 효율**: 항상 같은 L1/L2 cache 재사용.
- **NUMA 최적**: 메모리가 있는 노드의 CPU에 고정.
- **간섭 격리**: 중요한 워크로드를 특정 코어 전용.
Isolcpus: 코어 격리
부팅 시 일부 CPU를 스케줄러로부터 **완전히 분리**:
GRUB에 추가
isolcpus=4,5,6,7 nohz_full=4,5,6,7 rcu_nocbs=4,5,6,7
이 CPU들은:
- 일반 태스크가 자동으로 안 들어감.
- 명시적으로 `taskset`으로 할당한 태스크만 실행.
- `nohz_full`: tick interrupt도 안 들어감 (거의 진짜 bare metal).
**용도**: 초저지연 트레이딩, 고성능 네트워킹(DPDK), 실시간 처리.
10. 성능 관찰과 분석
top, htop
가장 기본. CPU 사용률, 우선순위, nice 값 확인.
top
htop # 더 예쁨
ps로 스케줄링 클래스 확인
ps -eLo pid,tid,class,rtprio,pri,nice,comm
↑ ↑ ↑ ↑
클래스 rt우선순위 실시간우선순위 nice
- `TS`: SCHED_NORMAL (CFS)
- `FF`: SCHED_FIFO
- `RR`: SCHED_RR
- `IDL`: SCHED_IDLE
- `DLN`: SCHED_DEADLINE
/proc/[pid]/sched
각 프로세스의 스케줄러 통계:
cat /proc/1234/sched
nr_switches, nr_voluntary_switches, run_delay, sum_exec_runtime, ...
**run_delay**: CPU 대기 시간. 높으면 스케줄링 부담.
/proc/schedstat
커널 전체 스케줄링 통계. 거의 원시 데이터라 해석 어려움.
perf sched
가장 강력한 도구. 스케줄링 이벤트 트레이스:
몇 초간 기록
sudo perf sched record -- sleep 10
분석
sudo perf sched latency # latency 통계
sudo perf sched map # 시각적 실행 맵
sudo perf sched timehist # 시간 히스토리
**출력 예시**:
Task Runtime ms Switches Avg delay Max delay
sshd:1234 5.2 12 0.102 0.342
chrome:5678 123.4 1543 1.234 8.901
Max delay가 크면 (수 ms+) 응답성 문제.
bcc/bpftrace
커널 내부 이벤트 추적:
스케줄링 지연 히스토그램
sudo runqlat 10
대기 시간 분포
Context switch 빈도
sudo cpudist -p 1234 10
vmstat 1
r b swpd free ...
3 0 0 1GB ... ← r: runnable, b: blocked
`r` (runnable)이 **CPU 수보다 계속 크면** 스케줄링 경합.
11. 실전 튜닝
인터랙티브 응답성 향상
/etc/sysctl.conf
kernel.sched_migration_cost_ns = 5000000
프로세스 이동 최소 비용. 높이면 캐시 locality 유지.
kernel.sched_autogroup_enabled = 1
데스크톱 autogroup (터미널 세션 단위 공정성)
서버 처리량 우선
선점 덜 자주
kernel.sched_min_granularity_ns = 10000000 # 10 ms
kernel.sched_wakeup_granularity_ns = 15000000 # 15 ms
Autogroup 끔
kernel.sched_autogroup_enabled = 0
낮은 지연 (DB, Real-time)
선점 더 자주
kernel.sched_min_granularity_ns = 1000000 # 1 ms
kernel.sched_wakeup_granularity_ns = 2000000 # 2 ms
Isolcpus + RT priority (부팅 옵션)
isolcpus=4-7 nohz_full=4-7
배치 작업: nice 활용
CI 빌드 스크립트
nice -n 19 ionice -c 3 ./build.sh
CPU와 디스크 모두 최저 우선순위. 다른 작업 방해 없음.
Kubernetes: Guaranteed QoS
Pod에 **integer CPU**를 요청하면 **static CPU manager**가 활성화되어 전용 코어가 할당된다:
resources:
requests:
cpu: "2" # 정수만!
memory: "4Gi"
limits:
cpu: "2" # requests == limits
memory: "4Gi"
낮은 지연이 필요한 워크로드에 유용.
12. 흔한 문제와 해결
문제 1: 프로세스가 "가끔" 느림
**증상**: 평균은 빠른데 가끔 수 ms pause.
**원인**:
- 다른 CPU 집약적 프로세스의 경합.
- Context switch 오버헤드.
- Load balancing migration.
**진단**:
perf sched record -- sleep 10
perf sched latency
**해결**:
- `taskset`으로 affinity 설정.
- `isolcpus` 로 코어 격리.
- Real-time priority 상승.
문제 2: RT 태스크가 시스템 동결
**증상**: 시스템이 응답 안 함.
**원인**: SCHED_FIFO 무한 루프.
**해결**:
- `sched_rt_runtime_us` 설정 (기본 950000). 이미 기본적으로 RT throttling.
- RT 코드 검증 (sleep 또는 I/O 포함).
**예방**: RT 태스크는 반드시 **주기적으로 양보** 또는 **I/O block**.
문제 3: Load average 높은데 CPU 여유
**증상**: `uptime` load average가 높지만 `top`의 CPU 사용률은 낮음.
**원인**: **D state (uninterruptible sleep)** 프로세스. Load average에 포함되지만 CPU 안 씀.
**진단**:
ps -eo state,pid,cmd | grep "^D"
**해결**: 디스크 I/O 또는 네트워크 block 원인 찾기. 실제 CPU 스케줄링 문제 아님.
문제 4: 특정 프로세스 starvation
**증상**: 특정 프로세스가 CPU 거의 못 받음.
**원인**:
- Nice 값 높음 (+19).
- 너무 많은 동등한 우선순위 프로세스.
- I/O 병목으로 ready 시간 짧음.
**해결**:
renice -5 -p <pid>
또는
chrt -r -p 50 <pid> # SCHED_RR
문제 5: 컨테이너 CPU throttling
**증상**: Kubernetes Pod이 latency spike.
**원인**: `limits.cpu` 100ms 주기 내 소진 → 나머지 강제 sleep.
**해결**:
- CPU limit 높이거나 제거.
- `requests`만 설정 (공정 몫만 확보).
- Static CPU manager (Guaranteed QoS).
- `cpu.cfs_period_us` 축소.
퀴즈로 복습하기
**A.**
**버린 것: 복잡한 인터랙티브 휴리스틱**
O(1) 스케줄러는 "이 프로세스가 인터랙티브인가?"를 판단하는 복잡한 규칙이 있었다:
- Sleep 시간을 측정해서 많이 자면 보너스.
- 긴 계산 작업은 페널티.
- 특정 threshold, 특정 scaling factor 등 마법 숫자 가득.
결과: 코너 케이스가 많았고, "왜 이 프로세스가 저 프로세스보다 우선인가?"에 답하기 어려웠다.
**얻은 것: 수학적으로 단순한 공정성 모델**
CFS는 하나의 개념만 쓴다: **vruntime**.
- 모든 프로세스의 vruntime을 "동시에" 증가시키려 함.
- 현재 가장 vruntime이 낮은 것(= 가장 덜 실행된 것)을 선택.
- Nice 값은 vruntime 증가 속도만 조절 (가중치).
이 모델의 우아함:
1. **휴리스틱 없음**: 인터랙티브/batch 구분 불필요. Sleep이 많은 프로세스는 자연스럽게 vruntime이 낮아서 우선.
2. **예측 가능**: nice 값이 가중치를 결정하고, CPU 시간은 가중치에 비례.
3. **증명 가능**: 수학적으로 공정성 보장.
**대가**: Red-black tree 사용으로 O(log N) (O(1)에서 열화). 그러나 실전에선 차이 무시할 정도.
**교훈**: 좋은 설계는 복잡한 휴리스틱을 **하나의 우아한 원리**로 대체한다. CFS는 이 철학의 모범 사례다. Ingo Molnar가 O(1)을 본인이 만들었음에도 CFS로 전환한 것은 "설계의 단순성"이 "성능 최적화"보다 장기적으로 가치 있다는 증거다.
**A.** CFS의 철학은 **"과거 공정성"** 이다. vruntime이 가장 낮은 것(= 가장 적게 실행된 것)을 선택한다. 이는 평균적으로 좋지만 **최악 지연 보장이 없다**.
**CFS의 문제**:
- 많은 프로세스가 있으면 한 바퀴 도는 데 오래 걸림.
- 인터랙티브 프로세스가 wake up 할 때 즉시 preempt되지 않을 수 있음.
- Wake-up 보너스 로직이 복잡하고 완벽하지 않음.
**EEVDF의 해결**:
각 프로세스에 **virtual deadline**을 부여한다:
vdeadline = 현재 + (요청한 time slice / 가중치)
스케줄러는 **가장 이른 deadline**을 가진 적격(eligible) 프로세스를 선택한다.
**결과**:
1. **최악 지연 상한**: deadline이 설정되므로 그 시간 내에 반드시 실행됨.
2. **Slice 유연성**: 프로세스가 "나는 2 ms만 필요해"를 요청 가능.
3. **공정성 유지**: eligible 개념이 과거 공정성도 보장.
**실전 효과**:
- **오디오 재생**: 버퍼 언더런 감소.
- **게임**: 프레임 타이밍 일관성.
- **Interactive shell**: 반응 시간 더 균일.
- **VoIP**: 패킷 처리 지연 감소.
**대부분 사용자는 차이를 못 느낀다**. 워크로드가 그리 까다롭지 않으면 CFS와 EEVDF는 비슷하게 동작한다. 그러나 특정 latency-sensitive 워크로드에선 EEVDF가 명확히 낫다.
또 하나 중요한 점: **EEVDF는 수학적 보장**이 있다. CFS는 경험적으로 잘 동작했지만 특정 코너 케이스에서의 보장이 없었다. EEVDF는 1995년 논문에서 공정성과 지연의 상한을 수학적으로 증명했다.
Linux 6.6 (2023년)에서 기본 스케줄러가 되었고, 앞으로 수년간 Linux의 주력이 될 것이다. CFS의 진화는 멈추지 않는다.
**A.** 둘 다 real-time 스케줄링 클래스지만 접근이 완전히 다르다.
**SCHED_FIFO**: **우선순위 기반**
- 1~99의 우선순위 레벨.
- 높은 우선순위가 낮은 것을 **항상 선점**.
- 같은 우선순위끼리는 FIFO 순서.
- **Time slice 없음**: 스스로 양보하거나 block까지 계속 실행.
- 사용: 우선순위 관계가 명확한 경우.
**문제점**:
- 여러 RT 태스크가 있을 때 "누가 몇 % 받아야 하는가"를 우선순위로만 표현하기 어려움.
- 실수로 무한 루프면 낮은 우선순위 태스크가 starvation.
- Rate monotonic analysis 등 수학적 도구 필요.
**SCHED_DEADLINE**: **공약(budget) 기반**
세 가지 값으로 표현:
- **Runtime**: 이만큼 CPU가 필요.
- **Deadline**: 이 시점까지.
- **Period**: 이 주기마다 반복.
예: "매 30 ms 주기마다, 10 ms의 CPU를, 30 ms 이내에 끝내줘".
**이점**:
1. **수학적 보장**: EDF + CBS 알고리즘으로 공정한 스케줄링.
2. **자원 격리**: 한 태스크의 런타임 초과가 다른 태스크에 영향 없음.
3. **스케줄 가능성 검증**: 커널이 admission control 수행 (받을지 거절할지 판단).
4. **Real-time 이론**과 일치: Liu-Layland 같은 고전 이론 직접 적용.
**사용 예**:
- **SCHED_FIFO**: 오래된 real-time 애플리케이션, 단순 제어 루프.
- **SCHED_DEADLINE**: 오디오 처리 (주기적), 비디오 디코딩, 로봇 제어, 산업 자동화.
**주의**:
- SCHED_DEADLINE은 root 권한 필요.
- admission control이 거부하면 설정 실패.
- 다른 SCHED_DEADLINE 태스크와의 관계 고려 필요.
현대 real-time 시스템은 SCHED_DEADLINE을 선호한다. 더 체계적이고 예측 가능하기 때문이다. SCHED_FIFO는 legacy 또는 단순한 경우에만.
**A.** **약 12배**다.
**계산**:
CFS는 nice 값을 가중치로 변환한다. 변환 테이블:
nice -5 → weight ~3121
nice 0 → weight 1024
nice +5 → weight ~335
두 프로세스만 있을 때:
- 총 weight: 3121 + 335 = 3456.
- nice -5 몫: 3121 / 3456 ≈ **90.3%**.
- nice +5 몫: 335 / 3456 ≈ **9.7%**.
**비율**: 90.3 / 9.7 ≈ **9.3배**.
조금 다르게 계산하면 10단계 차이마다 **약 10배**다. (1.25^10 ≈ 9.3)
실제 커널 테이블:
static const int sched_prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
**Nice 값의 유용성**:
- **nice -5**: 중요한 작업에 약 3배 더 많은 CPU (다른 것과 동시일 때).
- **nice +5**: 백그라운드 작업에 약 1/3.
- **nice +19**: 거의 idle만.
- **nice -20**: 거의 독점.
**실전 권장**:
- 배치/백업: `nice +10~+19`.
- 일반: `nice 0`.
- 대화형 shell: `nice 0` 또는 `-5`.
- 중요한 데몬: `nice -5 ~ -10`.
- **극단 값 (-20, +19)**: 특별한 경우만.
**주의**:
1. **CPU가 충분하면 nice 효과 없음**: 모두가 CPU를 받으니까.
2. **경쟁 상황**에서만 의미: 여러 CPU 집약적 작업 있을 때.
3. **cgroup과 함께**: cgroup이 우선적 메커니즘. Nice는 그 안에서.
**가중치의 기하급수성**: 1.25배씩 증가가 **절대적이 아니라 상대적**이다. nice -5는 nice 0 대비 3배지만, nice 0이 nice +5 대비해서도 3배다. 균등한 인상/하락 경험을 제공.
이것이 "10단계 차이면 10배"라는 Linux 규칙의 수학적 근거다. 엔지니어링의 아름다운 간결성.
**A.** 세 가지 주요 이유가 있다: **캐시 locality, NUMA locality, 간섭 감소**.
**1. 캐시 Locality**
CPU는 L1/L2/L3 캐시를 가진다. L1은 각 코어 전용, L3는 보통 공유.
Core 0 (L1: 32KB, L2: 256KB)
Core 1 (L1: 32KB, L2: 256KB)
Shared L3: 8MB
프로세스가 Core 0에서 실행되면 그 워킹셋이 Core 0의 L1/L2에 로드된다. 같은 프로세스가 다음에 **다른 코어로 이동하면** (스케줄러의 load balancing) 새 코어의 캐시는 비어 있어서 **모든 데이터를 다시 로드**. 캐시 miss 폭발 → 느려짐.
`taskset`으로 CPU를 고정하면 **항상 같은 캐시 사용** → 높은 hit rate → 빠른 실행.
**효과**: 캐시 민감 워크로드 (in-memory DB, 해시테이블 등)에서 **10~30% 향상** 가능.
**2. NUMA Locality**
멀티 소켓 서버에서 메모리는 NUMA 노드별로 분산되어 있다:
Node 0: [CPUs 0-15][RAM 128GB]
Node 1: [CPUs 16-31][RAM 128GB]
Core 0이 Node 0 RAM에 접근하면 로컬 → 빠름.
Core 0이 Node 1 RAM에 접근하면 원격 → 1.5~2배 느림.
프로세스가 Node 0 RAM에 할당되었는데 스케줄러가 Core 16으로 이동시키면 모든 메모리 접근이 원격. 재앙.
`numactl` + `taskset`으로 **프로세스와 메모리를 같은 노드에 고정**:
numactl --cpunodebind=0 --membind=0 ./myapp
**효과**: 큰 메모리 워크로드에서 **최대 2배 향상**.
**3. 간섭 감소**
일반 스케줄러는 "중요한 작업"과 "백그라운드 작업"을 구분하지 않고 섞는다. 중요한 작업이 실행 중 갑자기 선점되면 지연 발생.
**격리 기법**:
1. `isolcpus=4-7`로 CPU 4-7을 **커널 스케줄러로부터 제외**.
2. `taskset -c 4-7 ./important_app`으로 명시적 할당.
3. 이 코어에선 오직 `important_app`만 실행.
**효과**:
- p99 latency 극적 감소.
- 트레이딩 시스템의 마이크로초 수준 지연 일관성.
- DPDK 네트워킹의 라인레이트 달성.
**실전 사용**:
캐시 locality만 필요
taskset -c 0-3 ./my_cache_sensitive_app
NUMA locality 추가
numactl --cpunodebind=0 --membind=0 ./my_memory_app
완전 격리 (부팅 isolcpus=4-7 전제)
taskset -c 4-7 ./my_isolated_app
**함정**:
1. **Over-affinity**: 너무 엄격하게 고정하면 load balancing이 안 돼 다른 CPU가 idle이어도 활용 못 함.
2. **코어 수보다 많은 프로세스**: affinity 있어도 자기들끼리 경합.
3. **측정 필수**: 벤치마크로 확인. 이론만으론 효과 예측 어려움.
**요약**: `taskset`은 "스케줄러를 믿지 않고 내가 직접 관리"하는 도구다. 일반 워크로드엔 불필요하지만, **성능이 극도로 중요한** 시스템(DB, 트레이딩, 실시간 처리)에서 **필수적**이다. Linux 스케줄러가 똑똑해도, 특정 워크로드의 요구사항을 완전히 아는 건 개발자뿐이다.
마치며: 보이지 않는 조율자
핵심 정리
1. **CFS (2007~)**: 공정성 기반 스케줄러. vruntime으로 우아하게.
2. **EEVDF (2023+)**: CFS의 후계자. deadline 기반 latency 제어.
3. **Real-time**: SCHED_FIFO, SCHED_RR, SCHED_DEADLINE.
4. **Nice**: 프로세스 우선순위. 10단계 = 10배.
5. **cgroup**: 그룹 단위 공정성. Kubernetes의 기반.
6. **CPU affinity**: 캐시/NUMA locality 확보.
7. **isolcpus**: 극한 격리. 실시간 워크로드.
왜 이 지식이 가치 있는가
스케줄러는 **눈에 안 보이지만 모든 성능의 근원**이다. 이해 없이는:
- 왜 특정 프로세스가 느린지 설명 못함.
- CPU 제한이 어떻게 작동하는지 모름.
- Kubernetes CPU throttling에 당황.
- Real-time 요구사항 만족 못함.
- 서버 튜닝이 감에 의존.
이 글의 지식은 **수십 년간 수천 명의 커널 개발자가 쌓아 올린 공학**이다. 그것을 이해하면:
- **적절한 nice 값** 선택.
- **언제 taskset 쓸지** 판단.
- **cgroup 설정**을 의식적으로.
- **RT 클래스** 활용 여부 결정.
- **스케줄링 관련 문제** 논리적 디버깅.
마지막 교훈
O(1) → CFS → EEVDF. 20년의 진화는 **"좋은 해결책은 계속 발견된다"** 는 교훈을 준다. 각 단계는 이전 단계의 한계를 직시하고 새로운 원리로 해결했다. Ingo Molnar는 자기가 만든 O(1)을 버리고 CFS를 만들었고, 이제 CFS도 EEVDF에게 자리를 내줬다.
이는 소프트웨어 엔지니어링의 본질이다. **"영원한 최적"은 없다**. 시스템이 진화하고, 워크로드가 변하고, 하드웨어가 바뀌면 답도 바뀐다. 그리고 매번 더 우아하고 더 정확한 해결책이 등장한다.
당신이 다음에 `top`을 볼 때, 잠시 멈추고 생각해 보자: **이 모든 프로세스가 공평하게(?) 실행되도록 수천 번의 스케줄링 결정이 매 초마다 내려지고 있다**. 그 복잡한 연산이 당신의 컴퓨터를 "반응하는 살아있는 기계"로 만든다. 스케줄러는 조용한 조율자이며, 그 존재를 알아챌 때 비로소 당신은 시스템을 **진짜로 이해**하는 것이다.
참고 자료
- [CFS Scheduler Design Documentation](https://www.kernel.org/doc/html/latest/scheduler/sched-design-CFS.html)
- [EEVDF: An earliest eligible virtual deadline first scheduler (LWN)](https://lwn.net/Articles/925371/)
- [Linux Kernel Scheduler Documentation](https://www.kernel.org/doc/html/latest/scheduler/index.html)
- [Stoica & Abdel-Wahab: Earliest Eligible Virtual Deadline First (1995)](https://ieeexplore.ieee.org/document/6593073)
- [Con Kolivas' BFS scheduler debates (historical)](https://kolivas.org/) - 스케줄러 대안 역사
- [Robert Love: Linux Kernel Development, Ch.4 Process Scheduling](https://www.oreilly.com/library/view/linux-kernel-development/9780672329463/)
- [Understanding the Linux Kernel, Ch.7](https://www.oreilly.com/library/view/understanding-the-linux/0596005652/) - 전통적 설명
- [LWN: An EEVDF CPU scheduler for Linux](https://lwn.net/Articles/925371/)
- [The PREEMPT_RT real-time Linux project](https://wiki.linuxfoundation.org/realtime/start)
- [Brendan Gregg: Systems Performance Ch.6 CPUs](https://www.brendangregg.com/systems-performance-2nd-edition-book.html)
현재 단락 (1/541)
`htop`을 실행해 보자. 수백 개의 프로세스가 보인다. 그러나 CPU는 4개, 혹은 8개뿐이다. 어떻게 이 모든 프로세스가 "동시에" 실행되는가?