🎥 AI 시대 옵저버빌리티 전략 웨비나 | 무료 다시보기 (~4/9)
Top
도입문의
테크
2026-08-28

리눅스 CPU 사용률이 높은 이유와 확인 방법

서버 CPU 사용률이 90%를 넘으면 스케일업이나 인스턴스 증설부터 떠올리기 쉽습니다. 그런데 그 90%를 무엇이 채웠는지는 퍼센트 하나에 드러나지 않습니다. CPU 사용률은 일정 시간 동안 프로세서가 user, system, idle, iowait, steal 각 상태로 보낸 시간의 비율입니다. 흔히 말하는 사용률 90%는 idle을 뺀 나머지가 90%라는 뜻이지, 그 90%를 전부 계산에 썼다는 뜻은 아닙니다.

CPU 사용률은 어딘가에서 벌어진 일이 겉으로 드러난 결과 값입니다. 같은 90%라도 코드가 계산을 많이 하는 것과, 잠금을 두고 스레드가 경쟁하는 것, 디스크 응답을 기다리는 것은 서로 다른 문제입니다.

물리 서버와 클라우드 VM, 컨테이너에서 원인을 좁혀 가는 순서는 같습니다. 다만 클라우드 VM에서는 steal을, 컨테이너에서는 throttling(스로틀링)을 하나씩 더 봅니다.

CPU가 높을 때 확인하는 순서

알림을 받고 서버에 접속한 직후라면 아래 다섯 개를 순서대로 실행합니다.

top                            # us·sy·wa·st 중 무엇이 높은지, 상위 프로세스는 무엇인지
vmstat 1                       # 대기 프로세스(b)와 컨텍스트 스위칭(cs) 추이
iostat -xz 1                   # wa가 높을 때, 어느 디바이스가 느린지
pidstat -d 1                   # 그 디스크를 실제로 쓰는 프로세스
cat /sys/fs/cgroup/cpu.stat    # 컨테이너라면 throttling이 걸렸는지 (cgroup v2)

top 결과를 보고 나머지 방향을 정합니다.

지표 지표의 뜻 의심할 원인 다음에 볼 것
user (us) 애플리케이션 코드를 실행한 시간 무한 루프, 비효율 알고리즘, GC 상위 프로세스와 스레드 덤프
system (sy) 커널 코드를 실행한 시간 잦은 시스템 호출, 컨텍스트 스위칭 스레드 수, vmstat의 cs
iowait (wa) 입출력을 기다리며 유휴였던 시간 디스크 병목, 느린 스토리지 vmstat → iostat → pidstat
steal (st) 하이퍼바이저에 빼앗긴 시간 호스트 과밀, 이웃 인스턴스 경합 호스트 CPU 경합 여부, 이웃 부하

us·sy·wa·st가 모두 낮은데 응답만 느리다면 CPU 밖에서 원인을 찾습니다. 데이터베이스 쿼리와 외부 호출, 커넥션 풀 대기를 먼저 보고, 컨테이너라면 사용률이 낮아도 throttling이 걸렸는지 cpu.stat을 확인합니다.

리눅스 CPU 사용률은 us·sy·wa·st로 나눠 확인한다

리눅스 CPU 사용률 90%를 사용자 코드 us, 커널 작업 sy, 입출력 대기 wa, 빼앗긴 시간 st 네 종류로 나누고 각각 의심할 원인을 보여 주는 도식
us·sy·wa·st의 비중은 서버마다 다르고, 가장 높은 값이 먼저 확인할 지표입니다.

CPU 사용률을 볼 때 전체 퍼센트 하나만 보면 방향이 잡히지 않습니다. 먼저 사용률을 종류별로 나눕니다. top의 첫 화면 %Cpu(s) 줄과 vmstat의 cpu 항목이 이 값들을 us, sy, id(유휴), wa, st로 나눠 보여 줍니다.

각 값이 어떤 기준으로 집계되는지, 커널이 이 비율을 어떻게 계산하는지는 CPU 사용량, 어떻게 측정될까?에서 다룹니다. 이 글은 이미 높게 나온 값을 두고 원인을 찾아가는 쪽을 다룹니다.

top에서 CPU 사용률 높은 프로세스 확인하기

us가 높다면 어느 프로세스가 그 시간을 쓰는지부터 확인합니다. top은 기본이 %CPU 내림차순이고, 정렬이 바뀌어 있으면 P 키로 되돌립니다. 목록 맨 위에 오른 프로세스가 지금 CPU를 가장 많이 쓰고 있습니다. PID 열에서 프로세스 번호를, %CPU 열에서 점유율을 확인합니다.

대화형 화면 없이 상위 프로세스만 뽑으려면 ps를 씁니다. 알림 스크립트에 넣어 두면 장애 시점의 상위 프로세스를 자동으로 남길 수 있습니다.

ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%cpu | head

프로세스를 특정했다면 그 안의 어느 스레드가 CPU를 쓰는지 확인합니다. top -Hp <PID>는 프로세스 대신 그 프로세스의 스레드를 나열합니다.

Java 스레드 덤프로 문제 코드 위치까지 확인한다

Java 애플리케이션이라면 스레드 ID 하나로 문제 코드 위치까지 확인할 수 있습니다. top -Hp <PID>에서 CPU를 가장 많이 쓰는 스레드의 ID를 확인한 뒤 jstack <PID>jcmd <PID> Thread.print로 스레드 덤프를 뜹니다. top이 보여 주는 스레드 ID는 10진수인데 덤프에는 nid=0x... 형태의 16진수로 적히므로, printf "%x\n" <스레드ID>로 변환해 덤프에서 찾으면 그 스레드가 실행 중이던 코드 위치가 그대로 나옵니다. 상위를 차지한 것이 GC 스레드면 힙 쪽을, 여러 스레드가 같은 잠금 앞에 waiting to lock으로 몰려 있으면 락 경합을 봅니다.

리눅스 CPU 사용률이 높아지는 원인 4가지

사용률을 나눠 봤다면 다음은 그 시간을 무엇이 쓰고 있는지입니다. user와 system 시간이 높을 때 나오는 원인은 크게 네 가지입니다.

리눅스 CPU 사용률이 높아지는 원인 4가지, 무한 루프와 GC 폭주, 락 경합, 스레드 과다를 나란히 보여 주는 도식
무한 루프·GC 폭주·락 경합은 user 시간에, 스레드 과다는 system 시간에 잡힙니다.

첫째, 비효율 알고리즘이나 무한 루프입니다. 특정 조건에서 반복이 끝나지 않거나, 데이터가 커질수록 연산량이 급격히 늘어나는 코드가 한 스레드를 계속 점유합니다. 배포 직후 CPU가 튀었다면 새로 들어간 코드를 먼저 의심합니다.

둘째, GC(가비지 컬렉션, 사용하지 않는 메모리를 정리하는 작업) 폭주입니다. 메모리가 빠듯하면 GC가 지나치게 자주 돌면서 CPU를 씁니다. 이때 볼 것은 힙 크기와 메모리 사용 추이입니다. CPU만 들여다보면 원인을 놓칩니다.

셋째, 락 경합입니다. 여러 스레드가 같은 잠금을 두고 경쟁하면, 일부 구현에서는 잠금을 기다리며 짧게 반복 확인하는 스핀(spin) 동작이 CPU를 계속 씁니다. 일하는 것처럼 보이지만 실제로는 순서를 기다리는 시간입니다. 반대로 스레드가 대기 상태로 잠들어 순서를 기다리는 블로킹 방식이면 CPU는 오히려 낮게 나옵니다. 락 경합은 CPU가 높을 때도 낮을 때도 후보로 남습니다. 사용률만으로 지우지 말고, 스레드가 어느 잠금 앞에 멈춰 있는지 확인합니다.

넷째, 스레드 과다입니다. 코어 수에 비해 스레드가 너무 많으면 스레드를 번갈아 실행하려고 전환하는 컨텍스트 스위칭 비용이 커지고, 그 비용이 system 시간으로 나타납니다.

무한 루프·GC 폭주·락 경합·스레드 과다는 모두 CPU 안에서 벌어지는 일입니다. 원인이 CPU 밖에 있는 경우도 있습니다. 흔한 착각은 데이터베이스 대기를 CPU 문제로 보는 것입니다. 애플리케이션이 쿼리 응답을 기다리는 동안은 CPU를 거의 쓰지 않습니다.

쿼리 응답 대기나 커넥션 풀 대기처럼 CPU 밖에서 쌓이는 시간까지 요청 경로를 따라가며 확인하는 방법은 API 응답 시간이 느린 이유 7가지와 진단 순서에 정리해 두었습니다.

CPU 포화 자체가 지연의 원인일 때도 있습니다. 부하가 오를 때 처리 능력이 한계에 닿으면 요청이 밀려 게이트웨이 대기 시간을 넘기고, 사용자에게는 504로 보입니다. CPU가 튄 시간대에 504가 함께 몰렸다면 504 Gateway Timeout 원인과 해결 방법을 같이 봅니다.

iowait가 높으면 디스크를 세 단계로 좁힌다

iowait가 높게 잡혔다면 다음은 어느 디스크가 무엇 때문에 느린지를 찾는 단계입니다. 표준 리눅스 명령 세 개로 순서대로 추릴 수 있습니다.

iowait 진단 3단계 도식, vmstat으로 서버 전체를 보고 iostat으로 디바이스 하나를, pidstat으로 프로세스 하나를 좁혀 가는 흐름
한 단계를 건너뛰면 실제 병목이 아닌 디바이스나 프로세스를 지목하기 쉽습니다.

단계 명령 확인할 지표 가려지는 것
1 vmstat 1 b(대기 프로세스 수), wa 대기가 실제로 쌓이는가
2 iostat -xz 1 %util, await, aqu-sz 어느 디바이스인가
3 pidstat -d 1 프로세스별 읽기·쓰기량 어느 프로세스인가

먼저 vmstat 1로 대기가 실제로 쌓이는지 봅니다. procs 항목의 b는 디스크 읽기·쓰기 같은 입출력이 끝나기를 기다리며 멈춰 있는 프로세스의 수로, 0에서 벗어나 계속 남아 있으면 요청이 제때 처리되지 못하고 밀리는 중입니다. wa는 CPU가 유휴일 때만 올라가는 값이라 CPU까지 바쁘면 디스크가 밀려도 낮게 나옵니다. b는 CPU가 바쁜지와 무관하게 늘어나므로, 두 값을 함께 보고 둘 다 높으면 입출력 대기로 판단합니다.

vmstat 1
#  r  b   us sy id wa st
#  1  7   14  4 17 65  0

위 예시는 b가 7로 계속 남아 있고 wa는 65%까지 올라간 상태입니다. 입출력 대기가 쌓이는 전형적인 모습이라, 여기서 디스크 쪽으로 넘어가도 됩니다.

다음은 iostat -xz 1로 어느 디바이스가 병목인지 봅니다. iostat(1) 매뉴얼 기준으로 %util은 그 디바이스에 입출력 요청이 실려 있던 시간의 비율이고, await는 큐에서 대기한 시간과 실제 처리 시간을 합친 평균 응답 시간(밀리초)입니다. 특정 디스크의 %util이 계속 높게 유지되면서 await와 큐 길이(aqu-sz, 예전 버전은 avgqu-sz)까지 함께 늘어난다면 그 디바이스의 입출력 병목을 의심합니다.

디바이스를 특정했다면 마지막은 그 디스크를 쓰는 프로세스입니다. sysstat 패키지의 pidstat -d 1로 프로세스별 읽기·쓰기량을 보거나 iotop으로 실시간 순위를 확인합니다.

다만 %util 하나로 병목을 단정하지는 않습니다. iostat(1) 매뉴얼은 %util이 포화를 뜻하는 것은 요청을 하나씩 처리하는 장치일 때라고 적어 두었습니다. NVMe·SSD·RAID처럼 요청을 동시에 여러 개 처리하는 장치에서는 100%에 가까워도 아직 여유가 있을 수 있습니다.

디바이스와 프로세스를 확인했다면 남은 것은 그 입출력을 만든 원인입니다. 흔한 경우는 아래와 같습니다.

  • 짧은 시간에 쓰기가 몰리는 로그 폭주나 백업 작업
  • 메모리가 부족해 페이지가 디스크로 밀려나는 스왑
  • 여러 워크로드가 같은 디스크나 네트워크 스토리지를 나눠 쓰는 컨테이너·가상화 환경
  • 스토리지 자체의 처리 한계

대응은 두 가지입니다. 입출력 요청 자체를 줄이거나 분산하려면 로그·데이터·백업 볼륨을 나누고 동기 쓰기 빈도를 조정합니다. 처리 능력을 올리는 쪽이라면 SSD·NVMe로 교체하거나 캐시 계층을 둡니다.

클라우드 VM이라면 steal도 함께 본다

steal은 하이퍼바이저가 다른 인스턴스에 CPU를 넘겨준 탓에 내 인스턴스가 기다린 시간입니다. 물리 서버에는 없고 클라우드 VM에서만 나타납니다.

steal이 높다면 손댈 곳은 호스트입니다. 같은 물리 호스트에서 실행 중인 다른 인스턴스에 CPU 시간을 빼앗기는 상황이라, 호스트 상태를 확인하거나 인스턴스를 다른 노드로 옮겨야 합니다. 판별하는 방법과 대응은 CPU Steal Time이란? 클라우드 서버 지연 원인과 해결 방법에 정리해 두었습니다.

컨테이너에서는 사용률이 낮아도 느릴 수 있다

지금까지는 프로세스가 호스트 CPU를 직접 나눠 쓰는 환경을 다뤘습니다. 컨테이너에서는 확인할 것이 하나 더 있습니다. cgroup은 프로세스 묶음의 자원 사용을 제한하는 리눅스 기능입니다. CPU throttling은 컨테이너가 cgroup으로 정해진 CPU 할당량(quota)을 주기 안에 다 썼을 때, 커널이 남은 시간 동안 그 컨테이너의 실행을 멈추는 동작입니다.

throttling이 걸리면 지표를 그대로 믿기 어렵습니다. 호스트 top에는 코어가 남아도는 것으로 보이고, 컨테이너의 평균 CPU 사용률도 40 ~ 50%대로 한가해 보입니다. 그런데 리눅스 기본 스케줄러인 CFS(Completely Fair Scheduler)는 100밀리초 주기로 할당량을 계산하므로, 짧게 몰아 쓰는 요청은 주기 중간에 잘려 다음 주기까지 기다립니다. 평균은 여유로운데 개별 요청만 밀리는 상태가 됩니다.

그래서 컨테이너에서는 사용률 대신 throttling 지표를 봅니다. 쿠버네티스나 도커에서는 컨테이너 안으로 들어가 cgroup 버전에 맞는 파일을 열면 됩니다. cgroup v1은 /sys/fs/cgroup/cpu/cpu.stat에서 nr_throttled(실행이 멈춘 주기 수)와 throttled_time(나노초)을 봅니다. 최근 배포판에서 기본값이 된 cgroup v2는 /sys/fs/cgroup/cpu.stat에서 nr_throttledthrottled_usec(마이크로초)를 봅니다.

# cgroup v2
cat /sys/fs/cgroup/cpu.stat
# nr_periods 12045
# nr_throttled 3180       ← 전체 주기 대비 비율이 높으면 throttling 발생
# throttled_usec 8412300  ← 실행이 멈춰 있던 누적 시간

쿠버네티스라면 CPU 할당량은 파드의 resources.limits.cpu 값으로 정해집니다. 커널이 limits.cpu를 CFS 쿼터로 변환하므로, 노드에 CPU가 남아 있어도 컨테이너가 자기 할당량을 다 쓰면 커널이 그 컨테이너를 멈춥니다. 노드 지표만 보고 여유가 있다고 판단하면 원인을 놓칩니다.

nr_throttlednr_periods 대비 꾸준히 올라간다면 할당량 설정부터 봅니다. 다만 할당량을 올리는 것이 항상 맞는 대응은 아닙니다. 짧게 몰아 쓰는(버스트) 워크로드는 할당량을 조금 올리는 편이 효과가 크고, 스레드 수가 할당된 코어 수보다 훨씬 많아 서로 밀어내는 상황이라면 스레드 풀 크기를 할당량에 맞추는 것이 낫습니다.

파일을 직접 여는 것은 의심 컨테이너를 이미 지목한 뒤에 쓰는 방법입니다. 클러스터 전체를 상시로 지켜보려면 컨테이너 지표 수집기인 cAdvisor가 내보내는 container_cpu_cfs_throttled_periods_totalcontainer_cpu_cfs_periods_total로 나눠 throttling 비율을 지표로 만들고, 그 비율에 알림을 겁니다. 어느 비율부터 조정할지는 워크로드마다 다르므로 평상시 비율을 먼저 기록해 두고 그보다 뚜렷하게 높아질 때를 기준으로 잡습니다. 흔히 쓰는 출발점은 5 ~ 10%대입니다.

CPU 100%를 두고 흔히 하는 오해

리눅스 CPU 사용률 오진 세 가지와 대응, us·sy·wa 분해와 top 400% 해석, vmstat·iostat 디스크 지표 확인을 짝지은 도식
세 오해 모두 지표를 나눠 보기 전에 결론부터 내린 경우입니다.

가장 흔한 오판은 사용률 퍼센트만 보고 스케일업을 결정하는 것입니다. user·system·iowait·steal을 나눠 보지 않으면, CPU가 부족한 것인지 코드가 낭비하는 것인지 구분할 수 없습니다. 원인을 모른 채 코어를 늘리면 비효율 코드는 더 많은 코어를 쓸 뿐입니다.

멀티코어 환경의 표기도 자주 헷갈립니다. top에서 프로세스 CPU가 400%로 보이는 것은 4개 코어를 가득 쓰고 있다는 뜻이지 오류가 아닙니다. 코어 수를 기준으로 읽어야 실제 포화 여부를 알 수 있습니다.

iowait를 CPU 부족으로 읽는 것도 위험합니다. iowait는 CPU가 일한 시간이 아니라, CPU가 유휴 상태인 채로 아직 끝나지 않은 입출력을 안고 있던 시간입니다. 코어를 늘려도 이 값은 줄지 않습니다. 거꾸로 iowait가 낮다고 디스크가 한가한 것도 아니므로 wa에 대기 프로세스 수와 디바이스 지표를 겹쳐 놓고 판단합니다.

자주 묻는 질문

CPU 사용률이 100%면 서버를 증설해야 하나요?

반드시 그렇지는 않습니다. 증설이 바로 효과를 내는 것은 us가 높으면서 모든 코어가 고르게 차 있을 때입니다. 한 코어만 100%이고 나머지가 한가하다면 단일 스레드 병목이라, 코어를 늘려도 그 스레드는 그대로 한 코어만 씁니다. top을 띄운 상태에서 숫자 키 1을 누르면 CPU가 코어별로 나뉘어 표시되므로 두 경우를 바로 구분할 수 있습니다.

CPU 사용률이 높은데 애플리케이션은 느리지 않습니다. 문제인가요?

CPU를 많이 쓰더라도 응답이 목표 시간 안에 들어온다면 당장의 문제는 아닙니다. 다만 여유가 줄어든 상태라, 트래픽이 조금만 더 오르면 포화될 수 있습니다. user·system 비중을 확인해 낭비가 있는지 점검해 두는 것이 좋습니다.

CPU 사용률은 낮은데 응답이 느립니다. 어디를 봐야 하나요?

CPU가 낮다면 원인은 자원을 기다리는 시간에 있을 가능성이 큽니다. 다만 여기서 자주 막히는 지점은 애플리케이션 지표만 봤을 때 대기 시간이 어디에도 잡히지 않는 경우입니다. 커넥션 풀 대기는 쿼리 실행 시간에 포함되지 않아 슬로 쿼리 로그에 남지 않고, 스레드 풀 대기 역시 요청이 처리되기 전이라 애플리케이션 로그에 시작 시각조차 찍히지 않습니다. 대기 구간이 비어 보인다면 풀 크기와 대기 큐 길이를 직접 확인해야 원인이 드러납니다.

배포 직후부터 CPU가 계속 높습니다. 왜 그런가요?

먼저 계속 높은 것인지, 배포 직후 몇 분만 높았다가 내려가는 것인지 나눠 봅니다. 배포 직후 몇 분만 높았다가 내려가는 경우라면 JIT 컴파일과 캐시 적재가 겹치는 워밍업 구간이라 정상일 수 있습니다. 몇 시간이 지나도 이전 수준으로 돌아오지 않는다면 이번 배포에 들어간 변경을 봅니다. 코드뿐 아니라 라이브러리 버전, JVM 옵션, 스레드 풀·힙 설정 변경도 후보입니다. 판단이 서지 않으면 한 대만 이전 버전으로 되돌려 두 대의 CPU 추이를 나란히 비교하는 편이 빠릅니다.

마치며

CPU 사용률 하나로는 무엇을 고쳐야 할지 정해지지 않습니다. us·sy·wa·st로 나눠 방향을 잡고, user가 높으면 프로세스와 스레드를, iowait가 높으면 디바이스와 그 디스크를 쓰는 프로세스를 특정합니다. 컨테이너라면 사용률이 낮아도 throttling 지표를 함께 봅니다.

top과 iostat은 지금 이 순간만 보여 줍니다. CPU가 치솟았던 어제 오후로는 돌아갈 수 없습니다. 와탭 APM으로는 그 시점에 어떤 트랜잭션과 메서드가 실행 중이었는지, 스레드가 무엇을 기다렸는지를 히트맵과 트레이스로 되짚을 수 있습니다. 시점별로 실행 중이던 코드를 여는 방법은 히트맵/트레이스 분석 (와탭 공식 문서)에서 확인할 수 있습니다.

더 읽을거리

와탭 모니터링을 무료로 체험해보세요!