Python asyncio Event Loop Blocked: 무엇부터 확인할까
마지막 업데이트

Python asyncio Event Loop Blocked: 무엇부터 확인할까


Python asyncio event loop 가 막힌 것처럼 보일 때 문제는 보통 asyncio 자체가 아닙니다. 더 흔한 경우는 synchronous function, CPU-heavy section, 혹은 잘못 고른 라이브러리 호출 하나가 async 흐름 안에 들어와 control 을 돌려주지 않는 상황입니다.

짧게 말하면 핵심은 이것입니다. 런타임 전체를 의심하기 전에 loop 를 가장 오래 붙잡는 경로가 무엇인지 먼저 찾아야 합니다. 하나의 blocking path 만으로도 서로 무관한 coroutine 들이 함께 늦어지면서 서비스 전체가 얼어붙은 것처럼 보일 수 있습니다.


async 경로 안의 blocking work 부터 본다

event loop 가 “막힌다” 는 것은 보통 어떤 경로 하나가 yield 를 하지 않는다는 뜻입니다.

첫 의심 대상은 보통 이렇습니다.

  • async handler 안의 synchronous I/O
  • coroutine 안에 남아 있는 CPU-heavy work
  • async 처럼 보이지만 실제로 block 하는 라이브러리 호출
  • 제때 결과가 오지 않는 하나의 await path

핵심 구분은 loop 전체가 굶주린 건지, 개별 task 가 독립적으로 기다리는 건지입니다.


blocked loop 는 운영에서 어떻게 보이나

보통 이런 모습으로 나타납니다.

  • 많은 요청이 동시에 timeout 된다
  • 서로 다른 coroutine 들이 함께 늦어진다
  • 프로세스는 active 한데 useful throughput 이 낮다
  • heartbeat 나 scheduled job 이 늦게 실행된다
  • 운영자는 “asyncio 문제” 로 보지만 실제 원인은 blocking call 하나다

서로 무관한 task 가 함께 느려진다면 isolated task bug 보다 loop blockage 가능성이 훨씬 큽니다.


흔한 원인

1. synchronous work 가 event loop 안에서 실행된다

파일 접근, 네트워크 클라이언트, CPU-heavy transform 이 loop 를 직접 막을 수 있습니다.

async def handler():
    data = requests.get("https://example.com").json()
    return data

함수가 async 여도 requests 는 synchronous 이기 때문에 event loop 를 그대로 막습니다.

2. 하나의 await path 가 너무 오래 걸린다

느린 dependency 하나만으로도 다른 task 들이 제때 진행되지 못할 수 있습니다.

코드가 기술적으로 async 라고 해도, load 상황에서는 하나의 slow await 가 loop 체감을 지배할 수 있습니다.

3. CPU-heavy work 가 coroutine 코드에 너무 많이 남아 있다

async 구조라고 해서 CPU-heavy code 가 non-blocking 이 되는 것은 아닙니다.

비싼 parsing, compression, serialization, transformation 이 coroutine 안에 남아 있으면 loop 가 다른 task 로 돌아갈 시간을 충분히 못 얻을 수 있습니다.

4. pending task 가 너무 많아 loop 가 과부하된다

한 task 가 특별히 망가져 보이지 않아도 scheduled work 가 너무 많으면 지연이 커질 수 있습니다.

특히 이런 경우가 흔합니다.

  • fan-out 이 너무 큼
  • retry 가 loop 가 감당할 양보다 더 많은 task 를 만듦
  • background scheduling 이 강한 제한 없이 커짐

5. runtime 증상처럼 보여도 실제론 task 설계 문제다

모든 것이 늦어 보여 loop 가 의심되지만, 실제로는 poor task ownership, 약한 backpressure, endless pending work 가 upstream 에 있을 수 있습니다.

그래서 loop health 는 task lifecycle 과 queue behavior 와 함께 읽어야 합니다.


실전 점검 순서

1. loop 가 가장 오래 시간을 쓰는 경로를 찾는다

질문은 이렇습니다.

  • 어떤 경로가 가장 오래 yield 없이 달리는가
  • 그 경로에서 최근 무엇이 바뀌었는가
  • 특정 handler 하나가 loop 를 지배하는가

가장 가치가 큰 첫 단계입니다.

2. async handler 안의 synchronous call 을 찾는다

겉보기엔 harmless 해도 실제로 block 하는 코드와 라이브러리를 보세요.

  • requests
  • synchronous database client
  • file read
  • CPU-heavy conversion path

3. CPU-heavy work 와 I/O-bound coroutine path 를 분리해서 본다

문제가 CPU work 라면 async 구조만으로는 해결되지 않습니다.

즉 loop 문제가 아래 중 무엇인지 구분해야 합니다.

  • blocking I/O
  • 과도한 CPU work
  • 너무 많은 scheduled task

4. 긴 await 와 과도한 task fan-out 을 점검한다

느린 dependency 하나나 pending task flood 만으로도, 특정 coroutine 하나가 망가지지 않았는데 loop 전체가 stalled 된 것처럼 보일 수 있습니다.

5. 최근 변경 전후의 event-loop health 를 비교한다

새 라이브러리, 바뀐 fan-out pattern, 더 무거워진 handler 가 loop behavior 악화를 설명하는 경우가 많습니다.


예시: async 코드 안의 synchronous client

async def handler():
    response = requests.get("https://example.com")
    return response.text

함수는 async 이지만 HTTP call 이 도는 동안 loop 는 그대로 막힙니다.

보통 더 나은 방향은 이렇습니다.

  • truly async client 사용
  • blocking work 를 loop 밖으로 이동
  • request 당 blocking section 줄이기

blocking path 를 찾은 뒤 무엇을 바꾸면 좋나

synchronous I/O 가 문제라면

async client 로 바꾸거나 event loop 밖으로 빼야 합니다.

CPU-heavy path 가 문제라면

작업량을 줄이거나 batching 을 바꾸거나, CPU-intensive processing 을 main coroutine path 밖으로 옮겨야 합니다.

fan-out 이 너무 크다면

동시성을 bounded 하게 만들어 loop 가 pending task flood 에 잠기지 않게 해야 합니다.

dependency 하나가 느리다면

적절한 timeout 을 넣고, 하나의 wait 가 서비스 전체를 지배하지 않게 영향 범위를 줄여야 합니다.

upstream coordination 이 문제라면

loop health 보다 task ownership 과 queue design 을 메인 incident 로 다뤄야 합니다.


장애 중에 던져볼 질문

이 질문이 가장 유용한 경우가 많습니다.

control 을 다시 돌려주지 않은 채 event loop 를 가장 오래 붙잡는 정확한 코드 경로는 무엇인가?

이 질문이 “asyncio 가 느린가?” 보다 거의 항상 더 좋습니다.


FAQ

Q. async def 면 자동으로 non-blocking 인가

아닙니다. async 함수 안의 blocking code 는 여전히 loop 를 막습니다.

Q. 가장 빠른 첫 단계는 무엇인가

loop 안에서 가장 오래 도는 경로를 찾고, 제때 control 을 넘기는지 보는 것입니다.

Q. 느린 asyncio 서비스면 다 loop blockage 인가

아닙니다. healthy loop 인데 task ownership 이 나쁘거나 dependency 가 끝나지 않는 경우도 많습니다.

Q. task 가 너무 많아도 loop 가 막힐 수 있나

그렇습니다. fan-out 이 크고 pending task flood 가 오면 obvious culprit 하나 없이도 loop 가 sluggish 해질 수 있습니다.


Sources:

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.