서버가 느려졌다는 알림을 받았을 때, 무작정 재부팅하거나 인스턴스를 키우는 것은 근본 원인을 덮는 임시방편입니다. 리눅스 서버의 부하는 크게 CPU, 메모리, I/O 세 축으로 나뉘며, 각 축마다 확인해야 할 지표와 도구가 다릅니다.

이 글에서는 로드 애버리지를 오해 없이 읽는 법부터 시작해, CPU·메모리·디스크 I/O 병목을 각각 어떤 명령으로 특정하는지 실전 순서대로 정리합니다. 대부분 리눅스에 기본 포함되거나 sysstat 정도만 설치하면 되는 도구들이라 어떤 서버에서도 바로 따라 할 수 있습니다.

로드 애버리지부터 제대로 읽기

가장 먼저 보는 uptime의 로드 애버리지는 자주 오해됩니다. 이 값은 CPU 사용률이 아니라 실행 가능(runnable) + 중단 불가능 대기(uninterruptible, 주로 I/O) 상태의 태스크 수의 평균입니다. 즉 로드가 높은데 CPU는 한가할 수 있습니다. 대개 I/O 대기 때문입니다.

uptime
#  15:04:01 up 12 days,  load average: 8.21, 6.45, 4.10
#  1분/5분/15분 평균. CPU 코어 수와 비교해야 의미가 있음

nproc   # 코어 수 확인. 코어가 8인데 로드 8.21이면 포화 근처

로드를 코어 수로 나눈 값이 지속적으로 1을 넘으면 시스템이 감당 못 하는 것입니다. 다만 로드만으로는 원인(CPU냐 I/O냐)을 알 수 없으므로, 다음 단계로 세분화해야 합니다.

전체 그림: vmstat와 pressure stall

vmstat는 CPU·메모리·스왑·I/O를 한 화면에 요약해 첫 방향을 잡기 좋습니다. 1초 간격으로 실행합니다.

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  5      0 120340  33012 890210    0    0  1204  3402 5210 8102 12  4  9 74  1
  • r(run queue)이 코어 수보다 크게 유지되면 CPU 병목
  • b(blocked, I/O 대기)이 크고 wa가 높으면 I/O 병목
  • si/so(스왑 인/아웃)가 0이 아니면 메모리 부족으로 스와핑 중 — 성능 급락 신호

커널 4.20 이상이라면 PSI(Pressure Stall Information)가 더 직관적입니다. 자원별로 “얼마나 많은 시간을 대기로 낭비했는가”를 백분율로 보여줍니다.

cat /proc/pressure/cpu
cat /proc/pressure/io
cat /proc/pressure/memory
# some avg10=42.13 ...  -> 최근 10초간 42%의 시간이 해당 자원 대기로 지연됨

CPU 병목 특정하기

CPU가 원인으로 좁혀졌다면 mpstat로 코어별 분포를, top/pidstat로 범인 프로세스를 찾습니다. 특정 코어만 100%라면 싱글스레드 병목일 수 있습니다.

# 코어별 사용률(sysstat 필요)
mpstat -P ALL 1 3

# 프로세스별 CPU, 스레드까지
pidstat -u 1 3
pidstat -t -p <PID> 1 3   # 특정 프로세스의 스레드별

%usr이 높으면 애플리케이션 연산, %sys가 높으면 커널/시스템콜(과도한 컨텍스트 스위칭·네트워크 처리 등)을 의심합니다. %iowait가 높으면 사실 CPU가 아니라 I/O 문제입니다. 컨텍스트 스위칭이 비정상적으로 많은지도 확인하세요.

# 초당 컨텍스트 스위치/인터럽트 급증 여부
vmstat 1 3 | awk 'NR>2 {print "cs="$12, "in="$11}'

메모리 병목: free만으로는 부족하다

리눅스는 남는 메모리를 페이지 캐시로 적극 활용하므로, free의 “used”가 높다고 부족한 게 아닙니다. 핵심은 available 컬럼과 스왑 사용, 그리고 OOM 발생 여부입니다.

free -h
#               total   used   free   shared  buff/cache  available
# Mem:           15Gi   9Gi   400Mi    120Mi       5.6Gi      5Gi
#  -> available 5Gi면 아직 여유. 이 값이 0에 수렴하면 위험

실제 메모리 부족은 스와핑과 OOM 킬러로 드러납니다. 프로세스가 갑자기 죽었다면 커널 로그를 확인하세요.

# OOM으로 죽은 프로세스 흔적
dmesg -T | grep -i -E 'oom|killed process'
journalctl -k | grep -i 'out of memory'

# 프로세스별 실제 물리 메모리(RSS) 상위
ps aux --sort=-rss | head -6

더 정밀하게는 smem의 PSS(공유 메모리를 비례 배분한 값)가 프로세스별 실사용을 정확히 보여줍니다. 캐시가 계속 밀려나며 si/so가 발생한다면, 애플리케이션 힙 설정이나 인스턴스 메모리를 재검토해야 합니다.

I/O 병목: 디스크가 발목을 잡을 때

로드는 높은데 CPU는 한가하고 wa가 크다면 디스크 I/O입니다. iostat -x로 디바이스별 지표를 봅니다.

iostat -x 1 3
# Device  r/s   w/s  rkB/s   wkB/s  await  aqu-sz  %util
# nvme0n1 120   340  15200   43000   18.4    2.31    97.5
  • %util이 100%에 근접하면 디바이스 포화(단, NVMe/RAID처럼 병렬 처리 장치에서는 100%가 곧 포화는 아님)
  • await(요청당 평균 대기 ms)가 크게 튀면 지연 문제
  • aqu-sz(평균 큐 길이)가 길면 요청이 쌓이는 중

어떤 프로세스가 디스크를 때리는지는 iotop 또는 pidstat -d로 특정합니다.

pidstat -d 1 3          # 프로세스별 읽기/쓰기 KB/s
iotop -oPa             # 실제 I/O 발생 프로세스만(-o), 누적(-a)

# 특정 프로세스가 어떤 파일을 여는지
lsof -p <PID> | grep -E 'REG|DIR' | head

fsync가 잦은 데이터베이스, 로그 폭주, 스왑 I/O가 흔한 원인입니다. 로그가 원인이면 로테이션·비동기 로깅으로, DB라면 커밋 배칭이나 더 빠른 스토리지로 대응합니다.

진단 순서를 하나의 흐름으로

실전에서는 다음 순서로 좁혀 나가면 대부분 5분 안에 원인이 드러납니다.

# 1) 전체 감 잡기
uptime; vmstat 1 3; cat /proc/pressure/{cpu,io,memory}

# 2) 로드 원인 분기
#   r 높음 & wa 낮음  -> CPU  : mpstat, pidstat -u
#   b/wa 높음         -> I/O  : iostat -x, pidstat -d
#   si/so 발생        -> MEM  : free -h, ps --sort=-rss, dmesg

# 3) 범인 프로세스 확정 후 조치

고급 분석이 필요하면 perf top으로 어떤 커널/유저 함수가 CPU를 먹는지, bpftrace로 특정 시스템콜 지연을 추적할 수 있지만, 위 기본 도구만으로도 일상적 병목의 대부분은 잡힙니다.

마무리

서버 부하 진단의 핵심은 “느리다”는 모호한 증상을 CPU·메모리·I/O 중 어디의 문제인지 데이터로 특정하는 것입니다. 로드 애버리지는 방향을 가리키는 나침반일 뿐, 그 자체로 원인이 아닙니다. vmstat과 PSI로 큰 그림을 잡고, 로드가 CPU 때문인지 I/O 때문인지를 r·wa·si/so로 분기한 뒤, 각 축에 맞는 도구로 범인 프로세스까지 좁히는 흐름을 몸에 익혀 두면 장애 대응 시간이 극적으로 줄어듭니다. 재부팅과 스케일업은 원인을 확인한 다음의 선택지여야 합니다.