서버가 느린 이유 #1 CPU 사용률은 낮은데 느릴 때: run queue, I/O wait, 락 경합

6 분 소요

모니터링 대시보드의 CPU 사용률은 20%인데 응답 시간이 늘어지고 있습니다. CPU가 남아도는데 왜 느릴까요? 운영을 하다 보면 반드시 만나는 상황이고, 이 시리즈의 출발점이기도 합니다.

이 시리즈는 5편입니다. “스펙은 충분한데 느리다"는 증상을 자원별로 하나씩 추적합니다. CPU 사용률은 낮은데 느린 경우(1편), 메모리를 늘려도 빨라지지 않는 경우(2편), SSD인데도 느린 경우(3편), 대역폭은 충분한데 네트워크가 느린 경우(4편), 데이터베이스가 느려지는 경우(5편) 순서입니다. 하드웨어 기초·중급 시리즈가 자원과 지표의 개념을 잡는 커리큘럼이라면, 이 시리즈는 증상에서 출발해 원인으로 내려가는 사례 중심 진단입니다.

사용률이라는 지표가 놓치는 것 #

첫 번째로 확인할 것은 질문 자체입니다. “CPU 사용률이 낮다"는 사실은 “CPU가 병목이 아니다"를 보장하지 않습니다. 사용률은 일정 구간의 평균이고, 코어에 작업이 올라가 있던 시간의 비율일 뿐이기 때문입니다. 이 지표에서 두 가지가 빠집니다.

  • 평균에 묻히는 폭주: 1분 평균 20%는 59초 동안 5%였다가 1초 동안 100%를 치는 패턴과 구분되지 않습니다. 요청이 몰리는 순간의 대기는 평균 그래프에 나타나지 않습니다.
  • CPU 밖의 대기: 프로세스가 디스크 응답, 락, 다른 서비스의 응답을 기다리는 시간은 CPU를 쓰지 않는 시간이라 사용률에 잡히지 않습니다. 사용자가 체감하는 응답 시간의 대부분이 이 대기에서 나오는 경우가 흔합니다.

그래서 진단의 방향은 “CPU가 얼마나 바쁜가"가 아니라 “요청이 어디서 기다리는가"로 바뀌어야 합니다. 기다리는 곳은 크게 셋입니다. CPU 앞의 줄(run queue), 디스크 응답(I/O wait), 그리고 락입니다.

run queue: CPU 앞의 줄 #

실행할 준비가 끝났는데 코어가 비기를 기다리는 스레드의 줄이 run queue입니다. 사용률이 낮아도 이 줄은 길 수 있습니다. 짧은 작업이 순간적으로 한꺼번에 도착하면, 평균 사용률을 거의 올리지 않으면서도 각 작업은 줄을 서느라 지연됩니다.

vmstat의 첫 컬럼이 바로 이 줄의 길이입니다.

vmstat 1
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
12  0      0 811240 219880 5480032    0    0     0    24 9821 21453 18  4 78  0  0
14  0      0 810988 219880 5480040    0    0     0     0 10233 22871 19  5 76  0  0

r이 실행 대기 중인 스레드 수입니다. 이 서버는 사용률(us+sy) 23% 언저리인데 r이 코어 수(예: 8)를 크게 웃돌고 있습니다. 코어보다 긴 줄이 지속되면 스케줄링 지연이 응답 시간에 그대로 얹힙니다. 하드웨어 중급 #1에서 본 load average도 같은 신호를 주지만, load average는 I/O 대기 중인 태스크까지 포함한 값이라 원인 구분에는 vmstatrb(I/O 대기)를 나눠 보는 편이 낫습니다.

줄을 서는 시간 자체를 재고 싶다면 eBPF 도구 runqlat이 스케줄링 지연의 분포를 히스토그램으로 보여 줍니다. 도구 자체는 하드웨어 고급 #2에서 다뤘습니다. 사용률이 낮은데 runqlat의 꼬리가 밀리초 단위로 길다면, 순간 폭주나 코어 수 대비 과도한 스레드 수를 의심합니다. 스레드 풀 크기를 코어 수 대비 지나치게 크게 잡은 애플리케이션이 전형적인 사례입니다.

I/O wait: idle의 다른 얼굴 #

top%wa(iowait)는 “실행할 태스크가 없고, 미완료 디스크 I/O가 있는” 시간입니다. 여기에 함정이 둘 있습니다.

  • iowait은 idle의 일종입니다. CPU 사용률에는 포함되지 않으므로, “사용률 20% + iowait 40%“인 서버는 사용률 그래프만 보면 한가해 보입니다.
  • 반대로 iowait이 낮다고 I/O 병목이 없는 것도 아닙니다. 다른 태스크가 CPU를 쓰고 있으면 같은 I/O 대기가 iowait으로 집계되지 않습니다.

그래서 iowait은 “높으면 디스크를 본다"는 출발 신호로만 쓰고, 판정은 디스크 쪽 지표로 합니다. iostat -xawait(I/O 하나가 큐 대기까지 포함해 걸린 평균 시간)와 %util을 보고, 어느 프로세스인지는 pidstat -d로 좁힙니다.

iostat -x 1
$ iostat -x 1
Device            r/s     w/s   rkB/s   wkB/s  await  %util
nvme0n1         210.0  1830.0   840.0 29280.0   8.42   96.4

사용률 낮은 서버에서 await가 평소의 몇 배로 뛰고 %util이 90%를 넘고 있다면, 병목은 CPU가 아니라 스토리지입니다. SSD인데도 이런 숫자가 나오는 이유는 3편에서 다룹니다.

락 경합: CPU도 디스크도 아닌 대기 #

run queue도 짧고 디스크도 한가한데 느리다면, 남는 후보는 프로세스들이 서로를 기다리는 경우입니다. 뮤텍스, DB의 로우 락, 커넥션 풀의 빈 커넥션, 업스트림 API의 응답이 여기에 해당합니다. 이 대기들은 CPU를 전혀 쓰지 않아서 시스템 지표에는 거의 흔적이 남지 않습니다. 스레드는 잠들어 있고, 사용률은 낮고, 응답만 느립니다.

시스템 쪽에서 잡히는 단서는 컨텍스트 스위치입니다. 락을 얻지 못한 스레드가 계속 잠들었다 깨었다를 반복하면 vmstatcs(context switch)가 부하 대비 비정상적으로 높게 나옵니다. pidstat -w로 프로세스별 자발적 컨텍스트 스위치(cswch/s)를 보면 어떤 프로세스가 무언가를 기다리며 잠드는지 좁힐 수 있습니다.

pidstat -w 1
$ pidstat -w 1 -p 4321
UID       PID   cswch/s nvcswch/s  Command
 1001     4321   8412.0      12.0  api-server

초당 8천 번의 자발적 스위치는 이 프로세스가 일하다 멈추는 게 아니라 기다리다 깨기를 반복하고 있다는 뜻입니다. 여기서부터는 애플리케이션 내부의 문제라, 언어별 프로파일러로 스레드가 어떤 락에서 대기하는지 보는 off-CPU 분석으로 내려갑니다. 실무에서 가장 잦은 범인 둘은 커넥션 풀 고갈과, 느린 쿼리로 락을 잡은 채 오래 유지되는 트랜잭션인데, 이 둘은 5편에서 데이터베이스 쪽 시각으로 다시 봅니다.

클라우드라면: 스틸 타임 확인 #

가상 머신에서는 확인할 것이 하나 더 있습니다. top%st(steal)는 게스트가 실행할 준비가 됐는데 하이퍼바이저가 물리 코어를 주지 않은 시간입니다. 내 서버의 사용률은 낮은데 같은 물리 호스트의 다른 테넌트가 코어를 가져가고 있다면, 내 프로세스는 이유 없이 느려집니다. %st가 지속적으로 수 퍼센트를 넘으면 인스턴스 이전이나 타입 변경을 검토합니다. 버스터블 인스턴스(t 계열)의 CPU 크레딧 소진도 같은 증상을 만듭니다. 자세한 내용은 하드웨어 중급 #2에서 다뤘습니다.

진단 순서 정리 #

증상이 “CPU 사용률은 낮은데 느리다"일 때, 순서는 이렇습니다.

  1. 평균의 함정부터 제거: 모니터링 간격을 좁혀(1초 단위) 순간 폭주 여부를 확인합니다.
  2. vmstat 1: r이 코어 수를 웃돌면 스케줄링 지연(스레드 수 과다, 순간 폭주)을 의심하고, b가 쌓이면 I/O 쪽을 봅니다.
  3. iostat -x: await%util로 스토리지 병목을 판정합니다. 걸리면 3편의 영역입니다.
  4. pidstat -w: 자발적 컨텍스트 스위치가 높으면 락, 커넥션 풀, 업스트림 대기를 의심하고, 애플리케이션 프로파일링으로 내려갑니다.
  5. 가상 머신이면 %st: 스틸 타임과 CPU 크레딧을 확인합니다.

정리 #

  • CPU 사용률은 코어 점유 시간의 평균일 뿐, 요청이 어디서 기다리는지 알려 주지 않습니다. 진단의 질문은 “CPU가 바쁜가"가 아니라 “어디서 기다리는가"입니다.
  • run queue(vmstatr)가 코어 수를 지속적으로 웃돌면, 사용률이 낮아도 스케줄링 지연이 응답 시간에 얹힙니다.
  • iowait은 idle의 일종이라 사용률에 안 잡히고, 낮다고 I/O 병목이 없는 것도 아닙니다. 판정은 iostatawait%util로 합니다.
  • 락, 커넥션 풀, 업스트림 대기는 시스템 지표에 거의 안 남습니다. 단서는 높은 자발적 컨텍스트 스위치이고, 확정은 off-CPU 분석입니다.
  • 가상 머신에서는 스틸 타임과 CPU 크레딧이 같은 증상을 만듭니다.

다음 편은 메모리입니다. 메모리를 늘렸는데 빨라지지 않는 이유를 페이지 캐시, 스왑, 워킹셋으로 추적합니다.

X