Python asyncio task 가 계속 cancelled 로 끝날 때 문제는 cancellation 자체보다, timeout scope, parent-task ownership, shutdown flow, 혹은 더 오래 살아 있어야 할 작업을 어느 한 계층이 너무 빨리 cancel 하고 있는 경우가 많습니다.
짧게 말하면 핵심은 이것입니다. 누가 cancellation 을 시작하는지, 그리고 그 task 가 정말 그 cancelled work 의 lifecycle owner 인지 먼저 봐야 합니다. asyncio 에서 cancellation 은 자주 정상 동작입니다. 진짜 질문은 올바른 task 가 올바른 lifetime 을 소유하고 있는가입니다.
누가 누구를 cancel 하는지부터 본다
cancellation 은 자동으로 에러가 아닙니다.
runtime 관점에서는 cancelled task 가 정확히 지시받은 대로 움직인 것일 수 있습니다. incident 는 보통 아래 때 시작됩니다.
- timeout 이 너무 공격적이다
- parent scope 가 너무 넓다
- cleanup 이 너무 빨리 끊긴다
- 잘못된 task 가 잘못된 lifecycle 을 상속받는다
그래서 CancelledError 가 보였다는 사실보다 ownership 이 더 중요합니다.
cancellation 문제는 운영에서 어떻게 보이나
보통 이런 모습으로 나타납니다.
- 정상 부하인데도 task 가 너무 자주 cancelled 된다
- request timeout 때문에 background job 까지 사라진다
- shutdown 이 cleanup 이나 result handling 이 끝나기 전에 작업을 멈춘다
- 어떤 cancellation 은 예상된 것인데 운영자는 모두 실패로 본다
핵심은 intended cancellation 과 mis-scoped cancellation 을 구분하는 것입니다.
흔한 원인
1. timeout 설정이 너무 공격적이다
실제 작업 시간보다 deadline 이 짧아서 task 가 cancelled 될 수 있습니다.
task = asyncio.create_task(do_work())
await asyncio.wait_for(task, timeout=1)
do_work() 가 보통 1초보다 오래 걸린다면 cancellation 은 랜덤한 실패가 아니라 설정된 결과입니다.
2. parent scope 가 너무 넓다
parent task 하나를 cancel 할 때 child task 가 너무 많이 같이 취소될 수 있습니다.
특히 이런 상황이 위험합니다.
- request-scoped task 가 background work 를 띄움
- helper task 가 너무 짧은 handler lifecycle 을 상속받음
- structured cancellation boundary 가 분명하지 않음
3. shutdown flow 가 거칠다
애플리케이션 shutdown 이 아래 작업이 끝나기 전에 task 를 끊을 수 있습니다.
- cleanup
- checkpointing
- queue draining
- result delivery
이 경우 cancellation 은 기술적으로는 예상된 것이어도 운영적으로는 충분히 해로울 수 있습니다.
4. queue 와 consumer lifetime 이 맞지 않는다
producer, worker, cleanup path 가 언제 작업이 끝나야 하는지 서로 다르게 이해할 수 있습니다.
한 계층은 pipeline 이 끝났다고 생각하는데 다른 계층은 아직 completion 을 기대하면, cancellation 이 random 하게 느껴질 수 있습니다.
5. cancellation 을 잘못 처리한다
문제는 task 가 cancelled 되는 것보다, 그 cancellation 을 코드가 잘못 다루는 경우도 많습니다.
예를 들면:
- cancellation 을 잡아서 무시함
- cleanup loop 가 끝나지 않음
- cancellation 뒤 task state 를 잃음
이러면 정상 signal 이 지저분한 failure mode 로 바뀝니다.
실전 점검 순서
1. cancellation 이 어디서 시작되는지 찾는다
그걸 트리거하는 caller 또는 scope 를 찾으세요.
질문은 이렇습니다.
- timeout 인가
- parent task 인가
- shutdown logic 인가
- explicit manual cancel 인가
2. timeout 설정과 실제 task duration 을 비교한다
timeout 이 실제 작업 시간보다 짧다면, cancellation 은 미스터리가 아닙니다.
그냥 설정 mismatch 입니다.
3. parent-child task ownership 을 점검한다
cancelled task 가 정말 그 parent 의 lifecycle 을 상속받아야 하는지 확인하세요.
request-scoped background bug 가 많이 숨어 있는 부분입니다.
4. shutdown 과 cleanup 순서를 본다
shutdown 중 cancellation 이 일어난다면, 중요한 task 가 cleanup 이나 state handoff 를 끝낼 시간을 충분히 받는지 봐야 합니다.
5. intended task 만 cancellation 을 상속받는지 확인한다
마지막으로 cancellation boundary 가 실제 service ownership 과 맞는지 확인해야 합니다.
맞지 않으면 runtime 은 옳고 설계가 틀린 상태일 수 있습니다.
예시: request timeout 이 background work 까지 취소하는 경우
async def handler():
task = asyncio.create_task(store_result())
await asyncio.wait_for(fetch_data(), timeout=1)
await task
fetch_data() 가 timeout 되고 handler 가 cancel 되면, ownership 이 제대로 분리되지 않았을 때 store_result() 도 같이 사라질 수 있습니다.
이건 “request timeout” 과 “중요 background work 유실” 의 차이를 만듭니다.
cancellation path 를 찾은 뒤 무엇을 바꾸면 좋나
timeout 이 너무 짧다면
실제 task duration 에 맞게 조정하거나, 더 의미 있는 중간 경계에 deadline 을 둬야 합니다.
parent scope 가 너무 넓다면
background work 를 짧은 request ownership 에서 분리해야 합니다.
shutdown 이 너무 거칠다면
cleanup 순서를 명시적으로 만들고 중요한 task 가 critical exit path 를 마칠 시간을 줘야 합니다.
cancellation 을 잘못 처리한다면
CancelledError 를 무조건 삼키지 말고 의도적으로 다뤄야 합니다.
lifecycle boundary 가 불분명하다면
각 task 에 clear owner 와 intentional cancellation policy 를 줘야 합니다.
장애 중에 던져볼 질문
이 질문이 꽤 유용합니다.
이 task 가 정말 유용한 lifetime 을 다해서 cancel 되는 건가, 아니면 다른 곳의 잘못된 lifetime 을 상속받아서 cancel 되는 건가?
이 질문이 설계상의 진짜 버그를 잘 드러냅니다.
FAQ
Q. cancellation 은 항상 에러인가
아닙니다. 올바른 signal 일 수도 있지만, 잘못된 task 가 그 signal 을 받을 수 있습니다.
Q. 가장 빠른 첫 단계는 무엇인가
cancellation 을 트리거하는 caller 를 찾고, 그 scope 가 task 의 intended lifetime 과 맞는지 비교하는 것입니다.
Q. CancelledError 를 잡고 무시해도 되나
보통은 아닙니다. 잡더라도 shutdown behavior 를 망치지 않게 의도적으로 처리해야 합니다.
Q. 이건 주로 timeout 문제인가
때로는 그렇지만, parent scope 와 shutdown ownership 도 매우 흔한 원인입니다.
Read Next
- loop 자체가 blocked 된 것처럼 보이면 Python asyncio Event Loop Blocked 와 비교해 보세요.
- task 가 취소되는 게 아니라 아예 끝나지 않는 문제라면 Python asyncio Tasks Not Finishing 을 이어서 보세요.
- 전체 Python 분기 지도를 보려면 Python Troubleshooting Guide 로 가면 됩니다.
Related Posts
- Python asyncio Event Loop Blocked
- Python asyncio Tasks Not Finishing
- Python Troubleshooting Guide
- Python Celery Worker Concurrency Too Low
Sources:
- https://docs.python.org/3/library/asyncio-task.html
- https://docs.python.org/3/library/asyncio-dev.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를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.