asyncio task 가 끝나지 않을 때 원인은 보통 “asyncio 가 고장났다” 가 아닙니다. 더 흔한 경우는 특정 await 가 끝나지 않거나, queue 균형이 깨졌거나, clean 한 cancellation path 가 없거나, event loop 진행이 와야 하는데 그 조건이 오지 않는 경우입니다.
짧게 말하면 핵심은 이것입니다. 끝나지 않는 task 가 정확히 어디서 기다리고 있는지 먼저 찾아야 합니다. 많은 task 가 같은 queue, lock, future, await path 에 몰려 있다면, 이건 성능 문제보다 coordination failure 인 경우가 많습니다.
기다리는 지점부터 본다
가장 빠른 단서는 task 이름보다, unfinished task 가 실제로 시간을 보내는 위치입니다.
예를 들면:
- 같은 await path 가 반복되는가
- task 가 하나의 queue 또는 semaphore 에 몰려 있는가
- worker 가 이미 멈춘 producer 를 기다리는가
- 취소됐어야 할 task 가 아직 살아 있는가
이 구분이 단순히 “event loop 가 건강한가” 를 묻는 것보다 훨씬 도움이 됩니다.
”끝나지 않음” 은 운영에서 어떻게 보이나
보통 이런 모습으로 나타납니다.
- 요청이 끝나지 않는다
- shutdown 중에 background task 가 남아 프로세스가 멈춘다
- queue consumer 가 영원히 다음 작업을 기다린다
- 모니터링에는 pending task 가 많은데 실제 처리량은 낮다
- 운영자는 loop starvation 을 의심하지만 실제 문제는 task coordination 이다
같아 보여도 failure shape 가 조금씩 다르고, 어떤 waiting path 가 지배적인지에 따라 fix 도 달라집니다.
흔한 원인
1. awaited work 가 끝나지 않는다
하나의 awaited call 이 나머지 task tree 를 모두 묶을 수 있습니다.
보통 이런 경우입니다.
- timeout 없는 remote I/O 대기
- 끝나지 않는 다른 task 대기
- fulfill 되지 않는 future 대기
- 이미 멈춘 component 에 의존하는 cleanup 대기
task = asyncio.create_task(worker())
await task
worker() 가 끝나지 않는 I/O 를 기다리거나 shutdown path 를 놓치면 await task 는 돌아오지 않습니다.
2. cancellation 이 없거나 불완전하다
timeout 과 shutdown 경로가 task 를 clean 하게 취소하지 못해서, task 가 의도보다 오래 살아남습니다.
특히 이런 패턴이 흔합니다.
- create_task 했지만 추적하지 않음
- cancel 은 했지만 await 하지 않음
- background worker 가
CancelledError를 잘못 삼킴 - shutdown 이 task ownership 을 올바르게 cascade 하지 않음
3. queue 또는 producer-consumer 균형이 깨진다
파이프라인의 반대편이 느리거나 사라졌거나 이미 멈췄기 때문에 task 가 영원히 기다릴 수 있습니다.
예를 들면:
- producer 는 멈췄는데 consumer 는 계속 기다림
- consumer 가 너무 느려 completion 조건이 오지 않음
- sentinel 이나 shutdown signal 이 전달되지 않음
4. 하나의 coordination primitive 가 병목이 된다
lock, semaphore, queue 중 하나가 멈추면, 여러 task 가 동시에 멈춘 것처럼 보일 수 있습니다.
이 경우 broad event-loop issue 처럼 보여도 실제로는 하나의 synchronization point 문제일 수 있습니다.
5. loop 는 건강한데 task ownership 이 잘못되었다
runtime 은 멀쩡하고 task 설계가 문제인 경우도 많습니다.
task 에 명확한 owner 가 없고, 종료 조건이 없고, shutdown path 가 없다면, 사실 hang 라기보다 completion 이 보장되지 않는 lifecycle 인 셈입니다.
실전 점검 순서
1. unfinished task 가 어디서 기다리는지 찾는다
가장 가치가 큰 첫 단계입니다.
질문은 이렇습니다.
- 어떤 await 인가
- 어떤 queue 또는 lock 인가
- 많은 task 가 같은 곳에서 멈추는가
2. timeout 과 cancellation 흐름을 본다
원래 이 task 가 이미 멈췄어야 하는지 확인하세요.
멈췄어야 했다면:
- 누가 lifecycle owner 인가
- 누가 cancel 해야 하는가
- 누가 그 cancellation completion 을 기다리는가
3. producer 와 consumer 속도를 비교한다
queue 를 쓴다면 한쪽이 사라졌거나 심하게 뒤처졌을 수 있습니다.
이게 obvious error 없이 task 가 pending 상태로 남는 이유가 되곤 합니다.
4. queue, semaphore, lock 사용을 점검한다
공유 coordination primitive 하나가 막히면, 여러 task 가 한꺼번에 hang 된 것처럼 보입니다.
5. 최근 코드나 traffic 변화를 같이 본다
새 background task, 바뀐 queue 동작, 달라진 shutdown flow 가 task completion 문제를 갑자기 만들었을 수 있습니다.
예시: shutdown path 에서 task ownership 누락
async def main():
asyncio.create_task(worker())
await serve_requests()
겉으로는 평범해 보여도, worker() 를 추적하지 않고 shutdown 때 cancel 하지 않으면 background task 하나 때문에 프로세스가 hang 된 것처럼 보일 수 있습니다.
더 안전한 패턴은 보통 이렇습니다.
- 생성한 task 추적하기
- shutdown 중 명시적으로 cancel 하기
- cleanup 이 실제 끝날 때까지 await 하기
waiting pattern 을 찾은 뒤 무엇을 바꾸면 좋나
awaited work 가 끝나지 않는다면
timeout, ownership, failure path 를 더 분명하게 만들어서 무기한 대기가 생기지 않게 해야 합니다.
cancellation 이 빠졌다면
shutdown 과 timeout flow 를 명시적으로 만들고 created task 를 추적해야 합니다.
queue 균형이 깨졌다면
producer-consumer coordination, sentinel 전달, backpressure 가정을 다시 봐야 합니다.
하나의 primitive 가 병목이라면
shared coordination pressure 를 줄이거나 task flow 를 다시 설계해야 합니다.
task 에 lifecycle 이 없다면
각 background task 에 owner, stop condition, cleanup path 를 부여해야 합니다.
장애 중에 던져볼 질문
이 질문이 꽤 유용합니다.
이 task 가 끝나려면 정확히 어떤 조건이 만족돼야 하고, 그 조건을 만들어 줄 주체가 지금도 살아 있는가?
이 질문이 missing ownership 이나 missing completion signal 을 빨리 드러내 줍니다.
FAQ
Q. 이게 항상 event-loop 문제인가
아닙니다. 많은 incident 는 loop 자체보다 coordination 이나 cancellation 문제입니다.
Q. 먼저 무엇을 봐야 하나
await 지점, cancellation 경로, producer-consumer balance 입니다.
Q. pending task 가 많으면 무조건 overload 인가
그렇지 않습니다. 하나의 dependency 나 shutdown signal 이 끝나지 않아서 그럴 수도 있습니다.
Q. 로그만으로 충분한가
도움은 되지만, task dump 와 explicit lifecycle tracking 이 있어야 실제 waiting path 가 보이는 경우가 많습니다.
Read Next
- 문제의 핵심이 task completion 보다 loop starvation 에 가깝다면 Python asyncio Event Loop Blocked 와 같이 보세요.
- cancellation 이 너무 약한 게 아니라 너무 공격적인 경우라면 Python asyncio Task Cancelled 를 이어서 보세요.
- 전체 Python 분기 지도를 보려면 Python Troubleshooting Guide 로 가면 됩니다.
Related Posts
- Python asyncio Event Loop Blocked
- Python asyncio Task Cancelled
- Python Troubleshooting Guide
- Python Memory Usage High
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를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.