Skip to content
Published on

리눅스 CPU 스케줄러의 진화 — CFS에서 EEVDF까지

Authors

들어가며

"CPU 사용률은 60%인데 왜 응답 시간이 튀죠?" — 운영을 해본 분이라면 한 번쯤 마주친 질문입니다. 답은 대부분 스케줄러에 있습니다. CPU가 남아돌아도, 내 스레드가 런큐에서 기다리는 시간(스케줄링 지연)이 길면 사용자 입장에서는 느린 것입니다. 특히 쿠버네티스의 CPU limits로 인한 스로틀링, 시끄러운 이웃(noisy neighbor), 컨텍스트 스위치 폭증은 사용률 그래프에 잘 드러나지 않는 단골 범인입니다.

이 글에서는 리눅스 CPU 스케줄러가 풀려는 문제의 본질부터 시작해, 15년 넘게 기본이었던 CFS의 원리와 한계, 그리고 커널 6.6부터 기본 스케줄러가 된 EEVDF를 정리합니다. 후반부에서는 cgroup CPU 제어와 쿠버네티스의 연결, 저지연 워크로드 격리, 그리고 API 서버 테일 레이턴시를 단계별로 개선하는 실전 시나리오를 다룹니다.

스케줄링 문제의 본질 — 세 마리 토끼

CPU 스케줄러는 본질적으로 세 가지 목표 사이의 트레이드오프를 다룹니다.

        공정성 (Fairness)
        "모두가 가중치만큼 CPU를 받는다"
              /\
             /  \
            /    \
           /  ??  \
          /        \
         /----------\
   지연 (Latency)    처리량 (Throughput)
  "깨어난 태스크가     "컨텍스트 스위치를 줄이고
   빨리 실행된다"       캐시를 따뜻하게 유지한다"
  • 공정성: 같은 우선순위의 태스크는 같은 양의 CPU 시간을 받아야 합니다. nice 값이 다르면 가중치 비율대로 받아야 합니다.
  • 지연: I/O를 기다리다 깨어난 태스크(키 입력, 네트워크 응답)는 가능한 한 빨리 CPU를 잡아야 체감 반응성이 좋습니다.
  • 처리량: 태스크를 자주 바꿔치우면 컨텍스트 스위치 비용과 캐시 오염으로 전체 일량이 줄어듭니다.

지연을 줄이려면 자주 선점해야 하고, 처리량을 높이려면 길게 실행해야 합니다. 둘은 정면으로 충돌합니다. CFS에서 EEVDF로의 전환은 이 충돌을 "공정성을 유지하면서 지연 요구를 1급 시민으로 만드는" 방향으로 푼 이야기입니다.

CFS의 원리 — vruntime과 레드블랙 트리

CFS(Completely Fair Scheduler)는 2007년 커널 2.6.23부터 기본 스케줄러였습니다. 핵심 아이디어는 "이상적인 완전 공정 CPU"의 시뮬레이션입니다. 태스크가 N개라면 각 태스크는 CPU의 1/N씩을 동시에 받는 것이 이상적이지만, 실제 CPU는 한 번에 하나만 실행하므로, 각 태스크가 지금까지 받은 CPU 시간을 기록해 가장 적게 받은 태스크를 다음에 실행하는 방식으로 근사합니다.

그 "지금까지 받은 시간"이 vruntime(virtual runtime)입니다.

            런큐의 레드블랙 트리 (vruntime 순 정렬)

                        [ T3: 220ms ]
                       /             \
              [ T1: 180ms ]      [ T5: 300ms ]
              /          \              \
     [ T7: 150ms ]   [ T2: 200ms ]   [ T4: 350ms ]
        ^
        |
   가장 왼쪽 노드 = vruntime 최소 = 다음 실행 대상
   (O(1)로 캐시되어 있음, 삽입/삭제는 O(log N))

동작 규칙은 다음과 같습니다.

  1. 실행 가능한 태스크는 vruntime을 키로 레드블랙 트리에 정렬됩니다.
  2. 스케줄러는 항상 가장 왼쪽(=vruntime이 가장 작은) 태스크를 고릅니다.
  3. 태스크가 실행되는 동안 vruntime이 증가하고, 충분히 커지면 트리의 오른쪽으로 밀려나 다른 태스크에 차례가 갑니다.
  4. 잠들었다 깨어난 태스크는 vruntime이 작아서(=덜 받았으므로) 우선 실행되는 경향이 있습니다. 단, 너무 옛날 값으로 트리를 독점하지 못하게 최소값 근처로 보정됩니다.

nice 값(우선순위)은 vruntime이 증가하는 속도로 반영됩니다.

vruntime 증가량 = 실제 실행 시간 x (기준 가중치 1024 / 태스크 가중치)

nice  0 -> 가중치 1024  (실제 시간 그대로 누적)
nice -5 -> 가중치 3121  (vruntime이 약 1/3 속도로 증가 -> CPU 3배)
nice  5 -> 가중치 335   (vruntime이 약 3배 속도로 증가 -> CPU 1/3)

nice 한 단계 차이 = CPU 시간 약 1.25배 차이가 되도록 가중치 테이블 설계

높은 우선순위 태스크는 vruntime이 천천히 증가하므로 트리 왼쪽에 오래 머물고, 결과적으로 CPU를 더 받습니다. 우아한 설계입니다.

CFS의 한계 — 공정하지만 지연에는 둔감하다

CFS는 "장기적으로 공정"하지만, "단기적으로 언제 실행되는가"에 대한 보장이 약했습니다.

첫째, 지연 요구를 표현할 방법이 없었습니다. 오디오 처리 스레드와 배치 컴파일 작업이 같은 nice 0이라면 CFS는 둘을 동등하게 취급합니다. 오디오 스레드는 "CPU를 많이"가 아니라 "조금이라도 자주, 제때" 필요한데, nice는 양만 조절할 뿐 시점을 조절하지 못합니다.

둘째, 선점 시점이 휴리스틱에 의존했습니다. 깨어난 태스크가 현재 태스크를 선점할지 결정하는 wakeup_granularity, 한 번에 도는 최소 시간 조각 등 튜닝 노브가 많았고, 워크로드마다 좋은 값이 달라 "마법 상수" 논쟁이 끊이지 않았습니다.

이를 보완하려는 latency-nice 패치 시리즈가 수년간 논의됐지만, CFS 구조 위에 지연 우선순위를 덧대는 방식은 깔끔하게 정리되지 않았습니다. 결국 커뮤니티는 알고리즘 자체를 바꾸는 쪽을 선택합니다.

EEVDF — 커널 6.6의 새 기본 스케줄러

EEVDF(Earliest Eligible Virtual Deadline First)는 1995년 학계 논문에서 제안된 알고리즘으로, Peter Zijlstra가 커널에 구현해 6.6(2023년 10월)부터 CFS의 fair 클래스 내부 알고리즘을 대체했습니다. 외부에서 보면 여전히 SCHED_OTHER/SCHED_NORMAL이지만, 내부 선택 로직이 바뀐 것입니다.

핵심 개념은 두 가지입니다.

먼저 eligible(자격) 개념입니다. 각 태스크가 받아야 할 몫 대비 실제 받은 양을 추적해서, 이미 제 몫보다 많이 받은 태스크(lag가 음수)는 당분간 선택 자격이 없습니다. 이것이 공정성을 담당합니다.

다음으로 virtual deadline(가상 데드라인)입니다. 자격이 있는 태스크들 중에서는 가상 데드라인이 가장 이른 태스크를 고릅니다. 가상 데드라인은 대략 "지금 시점 + 그 태스크의 타임 슬라이스를 가중치로 나눈 값"입니다.

EEVDF 선택 로직:

1) eligible 필터:  lag >= 0 인 태스크만 후보
   (자기 몫을 초과해 받은 태스크는 제외 -> 공정성 보장)

2) deadline 선택:  후보 중 virtual deadline이 가장 이른 태스크 실행

   짧은 슬라이스를 요청한 태스크  -> 데드라인이 가까움
                                  -> 자주, 빨리 선택됨 (저지연)
   긴 슬라이스를 요청한 태스크    -> 데드라인이 멂
                                  -> 덜 자주, 길게 실행 (고처리량)

   => 받는 총량은 가중치로 동일하게 유지하면서,
      "잘게 자주" vs "굵게 가끔"을 태스크별로 선택 가능

여기서 중요한 진전이 나옵니다. 타임 슬라이스 길이가 의미를 갖게 되면서, "CPU 총량은 같게 받되 더 자주 실행되고 싶다"는 지연 요구를 sched_setattr의 슬라이스 요청으로 표현할 수 있는 기반이 생겼습니다(커널 6.12부터 sched_runtime 필드로 슬라이스 힌트 지정 가능). CFS 시절의 휴리스틱 노브 여러 개가 제거되고 알고리즘적 근거가 있는 선택으로 대체된 것도 큰 수확입니다.

CFS와 EEVDF를 비교하면 다음과 같습니다.

항목CFSEEVDF
선택 기준vruntime 최소eligible 중 virtual deadline 최소
공정성vruntime 수렴으로 보장lag 추적으로 보장(더 엄밀)
지연 표현불가(nice는 양만 조절)슬라이스 길이로 표현 가능
선점 결정wakeup granularity 휴리스틱데드라인 비교로 결정
튜닝 노브많음(마법 상수 논쟁)대폭 축소
이론적 근거직관적 공정 큐잉1995년 논문의 지연 한계 증명

운영 관점의 체감 변화는 "깨어난 지연 민감 태스크가 더 예측 가능하게 빨리 돈다", 그리고 "커널 버전을 6.6+로 올리면 스케줄러 동작이 미묘하게 달라질 수 있으므로 성능 회귀 테스트가 필요하다"는 점입니다.

# 현재 커널의 스케줄러 기능 확인
uname -r                          # 6.6 이상이면 EEVDF
cat /sys/kernel/debug/sched/features | tr ' ' '\n' | head

# 태스크별 스케줄링 통계
cat /proc/1234/sched | head -20   # vruntime, 슬라이스, 대기 시간 등

스케줄링 클래스 체계 — fair는 여럿 중 하나

EEVDF/CFS는 사실 전체 그림의 일부입니다. 리눅스 스케줄러는 우선순위가 다른 여러 클래스의 계층 구조이며, 위 클래스에 실행할 태스크가 있으면 아래 클래스는 아예 CPU를 받지 못합니다.

우선순위 높음
  +------------------+
  | stop             |  CPU 핫플러그 등 커널 내부 전용
  +------------------+
  | deadline (DL)    |  SCHED_DEADLINE: runtime/deadline/period 보장
  +------------------+
  | realtime (RT)    |  SCHED_FIFO / SCHED_RR: 정적 우선순위 1-99
  +------------------+
  | fair (EEVDF)     |  SCHED_OTHER / SCHED_BATCH: 일반 태스크 전부
  +------------------+
  | idle             |  SCHED_IDLE: 그 외 아무도 없을 때만
  +------------------+
우선순위 낮음
# 태스크의 클래스/우선순위 확인 및 변경
chrt -p 1234                      # 현재 정책 확인
chrt -f -p 50 1234                # SCHED_FIFO 우선순위 50으로
chrt -d --sched-runtime 5000000 --sched-deadline 10000000 \
     --sched-period 10000000 -p 0 1234   # deadline 클래스

# RT 태스크가 CPU를 독점하지 못하게 하는 안전장치
sysctl kernel.sched_rt_runtime_us  # 기본 950000 (1초당 95%)

실무 주의점: SCHED_FIFO를 잘못 쓰면 우선순위 99의 폭주 스레드가 시스템 전체를 멈출 수 있습니다. RT를 쓸 때는 반드시 sched_rt_runtime_us 안전장치를 확인하고, 가능하면 워치독을 두는 것이 좋습니다.

cgroup CPU 제어 — 쿠버네티스와의 연결

컨테이너 시대의 스케줄링은 태스크 단위가 아니라 cgroup 단위 제어가 중심입니다. cgroup v2의 CPU 컨트롤러가 제공하는 두 축은 다음과 같습니다.

인터페이스의미쿠버네티스 대응
cpu.weight (1-10000, 기본 100)경합 시 상대적 배분 비율requests.cpu에서 환산
cpu.max (quota period)주기당 사용 가능한 절대 상한limits.cpu에서 환산
cpu.stat사용량과 스로틀 통계스로틀링 진단의 핵심

cpu.weight는 "경합할 때만" 작동하는 비율 개념이고, cpu.max는 "남아돌아도" 강제로 자르는 절대 상한입니다. 이 차이가 운영에서 결정적으로 중요합니다.

# cgroup v2 CPU 제어 직접 실습
mkdir /sys/fs/cgroup/demo
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control

# 가중치 200 (기본 100인 형제보다 경합 시 2배)
echo 200 > /sys/fs/cgroup/demo/cpu.weight

# 100ms 주기당 50ms = CPU 0.5개 상한
echo "50000 100000" > /sys/fs/cgroup/demo/cpu.max

# 현재 셸을 이 cgroup에 넣고 부하 테스트
echo $$ > /sys/fs/cgroup/demo/cgroup.procs

쿠버네티스의 CPU limits는 cpu.max의 쿼터로 구현됩니다(CFS bandwidth control이라 불리던 메커니즘으로, EEVDF 이후에도 동일하게 동작합니다). 문제는 스로틀링입니다. 멀티스레드 애플리케이션이 8개 스레드로 동시에 돌면 100ms 주기의 쿼터를 수십 밀리초 만에 소진하고, 남은 주기 내내 전부 멈춥니다. CPU 사용률 평균은 낮은데 p99 지연이 튀는 전형적인 패턴입니다.

# 스로틀링 진단: 컨테이너 cgroup의 cpu.stat
cat /sys/fs/cgroup/kubepods.slice/.../cpu.stat
# nr_periods    -- 경과한 주기 수
# nr_throttled  -- 쿼터 소진으로 정지당한 주기 수
# throttled_usec -- 누적 정지 시간

# nr_throttled / nr_periods 비율이 1%만 넘어도 테일 레이턴시에 영향

대응 옵션은 세 가지입니다. limits를 올리거나 제거하고 weight(requests) 기반 보호에 의존하기, 애플리케이션의 워커 수를 쿼터에 맞추기(예: Go의 GOMAXPROCS, Java의 ActiveProcessorCount), 또는 지연 민감 파드에 Guaranteed QoS + static CPU manager로 전용 코어를 주기입니다.

CPU 어피니티와 격리 — 저지연 레시피

지연이 마이크로초 단위로 중요한 워크로드(트레이딩, 실시간 미디어, 패킷 처리)는 "스케줄러가 잘 해주길" 기대하는 대신 코어를 아예 떼어줍니다.

# 1) 부트 파라미터로 CPU 2-5를 일반 스케줄링에서 격리
#    (GRUB_CMDLINE_LINUX에 추가)
isolcpus=2-5 nohz_full=2-5 rcu_nocbs=2-5

# isolcpus:  스케줄러 로드밸런싱 대상에서 제외
# nohz_full: 해당 CPU에서 태스크 1개만 돌면 타이머 틱 정지
# rcu_nocbs: RCU 콜백 처리를 다른 CPU로 이관

# 2) IRQ도 격리 코어를 피하게
echo 3 > /proc/irq/default_smp_affinity   # CPU 0,1만 (마스크 0b0011)

# 3) 워크로드를 격리 코어에 고정
taskset -c 2-5 ./latency-critical-app
# 또는 실행 중 변경
taskset -cp 2-5 1234

# 4) 확인: 격리 코어에서 틱이 멈췄는지
cat /proc/interrupts | grep -i "local timer"

최신 커널에서는 부트 파라미터 대신 cpuset cgroup의 격리 파티션을 쓰는 방법도 있습니다.

# cgroup v2 cpuset으로 동적 격리 파티션 구성
mkdir /sys/fs/cgroup/rt-part
echo "+cpuset" > /sys/fs/cgroup/cgroup.subtree_control
echo 2-5 > /sys/fs/cgroup/rt-part/cpuset.cpus
echo isolated > /sys/fs/cgroup/rt-part/cpuset.cpus.partition

격리는 공짜가 아닙니다. 격리된 코어는 일반 워크로드가 못 쓰므로 전체 활용률이 떨어지고, 잘못 설정하면 격리 코어가 놀고 나머지가 과부하되는 역효과가 납니다. "측정으로 정당화되는 경우에만" 쓰는 도구입니다.

컨텍스트 스위치와 런큐 지연 관측

스케줄링 문제의 측정은 두 지표가 중심입니다. 컨텍스트 스위치 빈도와, 런큐에서 기다린 시간(스케줄링 지연)입니다.

# 시스템 전체 컨텍스트 스위치 빈도
vmstat 1            # cs 컬럼
# 프로세스별 자발/비자발 스위치
pidstat -w -p 1234 1
# voluntary: I/O 대기 등 스스로 양보 (정상적 패턴)
# nonvoluntary: 슬라이스 소진/선점 (경합 신호)

# perf sched로 정밀 기록/분석
perf sched record -- sleep 10
perf sched latency --sort max   # 태스크별 최대 스케줄링 지연
perf sched timehist             # 타임라인: wait time, sch delay 컬럼

# eBPF로 런큐 지연 히스토그램 (BCC runqlat)
runqlat 10 1
# 또는 bpftrace 원라이너
bpftrace -e 'tracepoint:sched:sched_wakeup { @qt[args->pid] = nsecs; }
  tracepoint:sched:sched_switch /@qt[args->next_pid]/
  { @lat = hist(nsecs - @qt[args->next_pid]); delete(@qt[args->next_pid]); }'

해석 기준을 간단히 말하면, 런큐 지연 p99가 수 밀리초를 넘는데 CPU 사용률이 높지 않다면 cgroup 스로틀링이나 코어 쏠림을 의심하고, 사용률도 높다면 진짜 CPU 부족이므로 증설이나 워크로드 이동이 답입니다. 비자발 스위치가 폭증하면 같은 코어를 두고 다투는 스레드가 많다는 뜻으로, 어피니티나 워커 수 조정을 검토합니다.

NUMA 균형 — 메모리까지 고려한 스케줄링

멀티소켓 서버에서 스케줄러는 CPU만 고르는 것이 아니라 "어느 메모리 가까이에서 돌릴지"도 결정합니다. 원격 NUMA 노드 메모리 접근은 로컬보다 수십 퍼센트 느리기 때문입니다.

# NUMA 토폴로지 확인
numactl --hardware
lscpu | grep -i numa

# 자동 NUMA 밸런싱 상태 (페이지 폴트 기반으로 태스크/메모리를 가깝게 이동)
sysctl kernel.numa_balancing

# 태스크별 NUMA 통계
cat /proc/1234/numa_maps | head
numastat -p 1234     # 노드별 메모리 분포

# 명시적 배치 (DB처럼 큰 메모리 워크로드에서)
numactl --cpunodebind=0 --membind=0 ./db-server

자동 NUMA 밸런싱은 대부분의 워크로드에 이득이지만, 페이지 폴트 기반 샘플링 비용이 있어 대용량 메모리 DB에서는 끄고 명시적으로 배치하는 경우도 많습니다. 이것도 측정이 우선입니다.

실전 시나리오 — API 서버 테일 레이턴시 개선

상황: 쿠버네티스에서 도는 Go API 서버. 평균 응답 5ms, 그런데 p99가 150ms까지 튑니다. CPU 사용률은 노드 기준 55%. 단계별로 진단합니다.

1단계, 스로틀링부터 확인합니다. 사용률이 낮은데 지연이 튀면 1순위 용의자입니다.

# 파드의 cgroup 경로를 찾아 cpu.stat 확인
kubectl exec api-pod -- cat /sys/fs/cgroup/cpu.stat
# nr_throttled가 nr_periods의 8%였다고 가정 -> 범인 후보 확정

2단계, 원인을 해석합니다. limits가 2 CPU인데 Go 런타임의 GOMAXPROCS가 노드 코어 수인 16으로 잡혀 있었습니다. 16개 워커가 동시에 돌면 200ms 쿼터(100ms 주기 x 2)를 순식간에 소진하고 주기 끝까지 전부 정지합니다.

3단계, 조치를 하나씩 적용하고 측정합니다.

# 조치 A: GOMAXPROCS를 쿼터에 맞춤 (Go 1.25+는 컨테이너 인지 자동화,
# 이전 버전은 automaxprocs 라이브러리 또는 env로)
kubectl set env deployment/api GOMAXPROCS=2

# 조치 B: 그래도 스로틀이 남으면 limits 상향 또는 제거를 검토
# (requests는 유지 -> cpu.weight 보호는 그대로)

# 조치 C: 노드 단위 경합이 의심되면 runqlat로 이웃 영향 확인
runqlat -p $(pgrep api-server) 10 1

4단계, 결과를 기록합니다. 이 사례 패턴에서는 조치 A만으로 nr_throttled가 0 근처로 떨어지고 p99가 안정되는 경우가 많습니다. 남은 꼬리는 GC나 네트워크 쪽일 수 있으므로 같은 방법으로 다음 구간을 좁힙니다.

핵심 교훈: limits 기반 스로틀링은 "평균에는 안 보이고 꼬리에만 보이는" 문제이며, cpu.stat의 nr_throttled가 가장 정직한 지표입니다.

한 걸음 더 — PSI와 sched_ext

마지막으로 스케줄링 관측과 실험의 최신 도구 두 가지를 소개합니다.

첫째, PSI(Pressure Stall Information)입니다. 런큐 지연을 eBPF로 직접 재는 것이 정밀하지만, PSI는 "실행 가능했지만 CPU를 기다린 시간의 비율"을 커널이 상시 집계해 파일로 제공합니다. cgroup 단위로도 제공되므로, 파드별 CPU 압박의 요약 지표로 매우 실용적입니다.

# 시스템 전체 CPU 압박
cat /proc/pressure/cpu
# some avg10=4.21 avg60=2.10 ... 최근 10초간 일부 태스크가
#   CPU를 기다리며 보낸 시간이 4.21%라는 뜻

# cgroup(파드) 단위 CPU 압박
cat /sys/fs/cgroup/kubepods.slice/cpu.pressure

# 메모리/IO 압박도 같은 형식 (멀티 리소스 병목의 구분에 유용)
cat /proc/pressure/memory /proc/pressure/io

"CPU 사용률은 낮은데 cpu.pressure의 some이 높다"는 조합은 스로틀링이나 코어 쏠림의 강력한 신호입니다. 사용률보다 압박(pressure)을 알람 기준으로 삼는 편이 꼬리 지연과 훨씬 잘 상관합니다.

둘째, sched_ext입니다. 커널 6.12에 병합된 확장 스케줄러 클래스(SCHED_EXT)로, 스케줄링 정책 자체를 eBPF 프로그램으로 작성해 실행 중에 적재/교체할 수 있는 프레임워크입니다.

항목fair (EEVDF)sched_ext
정책 결정 주체커널 내장 알고리즘사용자가 작성한 BPF 프로그램
교체 방법커널 빌드/부팅런타임 적재/해제
실패 시 안전장치해당 없음워치독이 기본 스케줄러로 자동 복귀
적합 용도범용 기본값게임/지연 특화, 실험, 워크로드 맞춤

게임 워크로드에 특화된 정책, 특정 서비스의 캐시 지역성에 맞춘 정책 등을 커널 재빌드 없이 실험할 수 있고, BPF 검증기와 워치독 덕분에 실패해도 시스템이 죽지 않습니다. 범용 서버의 기본값을 바꿀 일은 당분간 없겠지만, "스케줄러를 워크로드에 맞춘다"는 선택지가 생겼다는 점은 알아둘 가치가 있습니다.

함정과 안티패턴

  • nice 값으로 지연 문제를 풀려는 것. nice는 CPU 양의 배분이지 응답 시점 보장이 아닙니다. 지연이 문제면 EEVDF 슬라이스, RT 클래스, 격리를 검토해야 합니다.
  • CPU limits를 "안전장치"로 모든 파드에 거는 것. 경합이 없어도 스로틀이 걸려 테일 레이턴시를 해칩니다. weight(requests) 보호와 limits 상한의 역할 차이를 이해하고 선택해야 합니다.
  • 컨테이너 안 런타임이 호스트 코어 수를 보는 것(GOMAXPROCS, Java 스레드 풀). 쿼터와 워커 수의 불일치는 스로틀링의 최다 빈출 원인입니다.
  • SCHED_FIFO를 워치독 없이 쓰는 것. 폭주 시 시스템 행을 만듭니다.
  • isolcpus를 과도하게 잡는 것. 격리 코어는 유휴여도 일반 태스크가 못 씁니다.
  • 커널 메이저 업그레이드(특히 6.6 전후) 시 스케줄러 변화를 성능 테스트 없이 통과시키는 것.
  • 평균 CPU 사용률만 보는 것. 런큐 지연과 스로틀 카운터가 없으면 꼬리 문제는 보이지 않습니다.

운영 체크리스트

  • 커널 버전과 스케줄러(6.6+ EEVDF 여부)를 파악하고 있는가
  • cpu.stat의 nr_throttled/throttled_usec을 파드 지표로 수집하는가
  • 런큐 지연(runqlat/perf sched)을 측정할 수단이 준비되어 있는가
  • 컨테이너 런타임의 워커 수가 CPU 쿼터와 정합하는가
  • limits를 걸 파드와 걸지 않을 파드의 기준이 문서화되어 있는가
  • RT 클래스 사용 시 sched_rt_runtime_us 안전장치를 확인했는가
  • 저지연 워크로드의 격리(전용 코어, IRQ 어피니티)가 측정으로 정당화되는가
  • NUMA 토폴로지와 자동 밸런싱 설정을 인지하고 있는가
  • 비자발 컨텍스트 스위치 폭증에 대한 알람이 있는가
  • 스케줄러 관련 변경은 한 번에 하나씩, 전후 p99와 함께 기록하는가

마치며

CFS는 "모두에게 공정하게"라는 단순하고 강력한 원칙으로 15년을 버텼고, EEVDF는 그 공정성을 유지하면서 "언제 실행되는가"라는 지연의 차원을 알고리즘 안으로 들여왔습니다. 운영자 입장에서 스케줄러는 블랙박스가 아닙니다. cpu.stat, runqlat, perf sched라는 세 개의 창으로 들여다보면, "CPU는 남는데 느리다"는 미스터리의 답은 대부분 그 안에 있습니다. 다음 글에서는 스케줄러와 짝을 이루는 격리 기술, cgroups와 네임스페이스로 컨테이너의 실체를 해부합니다.

참고 자료