본문 바로가기
서버연구 블로그 서버연구 블로그

Linux CPU Scheduler는 프로세스를 어떻게 실행하는가? CFS 동작 원리

읽는 시간 약 18분

Linux 서버에서 여러 프로그램을 동시에 실행해도 사용자는 대부분 모든 프로그램이 동시에 동작하는 것처럼 느낀다.

Nginx가 요청을 처리하고, 데이터베이스가 Query를 실행하며, 백업 프로그램이 파일을 압축하는 동안에도 운영체제는 수많은 프로세스와 스레드를 계속 실행한다.

하지만 하나의 CPU 코어가 특정 순간에 실제로 실행할 수 있는 작업은 제한되어 있다.

그렇다면 Linux는 수십 개, 수백 개의 프로세스 가운데 어떤 프로세스를 먼저 CPU에서 실행할까?

이 역할을 담당하는 것이 CPU Scheduler다.

서버에서 CPU 사용률이 높아지고 실행해야 할 프로세스가 많아지면 단순히 CPU 성능만 문제가 되는 것이 아니다. CPU를 기다리는 작업이 증가하고 Scheduler가 CPU 시간을 분배하는 과정 자체가 애플리케이션 응답 시간에 영향을 줄 수 있다.

Linux 서버의 CPU 문제를 제대로 분석하려면 단순히 top에서 CPU 사용률을 보는 것을 넘어 Scheduler가 프로세스를 어떤 기준으로 선택하는지 이해할 필요가 있다.


CPU가 충분한데도 프로세스는 왜 기다릴까?

CPU Scheduler가 필요한 가장 큰 이유는 실행하려는 작업 수가 CPU 코어 수보다 많기 때문이다.

예를 들어 4코어 서버에서 동시에 실행 가능한 CPU 작업이 20개 있다고 가정해 보자.

CPU 0 → Process A
CPU 1 → Process B
CPU 2 → Process C
CPU 3 → Process D

대기
Process E
Process F
Process G
Process H
...

실제로 CPU에서 실행될 수 있는 작업은 4개뿐이다.

나머지 프로세스는 CPU를 사용할 수 있는 순서를 기다려야 한다.

Linux에서는 실행 가능한 상태의 작업을 일반적으로 Runnable Task라고 부른다.

프로세스 상태를 확인할 때 흔히 볼 수 있는 R 상태가 여기에 해당한다.

중요한 점은 R이 반드시 현재 CPU를 사용하고 있다는 의미는 아니라는 것이다.

CPU에서 실제로 실행 중이거나, CPU를 받을 준비가 된 상태에서 기다리고 있을 수도 있다.

Runnable Task가 많아지면 CPU를 받기 위해 기다리는 시간도 증가한다.

결국 서버의 응답 시간이 느려지는 원인은 단순히 CPU 사용률 100% 자체보다 CPU를 사용하려는 작업이 얼마나 많이 경쟁하고 있는가에 있을 수도 있다.


Linux CPU Scheduler는 무엇을 하는가?

Scheduler의 기본 역할은 매우 단순하다.

실행 가능한 Task
       ↓
   Run Queue
       ↓
   Scheduler
       ↓
   CPU 선택
       ↓
Task 실행

프로세스가 실행 가능한 상태가 되면 Scheduler가 관리하는 실행 대기 구조에 들어간다.

Scheduler는 여기서 다음으로 CPU를 사용할 Task를 선택한다.

하나의 Task가 일정 시간 CPU를 사용한 뒤 다른 Task가 CPU를 사용해야 한다면 실행 주체가 변경된다.

이 과정에서 발생하는 것이 Context Switch다.

Linux는 단순히 프로세스가 들어온 순서대로 CPU를 나누어 주지 않는다.

프로세스의 우선순위, 지금까지 사용한 CPU 시간, 실행 가능한 작업 수, CPU 코어 상태 등을 고려해 CPU를 분배한다.

Linux의 일반적인 프로세스 스케줄링을 오랫동안 담당해 온 핵심 설계가 **CFS(Completely Fair Scheduler)**다.

CFS는 Linux 2.6.23에 포함됐으며, 실제 CPU에서 여러 작업이 가능한 한 공정하게 CPU 시간을 나누어 갖도록 하는 것을 목표로 설계됐다. Linux 공식 문서는 이를 가상의 ‘이상적인 멀티태스킹 CPU’를 실제 하드웨어에서 최대한 비슷하게 구현하는 방식으로 설명한다.


CFS의 핵심은 CPU 시간을 똑같이 나누는 것이 아니다

CFS라는 이름 때문에 모든 프로세스가 정확히 같은 시간만큼 CPU를 받는다고 생각하기 쉽다.

실제로는 조금 다르다.

CFS는 각 Task가 얼마나 공정하게 CPU를 사용했는지 추적한다.

이를 위해 사용하는 대표적인 값이

vruntime

즉, Virtual Runtime이다.

예를 들어 실행 가능한 프로세스가 다음과 같이 있다고 가정해 보자.

Process A : vruntime 120
Process B : vruntime 180
Process C : vruntime 250

단순화해서 설명하면 A는 다른 Task보다 상대적으로 CPU를 적게 사용한 상태다.

기존 CFS의 핵심적인 선택 방식은 vruntime이 작은 Task에 CPU를 제공해 CPU 사용 기회를 맞추는 것이다.

A가 CPU에서 실행되면 A의 실제 실행 시간이 누적되고 이에 따라 vruntime 역시 증가한다.

A 실행

A : 120 → 170

다음 후보

A : 170
B : 180
C : 250

이 과정을 반복하면서 특정 프로세스 하나가 계속 CPU를 독점하지 못하도록 한다.

Linux 공식 CFS 문서에서도 각 Task의 vruntime을 추적하고, 이를 이용해 실제 CPU 시간을 이상적인 공정한 CPU 분배에 가깝게 만드는 것이 핵심이라고 설명한다.


nice 값은 CPU 우선순위에 어떻게 영향을 줄까?

Linux에서 프로세스를 확인하면 NI, 즉 nice 값을 볼 수 있다.

ps -eo pid,comm,pri,ni,%cpu --sort=-%cpu

예를 들어 다음과 같은 결과가 나올 수 있다.

PID   COMMAND     PRI   NI   %CPU
1250  java         20    0   85.3
1320  backup       30   10   35.2

nice 값은 일반 프로세스 사이의 CPU 배분 가중치에 영향을 준다.

일반적으로 nice 값이 높아질수록 CPU에 대한 우선순위는 낮아진다.

CFS에서는 이 차이를 vruntime 계산에 반영한다.

우선순위가 낮은 Task는 같은 실제 CPU 시간을 사용해도 vruntime이 상대적으로 빠르게 증가한다.

반대로 높은 가중치를 가진 Task는 vruntime이 더 천천히 증가한다.

결과적으로 높은 우선순위의 Task가 더 많은 CPU 시간을 받을 수 있다.

따라서 nice 값은 단순히

이 프로세스를 먼저 실행한다

라는 의미보다

CPU 경쟁이 발생했을 때
이 프로세스에 어느 정도의 CPU 몫을 줄 것인가

에 가깝게 이해하는 것이 좋다.


여러 CPU 코어에서는 어떻게 처리할까?

실제 서버에는 보통 여러 개의 CPU 코어가 존재한다.

예를 들어 8코어 서버라면 Linux는 단순히 모든 프로세스를 하나의 거대한 대기열에서 관리하는 방식으로만 동작하지 않는다.

개념적으로 각 CPU는 자신이 처리할 Runnable Task를 관리하며 Scheduler는 CPU 사이의 부하도 조정해야 한다.

CPU 0 Run Queue
A B C

CPU 1 Run Queue
D E

CPU 2 Run Queue
F

CPU 3 Run Queue
G H I

만약 CPU 0에는 실행 대기 Task가 많고 CPU 2는 상대적으로 한가하다면 그대로 두는 것은 비효율적이다.

Linux Scheduler는 CPU 사이의 작업량을 조정하는 Load Balancing도 수행한다.

따라서 멀티코어 서버에서는 전체 CPU 사용률뿐 아니라 특정 CPU에 부하가 집중되어 있는지도 중요하다.

다음 명령으로 CPU별 사용률을 확인할 수 있다.

mpstat -P ALL 1

전체 평균 CPU 사용률이 40%라고 하더라도 특정 CPU 하나가 계속 100%에 가까운 상태라면 애플리케이션 구조, CPU affinity, interrupt 처리 등의 문제를 추가로 살펴봐야 한다.


CPU Scheduler 문제가 생기면 어떤 증상이 나타날까?

Scheduler 자체가 고장 나는 경우보다 훨씬 흔한 것은 Runnable Task가 너무 많아 CPU 경쟁이 심해지는 상황이다.

예를 들어 웹 서버에 갑자기 많은 요청이 들어왔다고 가정해 보자.

Request 증가
     ↓
Application Thread 증가
     ↓
Runnable Task 증가
     ↓
CPU 경쟁 증가
     ↓
Scheduling 대기 증가
     ↓
응답 시간 증가

CPU 사용률이 높아지면서 다음과 같은 증상이 함께 나타날 수 있다.

  • API 응답 시간이 갑자기 증가한다.
  • Load Average가 상승한다.
  • Runnable Process가 증가한다.
  • Context Switch가 증가한다.
  • 요청 Timeout이 발생한다.
  • 애플리케이션 처리량이 더 이상 증가하지 않는다.

특히 CPU가 100% 가까이 사용되고 있는데 처리량은 거의 증가하지 않는다면 CPU 경쟁이나 Context Switch 비용까지 함께 확인할 필요가 있다.


vmstat의 r 값은 왜 중요할까?

CPU 경쟁 상태를 빠르게 확인할 때 유용한 명령어가 있다.

vmstat 1

출력에는 다음과 같은 항목이 나타난다.

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b    swpd   free   buff  cache   si  so    bi    bo   in   cs us sy id wa
 2  0       0  52000  12000 320000    0   0     2     4 1200 2300 45 10 45  0

여기서 CPU 분석에서 특히 주목해야 할 값이 r이다.

r은 실행 중이거나 CPU에서 실행될 준비가 되어 있는 작업 수를 파악하는 데 도움이 된다.

예를 들어 4코어 서버에서 r 값이 계속 다음과 같이 나타난다면

r = 2
r = 3
r = 4

CPU 경쟁이 심하다고 단정하기 어렵다.

반대로

r = 15
r = 20
r = 25

처럼 CPU 코어 수보다 훨씬 높은 상태가 지속된다면 많은 Task가 CPU 실행 기회를 기다리고 있을 가능성이 높다.

단, 순간적으로 r 값이 상승했다고 바로 장애라고 판단하면 안 된다.

지속시간과 CPU 사용률, 애플리케이션 응답 시간 등을 함께 비교해야 한다.


Context Switch도 함께 확인해야 한다

CPU Scheduler가 Task를 변경하면 Context Switch가 발생한다.

vmstat에서는 cs 값을 통해 Context Switch 발생량을 확인할 수 있다.

in     cs
1200   2500
1500   3500
1800   9500

Context Switch가 발생하면 CPU는 기존 Task의 실행 상태를 저장하고 새로운 Task의 상태를 불러와야 한다.

따라서 Context Switch가 지나치게 많으면 실제 애플리케이션 작업이 아닌 Task 전환에 CPU 자원이 사용될 수 있다.

프로세스별 Context Switch는 다음처럼 볼 수 있다.

pidstat -w 1

여기에서는 자발적 Context Switch와 비자발적 Context Switch를 구분해서 확인할 수 있다.

특히 CPU 경쟁이 심한 환경에서 비자발적 Context Switch가 크게 증가한다면 Scheduler가 실행 중인 Task를 교체하는 상황이 빈번한지 살펴볼 필요가 있다.

다만 Context Switch 숫자 하나만 보고 문제가 있다고 판단하면 안 된다.

웹 서버처럼 많은 Connection과 Thread를 처리하는 시스템은 정상 상태에서도 Context Switch가 많을 수 있기 때문이다.

항상 정상 시점의 값과 장애 시점의 값을 비교해야 한다.


Load Average와 CPU 사용률만 보면 안 되는 이유

서버가 느려지면 흔히 다음 두 가지부터 확인한다.

uptime
top

예를 들어 8코어 서버에서 Load Average가 다음처럼 나타났다고 가정해 보자.

load average: 16.20, 14.80, 12.30

숫자가 높다는 것은 분명 추가 분석이 필요한 신호다.

하지만 Load Average가 높다고 해서 무조건 CPU Scheduler 때문에 느리다고 판단해서는 안 된다.

Linux Load Average에는 CPU를 기다리는 Runnable Task뿐 아니라 특정 종류의 I/O 대기 상태도 영향을 줄 수 있기 때문이다.

따라서

Load Average
     ↓
CPU 사용률 확인
     ↓
vmstat r 확인
     ↓
iowait 확인
     ↓
Context Switch 확인
     ↓
프로세스별 CPU 확인

처럼 원인을 좁혀가는 것이 좋다.


CPU 경쟁은 어떻게 해결해야 할까?

먼저 가장 중요한 것은 CPU Scheduler 값을 무작정 튜닝하는 것이 아니다.

CPU 경쟁이 발생한 원인을 찾아야 한다.

애플리케이션 Thread가 과도하게 많다면 Thread 수를 조절해야 할 수 있다.

불필요한 백그라운드 작업이 CPU를 사용하고 있다면 실행 시간을 분리할 수도 있다.

특정 프로세스가 CPU를 독점하고 있다면 프로파일링을 통해 어떤 코드가 CPU를 사용하고 있는지 찾아야 한다.

필요하다면 프로세스 우선순위를 조정할 수도 있다.

nice -n 10 command

이미 실행 중인 프로세스라면

renice 10 -p PID

와 같은 방법을 사용할 수 있다.

다만 nice 값을 변경하는 것은 근본적인 CPU 부족 문제를 해결하는 방법이 아니다.

CPU 자체가 부족한 상황이라면 CPU 코어를 늘리거나 애플리케이션을 여러 서버로 분산하는 Scale Out이 필요할 수도 있다.

결국 Scheduler 튜닝보다 먼저 해야 할 일은

CPU 부족인가?

Runnable Task가 너무 많은가?

특정 프로세스가 CPU를 독점하는가?

Thread가 지나치게 많은가?

Context Switch가 과도한가?

를 구분하는 것이다.


최신 Linux에서는 CFS만 알면 충분할까?

여기서 한 가지 중요한 변화가 있다.

오랫동안 Linux 일반 프로세스 스케줄링을 설명할 때 CFS와 vruntime이 핵심이었지만, 최신 Linux Kernel은 기존 CFS의 Task 선택 방식을 EEVDF(Earliest Eligible Virtual Deadline First) 방식으로 전환하고 있다.

Linux 커널 공식 문서에 따르면 Linux 6.6부터 EEVDF로의 전환이 시작됐다.

EEVDF 역시 공정한 CPU 분배라는 기본 목적은 유지한다.

다만 단순히 가장 작은 vruntime을 가진 Task를 선택하는 방식에서 더 나아가 Task가 자신이 받아야 할 CPU 몫보다 부족하게 실행되었는지를 나타내는 lag와 virtual deadline을 이용한다.

개념적으로는 다음과 같다.

Runnable Task
      ↓
CPU 몫 계산
      ↓
lag 평가
      ↓
실행 가능한 Task 선택
      ↓
virtual deadline 비교
      ↓
다음 Task 실행

CPU 시간을 덜 받은 Task 가운데 virtual deadline이 빠른 Task를 선택하는 방식이다.

이 구조는 특히 짧은 실행 시간을 요구하는 latency-sensitive Task의 응답성을 개선하는 데 도움이 된다.

따라서 현대 Linux Scheduler를 이해할 때는

CFS
↓
vruntime을 이용한 공정한 CPU 분배 개념

EEVDF
↓
CFS의 공정성 개념을 이어받으면서
lag + virtual deadline을 활용한 Task 선택

처럼 이해하는 것이 좋다.

CFS의 개념이 의미가 없어졌다는 뜻은 아니다.

vruntime, CPU 가중치, Runnable Task, Run Queue 같은 개념은 현재 Linux Scheduler를 이해하는 데도 중요한 기반이다.


정리

Linux CPU Scheduler의 핵심 역할은 수많은 실행 가능한 Task 가운데 어떤 Task가 다음 CPU 시간을 사용할 것인지 결정하는 것이다.

CFS는 오랫동안 vruntime을 이용해 Task가 사용한 CPU 시간을 추적하고 가능한 한 공정하게 CPU 시간을 배분해 왔다.

하지만 서버 운영자 입장에서 더 중요한 것은 Scheduler 알고리즘 자체를 외우는 것이 아니다.

실제 장애 상황에서 다음 흐름을 이해할 수 있어야 한다.

요청 증가
   ↓
Runnable Task 증가
   ↓
CPU 경쟁
   ↓
Scheduling 대기 증가
   ↓
Context Switch 증가
   ↓
애플리케이션 응답 지연

서버가 느려졌을 때 CPU 사용률만 확인해서는 충분하지 않은 이유가 여기에 있다.

top, vmstat, mpstat, pidstat 등을 함께 사용해 CPU 사용률뿐 아니라 Runnable Task, CPU별 부하, Context Switch를 종합적으로 확인해야 한다.

그리고 최신 Linux에서는 기존 CFS의 철학을 이어받으면서 EEVDF 기반 스케줄링으로 변화하고 있다는 점도 알아둘 필요가 있다.

결국 CPU 성능 문제를 제대로 분석하려면 단순히

“CPU가 몇 % 사용되고 있는가?”

에서 끝나는 것이 아니라,

“CPU를 사용하려는 Task가 몇 개이며, 그 Task들이 CPU 시간을 어떻게 경쟁하고 있는가?”

까지 봐야 한다.

이 관점이 잡혀야 Linux 서버에서 발생하는 CPU 병목을 단순한 숫자가 아니라 Scheduler 내부의 실행 흐름으로 분석할 수 있다.

instone
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.