Skip to content

Split View: 로드 애버리지가 CPU 사용률이 아닌 이유 — load average 24에 CPU는 30%일 때

✨ Learn with Quiz
|

로드 애버리지가 CPU 사용률이 아닌 이유 — load average 24에 CPU는 30%일 때

들어가며 — load average 24, 그런데 CPU는 30%입니다

새벽에 알람이 옵니다. 8코어 서버의 로드 애버리지가 24를 넘겼습니다. 접속해서 top을 열면 CPU 유휴가 60% 넘게 남아 있습니다. 여기서 두 가지 결론 중 하나로 새기 쉽습니다. "모니터링이 잘못됐다" 또는 "CPU를 3배 늘려야 한다". 둘 다 틀렸습니다.

로드 애버리지는 CPU 사용률이 아닙니다. 단위부터 다릅니다. 사용률은 비율(0~100%)이고 로드는 개수(태스크 수)입니다. 그리고 리눅스는 이 개수를 셀 때 다른 유닉스가 세지 않는 것을 하나 더 셉니다. 그 하나가 이 글의 절반입니다.

uptime
#  03:41:52 up 12 days,  3:21,  2 users,  load average: 24.31, 22.08, 14.95

nproc
# 8

cat /proc/loadavg
# 24.31 22.08 14.95 2/1284 48210

마지막 줄의 2/1284를 보십시오. 앞의 2는 지금 이 순간 실행 가능한 스레드 수이고 1284는 전체 스레드 수입니다. 로드는 24인데 실행 가능한 스레드는 2개입니다. 나머지 22는 어디에 있을까요. 이 질문의 답이 진단의 출발점입니다.

세 숫자의 정체 — 5초 샘플링과 지수이동평균

커널은 LOAD_FREQ 주기, 즉 5초에 한 번씩(정확히는 5초 + 1틱, 주기적 인터럽트와 위상이 겹치는 것을 피하려는 의도입니다) 각 CPU 런큐에서 활성 태스크 수를 샘플링합니다. 활성 태스크는 다음과 같이 계산됩니다.

nr_active = rq->nr_running + rq->nr_uninterruptible

nr_running은 지금 실행 중이거나 실행 가능한 태스크, nr_uninterruptible은 중단 불가 대기 상태의 태스크입니다. 이 합을 모든 CPU에 대해 더한 값이 그 순간의 표본입니다.

그다음 갱신 식이 핵심입니다. 커널은 고정소수점 연산으로 다음을 계산합니다.

load = (load * EXP + active * (FIXED_1 - EXP)) >> FSHIFT

FSHIFT  = 11,  FIXED_1 = 2048
EXP_1   = 1884   (2048 * e^(-5/60))
EXP_5   = 2014   (2048 * e^(-5/300))
EXP_15  = 2037   (2048 * e^(-5/900))

상수를 검산해 보면 이해가 빨라집니다. 5초마다 갱신하면서 1분 시정수를 얻으려면 감쇠 계수가 e의 마이너스 5/60승, 즉 0.9200이어야 합니다. 0.9200에 2048을 곱하면 1884입니다. 5분과 15분도 같은 방식으로 2014, 2037이 나옵니다.

여기서 실무적으로 중요한 성질이 하나 나옵니다. 이것은 산술평균이 아니라 지수감쇠 평균입니다. 부하가 계단처럼 뛰어오르면 1분 평균은 정확히 1분 뒤에 최종값의 63.2%에 도달하고, 99%에 도달하려면 약 5분이 걸립니다. 15분 평균은 같은 논리로 한 시간 넘게 끌려다닙니다.

경과 시간1분 평균 반영률5분 평균 반영률15분 평균 반영률
1분63.2%18.1%6.5%
5분99.3%63.2%28.3%
15분100.0%95.0%63.2%
45분100.0%100.0%95.0%

그래서 "1분 평균이 5분·15분보다 크면 부하가 오르는 중"이라는 관용적 해석이 성립합니다. 반대로 장애가 끝난 직후 15분 평균만 높게 남아 있는 것은 정상입니다. 그 숫자로 알람을 걸면 복구 후 20분간 계속 울립니다.

리눅스만의 결정적 차이 — D 상태까지 센다

전통적인 유닉스는 로드 애버리지를 런큐 길이로만 정의했습니다. 즉 CPU를 기다리는 태스크 수입니다. 리눅스는 1993년에 여기에 TASK_UNINTERRUPTIBLE 상태를 더했습니다. 의도는 명확했습니다. CPU 수요만이 아니라 시스템 자원 전반에 대한 수요를 재겠다는 것이었습니다.

TASK_UNINTERRUPTIBLEps에서 D로 표시되는 상태입니다. 시그널로도 깨울 수 없는 커널 내부 대기이고, 대표적으로 이런 것들입니다.

  • 블록 디바이스 I/O 완료 대기 (io_schedule)
  • 페이지 캐시 잠금 대기 (folio_wait_bit_common)
  • 하드 마운트된 NFS 서버 응답 대기
  • 일부 커널 뮤텍스와 세마포어 대기
  • 디바이스 드라이버의 펌웨어 응답 대기

이 목록에 CPU는 없습니다. 그러니까 CPU가 완전히 놀고 있어도 로드 애버리지는 얼마든지 올라갑니다. NFS 서버 하나가 응답하지 않으면 그 마운트에 접근한 프로세스가 전부 D 상태로 쌓이고, 8코어 서버의 로드가 200을 찍는데 CPU 그래프는 평평합니다. 이것이 정상 동작입니다.

한 가지 예외를 알아 두면 유용합니다. 커널 4.2부터 TASK_IDLE(TASK_UNINTERRUPTIBLE | TASK_NOLOAD)이 도입되어, 할 일 없이 대기하는 커널 스레드는 로드에 포함되지 않습니다. ps에서 I로 보이는 것들이 여기 해당합니다.

ps -eo state,pid,comm --no-headers | awk '{c[$1]++} END {for (s in c) print c[s], s}' | sort -rn
#    1183 S
#      68 I
#      22 D
#       2 R
#       9 Z

D가 22, R이 2. 앞에서 본 로드 24와 정확히 맞아떨어집니다. 이 시점에서 결론은 하나입니다. 이 서버는 CPU가 모자란 것이 아니라 어딘가에서 I/O를 기다리고 있습니다.

여기서 흔히 보는 잘못된 설명을 짚고 갑니다. "로드 애버리지는 CPU 대기열 길이"라는 문장은 다른 유닉스에서는 맞고 리눅스에서는 틀립니다. 그리고 "로드가 코어 수보다 낮으면 안전하다"는 규칙도, 그 값에 D 상태가 섞여 있는 이상 안전을 보장하지 않습니다.

"코어 수로 나눠라"가 어디서 무너지는가

로드를 nproc으로 나눠 1.0 미만이면 여유, 1.0 이상이면 포화라는 관행은 출발점으로는 쓸 만합니다. 하지만 다음 다섯 가지 상황에서 무너집니다.

첫째, D 상태 오염입니다. 앞 절 그대로입니다. 24/8 = 3.0이지만 CPU 포화와는 아무 관계가 없습니다.

둘째, 평균의 지연입니다. 30초짜리 스파이크는 1분 평균에서 절반도 드러나지 않습니다. 반대로 이미 끝난 장애가 15분 평균에 계속 남습니다.

셋째, SMT(하이퍼스레딩)입니다. nproc은 논리 CPU를 셉니다. 물리 코어 8개에 SMT가 켜져 있으면 16이 나오지만, 실제 처리 능력은 16코어가 아닙니다. 로드 12가 "16코어 중 75%"를 뜻하지 않습니다.

lscpu | grep -E '^CPU\(s\)|Thread|Core|Socket'
# CPU(s):                          16
# Thread(s) per core:              2
# Core(s) per socket:              8
# Socket(s):                       1

넷째, cgroup 쿼터입니다. 컨테이너에 CPU limit이 걸려 스로틀링당하는 태스크는 런큐에서 아예 빠지기 때문에 로드 애버리지에 잡히지 않습니다. 64코어 호스트에서 0.5 CPU로 제한된 파드가 심각하게 느려도 호스트 로드는 조용합니다. 이 조합은 뒤에서 다시 다룹니다.

다섯째, 귀속 정보의 부재입니다. 로드는 시스템 전역 스칼라 하나입니다. 어느 컨테이너, 어느 서비스, 어느 자원 때문인지 아무것도 알려주지 않습니다. 알람을 걸 수는 있지만 그 알람으로 할 수 있는 일은 "들어가서 봐라" 뿐입니다.

로드는 높은데 CPU는 한가한 경우 — 진단 경로

순서가 있습니다. 위에서 아래로 3분이면 끝납니다.

# 1단계: 로드의 구성 확인 — R이 적고 D가 많으면 I/O 경로
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  2 22      0 512344  84120 6231044    0    0     0     0 2103 4102  4  3 21 72  0
#  1 23      0 511208  84120 6231180    0    0 132096   412 2288 4390  3  4 19 74  0
#  2 22      0 510992  84120 6231212    0    0 128512   380 2251 4301  4  3 20 73  0

b 열이 22~23입니다. 이 열이 바로 중단 불가 대기 태스크 수입니다. wa(iowait)가 72%인 것도 같은 이야기를 합니다. iowait는 "CPU가 유휴인데 그 CPU에 I/O 대기 태스크가 있는 시간"이므로, 사실 유휴 시간의 일종입니다. 높다고 그 자체로 나쁜 것은 아니지만 b 열과 함께 보면 방향이 분명합니다.

# 2단계: 어느 디바이스가 포화됐는지
iostat -x 1 3
# Device      r/s     rkB/s  rrqm/s %rrqm r_await rareq-sz    w/s   wkB/s  w_await  aqu-sz  %util
# nvme0n1   1842.0   58944.0    0.0  0.00   12.85    32.00  210.0  3360.0     3.02   24.30  99.60
# nvme1n1      2.0      64.0    0.0  0.00    0.31    32.00    1.0    12.0     0.22    0.00   1.20

읽어야 할 것은 세 가지입니다. %util이 100%에 붙었는지(디바이스가 쉬지 못함), r_await/w_await가 평소의 몇 배인지(요청당 지연), aqu-sz가 얼마나 깊은지(큐에 쌓인 평균 요청 수). NVMe에서 %util 100%는 병렬 처리 때문에 포화를 뜻하지 않을 수 있으니, 판단은 await와 큐 깊이로 하십시오.

# 3단계: D 상태 프로세스를 개별로 확인
ps -eo state,pid,ppid,comm --no-headers | awk '$1 == "D"' | head
# D  4821  4102 postgres
# D  4822  4102 postgres
# D  7710     1 kworker/u33:2
# 4단계: 그 프로세스가 커널의 어디서 멈춰 있는지 (루트 권한 필요)
sudo cat /proc/4821/stack
# [<0>] io_schedule+0x46/0x80
# [<0>] folio_wait_bit_common+0x131/0x330
# [<0>] filemap_fault+0x5f0/0xa50
# [<0>] __do_fault+0x39/0x120
# [<0>] handle_mm_fault+0xd4e/0x1090

스택 최상단이 io_schedule이면 블록 I/O, rpc_wait_bit_killable이나 nfs_wait_on_request가 보이면 NFS, __lock_page 계열이면 페이지 캐시 경합입니다. /proc/PID/stack이 비어 있거나 권한 오류가 나면 CONFIG_STACKTRACE가 없는 커널이거나 권한이 부족한 것입니다.

NFS가 의심되면 마운트 통계를 직접 봅니다.

# 서버별 RTT와 재전송 확인
nfsstat -c | head -20
mountstats --nfs /mnt/shared 2>/dev/null | grep -A3 'READ:'

# 응답 없는 서버 찾기 (하드 마운트는 무한 대기합니다)
grep nfs /proc/mounts
# 10.0.9.4:/export/data /mnt/shared nfs4 rw,hard,proto=tcp,timeo=600,retrans=2 0 0

hard 마운트는 서버가 돌아올 때까지 프로세스를 D 상태로 붙잡습니다. 이 상태에서는 kill -9도 듣지 않습니다. 운영 관점에서는 softerr나 짧은 timeo를 검토하되, 데이터 정합성 요구를 먼저 확인해야 합니다.

로드는 낮은데 느린 경우 — 로드가 못 보는 것들

반대 방향도 흔합니다. 로드는 0.8인데 p99가 3초입니다. 이때 로드가 구조적으로 볼 수 없는 것들을 나열하면 후보가 좁혀집니다.

  • 인터럽터블 대기(S 상태)는 로드에 포함되지 않습니다. 소켓 응답을 기다리는 시간, 즉 하류 API나 DB의 지연은 전부 여기 해당합니다. 애플리케이션이 느린 이유의 다수가 이쪽입니다.
  • 유저 공간 락 경합도 대부분 S 상태입니다. futex 대기는 인터럽터블이므로 아무리 심해도 로드가 오르지 않습니다.
  • cgroup CPU 스로틀링은 앞서 말했듯 런큐에서 태스크를 빼기 때문에 보이지 않습니다.
  • 단일 스레드 병목은 정의상 로드 1입니다. 코어가 64개여도 로드는 1을 넘지 않습니다.
  • 하이퍼바이저 steal은 CPU를 뺏긴 것이지 대기열이 길어진 것이 아닙니다.

이 다섯 가지를 가르는 명령은 다음과 같습니다.

# S 상태에서 무엇을 기다리는지 (소켓 대기는 여기 안 잡히니 스택으로)
sudo cat /proc/9931/stack | head -5

# 락 경합: 컨텍스트 스위치 폭증 여부
vmstat 1 3        # cs 열이 평소의 몇 배인지
pidstat -w -p 9931 1 3
# UID  PID   cswch/s nvcswch/s  Command
# 1000 9931  18422.0   34110.0  java

# 스로틀링 (cgroup v2)
cat /sys/fs/cgroup/cpu.stat
# nr_periods 86400
# nr_throttled 41287
# throttled_usec 2914837261

# steal
mpstat -P ALL 1 3 | tail -3

nvcswch/s(비자발적 컨텍스트 스위치)가 크면 CPU 경합, cswch/s(자발적)가 크면 락이나 I/O 대기로 잠들었다 깨는 패턴입니다.

PSI — 로드 애버리지가 답하지 못한 질문에 답하는 지표

로드 애버리지의 근본 문제는 "몇 개가 기다리는가"만 말하고 "얼마나 오래 멈춰 있었는가"는 말하지 않는다는 점입니다. 커널 4.20부터 들어온 PSI가 정확히 후자를 잽니다.

cat /proc/pressure/io
# some avg10=78.23 avg60=71.04 avg300=48.92 total=8172635241
# full avg10=61.17 avg60=55.83 avg300=37.10 total=6019283746

cat /proc/pressure/cpu
# some avg10=2.41 avg60=1.88 avg300=1.02 total=91827364
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=142
# full avg10=0.00 avg60=0.00 avg300=0.00 total=88

읽는 법은 이렇습니다.

  • some은 최소 한 개의 태스크가 그 자원 때문에 멈춰 있던 시간의 비율입니다.
  • full은 실행 가능한 모든 태스크가 동시에 멈춰 있던 시간의 비율입니다. 즉 그 시간 동안 시스템은 아무 유용한 일도 하지 못했습니다.
  • avg10, avg60, avg300은 각각 10초, 60초, 300초 창의 백분율이고 total은 누적 마이크로초입니다.
  • 시스템 전역 cpufull은 정의상 의미가 없어 항상 0입니다(커널 5.13부터 줄 자체는 출력됩니다).

위 출력이 말하는 바는 분명합니다. I/O full이 61%라는 것은 최근 10초 중 6초 이상 시스템 전체가 I/O 때문에 멈춰 있었다는 뜻입니다. 로드 24라는 숫자보다 훨씬 직접적인 정보이고, 무엇보다 손실된 처리량으로 곧장 번역됩니다.

PSI의 두 번째 장점은 cgroup 단위로 존재한다는 것입니다. 로드 애버리지에는 없는 귀속 정보입니다.

# 어느 cgroup이 I/O로 가장 오래 멈춰 있었나
for f in /sys/fs/cgroup/**/io.pressure; do
  v=$(awk '/^full/ {print $2}' "$f" | cut -d= -f2)
  printf '%6s  %s\n' "$v" "${f%/io.pressure}"
done 2>/dev/null | sort -rn | head -5
#  58.12  /sys/fs/cgroup/system.slice/postgresql.service
#   3.04  /sys/fs/cgroup/system.slice/containerd.service
#   0.00  /sys/fs/cgroup/user.slice

/proc/pressure가 없다면 CONFIG_PSI가 꺼져 있거나, CONFIG_PSI_DEFAULT_DISABLED=y로 빌드된 커널이라 부트 파라미터 psi=1이 필요한 경우입니다.

지표재는 것D 상태 반영시간 창cgroup 단위
load average대기 중인 태스크 개수포함1/5/15분 EMA불가
CPU 사용률CPU를 실제로 쓴 시간 비율미포함임의가능
iowait유휴 CPU 중 I/O 대기가 있던 것간접임의불가
PSI cpu.someCPU 때문에 멈춰 있던 시간미포함10/60/300초가능
PSI io.fullI/O로 전체가 멈춰 있던 시간사실상 이것10/60/300초가능

마치며 — 로드 애버리지는 대기열 길이지 사용률이 아닙니다

하나만 기억한다면 이것입니다. 리눅스의 로드 애버리지는 CPU를 기다리는 태스크와 I/O를 기다리는 태스크를 구분 없이 합친 개수의 지수이동평균입니다. 그래서 이 숫자 하나만으로는 "CPU를 늘려야 하는지"에 답할 수 없습니다.

실무 규칙으로 정리하면 이렇습니다.

  1. 로드가 높으면 먼저 /proc/loadavg의 네 번째 필드와 vmstat 1b 열을 봅니다. R과 D의 비율이 방향을 정합니다.
  2. D가 많으면 iostat -x의 await와 /proc/PID/stack으로 내려갑니다. CPU 증설은 답이 아닙니다.
  3. 알람은 로드가 아니라 PSI에 겁니다. cpu.some avg60io.full avg60이 임계를 넘는 것이 훨씬 적은 오탐으로 실제 지연과 상관됩니다.
  4. 로드는 대시보드에 남겨 두되 추세 지표로만 씁니다. 1분 값이 5분·15분보다 크면 오르는 중이라는 것, 딱 그만큼만 읽습니다.
  5. 컨테이너 환경이라면 호스트 로드는 파드 성능을 설명하지 못합니다. cgroup의 cpu.stat*.pressure를 봐야 합니다.

Why Load Average Is Not CPU Utilization — load average 24 With the CPU at 30%

Introduction — load average 24, and yet the CPU is at 30%

The alert arrives before dawn. The load average on an 8 core server has gone past 24. You log in, open top, and more than 60% of the CPU is still idle. It is easy to slide into one of two conclusions here. "Monitoring is broken" or "we need three times the CPU". Both are wrong.

The load average is not CPU utilization. Even the unit is different. Utilization is a ratio (0 to 100%) and load is a count (a number of tasks). And when Linux counts that number, it counts one more thing that other Unix systems do not. That one extra thing is half of this article.

uptime
#  03:41:52 up 12 days,  3:21,  2 users,  load average: 24.31, 22.08, 14.95

nproc
# 8

cat /proc/loadavg
# 24.31 22.08 14.95 2/1284 48210

Look at 2/1284 on the last line. The 2 in front is the number of threads that are runnable at this instant, and 1284 is the total number of threads. The load is 24, yet only 2 threads are runnable. Where are the other 22? The answer to that question is the starting point of the diagnosis.

What the three numbers really are — 5 second sampling and an exponentially damped moving average

The kernel samples the number of active tasks on each CPU run queue once per LOAD_FREQ period, that is, once every 5 seconds (precisely 5 seconds plus 1 tick, deliberately so that the sampling does not stay in phase with periodic interrupts). Active tasks are computed as follows.

nr_active = rq->nr_running + rq->nr_uninterruptible

nr_running is the tasks that are currently running or runnable, and nr_uninterruptible is the tasks in uninterruptible sleep. Summing this across every CPU gives the sample for that instant.

The update formula that follows is the heart of it. The kernel computes the following in fixed point arithmetic.

load = (load * EXP + active * (FIXED_1 - EXP)) >> FSHIFT

FSHIFT  = 11,  FIXED_1 = 2048
EXP_1   = 1884   (2048 * e^(-5/60))
EXP_5   = 2014   (2048 * e^(-5/300))
EXP_15  = 2037   (2048 * e^(-5/900))

Checking the constants by hand makes this click faster. To update every 5 seconds and still obtain a 1 minute time constant, the decay factor has to be e to the power of minus 5/60, that is, 0.9200. Multiply 0.9200 by 2048 and you get 1884. The 5 minute and 15 minute values come out as 2014 and 2037 the same way.

One property that matters in practice falls out of this. This is not an arithmetic mean but an exponentially damped average. When load jumps like a step function, the 1 minute average reaches exactly 63.2% of the final value after 1 minute, and reaching 99% takes about 5 minutes. By the same logic, the 15 minute average keeps getting dragged along for more than an hour.

Elapsed time1 min average reflected5 min average reflected15 min average reflected
1 min63.2%18.1%6.5%
5 min99.3%63.2%28.3%
15 min100.0%95.0%63.2%
45 min100.0%100.0%95.0%

That is why the idiomatic reading "if the 1 minute average is larger than the 5 and 15 minute ones, load is rising" holds. Conversely, it is normal for only the 15 minute average to stay high right after an incident ends. Put an alert on that number and it will keep ringing for 20 minutes after recovery.

The decisive Linux difference — D state is counted too

Traditional Unix defined the load average purely as run queue length, that is, the number of tasks waiting for CPU. In 1993 Linux added the TASK_UNINTERRUPTIBLE state to that. The intent was explicit. The goal was to measure demand across system resources in general, not demand for CPU alone.

TASK_UNINTERRUPTIBLE is the state that shows up as D in ps. It is a wait inside the kernel that not even a signal can break, and the representative cases are these.

  • Waiting for block device I/O to complete (io_schedule)
  • Waiting on a page cache lock (folio_wait_bit_common)
  • Waiting for a response from a hard mounted NFS server
  • Waiting on certain kernel mutexes and semaphores
  • Waiting for a firmware response in a device driver

CPU is not on that list. So the load average can climb as high as you like while the CPU sits completely idle. If a single NFS server stops responding, every process that touched that mount piles up in D state, and an 8 core server hits a load of 200 while the CPU graph stays flat. This is correct behavior.

One exception is useful to know. Since kernel 4.2, TASK_IDLE (TASK_UNINTERRUPTIBLE | TASK_NOLOAD) was introduced, so kernel threads that sit waiting with nothing to do are not counted in the load. The ones that appear as I in ps belong here.

ps -eo state,pid,comm --no-headers | awk '{c[$1]++} END {for (s in c) print c[s], s}' | sort -rn
#    1183 S
#      68 I
#      22 D
#       2 R
#       9 Z

D is 22 and R is 2. That lines up exactly with the load of 24 seen earlier. At this point there is only one conclusion. This server is not short on CPU, it is waiting on I/O somewhere.

Here it is worth naming a wrong explanation that turns up often. The sentence "the load average is the length of the CPU wait queue" is right on other Unix systems and wrong on Linux. And the rule that "load below the core count is safe" guarantees no safety either, as long as D state is mixed into that value.

Where "divide by the core count" breaks down

Dividing the load by nproc and calling under 1.0 headroom and over 1.0 saturation is usable as a starting point. But it breaks down in the following five situations.

First, D state contamination. Exactly as in the previous section. 24/8 = 3.0, yet that has nothing to do with CPU saturation.

Second, the lag of the average. A 30 second spike does not surface as even half of itself in the 1 minute average. Conversely, an incident that already ended lingers in the 15 minute average.

Third, SMT (hyperthreading). nproc counts logical CPUs. With 8 physical cores and SMT enabled you get 16, but the actual processing capacity is not 16 cores. A load of 12 does not mean 75% of 16 cores.

lscpu | grep -E '^CPU\(s\)|Thread|Core|Socket'
# CPU(s):                          16
# Thread(s) per core:              2
# Core(s) per socket:              8
# Socket(s):                       1

Fourth, cgroup quotas. A task throttled by a CPU limit on a container drops out of the run queue altogether, so it never registers in the load average. On a 64 core host, a pod capped at 0.5 CPU can be severely slow while the host load stays quiet. This combination comes up again later.

Fifth, the absence of attribution. The load is a single system wide scalar. It tells you nothing about which container, which service, or which resource is responsible. You can raise an alert on it, but the only thing that alert lets you do is go in and look.

High load with an idle CPU — the diagnostic path

There is an order to this. Top to bottom, three minutes and you are done.

# Step 1: check the composition of the load — few R and many D means the I/O path
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  2 22      0 512344  84120 6231044    0    0     0     0 2103 4102  4  3 21 72  0
#  1 23      0 511208  84120 6231180    0    0 132096   412 2288 4390  3  4 19 74  0
#  2 22      0 510992  84120 6231212    0    0 128512   380 2251 4301  4  3 20 73  0

The b column is 22 to 23. That column is exactly the number of tasks in uninterruptible sleep. wa (iowait) at 72% tells the same story. iowait is "the time in which the CPU was idle while a task on that CPU was waiting on I/O", so it is really a kind of idle time. A high value is not bad in itself, but read together with the b column the direction is unmistakable.

# Step 2: find out which device is saturated
iostat -x 1 3
# Device      r/s     rkB/s  rrqm/s %rrqm r_await rareq-sz    w/s   wkB/s  w_await  aqu-sz  %util
# nvme0n1   1842.0   58944.0    0.0  0.00   12.85    32.00  210.0  3360.0     3.02   24.30  99.60
# nvme1n1      2.0      64.0    0.0  0.00    0.31    32.00    1.0    12.0     0.22    0.00   1.20

There are three things to read. Whether %util is pinned at 100% (the device never rests), how many times the usual value r_await/w_await have become (latency per request), and how deep aqu-sz is (average number of requests queued). On NVMe, %util at 100% may not mean saturation because of parallelism, so make the call from await and queue depth.

# Step 3: check the D state processes one by one
ps -eo state,pid,ppid,comm --no-headers | awk '$1 == "D"' | head
# D  4821  4102 postgres
# D  4822  4102 postgres
# D  7710     1 kworker/u33:2
# Step 4: find where in the kernel that process is stuck (root privileges required)
sudo cat /proc/4821/stack
# [<0>] io_schedule+0x46/0x80
# [<0>] folio_wait_bit_common+0x131/0x330
# [<0>] filemap_fault+0x5f0/0xa50
# [<0>] __do_fault+0x39/0x120
# [<0>] handle_mm_fault+0xd4e/0x1090

If the top of the stack is io_schedule it is block I/O, if rpc_wait_bit_killable or nfs_wait_on_request shows up it is NFS, and if it is in the __lock_page family it is page cache contention. If /proc/PID/stack is empty or returns a permission error, the kernel was built without CONFIG_STACKTRACE or you lack the privileges.

When NFS is the suspect, look at the mount statistics directly.

# Check per server RTT and retransmits
nfsstat -c | head -20
mountstats --nfs /mnt/shared 2>/dev/null | grep -A3 'READ:'

# Find the server that is not answering (a hard mount waits forever)
grep nfs /proc/mounts
# 10.0.9.4:/export/data /mnt/shared nfs4 rw,hard,proto=tcp,timeo=600,retrans=2 0 0

A hard mount pins processes in D state until the server comes back. In that state even kill -9 has no effect. Operationally, consider softerr or a shorter timeo, but confirm the data consistency requirements first.

Low load and yet slow — the things load cannot see

The opposite direction is common too. The load is 0.8 and p99 is 3 seconds. Listing what load structurally cannot see narrows the candidates.

  • Interruptible waits (S state) are not counted in the load. Time spent waiting on a socket response, that is, latency in a downstream API or database, all belongs here. Most of the reasons an application is slow live on this side.
  • Userspace lock contention is mostly S state as well. A futex wait is interruptible, so no matter how bad it gets the load does not rise.
  • cgroup CPU throttling, as noted above, is invisible because it removes tasks from the run queue.
  • Single thread bottlenecks are load 1 by definition. Even with 64 cores the load never goes above 1.
  • Hypervisor steal time means the CPU was taken away, not that the queue got longer.

The commands that separate these five are as follows.

# What an S state task is waiting on (socket waits do not show up here, so use the stack)
sudo cat /proc/9931/stack | head -5

# Lock contention: whether context switches have exploded
vmstat 1 3        # how many times the usual value the cs column has become
pidstat -w -p 9931 1 3
# UID  PID   cswch/s nvcswch/s  Command
# 1000 9931  18422.0   34110.0  java

# Throttling (cgroup v2)
cat /sys/fs/cgroup/cpu.stat
# nr_periods 86400
# nr_throttled 41287
# throttled_usec 2914837261

# steal
mpstat -P ALL 1 3 | tail -3

A large nvcswch/s (involuntary context switches) means CPU contention, and a large cswch/s (voluntary) means a pattern of falling asleep and waking again on locks or I/O.

PSI — the metric that answers what load average could not

The fundamental problem with the load average is that it says only how many are waiting and never how long anything was stalled. PSI, which arrived in kernel 4.20, measures exactly the latter.

cat /proc/pressure/io
# some avg10=78.23 avg60=71.04 avg300=48.92 total=8172635241
# full avg10=61.17 avg60=55.83 avg300=37.10 total=6019283746

cat /proc/pressure/cpu
# some avg10=2.41 avg60=1.88 avg300=1.02 total=91827364
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=142
# full avg10=0.00 avg60=0.00 avg300=0.00 total=88

Here is how to read it.

  • some is the fraction of time in which at least one task was stalled because of that resource.
  • full is the fraction of time in which every runnable task was stalled at once. That is, during that time the system did no useful work at all.
  • avg10, avg60, and avg300 are percentages over 10 second, 60 second, and 300 second windows, and total is cumulative microseconds.
  • The full line for the system wide cpu is meaningless by definition and is always 0 (the line itself has been printed since kernel 5.13).

What the output above says is clear. I/O full at 61% means that for more than 6 of the last 10 seconds the whole system was stalled on I/O. That is far more direct information than the number 24, and above all it translates straight into lost throughput.

The second advantage of PSI is that it exists per cgroup. That is the attribution the load average does not have.

# Which cgroup was stalled on I/O the longest
for f in /sys/fs/cgroup/**/io.pressure; do
  v=$(awk '/^full/ {print $2}' "$f" | cut -d= -f2)
  printf '%6s  %s\n' "$v" "${f%/io.pressure}"
done 2>/dev/null | sort -rn | head -5
#  58.12  /sys/fs/cgroup/system.slice/postgresql.service
#   3.04  /sys/fs/cgroup/system.slice/containerd.service
#   0.00  /sys/fs/cgroup/user.slice

If /proc/pressure is missing, either CONFIG_PSI is off, or the kernel was built with CONFIG_PSI_DEFAULT_DISABLED=y and needs the boot parameter psi=1.

MetricWhat it measuresD state reflectedTime windowPer cgroup
load averagenumber of waiting tasksincluded1/5/15 min EMAno
CPU utilizationfraction of time the CPU was truly usedexcludedarbitraryyes
iowaitidle CPU that had an I/O wait on itindirectarbitraryno
PSI cpu.sometime stalled because of CPUexcluded10/60/300 syes
PSI io.fulltime everything was stalled on I/Othis, in effect10/60/300 syes

Wrapping up — load average is queue length, not utilization

If you remember only one thing, remember this. The Linux load average is an exponentially damped moving average of tasks waiting for CPU and tasks waiting for I/O summed together with no distinction. So this one number alone cannot answer whether you need more CPU.

Boiled down into practical rules, it comes out like this.

  1. When the load is high, first look at the fourth field of /proc/loadavg and the b column of vmstat 1. The ratio of R to D sets the direction.
  2. If D dominates, go down into the await values of iostat -x and into /proc/PID/stack. Adding CPU is not the answer.
  3. Put the alert on PSI, not on load. cpu.some avg60 and io.full avg60 crossing a threshold correlates with real latency at far fewer false positives.
  4. Leave load on the dashboard but use it only as a trend indicator. If the 1 minute value is above the 5 and 15 minute ones, load is rising, and read exactly that much and no more.
  5. In a container environment, host load does not explain pod performance. You have to look at the cgroup cpu.stat and *.pressure.