1. htop 켜놓고 숫자만 멍하니 보고 있진 않나요?
운영 중인 서버가 느려졌다는 알람이 오면 십중팔구 SSH로 붙어서 htop부터 친다. 그런데 막상 화면을 보면 뭘 봐야 할지 애매한 순간이 많다. CPU 막대가 빨간색으로 꽉 찼는데 정작 CPU를 잡아먹는 프로세스는 안 보이고, Load Average는 8인데 CPU 사용률은 20%밖에 안 되고, 메모리 VIRT는 20GB라는데 실제 서버 램은 8GB인 상황 같은 것들 말이다.
이런 게 헷갈리는 이유는 대부분 각 지표가 커널 입장에서 실제로 뭘 세고 있는지를 모르기 때문이다. 원문(peteris.rocks/blog/htop)의 저자도 처음엔 "2코어 머신에서 Load Average 1.0이면 CPU 50%겠지"라고 생각했다가 그게 틀렸다는 걸 알고 전부 파헤쳐봤다고 한다. 나도 실무에서 이 착각 때문에 엉뚱한 데를 붙잡고 시간 날린 적이 여러 번 있다.
이 글에서는 htop/top의 각 지표가 커널의 프로세스 스케줄링, 메모리 구조와 어떻게 연결되는지 실무 시선에서 정리한다. 2019년 원문이지만 이 내용은 커널 근본 동작이라 지금도 그대로 유효하다.
2. 핵심: 지표들이 실제로 세고 있는 것
CPU 상태값 — us, sy, ni, id, wa, hi, si, st
top 상단 CPU 라인에 나오는 이 알파벳들은 CPU가 시간을 어디에 썼는지를 백분율로 쪼갠 것이다. htop에서는 막대 색으로도 표현된다.
- us (user): 유저 공간 프로세스 실행 시간. 애플리케이션 코드가 CPU를 쓰는 시간.
- sy (system): 커널 공간 실행 시간. 시스템 콜, 네트워크 스택 처리 등.
- ni (nice): nice 값이 조정된(우선순위 낮춘) 프로세스의 유저 시간.
- id (idle): CPU가 놀고 있는 시간.
- wa (iowait): CPU가 디스크/네트워크 I/O 완료를 기다리며 놀고 있는 시간. 이게 높으면 CPU가 아니라 I/O가 병목이다.
- hi (hardware irq): 하드웨어 인터럽트 처리 시간.
- si (software irq): 소프트웨어 인터럽트 처리 시간.
- st (steal): 가상화 환경에서 하이퍼바이저가 다른 VM에 CPU를 줘서 내 VM이 뺏긴 시간.
실무에서 특히 중요한 건 wa와 st다. wa가 높으면 CPU를 아무리 증설해도 소용없고 디스크나 네트워크를 봐야 한다. st가 꾸준히 높으면 클라우드에서 "이웃 VM 때문에 내 CPU가 굶고 있다"는 신호다 — AWS 같은 데서 t 계열 인스턴스 크레딧 소진이나 노이지 네이버 문제를 의심해볼 수 있다.
순간 CPU 사용률만 딱 보고 싶으면 mpstat가 편하다. 원문에서도 이 도구를 쓴다.
$ sudo apt install sysstat -y
$ mpstat 1
Linux 4.4.0-47-generic (hostname) 12/03/2016 _x86_64_ (1 CPU)
10:16:21 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
10:16:21 PM all 0.00 0.00 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
위 예시는 cat /dev/urandom > /dev/null을 돌리는 중이라 %sys가 100%다. urandom 생성이 커널 작업이라 유저 시간이 아니라 시스템 시간으로 잡힌 것. 이런 식으로 어느 컬럼이 튀는지 보면 병목 지점이 보인다.
메모리 — VIRT, RES, SHR의 차이
이 세 개를 헷갈리면 "메모리 누수 났다!"라고 오진하기 딱 좋다.
- VIRT (Virtual): 프로세스가 예약(reserve)한 전체 가상 메모리 주소 공간. 실제로 물리 메모리에 올라와 있지 않은 것도 포함된다. mmap한 파일, 아직 안 건드린 힙, 공유 라이브러리까지 다 합친 숫자라 실제 램보다 훨씬 클 수 있다.
- RES (Resident): 실제로 물리 RAM에 올라와 있는 크기. 실무에서 "이 프로세스가 진짜 램을 얼마나 먹나"를 볼 때 보는 값이 이거다.
- SHR (Shared): RES 중에서 다른 프로세스와 공유 가능한 부분(공유 라이브러리 등).
JVM이나 Go처럼 큰 가상 주소 공간을 미리 잡아두는 런타임을 보면 VIRT가 수십 GB로 찍히는 게 정상이다. VIRT 숫자에 놀라지 말고 RES를 봐야 한다.
프로세스 상태값 — R, S, D, Z, T
ps의 STAT 컬럼이나 htop의 S 컬럼에 나오는 값이다.
- R (Running): CPU에서 실행 중이거나 실행 대기열에 올라와 순서를 기다리는 상태.
- S (Sleeping): 인터럽트 가능한 대기. 뭔가 이벤트를 기다리며 자고 있음. 대부분의 유휴 프로세스가 여기.
- D (Uninterruptible sleep): 주로 디스크/네트워크 I/O를 기다리는 중단 불가 대기. 이게 많으면 I/O 병목 신호다. kill -9로도 잘 안 죽는 경우가 이 상태다.
- Z (Zombie): 종료됐지만 부모가 아직 exit 코드를 회수(wait) 안 한 상태. 프로세스 슬롯만 차지.
- T (Stopped): 시그널로 멈춘 상태(Ctrl+Z 등).
원문 설명대로 Load Average의 "load number"는 R 상태 + D 상태 프로세스 수를 센다. 그래서 CPU가 한가해도 D 상태가 잔뜩 쌓이면 Load Average가 치솟는 것이다.
Load Average — 코어 수와 함께 봐야 의미가 산다
원문에서 가장 강조하는 포인트다. Load Average는 실행 중이거나 실행 대기 중인 프로세스(R) + 중단 불가 대기 프로세스(D)의 개수를 지수 감쇠 이동평균으로 낸 값이다. 순수 CPU 사용률이 아니다.
핵심 규칙: Load Average를 코어 수로 나눠서 봐라.
- 1코어 머신에서 Load 1.00 → CPU가 딱 포화 (한 번에 프로세스 하나만 돌리니까).
- 2코어 머신에서 Load 1.00 → 절반만 쓰는 셈.
- 2코어 머신에서 CPU 100% 포화 상태의 Load는 2.00.
내 코어 수는 이렇게 확인한다.
$ nproc
2
$ cat /proc/loadavg
1.00 0.69 0.35 2/124 1679
앞 세 숫자가 각각 1분/5분/15분 평균이고, 네 번째 2/124는 (실행 중 프로세스 수)/(전체 프로세스 수), 마지막은 최근 할당된 PID다. 여기서 Load 1.00은 원문 예시처럼 cat /dev/urandom 같은 CPU 바운드 프로세스 하나가 1코어를 꽉 잡고 있는 상황이다.
단, 원문이 명확히 짚듯이 Load Average에는 D 상태(I/O 대기)도 포함되므로 Load Average만 보고 CPU 사용률을 역산하는 건 부정확하다. "Load는 높은데 CPU는 놀고 있는" 상황이 바로 이 때문이다.
3. 실무 관점: 함정과 트러블슈팅
흔한 함정 1 — Load 8인데 CPU는 20%, 대체 왜?
D 상태 프로세스가 원인일 가능성이 높다. 디스크가 느리거나(EBS 스로틀링, 노후 HDD), NFS 마운트가 응답을 안 하거나 할 때 프로세스들이 I/O를 기다리며 D 상태로 쌓인다. 이때 CPU %는 낮은데 Load만 치솟는다. 우선 wa 값과 D 상태 프로세스를 확인해라.
# D 상태(uninterruptible) 프로세스만 골라보기
$ ps -eo pid,stat,comm | awk '$2 ~ /D/ {print}'
1834 D mysqld
2001 D kworker/u8:2
NFS가 죽어서 프로세스가 D 상태로 박히면 ls 같은 명령조차 이런 식으로 무한 대기하거나, 강제 종료 시 아래 메시지를 만난다.
$ kill -9 1834
$ ps -o pid,stat,comm -p 1834
PID STAT COMMAND
1834 D mysqld
# kill -9를 보내도 D 상태 프로세스는 즉시 죽지 않는다.
# 커널 I/O가 완료(또는 타임아웃)될 때까지 반환되지 않기 때문.
또 NFS 하드마운트 환경에서 자주 보는 커널 로그:
nfs: server 10.0.1.20 not responding, still trying
이 메시지가 dmesg나 /var/log/messages에 뜨면 CPU를 볼 게 아니라 스토리지/네트워크를 봐야 한다는 확실한 신호다.
흔한 함정 2 — "메모리 누수다!" (사실은 VIRT를 본 것)
모니터링 알람이나 htop에서 VIRT 숫자만 보고 메모리 누수로 오진하는 경우가 정말 많다. 앞서 말했듯 VIRT는 예약된 가상 공간이라 실제 소비량과 다르다. 반드시 RES를 기준으로 봐라. RES가 시간에 따라 계단식으로 꾸준히 증가하고 GC나 재시작으로도 안 돌아온다면 그때 진짜 누수를 의심한다.
# RES 기준 상위 메모리 소비 프로세스 (ps로)
$ ps -eo pid,comm,rss,vsz --sort=-rss | head -5
PID COMMAND RSS VSZ
1834 java 2450112 12583920 # RES 약 2.4GB, VIRT 약 12GB
2201 postgres 310220 890112
# RSS = htop의 RES(KB), VSZ = htop의 VIRT(KB)
흔한 함정 3 — 가상 CPU steal(st) 무시하기
온프레미스에서 클라우드로 옮긴 뒤 "같은 스펙인데 왜 느리지?" 싶으면 st를 봐라. 하이퍼바이저가 CPU를 뺏어가는 시간이 st로 잡힌다. 특히 버스트형 인스턴스는 크레딧이 소진되면 성능이 조여지는데, 이게 애플리케이션 지표로는 원인이 안 보이고 st 상승으로만 드러나는 경우가 있다. (인스턴스별 정확한 크레딧 동작은 클라우드 공식 문서 확인 필요.)
대안 도구
htop이 만능은 아니다. 상황별로 이런 도구가 낫다.
- mpstat: 순간 CPU 사용률을 상태별로 정확히.
- iostat / iotop: wa가 높을 때 어느 디스크·프로세스가 I/O를 잡는지.
- pidstat: 특정 프로세스의 CPU/메모리/I/O를 시계열로.
- /proc/<pid>/ 직접 읽기: 원문이 강조하듯 htop/top/ps 전부 결국 여기서 정보를 읽는다. 스크립트로 자동화할 땐 파일을 직접 파싱하는 게 낫다.
# 특정 프로세스의 상태 직접 확인
$ cat /proc/1834/status | grep -E 'State|VmRSS|VmSize'
State: D (disk sleep)
VmSize: 12583920 kB
VmRSS: 2450112 kB
4. 정리
한 줄 요약: htop의 숫자는 커널이 세고 있는 대상을 알아야 의미가 생긴다 — CPU는 wa/st를, 메모리는 VIRT 아닌 RES를, Load Average는 코어 수로 나눠서 D 상태와 함께 봐라.
- CPU 병목 의심 → us/sy 높음 + Load ≈ 코어 수. mpstat로 확인.
- I/O 병목 의심 → wa 높음 + D 상태 프로세스 다수 + Load만 치솟음. iostat/iotop으로.
- 메모리 누수 의심 → RES가 지속 증가. VIRT는 무시.
- 클라우드에서 원인 불명 성능 저하 → st 확인.
이 원리는 신규 인프라 엔지니어가 반드시 익혀두면 트러블슈팅 속도가 확 빨라지는 기본기다. 지표 하나하나가 커널의 어떤 상태를 반영하는지 알면, 알람을 받았을 때 엉뚱한 데를 파는 시간이 줄어든다.
참고 자료
'Tech_News' 카테고리의 다른 글
| 스위스는 25Gbit 대칭형 광인터넷이 되는데 왜 미국은 안 되나: 오픈액세스 망 아키텍처 뜯어보기 (0) | 2026.07.05 |
|---|---|
| Linux 6.9 LUKS suspend 회귀 버그: 메모리에 암호화 키가 남는다 (0) | 2026.07.04 |
| Google Copybara 실전 가이드: 내부 저장소와 공개 저장소를 동기화하는 법 (0) | 2026.07.03 |
| Claude Code가 요청에 심는 보이지 않는 마커: ANTHROPIC_BASE_URL 스테가노그래피 분석 (0) | 2026.07.02 |
| Qwen 3.6 27B로 로컬 코딩 어시스턴트 굴려보기 — Dense vs MoE, 실전 통합까지 (0) | 2026.07.01 |
