들어가며 — 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_UNINTERRUPTIBLE은 ps에서 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은 누적 마이크로초입니다.- 시스템 전역
cpu의full은 정의상 의미가 없어 항상 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.some | CPU 때문에 멈춰 있던 시간 | 미포함 | 10/60/300초 | 가능 |
| PSI io.full | I/O로 전체가 멈춰 있던 시간 | 사실상 이것 | 10/60/300초 | 가능 |
마치며 — 로드 애버리지는 대기열 길이지 사용률이 아닙니다
하나만 기억한다면 이것입니다. 리눅스의 로드 애버리지는 CPU를 기다리는 태스크와 I/O를 기다리는 태스크를 구분 없이 합친 개수의 지수이동평균입니다. 그래서 이 숫자 하나만으로는 "CPU를 늘려야 하는지"에 답할 수 없습니다.
실무 규칙으로 정리하면 이렇습니다.
- 로드가 높으면 먼저
/proc/loadavg의 네 번째 필드와vmstat 1의b열을 봅니다. R과 D의 비율이 방향을 정합니다. - D가 많으면
iostat -x의 await와/proc/PID/stack으로 내려갑니다. CPU 증설은 답이 아닙니다. - 알람은 로드가 아니라 PSI에 겁니다.
cpu.some avg60과io.full avg60이 임계를 넘는 것이 훨씬 적은 오탐으로 실제 지연과 상관됩니다. - 로드는 대시보드에 남겨 두되 추세 지표로만 씁니다. 1분 값이 5분·15분보다 크면 오르는 중이라는 것, 딱 그만큼만 읽습니다.
- 컨테이너 환경이라면 호스트 로드는 파드 성능을 설명하지 못합니다. cgroup의
cpu.stat과*.pressure를 봐야 합니다.
현재 단락 (1/99)
새벽에 알람이 옵니다. 8코어 서버의 로드 애버리지가 24를 넘겼습니다. 접속해서 `top`을 열면 CPU 유휴가 60% 넘게 남아 있습니다. 여기서 두 가지 결론 중 하나로 새기...