힙은 남았는데 서비스가 멈췄다 · 스레드 덤프로 멈춘 이유를 찾는다 · 실습
스레드 덤프로 멈춘 이유를 찾는다
목표
"힙은 남았는데 서비스가 멈췄다" 를 직접 만들고 스레드 덤프로 원인을 짚는다 — 잠금을 쥔 스레드, 줄 선 스레드, 교착, 그리고 tryLock 타임아웃으로 하는 복구.
왜 중요한가
프로세스도 포트도 살아 있는데 응답이 없을 때, 힙 그래프는 아무것도 말해 주지 않는다. 스레드 덤프만이 "누가 무엇을 기다리는가" 를 보여 준다. 재시작하면 증거가 사라지므로, 멈춘 순간에 덤프를 찍는 손이 먼저다. 재료 Stuck.java 는 127.0.0.1:8085 에서 처리 스레드 4개(worker-N)로 동작하는 HTTP 서비스다 — /health 는 DB 를 건드리지 않고, / 는 DB 잠금을 잡고 5ms 일하며, /poison 은 잠금을 잡은 채 영원히 잠든다. -Ddb.lock.timeout.ms=<ms> 를 주면 synchronized 대신 ReentrantLock.tryLock 을 써서 그 시간 안에 못 잡으면 503 을 돌려준다.
단계
1. /root/jvm/threads 에 Stuck.java 를 컴파일하고 nohup java -cp /root/jvm/threads Stuck > /root/jvm/threads/server.log 2>&1 & 로 띄운 뒤 pid 를 /root/jvm/threads/server.pid 에 적는다. curl -s http://127.0.0.1:8085/health 가 ok 를 돌려줘야 한다.
2. jcmd -l > /root/jvm/threads/jcmd.txt 로 JVM 목록을 남긴다. Stuck 이 server.pid 의 pid 로 보여야 한다.
3. 정상 상태의 덤프를 jcmd <pid> Thread.print > /root/jvm/threads/dump-idle.txt 로 남긴다. Full thread dump 와 "worker-1" 이 있어야 한다.
4. 사건을 만든다: curl -s -m 20 http://127.0.0.1:8085/poison & 로 잠금을 영원히 쥐게 한 뒤 curl -s -m 20 http://127.0.0.1:8085/ & 를 세 번 더 보낸다(전부 백그라운드). 2초 뒤 jcmd <pid> Thread.print > /root/jvm/threads/dump-stuck.txt. 덤프에 waiting to lock 이 3줄 이상, BLOCKED 이 있어야 하고 /health 도 응답하지 않게 된다.
5. dump-stuck.txt 에서 - locked <주소> (a Stuck$Db) 를 쥔 스레드 이름과 같은 주소를 waiting to lock 하는 스레드 수를 읽어 /root/jvm/threads/diagnosis.txt 에 holder=<스레드이름> 과 blocked=<n> 두 줄로 적는다. 채점기는 살아 있는 프로세스의 덤프와 대조한다.
6. Deadlock.java 를 컴파일해 java -cp /root/jvm/threads Deadlock > /root/jvm/threads/deadlock.log 2>&1 & 로 띄우고(그대로 둔다) 1초 뒤 jcmd <pid> Thread.print > /root/jvm/threads/dump-deadlock.txt. Found 1 deadlock. 이 있어야 한다.
7. 복구: server.pid 의 프로세스를 kill 하고 -Ddb.lock.timeout.ms=500 을 붙여 다시 띄워 server.pid 를 갱신한다. 다시 /poison 을 백그라운드로 보낸 뒤 curl -s -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:8085/ 의 결과를 /root/jvm/threads/after.txt 에 저장한다 — 이제 503 이 5초 안에 돌아와야 한다.
8. 서비스가 떠 있는 채로 /root/jvm/threads/dumps/dump-1.txt, dump-2.txt, dump-3.txt 를 2초 간격으로 남긴다. 세 파일의 시각 줄(pid 줄 다음의 둘째 줄)이 서로 달라야 한다.
참고
- 잠금 주소 찾기:
grep -n 'locked <.*Stuck\$Db' dump-stuck.txt와grep -c 'waiting to lock <주소>' dump-stuck.txt. 스레드 이름은 그 줄보다 위에서 가장 가까운"..."로 시작하는 줄이다. - 같은 포트에 두 번 띄우면 두 번째는 바인딩 실패로 죽습니다. 7단계에서 먼저 kill 하세요.
- 흔한 실수: 덤프를 한 장만 찍는 것, 줄 선 스레드 수만 세고 잠금을 쥔 스레드를 안 찾는 것.
단계 8개
- 서비스를 띄우고 pid 를 남긴다
- JVM 목록
- 정상 상태의 덤프
- 사건을 만들고 덤프를 찍는다
- 잠금을 쥔 스레드와 줄 선 스레드
- 교착은 JVM 이 이름을 불러 준다
- 무한 대기를 503 으로 바꾼다
- 덤프는 세 장