Physical Address
South Korea
Physical Address
South Korea


서버 운영해 본 사람이라면 누구나 한 번쯤 심장이 철렁하는 순간이 있습니다. 금요일 퇴근 직전이나 새벽 한가운데, 모니터링 알람이 울리며 “CPU 사용률 100%”, “메모리 OOM(Out of Memory)으로 DB 프로세스 강제 종료” 같은 비보가 날아오는 때죠.
실제로 몇 달 전, 클라우드로 이전한 고객사의 RHEL(Red Hat Enterprise Linux) 8.4 서버 한 대가 이유 없이 주기적으로 뻗는 일이 있었습니다. 모니터링 툴상으로는 단순히 메모리가 부족해서 리눅스 커널이 오라클 DB를 죽인 걸로 나왔죠. 현장 담당자는 서버 스펙을 2배로 올리는 ‘스케일업’만 고집하고 있었고요.
하지만 스펙을 올린 지 불과 사흘 만에 똑같은 장애가 재발했습니다.
그때 제가 직접 커널 내부를 들여다보고 나서야 깨달았습니다. 단순한 리소스 부족이 아니라, 특정 커널 객체가 메모리를 야금야금 갉아먹고 있었던 ‘메모리 핫스팟’ 현상이었다는 걸요.
top 명령어 화면만 들여다봐서는 절대 원인을 찾을 수 없습니다. 오늘은 제가 지난 20년간 데이터센터와 엔터프라이즈 인프라 현장에서 산전수전 겪으며 정리한 RHEL CPU 및 메모리 핫스팟 정밀 추적 노하우를 숨김없이 공유해 보려고 합니다.
서버 부하(Load Average)가 껑충 뛰면 보통 다들 top부터 띄웁니다. 하지만 top은 전체적인 분위기만 보여줄 뿐, 어떤 코어가 고통받고 있는지 세세하게 알려주지 않습니다.
가장 먼저 해야 할 일은 CPU가 순수하게 연산(User/System)을 하느라 바쁜 건지, 아니면 디스크나 network I/O를 기다리느라 목이 빠지는 중(iowait)인지를 갈라내는 겁니다.
단순히 전체 CPU 평균만 보면 착시 현상에 속기 쉽습니다. 예를 들어 64코어 서버에서 특정 1개 코어만 100%를 치고 나머지가 놀고 있으면, 전체 평균 사용률은 2%도 안 되게 찍히거든요.
이럴 땐 sysstat 패키지에 포함된 mpstat 명령어로 모든 코어를 1초 간격으로 펼쳐봐야 합니다.
Bash
# 1초 간격으로 모든 CPU 코어(-P ALL)의 상태를 5회 출력
mpstat -P ALL 1 5
이 결과를 볼 때 눈여겨봐야 할 지표는 딱 두 가지입니다.
%iowait가 지속적으로 높을 때: CPU는 놀고 싶은데 하부 스토리지(SAN, NVMe)나 파일 시스템이 병목을 유발해 데이터를 못 건네주고 있는 상황입니다. 이 경우 CPU를 바꿀 게 아니라 스토리지 IOPS나 큐 디프스(Queue Depth)를 점검해야 합니다.%usr 또는 %sys가 특정 코어에만 100% 쏠릴 때: 이게 바로 전형적인 CPU 핫스팟입니다. 애플리케이션의 특정 스레드가 무한 루프에 빠졌거나 데드락(Deadlock)이 걸렸을 확률이 99%입니다.top을 통해 CPU를 끌어쓰는 프로세스 ID(PID)를 찾았다고 끝이 아닙니다. 요즘 자바(Java)나 멀티스레드 기반 DB 애플리케이션은 PID 하나 밑에 수십, 수 수백 개의 스레드가 돌고 있으니까요.
어떤 스레드가 미쳐서 돌고 있는지 알아내야 개발팀에 “여기 몇 번 스레드가 무한 루프 도니까 코드 확인하세요”라고 정확히 던져줄 수 있습니다.
Bash
# PID가 1234인 프로세스 내부의 스레드(-t)별 CPU 사용률 추적
pidstat -t -p 1234 1 5
실제로 예전에 Tomcat 서버가 멈췄을 때 이 명령어로 TID 5678이 CPU 100%를 혼자 먹고 있는 걸 찾았습니다. 이후 jstack으로 자바 스레드 덤프를 떠서 해당 TID를 16진수로 변환(0x162e)해 검색하니, 개발자가 실수로 만든 무한 while 문 위치가 3분 만에 드러나더군요.
메모리 문제는 CPU보다 훨씬 까다롭습니다. 보통 free -m을 쳐보고 used 메모리가 차 있으면 “메모리가 모자라네?” 하고 덜컥 증설부터 하곤 하죠.
하지만 리눅스 메모리 구조를 이해하면 이게 얼마나 위험한 착각인지 알게 됩니다.
한번은 이런 일이 있었습니다. free -m을 보면 메모리는 98% 쓰고 있다고 나오는데, 정작 top에서 프로세스들이 쓰는 메모리를 다 더해보면 고작 20%밖에 안 되는 이상한 현상이었습니다. 나머지 80%는 도대체 어디로 증발했을까요?
범인은 애플리케이션이 아니라 리눅스 커널 영역(Slab Allocation)이었습니다.
파일을 엄청나게 자주 생성하고 지우는 작업이 반복되면서 커널의 dentry(디렉토리 엔트리 캐시)와 buffer_head 객체가 메모리를 비대하게 잡아먹고 놓아주지 않았던 것이죠. 이럴 때는 slabtop 명령어로 커널 메모리 내부를 째려봐야 합니다.
Bash
# 커널 Slab 메모리 점유 상태를 사용량 순으로 실시간 확인
slabtop -o
만약 dentry나 inode_cache가 상위권을 싹 쓸어 담고 있다면, 커널이 파일 시스템 정보를 너무 많이 쥐고 있는 겁니다. 당장 서버를 리부팅할 수 없는 긴급 상황이라면 아래 명령어로 커널 캐시를 안전하게 비워내 한숨 돌릴 수 있습니다.
Bash
# 버퍼 및 페이지 캐시, dentry, inode 캐시 비우기 (운영 중 수행 가능)
sync; echo 3 > /proc/sys/vm/drop_caches
(주의: 생산 환경에서는 순간적인 I/O 튐 현상이 발생할 수 있으므로 sync를 먼저 수행한 뒤 가급적 트래픽이 적은 시점에 실행하는 것을 권장합니다.)
리눅스 커널은 메모리가 극도로 고갈되면 시스템 전체가 뻗는 걸 막기 위해 가장 메모리를 많이 먹는 프로세스를 강제로 죽입니다. 이걸 OOM(Out of Memory) Killer라고 부르죠.
문제는 이 녀석이 눈치가 없어서, 가장 귀하고 메모리를 많이 쓰는 Oracle, PostgreSQL, Redis 같은 핵심 DB 프로세스를 1순위로 타겟팅해 죽여버린다는 점입니다. 새벽에 DB가 죽어 서비스가 넘어가는 불상사의 원인이 바로 이겁니다.
절대 죽으면 안 되는 핵심 프로세스는 OOM 점수 가산점(/proc/PID/oom_score_adj)을 조작해서 OOM Killer의 사냥 목록에서 아예 빼버려야 합니다.
Bash
# DB 프로세스(PID: 5678)를 OOM Killer 대상에서 완전 제외 (-1000)
echo -1000 > /proc/5678/oom_score_adj
-1000을 입력하면 커널은 메모리가 아무리 마지노선까지 차올라도 해당 PID만큼은 절대 건드리지 않습니다. 대신 우선순위가 낮거나 메모리를 덜 먹는 보조 세션이나 잡다한 데몬들을 대신 정리하게 되죠.
운영 체제 단에서 할 수 있는 가장 확실한 ‘서비스 생존 안전장치’ 중 하나입니다.
시스템 트러블슈팅을 수없이 겪으며 얻은 결론은 하나입니다. “현상이 발생하고 터진 뒤에 원인을 찾으려 하면 이미 늦는다”는 것입니다.
단순히 “서버 멈추면 CPU 스펙 올리지 뭐”라는 식의 단순 접근(Scale-up)은 비용만 낭비하고 근본적인 결함은 그대로 방치하게 만듭니다.
오늘 다룬 mpstat, pidstat, slabtop, 그리고 oom_score_adj 설정은 RHEL 시스템을 운용할 때 선택이 아닌 필수 무기들입니다. 쉘 스크립트나 Prometheus, Datadog 같은 모니터링 도구에 이러한 커널 레벨 지표들을 주기적으로 수집하도록 등록해 두세요.
평소 시스템의 ‘건강한 상태(Baseline)’ 데이터를 모아두어야, 핫스팟의 징후가 피어오를 때 3초 만에 이상을 감지하고 선제 타격을 날릴 수 있습니다.
“지표는 정상인데 핑이 안 나가요” MS-SQL 서버 먹통, RTT 급증