FastAPI 비동기함수안에 동기함수가 있다면?

김영상 (dudtkd1221)·2024년 12월 5일
0
post-thumbnail

문제 상황

서비스 내에서 LLM의 응답시간이 긴 API가 있는데 해당 API가 호출되었을 때 다른 API요청들이 blocking 당해 해당 API요청이 끝날때까지 다른 API 용청들이 지연되는 상황이 발생했습니다.

정상

61.23ms

비정상

20.11s

원인

uvicorn

FastAPI 웹서버의 역할을 담당하는 Uvicorn은 ASGI(Application Server Gateway Interface)로 코루틴 기반의 이벤트 루프를 사용하며 비동기 I/O 작업들을 지원, 싱글 스레드로 작동합니다.
이런 특성으로 인해 한 코루틴이 I/O 작업 등으로 대기 상태가 되면, 다른 코루틴이 실행될 수 있습니다.

코루틴이란?

비선점적(non-preemtive) 멀티 태스킹으로, 다중 진입 지점(multi entry point)를 가지며 파이썬에서는 async def 키워드로 코루틴 객체를 생성할 수 있으며, await으로 작업을 대기(넘겨줄)할 수 있습니다.

uvicorn 동작

server.run()으로 서버를 시작하면


self.serve()라는 asyncio.run()으로 실행합니다.


serve()는 _serve()를 호출하고 _serve()에서는 self.startup()을 호출합니다.


startup()함수 안에서는 현재 이벤트 루프를 가져와서


loop.create_server()함수로 서버를 생성합니다.

문제원인


위 사진처럼 API요청을 async def를 활용해 비동기 라우터로 처리하고 있었는데, 이런 비동기 라우터안에 동기함수(langchain 라이브러리의 .batch메소드)가 구현되어있어 해당 요청이 끝날때까지 다른 요청들이 blocking 되는게 문제였습니다.

해결


위 처럼 .batch 메소드를 await abatch()로 바꿔주면 I/O 작업이 발생했을때 다른 작업이 실행될 수 있도록 함으로써 다른 호출들에 대한 지연이 사라집니다.

동일한 thread에서 실행되는지 확인

업로드중..

위처럼 다른 함수에서 호출된 함수이름과 현재 thread의 id를 출력하도록 했습니다.


동일한 thread id를 가지는 것이 확인됨을 볼 수 있습니다

궁금증

동기 라우터는 어떻게 처리되는지?
FastAPI는 asnyc def 라우터(비동기)와 def 라우터(동기)를 별도의 스레드에서 실행시킨다고 합니다.

starlette/routing.py
위 코드에서 처럼 is_async_callabe(func)이 false(def, 동기 함수)이면 이벤트 루프에서 실행되는 것이 아닌 별도의 백그라운드 thread pool에서 실행됩니다.

multi process로 uvicorn을 실행하면 어떻게 되는지?
uvicorn은 아래 그림처럼 multi process로도 실행이 가능합니다.

출처: DEVOCEAN

--workers 2 로 지정이 가능하며 default 값은 1입니다.
--reload로 설정하였을 경우에는 "workers" flag가 무시된다고 합니다.

uvicorn/config.py

uvicorn main:app --workers 2

workers를 2로 지정해서 실행하면


위 처럼 8000port에서 3개의 프로세스가 실행되고 있는 것을 볼 수 있습니다. workers를 2로 지정했는데 프로세스가 3개인 이유는 uvicorn은 master worker구조로 이루어져 있기 때문에, 35552는 master를 담당하는 부모 process 입니다.

위 두개의 worker로 실행했을때 하나의 동기함수가 이벤트 루프를 선점하고 있을때 다른 요청이 오면 다른 이벤트 루프 thread에서 실행되는 것을 볼 수 있습니다.

참고자료

profile
아직 배고프다

1개의 댓글

comment-user-thumbnail
2024년 12월 16일

are u still hungry?

답글 달기