서버가 느려졌을 때 원인 찾는 방법
어제까지 잘 돌아가던 서버가 갑자기 느려지면 당황스럽습니다. 페이지 로딩이 몇 초씩 걸리거나, API 응답이 타임아웃 나거나, 심하면 SSH 접속조차 버벅거립니다. 이런 상황에서 무작정 서버를 재시작하면 당장은 나아질 수 있지만 원인을 모르면 또 같은 일이 반복됩니다. 서버가 느려지는 원인은 크게 CPU, 메모리, 디스크, 네트워크 네 가지로 나뉩니다. 이번 글에서는 서버가 느려졌을 때 원인을 체계적으로 찾아가는 방법을 정리해보겠습니다.
가장 먼저 할 일 — 전체 상태 훑어보기
서버에 SSH로 접속했으면 가장 먼저 전체적인 상태를 확인합니다. 이전 글에서 다뤘던 htop이 설치되어 있으면 htop을 실행하고, 없으면 top을 실행합니다.
htop
htop 화면에서 바로 확인할 수 있는 것들이 있습니다. 화면 상단의 CPU 막대가 전부 빨간색으로 꽉 차 있으면 CPU 문제입니다. 메모리 막대가 거의 다 차 있고 스왑도 많이 사용 중이면 메모리 문제입니다. CPU와 메모리가 둘 다 여유가 있는데 느리다면 디스크 I/O나 네트워크 쪽을 의심해야 합니다.
htop만으로 대략적인 방향이 잡히는 경우가 많습니다. 여기서 어떤 쪽이 문제인지 파악하고 해당 영역을 더 깊게 파고들면 됩니다.
uptime 명령어로 로드 애버리지도 확인합니다.
uptime
결과에 세 개의 숫자가 나오는데 각각 1분, 5분, 15분 평균 로드입니다. 이 숫자가 서버의 CPU 코어 수보다 높으면 서버에 부하가 걸려 있다는 뜻입니다. 코어가 2개인 서버에서 로드가 4라면 처리 대기 중인 작업이 쌓이고 있는 겁니다. 1분 평균이 높고 15분 평균이 낮으면 최근에 갑자기 부하가 생긴 것이고, 15분 평균도 높으면 한동안 계속 부하 상태였다는 뜻입니다.
CPU가 원인인 경우
htop에서 특정 프로세스가 CPU를 거의 다 차지하고 있다면 원인이 명확합니다. CPU 사용률 순으로 정렬해서 어떤 프로세스가 CPU를 많이 먹고 있는지 확인합니다. htop에서는 기본으로 CPU 순 정렬이 되어 있고, top에서는 Shift+P를 누르면 됩니다.
Node.js 앱이 CPU를 100% 가까이 사용하고 있다면 코드에 무한 루프가 있거나, 매우 무거운 연산을 동기적으로 처리하고 있거나, 요청이 갑자기 폭증한 경우입니다.
MySQL이나 PostgreSQL 같은 데이터베이스가 CPU를 잡아먹고 있다면 느린 쿼리가 실행되고 있을 가능성이 높습니다. 인덱스 없이 대량의 데이터를 풀스캔하는 쿼리가 대표적입니다.
의도하지 않은 프로세스가 CPU를 사용하고 있다면 보안 문제일 수 있습니다. 서버가 해킹당해서 암호화폐 채굴 프로그램이 돌아가는 경우가 실제로 있습니다. 모르는 프로세스가 CPU를 점유하고 있다면 해당 프로세스의 경로를 확인해야 합니다.
ls -l /proc/프로세스PID/exe
이 명령어로 해당 프로세스의 실행 파일 경로를 확인할 수 있습니다.
특정 프로세스를 당장 중지해야 한다면 kill 명령어를 사용합니다.
kill 프로세스PID
강제 종료가 필요하면 -9 옵션을 붙입니다.
kill -9 프로세스PID
메모리가 원인인 경우
메모리 문제는 두 가지로 나뉩니다. 물리 메모리가 부족한 경우와 메모리 누수가 발생하는 경우입니다.
먼저 전체 메모리 상태를 확인합니다.
free -h
결과에서 봐야 할 항목은 available 값입니다. 이 값이 전체 메모리의 10% 미만이면 메모리가 부족한 상태입니다. used가 높더라도 available이 충분하면 괜찮습니다. 리눅스는 사용하지 않는 메모리를 캐시로 활용하기 때문에 used가 높게 나오는 게 정상입니다.
Swap 항목도 확인합니다. 스왑은 디스크를 메모리처럼 사용하는 건데, 디스크는 메모리보다 훨씬 느리기 때문에 스왑이 많이 사용되면 서버가 급격히 느려집니다. 스왑 사용량이 지속적으로 높다면 물리 메모리가 부족하다는 신호입니다.
어떤 프로세스가 메모리를 많이 사용하는지 확인하려면 htop에서 F6을 눌러 메모리 순으로 정렬하거나, top에서 Shift+M을 누릅니다.
메모리 누수는 시간이 지남에 따라 특정 프로세스의 메모리 사용량이 계속 증가하는 현상입니다. 앱을 처음 시작했을 때는 100MB를 사용하다가 며칠 후에 2GB까지 올라갔다면 메모리 누수를 의심해야 합니다. Node.js에서는 이벤트 리스너를 해제하지 않거나, 전역 변수에 데이터를 계속 쌓거나, 클로저에서 참조를 놓지 않는 경우에 많이 발생합니다.
메모리가 당장 부족해서 급한 상황이라면 메모리를 많이 사용하는 불필요한 프로세스를 종료하거나, 앱을 재시작해서 메모리를 반환받는 게 임시 조치입니다. 근본적으로는 메모리 누수를 코드에서 찾아 수정해야 합니다.
스왑이 아예 없는 서버라면 스왑 파일을 추가하는 것도 방법입니다. 메모리가 꽉 차면 앱이 바로 죽는 것보다는 느리더라도 돌아가는 게 낫기 때문입니다.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
영구적으로 적용하려면 /etc/fstab에 추가합니다.
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
디스크가 원인인 경우
CPU와 메모리가 둘 다 여유가 있는데 서버가 느리다면 디스크 I/O를 확인해야 합니다.
먼저 디스크 용량을 확인합니다.
df -h
Use%가 90%를 넘는 파티션이 있으면 디스크가 거의 찬 겁니다. 디스크가 꽉 차면 로그를 못 쓰거나 데이터베이스가 멈추면서 서비스가 중단될 수 있습니다. 이전 글에서 다뤘던 du 명령어로 어떤 디렉토리가 용량을 많이 차지하는지 찾습니다.
sudo du -sh /var/log/*
sudo du -sh /var/www/*
로그 파일이 몇 기가바이트씩 쌓여 있는 경우가 가장 흔합니다. PM2 로그, Nginx 액세스 로그, 시스템 로그가 대표적입니다. 오래된 로그를 삭제하고, 이전 글에서 다뤘던 크론탭으로 주기적으로 정리하는 설정을 추가합니다.
디스크 용량은 충분한데 I/O 자체가 느린 경우도 있습니다. 이전 글에서 다뤘던 iotop으로 확인합니다.
sudo iotop
어떤 프로세스가 디스크를 많이 읽고 쓰는지 실시간으로 보여줍니다. 데이터베이스가 디스크 I/O를 과도하게 사용하고 있다면 쿼리 최적화나 인덱스 추가가 필요합니다. 백업 작업이 I/O를 잡아먹고 있다면 백업 시간을 트래픽이 적은 시간대로 옮기는 걸 고려합니다.
vmstat으로도 I/O 상태를 확인할 수 있습니다.
vmstat 2 10
wa 칼럼이 I/O 대기 시간의 비율을 나타냅니다. 이 값이 지속적으로 20% 이상이면 디스크 I/O가 병목입니다.
네트워크가 원인인 경우
서버 자체는 멀쩡한데 응답이 느리다면 네트워크 문제일 수 있습니다.
먼저 서버에서 외부로의 연결을 확인합니다.
ping -c 5 8.8.8.8
응답 시간이 비정상적으로 높거나 패킷 유실이 있으면 서버의 네트워크 연결 자체에 문제가 있는 겁니다.
이전 글에서 다뤘던 nethogs로 프로세스별 네트워크 사용량을 확인합니다.
sudo nethogs
의도하지 않은 프로세스가 대역폭을 차지하고 있다면 원인을 파악해야 합니다.
현재 서버에 연결된 네트워크 연결 수를 확인하는 것도 유용합니다.
ss -s
이 명령어는 현재 연결 상태의 요약을 보여줍니다. TCP 연결 수가 비정상적으로 많다면 DDoS 공격이나 커넥션 누수를 의심할 수 있습니다.
어떤 IP에서 연결이 많이 들어오고 있는지 확인하려면 이렇게 합니다.
ss -tn | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20
특정 IP에서 비정상적으로 많은 연결이 들어오고 있다면 해당 IP를 방화벽으로 차단할 수 있습니다.
sudo ufw deny from 공격IP주소
데이터베이스가 원인인 경우
앱 서버의 CPU, 메모리, 디스크가 전부 정상인데 응답이 느리다면 데이터베이스를 확인해야 합니다. 웹 애플리케이션에서 응답이 느린 원인의 상당수가 데이터베이스 쿼리입니다.
MySQL에서 현재 실행 중인 쿼리를 확인합니다.
mysql -u root -p -e "SHOW PROCESSLIST;"
Time 값이 큰 쿼리가 있으면 그 쿼리가 오래 걸리고 있는 겁니다. State가 Sending data이거나 Sorting result이면 대량의 데이터를 처리하고 있다는 뜻입니다.
느린 쿼리를 찾으려면 슬로우 쿼리 로그를 활성화합니다. MySQL 설정 파일을 열어서 다음 내용을 추가합니다.
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
2초 이상 걸리는 쿼리를 전부 로그로 남깁니다. 이 로그를 분석해서 문제가 되는 쿼리를 찾고, 인덱스를 추가하거나 쿼리를 개선합니다.
데이터베이스 연결 수가 한계에 도달한 경우도 있습니다. 앱에서 데이터베이스 연결을 열고 닫지 않으면 연결이 쌓여서 새 요청을 처리할 수 없게 됩니다. 커넥션 풀을 사용하는지, 풀 크기가 적절한지 확인해야 합니다.
Nginx 로그로 원인 좁히기
어떤 요청이 느린지 Nginx 액세스 로그에서 확인할 수 있습니다.
sudo tail -f /var/log/nginx/access.log
실시간으로 들어오는 요청을 볼 수 있습니다. 특정 URL로 요청이 폭주하고 있다면 해당 엔드포인트의 코드를 확인해야 합니다.
에러 로그도 확인합니다.
sudo tail -100 /var/log/nginx/error.log
502 Bad Gateway 에러가 많이 찍혀 있다면 앱 서버가 응답을 못 하고 있다는 뜻입니다. PM2에서 앱이 계속 재시작되고 있는지 확인합니다. 504 Gateway Timeout이 많다면 앱은 살아있지만 처리가 너무 오래 걸려서 Nginx가 기다리다 포기한 겁니다.
응답 시간을 로그에 기록하고 싶으면 Nginx 설정에서 로그 포맷에 $request_time을 추가할 수 있습니다. 어떤 요청이 몇 초 걸렸는지 기록되기 때문에 느린 요청을 특정하기 쉬워집니다.
진단 순서 정리
서버가 느려졌을 때 확인하는 순서를 정리하면 이렇습니다.
첫째, htop이나 top으로 전체 상태를 훑어봅니다. CPU, 메모리 사용률과 어떤 프로세스가 리소스를 많이 쓰는지 확인합니다.
둘째, free -h로 메모리와 스왑 상태를 확인합니다.
셋째, df -h로 디스크 용량을 확인합니다.
넷째, CPU와 메모리가 정상이면 iotop으로 디스크 I/O를 확인합니다.
다섯째, 서버 리소스가 전부 정상이면 데이터베이스 쿼리와 네트워크 연결을 확인합니다.
여섯째, Nginx 로그에서 어떤 요청이 문제인지 좁혀갑니다.
이 순서대로 하면 대부분의 경우 원인을 찾을 수 있습니다. 원인을 찾으면 해결 방법은 상황마다 다르지만, 최소한 어디가 문제인지 모르고 이것저것 건드리는 것보다는 훨씬 효율적입니다.
마무리
서버가 느려지는 원인은 결국 CPU, 메모리, 디스크, 네트워크 중 하나입니다. htop 하나로 대략적인 방향을 잡고, 상황에 맞는 도구로 세부 원인을 파고들면 됩니다. 중요한 건 평소에 서버의 정상 상태를 알고 있는 것입니다. CPU가 보통 몇 퍼센트인지, 메모리가 얼마나 사용되는지를 알고 있어야 비정상을 감지할 수 있습니다. 이전 글에서 다뤘던 모니터링 도구를 활용해서 평소 상태를 파악해두는 습관이 결국 문제 대응 속도를 높여줍니다. 다음 글에서는 서버 운영 중에 자주 만나는 502 Bad Gateway 오류의 원인과 해결법을 다뤄보겠습니다.
Leave a Reply