Java 서비스가 진행을 멈춘 것처럼 보일 때는 진짜 thread deadlock 일 수도 있고, 비슷하게 보이는 heavy lock contention 이나, 소수의 blocked worker 뒤에 queue 가 막힌 상황일 수도 있습니다. 밖에서 보기에는 거의 비슷하지만, 실제 fix 방향은 완전히 다릅니다.
짧게 말하면 핵심은 이것입니다. 프로세스를 재시작하기 전에 true deadlock 인지 generic waiting 인지 먼저 구분해야 합니다. 진짜 deadlock 은 최소 두 개 이상의 실행 경로가 서로를 기다리는 cycle 이 있는 상태입니다. 반면 contention 이나 worker starvation 도 얼어붙은 것처럼 보일 수 있지만 thread state 는 다르게 나타납니다.
Java 전체 증상 분기부터 다시 보고 싶다면 Java Troubleshooting Guide 로 먼저 돌아가도 좋습니다.
thread state 와 lock ownership 부터 본다
deadlock 의 본질은 wait cycle 입니다.
그래서 request latency 만 보는 것보다 아래 자료가 훨씬 중요합니다.
- thread dump
- monitor ownership
- lock ordering
- 반복해서 나타나는 blocked/waiting 관계
재시작 전에 이 상태를 못 남기면, 실제 원인을 보여 주는 가장 중요한 증거를 잃는 경우가 많습니다.
deadlock 은 운영에서 어떻게 보이나
운영 환경에서는 보통 이렇게 보입니다.
- 요청이 무기한 멈춘다
- worker thread 가 같은 긴 시간 동안 계속 blocked 상태다
- 소수의 stuck thread 뒤에서 queue 가 커진다
- CPU 는 아주 높지 않은데 throughput 이 거의 없다
- 여러 번 뜬 thread dump 에서 같은 waiting 관계가 반복된다
같은 thread 가 같은 lock 을 두고 여러 dump 에서 계속 기다리고 있다면, true cycle 가능성이 훨씬 커집니다.
흔한 원인
1. lock ordering 이 일관되지 않다
가장 전형적인 원인입니다.
한 코드 경로는 lock 을 한 순서로 잡고, 다른 경로는 같은 lock 을 반대 순서로 잡는 경우입니다.
synchronized (a) {
synchronized (b) {
// work
}
}
다른 경로가 b 다음 a 를 잡으면, deadlock 은 특정 타이밍만 맞으면 바로 생길 수 있습니다.
2. 여러 lock 을 넓은 critical section 안에서 함께 잡는다
lock ordering 이 대부분 안전해 보여도, synchronized scope 가 넓으면 두 흐름이 나쁘게 겹칠 확률이 커집니다.
특히 아래일 때 더 위험합니다.
- 여러 lock 을 nested 하게 잡는다
- critical section 안에서 state mutation 외 일을 많이 한다
- 여러 request path 가 같은 shared object 를 만진다
3. synchronized 안에서 blocking work 를 한다
I/O 나 느린 작업이 lock 안에 있다고 해서 자동으로 true deadlock 이 생기는 것은 아니지만, contention 과 waiting cascade 를 훨씬 악화시킵니다.
또 incident 를 해석하기 어렵게 만듭니다. 많은 thread 가 실제 cycle 이 아니라 한 stalled path 뒤에 줄을 서기 때문입니다.
4. worker starvation 이 진짜 원인을 가린다
실제로는 소수 thread 만 막혔는데 나머지가 뒤에 기다리면서 queue 가 계속 커질 수 있습니다.
이 경우 운영자는 deadlock 으로 보지만, 실제로는:
- free worker 부족
- same-pool nested wait
- 하나의 stuck dependency path
같은 문제일 수 있습니다.
5. true deadlock 이 아니라 heavy contention 이다
아주 흔한 오진입니다.
strict 한 cyclic wait 는 없지만 contention 이 심해서 서비스가 멈춘 것처럼 보이는 경우입니다. 진행은 되지만 너무 느린 상태죠.
실전 점검 순서
1. incident 시점의 thread dump 를 잡는다
가능하면 한 번만이 아니라 여러 번 잡는 편이 좋습니다.
보고 싶은 것은 같은 thread 가 계속:
BLOCKEDWAITINGTIMED_WAITING
상태로 같은 lock 과 owner 주변에 머무는지입니다.
2. owner, waiting, blocked 관계를 찾는다
의심 lock 마다 아래를 찾으세요.
- 누가 lock 을 가지고 있는가
- 누가 그 lock 을 기다리는가
- owner 가 또 다른 lock 을 기다리고 있는가
이렇게 해야 cycle 이 눈에 보입니다.
3. 코드 경로별 lock order 를 비교한다
synchronized 또는 lock 획득 경로를 검색해서 acquisition 순서를 비교해 보세요.
같은 lock 쌍을 코드 경로마다 다른 순서로 잡고 있다면 root cause 가 상당히 선명해집니다.
4. blocked thread 와 queue growth, worker starvation 을 같이 본다
backlog 가 커지지만 진짜로 stuck 된 thread 가 소수라면, lock cycle 보다 downstream blocking 이나 executor starvation 일 가능성도 큽니다.
5. 충분한 증거를 남긴 뒤에만 restart 한다
restart 는 availability 는 회복할 수 있지만, 실제 원인을 고치는 데 필요한 상태도 같이 없애 버립니다.
정말 restart 해야 한다면, 그 전에 thread 와 lock 상태를 최대한 많이 남겨야 합니다.
예시: 반대 순서의 lock 획득
// path 1
synchronized (a) {
synchronized (b) {
update();
}
}
// path 2
synchronized (b) {
synchronized (a) {
update();
}
}
이 코드는 한동안 아무 문제 없이 돌 수도 있습니다. 그러다가 어느 날 부하 중에 timing 이 맞는 순간 deadlock 이 드러납니다.
그래서 “테스트에서는 괜찮았다” 가 lock-order 버그를 부정하지는 못합니다.
문제를 확인한 뒤 무엇을 바꾸면 좋나
lock order 를 전체 코드에서 하나로 강제한다
고전적인 deadlock 에는 가장 직접적인 fix 입니다.
nested locking 을 줄인다
한 번에 여러 lock 을 잡는 경로가 많다면 ownership 과 critical section 을 단순화해야 합니다.
synchronized 안의 느린 작업을 밖으로 뺀다
root deadlock 이 아니어도 이런 작업은 incident 를 훨씬 크게 만듭니다.
deadlock 과 starvation 패턴을 분리한다
실제 문제가 pool starvation 이나 queue buildup 이라면, monitor cycle 에만 집중하면 해결이 늦어집니다.
진단 장치를 더 둔다
thread dump, blocked thread metric, lock incident playbook 이 있으면 다음 장애 때 훨씬 빨라집니다.
장애 중에 던져볼 질문
이 질문이 꽤 유용합니다.
둘 이상의 thread 가 서로를 기다리는 stable cycle 인가, 아니면 하나의 느리거나 contended path 뒤에 많은 thread 가 쌓인 것뿐인가?
이 구분이 fix 방향을 완전히 바꿉니다.
FAQ
Q. blocked thread 면 다 deadlock 인가
아닙니다. 많은 incident 는 contention 이나 starvation 입니다.
Q. 가장 빠른 첫 단계는 무엇인가
thread dump 를 잡고 같은 lock 주변의 waiting cycle 이 반복되는지 보는 것입니다.
Q. 바로 restart 해야 하나
deadlock, contention, backlog 중 무엇인지 이해할 최소한의 상태를 남긴 뒤가 좋습니다.
Q. deadlock 조사 중에도 CPU 가 높을 수 있나
그렇습니다. 일부 thread 는 여전히 spin, retry, backlog 처리 중일 수 있고, 진짜 deadlocked thread 만 따로 멈춰 있을 수 있습니다.
Read Next
- lock ownership 보다 backlog 가 더 잘 보인다면 Java Thread Pool Queue Keeps Growing 를 이어서 보세요.
- blocked monitor 보다 spinning thread 와 runtime heat 가 더 두드러지면 Java JVM CPU High 와 비교해 보세요.
- true cycle 보다 hot locking 에 가깝다면 Java Thread Contention High 를 같이 보세요.
- 전체 Java 분기 지도를 보려면 Java Troubleshooting Guide 로 돌아가면 됩니다.
Related Posts
- Java JVM CPU High
- Java Thread Pool Queue Keeps Growing
- Java Thread Contention High
- Java Troubleshooting Guide
Sources:
- https://docs.oracle.com/en/java/javase/21/troubleshoot/
- https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis vs RabbitMQ vs Kafka 개발자를 위한 미들웨어 트러블슈팅 허브 글입니다. Redis, RabbitMQ, Kafka 중 어떤 증상부터 먼저 봐야 하는지와 어떤 문제 패턴이 각 시스템에 가까운지 정리합니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Kafka consumer lag가 계속 늘 때: 트러블슈팅 가이드 Kafka consumer lag가 계속 늘어날 때 무엇부터 봐야 하는지 정리합니다. poll 주기, 처리 속도, rebalance, consumer 설정까지 실전 기준으로 다룹니다.
- Kafka Rebalancing Too Often 가이드 Kafka consumer group에서 rebalance가 너무 자주 일어날 때 membership flapping, poll timing, protocol, assignment churn을 어떤 순서로 봐야 하는지 설명하는 실전 가이드입니다.
- Docker container가 계속 재시작될 때: 먼저 확인할 것들 exit code, command failure, environment mistake, health check 관점에서 Docker restart loop를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.