- 출처 / 참고: Oracle JVM Diagnostics Guide (Troubleshooting System Hangs and Deadlocks), Apache Tomcat Documentation (Monitoring and Managing Tomcat)
명령어:
top -H,jstack,jcmd,printf,ps,awk
키워드: Thread Dump, CPU 100%, Tomcat Hang, nid, top -H, Busy Thread, Deadlock, RUNNABLE, Infinite Loop
사용처: 톰캣 WAS의 CPU 점유율이 100%에 도달하며 웹 요청 처리가 멈추는(Hang) 장애 발생 시 원인 스레드 및 코드 라인 추적
실행예제
톰캣 프로세스가 특정 코어 또는 시스템 전체 CPU를 100% 점유하면서 신규 요청을 처리하지 못할 때, CPU를 가장 많이 소비하고 있는 특정 LWP(Lightweight Process / OS 스레드 ID)를 찾아 16진수(Hex) nid로 변환한 후 스레드 덤프의 실행 스택을 정확히 매핑하는 과정입니다.
# 1. 시스템 전체 부하 및 톰캣 메인 PID 확인
$ top -c
# PID: 28450 | USER: tomcat | %CPU: 398.5 | COMMAND: /usr/bin/java -Djava.util.logging.config.file=...
# 2. 톰캣 프로세스 내부에서 CPU를 과점하고 있는 개별 스레드(TID/LWP) 실시간 추적 (-H 옵션)
$ top -H -p 28450
# PID(스레드ID) USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 28512 tomcat 20 0 8520448 4.215g 18420 R 99.1 26.3 5:12.34 java
# 28513 tomcat 20 0 8520448 4.215g 18420 R 98.7 26.3 5:10.12 java
# -> 28512, 28513 번 OS 스레드가 지속적으로 CPU 99%를 소모 중임을 확인
# 3. CPU 과점 스레드 ID(28512)를 16진수(Hexadecimal) nid 형태로 변환
$ printf "0x%x\n" 28512
0x6f60
# 4. jstack 또는 jcmd로 스레드 덤프 추출
$ sudo -u tomcat jstack -l 28450 > /tmp/tomcat_threads.tdump
# 또는 최신 JDK jcmd 사용 시:
$ sudo -u tomcat jcmd 28450 Thread.print -l > /tmp/tomcat_threads.tdump
# 5. 16진수 nid(0x6f60)로 스레드 덤프 내 실행 중인 정확한 클래스 및 메서드 라인 검색
$ grep -A 30 "nid=0x6f60" /tmp/tomcat_threads.tdump
# [출력 예시]
# "http-nio-8080-exec-12" #45 daemon prio=5 os_prio=0 cpu=312450.12ms elapsed=420.15s tid=0x00007f9c8402a000 nid=0x6f60 runnable [0x00007f9c564e9000]
# java.lang.Thread.State: RUNNABLE
# at com.example.service.OrderService.calculateDiscount(OrderService.java:142)
# at com.example.service.OrderService.processOrder(OrderService.java:58)
# at com.example.controller.OrderController.checkout(OrderController.java:34)
# -> OrderService.java 142번 라인(무한 루프 또는 정규표현식 재귀 연산)에서 멈추어 있음을 즉시 규명
스크립트
구동 중인 톰캣 프로세스를 찾아 CPU를 가장 많이 소비하고 있는 상위 3개 스레드를 탐색하고, 16진수 변환 및 스레드 덤프 캡처를 한 번에 수행하여 문제 코드 스택을 추출해 주는 자동 분석 쉘 스크립트입니다.
#!/bin/bash
# trace_tomcat_cpu_threads.sh - Auto-trace high CPU threads and map with Java Thread Dump
TOMCAT_PID=${1:-""}
echo "=========================================="
echo " [톰캣 CPU 100% 과점 스레드 자동 추적기]"
echo "=========================================="
# 1. 톰캣 PID 자동 감지
if [ -z "$TOMCAT_PID" ]; then
TOMCAT_PID=$(pgrep -f "org.apache.catalina.startup.Bootstrap" | head -n 1)
fi
if [ -z "$TOMCAT_PID" ]; then
echo " [오류] 톰캣 프로세스를 찾을 수 없습니다. PID를 인자로 전달하세요."
echo " 사용법: sudo $0 [Tomcat_PID]"
exit 1
fi
TOMCAT_USER=$(ps -o user= -p "$TOMCAT_PID")
echo " - 대상 톰캣 PID: ${TOMCAT_PID}"
echo " - 톰캣 실행 계정: ${TOMCAT_USER}"
# 2. CPU를 가장 많이 쓰는 상위 3개 스레드(LWP) 추출 (ps -eLo 활용)
echo -e "\n[1] CPU 과점 상위 스레드 식별 중..."
TOP_THREADS=$(ps -eLo pid,lwp,pcpu,comm --sort=-pcpu | awk -v pid="$TOMCAT_PID" '$1==pid && $4 ~ /java/ {print $2, $3}' | head -n 3)
if [ -z "$TOP_THREADS" ]; then
echo " [알림] 톰캣 스레드 정보를 읽어올 수 없습니다."
exit 1
fi
echo " LWP(TID) | %CPU"
echo " ------------------"
echo "$TOP_THREADS" | awk '{printf " %-8s | %s%%\n", $1, $2}'
# 3. 스레드 덤프 생성
DUMP_FILE="/tmp/tomcat_dump_${TOMCAT_PID}_$(date +%Y%m%d_%H%M%S).tdump"
echo -e "\n[2] 스레드 덤프 생성 중 -> ${DUMP_FILE}"
sudo -u "$TOMCAT_USER" jstack -l "$TOMCAT_PID" > "$DUMP_FILE" 2>/dev/null
if [ ! -s "$DUMP_FILE" ]; then
echo " [오류] jstack 스레드 덤프 생성에 실패했습니다. (JDK 유틸리티 설치 여부 확인 필요)"
exit 1
fi
# 4. 교차 분석 및 문제 코드 라인 출력
echo -e "\n[3] 과점 스레드별 호출 스택(Call Stack) 역추적:"
while read -r TID CPU_USAGE; do
HEX_NID=$(printf "0x%x" "$TID")
echo "================================================================="
echo " 스레드 ID: ${TID} (16진수 nid: ${HEX_NID}) | CPU 점유율: ${CPU_USAGE}%"
echo "================================================================="
# 덤프에서 해당 nid 검색 및 상위 25라인 스택 추출
grep -A 25 -i "nid=${HEX_NID}\b" "$DUMP_FILE" | sed 's/^/ /'
echo ""
done <<< "$TOP_THREADS"
# 5. 데드락(Deadlock) 탐지 여부 검사
echo "[4] 데드락(Deadlock) 여부 검증:"
DEADLOCK_INFO=$(grep -i "Found one Java-level deadlock:" -A 10 "$DUMP_FILE")
if [ -n "$DEADLOCK_INFO" ]; then
echo " [치명적 위험] JVM 수준의 데드락이 감지되었습니다!"
echo "$DEADLOCK_INFO" | sed 's/^/ /'
else
echo " [정상] 덤프에서 감지된 Java 데드락은 없습니다."
fi
echo -e "\n=========================================="
echo " [진단 완료] 전체 덤프 파일: ${DUMP_FILE}"
echo "=========================================="
해설
톰캣 서버가 응답을 멈추고 CPU 사용량이 100%를 지속하는 행(Hang) 상태는 크게 1) 잘못된 애플리케이션 코드(무한 루프/정규식 백트래킹)와 2) 힙 메모리 고갈로 인한 JVM의 연속 Full GC(Garbage Collection 폭주)의 두 가지 원인으로 압축됩니다.
1. 주요 발생 원인
- 애플리케이션 무한 루프 (Infinite Loop):
- 컬렉션 순회 중 종료 조건 미충족,
while루프 탈출 실패, 잘못 작성된 정규표현식의 재귀 백트래킹(Catastrophic Backtracking)으로 인해 단일 워커 스레드가 100%의 CPU 코어를 풀로 사용하는 경우.
- 컬렉션 순회 중 종료 조건 미충족,
- JVM GC 스레드의 CPU 독점 (GC Thrashing):
- 힙 공간(
Java heap space)이 한계에 도달하여 가비지 컬렉터가 메모리를 확보하기 위해 모든 CPU 코어를 동원하여 Full GC를 쉴 새 없이 돌리는 상태입니다. 이때top -H를 보면VM Thread또는GC Thread#...이름의 스레드들이 CPU 100%를 차지하고 있습니다.
- 힙 공간(
- 데드락(Deadlock) 및 스레드 풀 고갈에 따른 연쇄 정지:
- CPU는 높지 않지만 요청이 전혀 처리되지 않는 행(Hang) 상태라면 복수의 스레드가 상호 락(Lock)을 쥐고 대기하는 데드락이거나, DB 커넥션 풀을 반환받지 못해 모든
http-nio-exec스레드가WAITING상태에 빠진 경우입니다.
- CPU는 높지 않지만 요청이 전혀 처리되지 않는 행(Hang) 상태라면 복수의 스레드가 상호 락(Lock)을 쥐고 대기하는 데드락이거나, DB 커넥션 풀을 반환받지 못해 모든
2. 스레드 덤프 상태값(State) 핵심 분석법
| 스레드 상태 | 의미 | 분석 시 체크 포인트 |
|---|---|---|
RUNNABLE |
현재 CPU를 할당받아 연산 중인 상태 | CPU 100%의 주범. 5~10초 간격으로 덤프를 3회 떴을 때도 동일한 코드 라인에 머물러 있다면 무한 루프 확정. |
WAITING / TIMED_WAITING |
특정 조건(알림, 타이머, I/O)을 대기 중인 상태 | 외부 API 응답 지연, DB 쿼리 행(Lock Wait), Thread.sleep 여부 확인. |
BLOCKED |
다른 스레드가 쥐고 있는 synchronized 락 획득 대기 |
다수의 스레드가 동일한 객체 모니터 락(waiting to lock <0x000...> )에 묶여 있다면 병목 지점임. |
3. 단계별 디버깅 워크플로우
- 다중 스레드 덤프 확보:
- 스레드가 일시적으로 무거운 연산을 하는 것인지 영구 무한 루프인지 확인하기 위해 반드시 5초 간격으로 최소 3회 연속 스레드 덤프를 수집합니다.
- LWP와
nid매핑:- OS 레벨의 스레드 번호(TID)를
printf "0x%x"를 통해 16진수로 변환하고 덤프의nid값과 1:1 대조합니다.
- OS 레벨의 스레드 번호(TID)를
- VM 스레드 여부 분기:
- 만약 CPU를 점유한 스레드가
VM Thread라면 애플리케이션 코드가 아니라 GC 병목이므로, 힙 메모리 크기(-Xmx)를 증설하고 힙 덤프를 떠서 메모리 릭을 해결해야 합니다. http-nio-8080-exec-*스레드라면 표시된 소스 코드 라인을 즉시 패치해야 합니다.
- 만약 CPU를 점유한 스레드가
주의사항
- 스레드 덤프는 반드시 여러 번(3회 이상) 수집할 것:
- 단 1회의 스레드 덤프는 특정 순간의 스냅샷일 뿐입니다. CPU를 많이 쓰는 정상적인 암호화/압축 연산 중이었는지, 탈출할 수 없는 무한 루프인지는 여러 덤프를 대조하여 스택 트레이스의 코드 라인이 변경되지 않고 완전히 정체되어 있는지를 확인해야 증명됩니다.
- JRE 환경에서의 진단 유틸리티 누락:
- 상용 서버에 JDK가 아닌 최소 JRE만 설치되어 있으면
jstack,jstat,jcmd명령어가 없습니다. 긴급 상황 시kill -3 <Tomcat_PID>를 실행하면catalina.out콘솔 출력 로그에 스레드 덤프가 강제로 덤프되므로 이를 활용할 수 있습니다.
- 상용 서버에 JDK가 아닌 최소 JRE만 설치되어 있으면
top실행 시-H옵션 필수:- 일반
top명령어는 프로세스 단위의 부하만 보여주므로 수십 개의 스레드 중 어떤 스레드가 범인인지 분리할 수 없습니다. 반드시 스레드 단위 뷰를 제공하는top -H -p <PID>를 사용해야 개별 OS 스레드 ID(LWP)를 얻을 수 있습니다.
- 일반