금요일 저녁 7시, 퇴근길 지하철에서 메시지가 온다. "사이트가 좀 느린데요?" 노트북을 열 자리도 없는데 마음은 이미 급해진다. 이럴 때 가장 위험한 습관이 하나 있다. 원인을 짐작해서 아무거나 재시작하는 것이다. 재시작하면 증상은 잠깐 사라지지만, 원인은 그대로 남아 새벽 3시에 다시 찾아온다.
서버 진단은 감이 아니라 순서다. 확인할 것이 딱 네 가지밖에 없다고 생각하면 마음이 한결 편해진다. CPU, 메모리, 디스크, 그리고 로그. 이 네 가지를 정해진 순서로 훑으면 대부분의 "갑자기 느려졌어요"는 5분 안에 범위가 좁혀진다.
1단계: 지금 이 서버가 힘든가부터 본다
가장 먼저 볼 것은 부하 평균(load average)이다. uptime 한 줄이면 충분하다.
uptime
# 19:04:12 up 41 days, 3:12, 1 user, load average: 8.21, 5.40, 2.13세 숫자는 각각 최근 1분·5분·15분 평균 대기 작업 수다. 여기서 중요한 건 절대값이 아니라 CPU 코어 수와의 비교다. 코어가 4개인데 부하가 8이면 처리 능력의 두 배가 몰려 있다는 뜻이고, 코어가 16개라면 같은 8도 여유가 있는 상태다. 코어 수는 nproc으로 바로 확인한다.
숫자의 배열도 이야기를 해준다. 위 예시처럼 8.21, 5.40, 2.13이면 15분 전에는 멀쩡했는데 지금 급격히 올라가는 중이다. 반대로 2.10, 5.40, 8.21이라면 고비는 지났고 회복 중이라는 뜻이다. 전화를 받은 시점이 언덕의 오르막인지 내리막인지 아는 것만으로도 대응이 달라진다.
부하 평균은 "얼마나 아픈가"가 아니라 "어느 방향으로 가고 있는가"를 알려주는 숫자다.
2단계: 범인이 CPU인지 디스크인지 가른다
부하가 높다는 건 알았는데, 무엇 때문인지는 아직 모른다. top을 열면 위쪽에 CPU 사용률이 여러 항목으로 쪼개져 나온다. 여기서 봐야 할 건 딱 두 글자다.
- us / sy: 사용자 프로세스와 커널이 실제로 계산 중. 진짜 CPU 부족.
- wa: I/O 대기. CPU는 놀고 있는데 디스크나 네트워크 응답을 기다리는 중.
- id: 유휴. 이 값이 높은데 서버가 느리다면 병목은 CPU 바깥에 있다.
wa가 30~40%를 넘나든다면 CPU를 아무리 늘려도 소용없다. 느린 쿼리가 디스크를 긁고 있거나, 로그를 과도하게 쓰고 있거나, 백업 작업이 겹쳤을 가능성이 높다. 실제로 겪어보면 "서버 증설했는데 그대로예요"의 상당수가 이 wa 케이스다.
프로세스 목록은 top 상태에서 Shift+P(CPU순), Shift+M(메모리순)으로 정렬만 바꿔가며 보면 된다. 상위 한두 개가 압도적으로 크다면 범위는 이미 좁혀진 셈이다.
3단계: 메모리는 free보다 available을 본다
free -h를 처음 본 사람들이 가장 많이 놀라는 지점이 있다. free 값이 거의 0에 가깝다는 것이다. 하지만 이건 정상이다. 리눅스는 남는 메모리를 놀리지 않고 디스크 캐시로 빌려 쓰기 때문이다. 필요해지면 즉시 반납한다.
free -h
# total used free shared buff/cache available
# Mem: 7.7G 4.1G 210M 180M 3.4G 3.1G실제로 봐야 할 값은 맨 오른쪽 available이다. 위 예시는 free가 210M이지만 available이 3.1G이므로 여유가 있는 상태다. 반대로 available이 전체의 10% 아래로 떨어지고 swap 사용량이 계속 늘고 있다면, 서버는 메모리를 디스크로 밀어내며 버티는 중이고 체감 속도는 급격히 나빠진다.
메모리 부족이 심해지면 커널이 프로세스를 강제 종료한다. 원인 모를 서비스 중단이 있었다면 dmesg -T | grep -i oom 으로 흔적을 확인해볼 수 있다. 여기에 기록이 남아 있다면 이야기는 명확해진다.
4단계: 디스크는 용량과 inode를 함께 본다
의외로 흔한 원인이 디스크 가득 참이다. 로그가 조용히 쌓이다가 어느 날 임계점을 넘고, 데이터베이스가 임시 파일을 못 만들어 이상한 에러를 뱉기 시작한다.
df -h # 파티션별 용량
df -i # inode 사용량
du -sh /var/log/* | sort -h | tail -10df -h가 멀쩡한데도 "공간이 없다"는 에러가 나온다면 inode 고갈을 의심한다. 용량은 남았지만 파일 개수 한도를 다 쓴 상태로, 세션 파일이나 캐시 조각이 수백만 개 쌓인 서버에서 종종 발생한다.
범인을 찾을 때는 du를 위에서 아래로 내려가며 좁힌다. /부터 한 번에 훑으면 오래 걸리니, /var, /home처럼 의심 가는 디렉터리부터 sort -h로 정렬해 큰 것만 따라가는 편이 빠르다. 참고로 이미 삭제했는데 용량이 안 줄어드는 경우가 있는데, 프로세스가 파일을 잡고 있어서다. lsof | grep deleted로 확인하고 해당 프로세스를 재시작하면 반환된다.
5단계: 로그는 시간대를 맞춰서 읽는다
여기까지 왔으면 어느 자원이 문제인지는 안다. 마지막은 "언제부터"다. 로그를 볼 때 가장 중요한 건 증상이 시작된 시각 앞뒤 몇 분으로 범위를 좁히는 것이다.
journalctl -u nginx --since "19:00" --until "19:10"
tail -f /var/log/mysql/slow.log
grep -c "500 " /var/log/nginx/access.logMySQL을 쓴다면 슬로우 쿼리 로그가 가장 값진 단서다. 특정 시간대에 같은 쿼리가 반복해서 찍힌다면, 그 쿼리 하나가 서버 전체를 붙잡고 있었을 가능성이 크다. 앞서 본 wa 수치와 이 로그가 시간상 겹치면 퍼즐은 거의 맞춰진 것이다.
| 증상 | 먼저 볼 것 | 자주 나오는 원인 |
|---|---|---|
| 전체적으로 느림 | top의 us/wa | 무거운 쿼리, 트래픽 급증 |
| 간헐적 응답 지연 | free -h의 available·swap | 메모리 부족, 스왑 |
| 저장·업로드 실패 | df -h, df -i | 로그 누적, inode 고갈 |
| 특정 시각에만 발생 | crontab -l | 배치·백업 작업 충돌 |
다시 그 전화를 받는다면
순서를 외워두면 손이 먼저 움직인다. uptime으로 방향을 보고, top으로 CPU인지 I/O인지 가르고, free -h의 available을 확인하고, df -h와 df -i로 디스크를 보고, 마지막에 로그의 시간대를 맞춘다. 다섯 걸음이면 대부분 "여기가 문제였네" 지점에 도착한다.
그리고 한 가지 더. 원인을 찾았다면 그 순간의 출력값을 어딘가에 붙여넣어 남겨두는 습관을 권한다. 다음에 비슷한 증상이 왔을 때, 과거의 내가 남긴 숫자 몇 줄이 가장 빠른 힌트가 되어준다.
급한 순간일수록 재시작 버튼은 잠시 미뤄두자. 5분만 순서대로 훑어도, 새벽에 다시 깨지 않을 확률이 훨씬 높아진다.

