JDK에 포함된 표준 도구입니다. 스레드 외에 힙 정보나 시스템 속성도 같은 명령으로 볼 수 있습니다.
이 파트에서 다루는 내용
스레드 덤프는 그 순간의 사진입니다
애플리케이션이 응답하지 않을 때, 스레드 덤프는 모든 스레드가 지금 어느 코드 줄에 있고 무엇을 기다리는지를 한 장에 보여 줍니다.
멈춤 증상에서는 이것보다 확실한 증거가 없습니다. 로그는 무언가 일어났을 때만 남지만, 스레드 덤프는 아무 일도 안 일어나고 있는 상태 자체를 보여 주기 때문입니다.
부하도 거의 없습니다. 운영 중에 떠도 되고, 그래서 판단이 서지 않을 때 일단 떠 두는 것이 맞습니다.
뜨는 방법은 세 가지입니다
jcmd가 없거나 안 먹을 때 씁니다. 출력이 애플리케이션 로그에 섞이므로 위치를 알아 둬야 합니다.
컨테이너 안에서 실행해야 하고 JDK 도구가 이미지에 없을 수 있습니다. 운영 이미지에 진단 도구를 포함할지 미리 정해 둡니다.
직접 못 붙으면 Part 1의 요청 목록으로 전달합니다. 명령을 그대로 복사할 수 있게 적어 주는 것이 중요합니다.
# 대상 프로세스 확인
jcmd -l
# 또는
ps -ef | grep java
# 1) jcmd (권장)
jcmd <PID> Thread.print > thread_1.txt
# 2) jstack
jstack <PID> > thread_1.txt
# 응답이 없어 위 명령이 안 먹을 때
jstack -F <PID> > thread_1.txt
# 3) kill -3 (JVM 표준 출력으로 나감)
kill -3 <PID>
# 결과는 catalina.out 이나 nohup.out 등 표준 출력 파일에 붙습니다실행 계정이 다르면 권한 오류가 납니다. 애플리케이션을 띄운 계정으로 실행해야 합니다. kill -3 은 프로세스를 죽이지 않습니다. 이름 때문에 오해하기 쉬운데 신호를 보내 덤프를 출력하게 하는 것뿐입니다.
읽는 순서가 있습니다
덤프 파일을 열면 수백 개의 스레드가 나옵니다. 처음부터 끝까지 읽으면 아무것도 못 찾습니다. 순서를 정해 좁혀 갑니다.
- **1단계.** 파일 맨 아래에서 `Found one Java-level deadlock` 문구를 먼저 찾습니다. 있으면 데드락이 확정이고 여기서 끝납니다.
- **2단계.** 상태별 개수를 셉니다. BLOCKED나 WAITING이 비정상적으로 많으면 그쪽입니다.
- **3단계.** 요청 처리 스레드만 봅니다. 톰캣이면 `http-nio-8080-exec-` 로 시작하는 것들입니다.
- **4단계.** 같은 스택 지점에 몰려 있는 스레드를 찾습니다. 100개 스레드가 같은 줄에 있으면 그게 병목입니다.
- **5단계.** 그 지점이 무엇을 기다리는지 봅니다. 다음 섹션의 패턴과 대조합니다.
CPU를 쓰고 있거나 네트워크 I/O 대기 중입니다. socketRead가 보이면 실제로는 외부 응답을 기다리는 중입니다.
다른 스레드가 잡은 모니터 락을 기다립니다. 누가 잡고 있는지 덤프에 함께 나옵니다.
조건이 충족될 때까지 기다립니다. 커넥션 풀 대기가 여기 잡히는 경우가 많습니다.
정상적인 대기인 경우가 많습니다. 스레드 풀의 유휴 스레드가 여기 있습니다.
# 스레드 상태 분포
grep "java.lang.Thread.State" thread_1.txt | sort | uniq -c | sort -rn
# 요청 처리 스레드 개수
grep -c "http-nio" thread_1.txt
# 스택 최상단(지금 실행 중인 지점)이 같은 스레드 세기
grep -A 1 "java.lang.Thread.State" thread_1.txt \
| grep "^\s*at " | sort | uniq -c | sort -rn | head -20마지막 명령이 핵심입니다. 가장 많은 스레드가 걸려 있는 코드 지점이 맨 위에 나옵니다. 대개 여기서 원인이 보입니다.
3장을 비교해야 판단이 됩니다
Part 2에서 스레드 덤프를 5초 간격으로 3회 뜨라고 한 이유가 여기 있습니다.
한 장만 보면 그 스레드가 멈춰 있는 것인지 그냥 지나가는 중인지 구분할 수 없습니다. 어떤 스레드가 특정 줄에 있는 사진 한 장은, 그 줄에 1밀리초 있었는지 10분째 있는지 말해 주지 않습니다.
3장에서 같은 스레드가 같은 스택에 계속 있으면 멈춘 것입니다. 스택이 계속 바뀌면 정상적으로 일하고 있는 것입니다.
# 특정 스레드의 스택을 3장에서 나란히 보기
for f in thread_1.txt thread_2.txt thread_3.txt; do
echo "=== $f ==="
grep -A 12 '"http-nio-8080-exec-42"' $f
done스택이 세 번 모두 같으면 그 지점에서 최소 10초 이상 멈춰 있는 것입니다. 그 줄이 무엇을 하는 코드인지 확인하면 원인에 도달합니다.
5초 간격 3회가 일반적인 출발점입니다. 증상이 아주 짧게 나타났다 사라지면 1~2초로 줄이고, 완전히 멈춘 상태라면 간격은 중요하지 않습니다. 중요한 것은 최소 2장 이상 확보하는 것입니다.
전형적인 네 가지 패턴
다수 스레드가 커넥션을 얻으려 대기합니다. 스택에 풀에서 커넥션을 가져오는 메서드가 보입니다. DB 코스 Part 16과 이어지는 증상입니다.
스택에 소켓 읽기가 보이고 상태는 RUNNABLE입니다. 실행 중처럼 보이지만 실제로는 상대 응답을 기다리는 중입니다.
BLOCKED 스레드가 다수이고 모두 같은 객체를 기다립니다. 그 락을 잡고 있는 스레드가 무엇을 하는지 봅니다.
JVM이 감지해 덤프 하단에 명시합니다. 두 스레드가 서로의 락을 기다립니다. 발견되면 원인은 확정입니다.
[1] 커넥션 풀 고갈 - 같은 스택에 수십~수백 개
"http-nio-8080-exec-42" #142 waiting on condition
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(Native Method)
at com.zaxxer.hikari.pool.HikariPool.getConnection(...)
at com.example.OrderRepository.findAll(OrderRepository.java:58)
[2] 외부 호출 대기 - 상태는 RUNNABLE 이지만 실제로는 대기
"http-nio-8080-exec-17" #117 runnable
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at com.example.PaymentClient.request(PaymentClient.java:73)
[3] 락 경합 - BLOCKED 와 누가 잡고 있는지
"http-nio-8080-exec-31" #131 waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.CacheManager.refresh(CacheManager.java:44)
- waiting to lock <0x000000071ab2c3d8> (a java.lang.Object)
- locked by "scheduler-1"
[4] 데드락 - 덤프 하단에 JVM 이 명시
Found one Java-level deadlock:
=============================
"worker-3":
waiting to lock monitor 0x00007f8a1c0060a8,
which is held by "worker-7"
"worker-7":
waiting to lock monitor 0x00007f8a1c003fd8,
which is held by "worker-3"2번이 특히 헷갈립니다. 상태가 RUNNABLE이라 정상 실행처럼 보이지만 소켓 읽기는 응답을 기다리는 것입니다. 외부 연동에 타임아웃이 설정돼 있는지 확인하는 계기가 됩니다.
스레드가 부족한 것이 아니라 스레드가 돌아오지 않는 것입니다. 그래서 스레드 풀 크기를 늘리는 것은 대개 해법이 아닙니다. 무엇을 기다리다 안 돌아오는지 찾아 그것을 고쳐야 합니다. DB 코스 Part 16에서 커넥션 풀을 키우기 전에 확인할 순서를 다룬 것과 같은 논리입니다.