Java thread pool queue 가 계속 커진다는 것은 한 가지를 분명히 말해 줍니다. 들어오는 작업 속도가 끝나는 작업 속도보다 빠르다는 뜻입니다. 다만 더 어려운 건 왜 그런지를 밝히는 일입니다. pool 이 작아서일 수도 있고, worker 가 느린 dependency 를 기다려서일 수도 있고, queue 가 overload 를 너무 오래 숨기고 있어서일 수도 있습니다.
짧게 말하면 핵심은 이것입니다. queue depth, task duration, active worker 상태를 함께 봐야 합니다. queue 숫자 하나만으로는 실제 문제가 느린 작업인지, blocked work 인지, executor sizing 인지, backpressure 부재인지 구분되지 않습니다.
Java 전체 증상 분기부터 다시 보고 싶다면 Java Troubleshooting Guide 로 돌아가도 좋습니다.
pool size 하나만 보지 말고 throughput 부터 본다
queue 가 크다고 해서 자동으로 pool 이 너무 작은 것은 아닙니다.
queue 는 아래 이유로 커질 수 있습니다.
- task 가 예전보다 느려졌다
- task 가 downstream service 를 기다린다
- retry 가 추가 작업을 계속 밀어 넣는다
- queue 가 너무 커서 overload 를 오래 숨긴다
그래서 “thread 를 늘리자” 는 보통 가장 좋은 첫 반응이 아닙니다.
queue growth 는 운영에서 어떻게 보이나
보통 다음과 같이 같이 나타납니다.
- 시간이 갈수록 response time 이 나빠진다
- active thread 가 limit 근처에 붙어 있다
- traffic burst 때 backlog 가 더 빨리 커진다
- queued task 가 데이터를 잡고 있어 memory pressure 도 함께 올라간다
- 운영자는 pool size 를 의심하지만 실제 원인은 downstream latency 이다
queue 가 계속 커지는데 completion rate 가 회복되지 않으면, 시스템이 따라가지 못하는 이유를 먼저 설명해야 합니다.
흔한 원인
1. task duration 이 길어졌다
가장 직접적인 원인입니다.
아래 같은 이유로 task 하나하나가 오래 걸리면:
- 느린 database call
- remote service latency
- blocking I/O
- 더 커진 payload
- 비싼 business logic
executor 설정이 같아도 queue 는 커집니다.
2. pool sizing 이 workload 와 맞지 않는다
현재 workload 모양에 비해 worker capacity 가 정말 부족할 수도 있습니다.
하지만 그 판단은 task 가 아래 중 무엇인지 안 뒤에 해야 합니다.
- CPU-bound
- I/O-bound
- dependency wait 에 막힌 상태
같은 pool size 도 workload 성격에 따라 충분할 수도, 형편없을 수도 있습니다.
3. queue 가 overload 를 숨기고 있다
unbounded queue 나 너무 큰 queue 는 겉으로는 편해 보여도 장애를 더 길게 만들 때가 많습니다.
즉각적인 실패 대신 아래로 바꿔 버리기 때문입니다.
- 긴 latency
- 더 큰 backlog
- 더 큰 memory retention
- traffic 이 줄어도 늦은 recovery
4. downstream dependency 가 포화되었다
worker 는 존재하지만 대부분의 시간을 기다리며 쓸 수 없는 상태일 수 있습니다.
예를 들면:
- database connection wait
- HTTP client wait
- remote API wait
- lock wait
이런 경우 queue 가 커지는 이유는 executor 가 고장 나서가 아니라 completion 이 느려졌기 때문입니다.
5. backpressure 가 없거나 너무 약하다
caller 가 거의 제약 없이 계속 task 를 넣을 수 있다면, queue 는 overload 가 숨어드는 장소가 됩니다.
즉 queue 는 단순한 증상이 아니라 failure mode 의 일부입니다.
실전 점검 순서
1. queue depth 와 task duration 을 같이 본다
queue metric 하나만 봐서는 부족합니다.
알고 싶은 것은:
- queue 가 얼마나 빨리 커지는가
- task completion 에 얼마나 걸리는가
- 최근 task duration 이 바뀌었는가
배포 이후나 dependency slowdown 이후 task duration 이 두 배가 됐다면, queue 문제는 2차 증상일 가능성이 큽니다.
2. active worker count 와 saturation 을 확인한다
worker 가 실제로 바쁘게 일하고 있는지, limit 근처에 계속 붙어 있는지를 보세요.
thread 가 충분히 active 하지 않다면, 문제는 raw capacity 보다 blocking 이나 executor 동작 방식에 있을 수 있습니다.
3. task 내부의 blocking dependency 를 찾는다
아래를 우선 보세요.
- database wait
- remote service latency
- connection pool wait
- nested future 또는 executor wait
worker 대부분이 기다리기만 한다면, thread 를 늘리는 것은 blocked work 만 늘릴 가능성이 큽니다.
4. queue 가 의도적으로 bounded 인지 확인한다
질문은 이렇습니다.
- queue 가 사실상 무한정 늘어나는가
- 얼마나 커져야 caller 가 pain 을 느끼는가
- pool 이 뒤처질 때 시스템이 무엇을 하는가
절대 push back 하지 않는 queue 는 incident 를 길고 해석 어렵게 만드는 경향이 있습니다.
5. bottleneck 이 분명해진 뒤에만 executor 와 backpressure 를 조정한다
느린 작업인지, blocked work 인지, 진짜 capacity shortage 인지 이해한 뒤에야 executor tuning 이 의미가 있습니다.
예시: healthy pool, unhealthy task duration
ExecutorService pool = Executors.newFixedThreadPool(8);
for (Task t : tasks) {
pool.submit(() -> slowCall(t));
}
slowCall 이 remote dependency 악화로 더 오래 걸리기 시작하면, thread pool 자체는 멀쩡해 보여도 queue length 는 계속 커집니다.
그래서 thread pool 장애는 thread 수 계산보다 task 분석에서 시작하는 경우가 많습니다.
패턴을 찾은 뒤 무엇을 바꾸면 좋나
task 가 느려졌다면
먼저 downstream dependency 나 비싼 경로를 고쳐야 합니다.
queue 가 overload 를 숨기고 있다면
bounded queue 와 더 분명한 backpressure 가 필요합니다.
task 가 blocked 상태라면
workload 종류를 분리하거나 worker 경로의 blocking 을 줄여야 합니다.
pool 이 정말 undersized 라면
실제 workload 데이터에 기반해 worker count 를 조정해야 합니다.
backlog 가 memory pressure 도 만든다면
queue 를 단순 throughput metric 이 아니라 retention source 로도 봐야 합니다.
장애 중에 던져볼 질문
이 질문이 꽤 유용합니다.
queue 가 커지는 이유가 pool 이 작아서인가, 아니면 pool 안의 작업이 예상보다 느리거나 더 많이 막혀서인가?
이 질문이 가장 흔한 오진을 많이 막아 줍니다.
FAQ
Q. 먼저 thread count 를 늘려야 하나
task 가 CPU-bound 인지, I/O-bound 인지, 다른 곳에서 막혀 있는지 확인하기 전에는 보통 아닙니다.
Q. unbounded queue 는 안전한 기본값인가
대개는 아닙니다. overload 를 숨기면서 latency 와 memory 를 같이 키울 수 있습니다.
Q. 가장 먼저 무엇을 봐야 하나
queue depth, task duration, active worker utilization 입니다.
Q. queue 문제 가 memory 문제로도 번질 수 있나
그렇습니다. 큰 backlog 는 task object, payload, reference 를 오래 붙잡아 JVM memory pressure 를 키울 수 있습니다.
Read Next
- queue growth 가 memory pressure 처럼 보이기 시작하면 Java OutOfMemoryError 를 이어서 보세요.
- 같은 executor 가 blocked task 로도 포화된 것 같다면 Java ExecutorService Tasks Stuck 와 비교해 보세요.
- backlog 와 함께 CPU 도 오른다면 Java JVM CPU High 도 같이 보세요.
- 전체 Java 분기 지도를 다시 보려면 Java Troubleshooting Guide 로 돌아가면 됩니다.
Related Posts
Sources:
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: 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를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.