uvicorn main:app --reloadUvicorn은 ASGI(Asynchronous Server Gateway Interface)라는 표준을 구현한 서버로 FastAPI에서 짜여진 코드는 단독으로 실행되어서 웹 API로써 동작할 수 없다. 실제로 공식문서를 따라 아래와 같이 간단한 웹 API를 구현해 파이썬 프로그램을 실행하면 바로 종료가 된다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
python main.py // 바로 종료됨
이는 애초에 작성된 main.py 코드가 서버를 실행시키는 코드가 아니며 위의 코드는 FastAPI app 인스턴스를 만들고 라우터를 등록하는 코드이고 HTTP 요청을 받아 FastAPI 인스턴스에게 전달하고 인스턴스로부터 데이터를 받아 HTTP 응답을 제공하는 서버의 역할을 하는 것은 Uvicorn이다. 정리하면 웹 요청과 응답의 flow는 아래와 같다.
http request를
asgi 프로토콜에 맞게
http request 변환
client ---------------> uvicorn ---------------> fastapi app instance
main.py에 구현되어 있는 fastapi app 인스턴스는 Uvicorn에게 ASGI 프로토콜에 맞는 요청을 받아 데이터를 처리한 후 Uvicorn에게 값을 전달해준다. 여기서 Uvicorn은 HTTP 요청을 ASGI 프로토콜에 맞춰 변환하며 또 인스턴스에서 반환한 데이터를 HTTP 응답으로 변환해 client에게 전달하는 웹 서버의 역할을 한다.
FastAPI에서는 Uvicorn 만으로도 여러 워커를 실행하도록 아래와 같이 웹 API 서버를 실행시킬 수 있다.
uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4
하지만 실제 운영 환경에서 멀티 프로세스 서버를 사용할 때는 Gunicorn을 함께 사용하는 방식이 프로세스가 갑작스레 종료될 경우 등 비정상적인 상황이 발생했을 때 회복능력이 뛰어나기 때문에 Gunicorn을 사용해서 멀티 프로세스 서버를 많이 띄운다.
하지만 Gunicorn은 WSGI를 구현한 서버이며 FastAPI는 ASGI 표준에 맞게 구현이 되어있기 때문에 바로 Gunicorn과 연결해서 사용할 수는 없으며
그 대신 Gunicorn은 어떤 워커 프로세스 클래스를 사용할 지 지정을 할 수 있기 때문에 Uvicorn의 워커 클래스를 사용하는 방식으로 간접적으로 연결해서 사용한다.
이 방식으로 서버를 실행할 경우 Gunicorn은 단순히 Uvicorn worker class가 실행되는 프로세스의 매니져 역할만을 한다.
// worker 4개로 실행, worker class로 uvicorn.workers.UvicornWorker 클래스 지정
gunicorn main:app --workers 4 --worker-class \
uvicorn.workers.UvicornWorker --bind 0.0.0.0:80

Gunicorn
마스터-워크 구조
Gunicorn은 마스터-워커 프로세스 구조로 동작하여 마스터 프로세스는 워커 프로세스들을 관리하고, 트래픽 부하를 워커에게 적절히 분배하며, 필요할 때 워커를 재시작하는 미들웨어 서버 역할을 한다. 이렇게 프로세스를 관리하는 마스터가 있기 때문에 워커 프로세스가 비정상 종료되어도 즉시 새로운 워커를 생성하여 애플리케이션이 중단되지 않도록 보호할 수 있다.
자동 재시작 및 워커 관리
Gunicorn은 워커가 충돌하거나 메모리 누수 등의 문제가 발생하면 자동으로 워커를 재시작할 수 있다. 이를 통해 프로세스 장애 복구가 가능하고, 장기 실행 애플리케이션에서 발생할 수 있는 메모리 누수나 기타 문제가 장기적으로 시스템에 영향을 주는 것을 막아준다.
동시성 제어
Gunicorn은 --workers와 --threads 등의 옵션을 통해 동시성 수준을 유연하게 조정할 수 있다. 이로 인해 예상되는 트래픽에 따라 적절한 수의 워커와 쓰레드를 설정해 부하 분산을 효과적으로 할 수 있는데 이는 특히 트래픽이 변동이 큰 환경에서 서버의 안정성에 크게 기여한다.
안전한 종료 및 핸들링 기능
Gunicorn은 SIGTERM, SIGINT와 같은 시스템 신호를 받아들이며, 안전한 종료 절차를 통해 애플리케이션이 정리된 상태로 종료되도록 해준다. 이를 통해 갑작스러운 종료로 인해 데이터 손실이나 비정상 상태가 발생하지 않도록 보호할 수 있다.
uvicorn
각 워커는 독립된 프로세스
Gunicorn은 기본적으로 프로세스 기반의 워커를 생성한다. 따라서 Uvicorn을 워커 클래스로 사용할 때도 각 워커는 개별 프로세스로 실행이 된다. 이 구조는 프로세스 간의 독립성을 보장하여 하나의 워커가 문제가 생기더라도 다른 워커에 영향을 주지 않는 이점이 있다.
쓰레드 기반이 아님
기본적으로 Uvicorn은 비동기 I/O(asyncio 또는 uvloop)을 사용하여 다중 클라이언트 요청을 처리한다. 쓰레드 대신 비동기 코루틴을 사용하기 때문에 단일 프로세스 내에서도 다수의 연결을 처리할 수 있습니다. Uvicorn 워커가 프로세스 내에서 쓰레드를 사용하는 방식이 아니라, 비동기 이벤트 루프를 통해 비동기 작업을 병렬로 처리하는 것이다.
쓰레드 사용 옵션
Gunicorn의 경우, --threads 옵션을 사용하여 각 워커 프로세스 내에서 쓰레드를 늘릴 수도 있다. 예를 들어 --threads 4로 설정하면, 각 Uvicorn 워커 프로세스는 4개의 쓰레드를 추가로 생성하여 동작한다. 다만, Uvicorn 자체는 비동기 I/O를 기본으로 사용하기 때문에, FastAPI와 같은 비동기 프레임워크와 함께라면 쓰레드 대신 비동기 이벤트 루프를 사용하는 것이 더 효율적이다.
Event Loop
- 하나의 이벤트 루프가 모든 작업을 관리
- 이벤트 루프는 하나의 쓰레드에서 돌아가며, 모든 비동기 작업을 순차적으로 관리한다. 작업이 I/O(예: 데이터베이스 요청, 파일 읽기/쓰기, 네트워크 통신) 때문에 블로킹될 경우, 해당 작업을 기다리지 않고, 대기 시간이 필요 없는 다른 작업을 진행한다.
- 비동기 작업들은 코루틴(coroutine)이라는 비동기 함수로 실행되며, 이벤트 루프가 코루틴들 간에 적절하게 전환해가며 요청을 처리한다.
- 비동기 작업의 병렬 처리처럼 보이는 효과
- 이벤트 루프는 실제로 병렬로 처리되는 것이 아니라, 매우 빠르게 작업을 전환하면서 다수의 작업을 처리하는 것이다.
- 예를 들어, 작업 A가 네트워크 요청을 보내고 대기 상태에 들어가면, 이벤트 루프는 바로 작업 B로 전환하여 실행한다. 이 전환이 매우 빠르게 일어나기 때문에 병렬 처리가 이루어지는 것처럼 보이지만, 실제로는 싱글 스레드에서 순차적으로 전환하면서 작업을 처리하는 것이다.
- 하나의 이벤트 루프와 CPU 코어 한 개 사용의 의미
- 이벤트 루프는 단일 CPU 코어에서만 실행되기 때문에, CPU 집약적인 작업(예: 복잡한 계산, 머신 러닝 모델 실행 등)에는 적합하지 않다. I/O 중심의 작업(예: 데이터베이스 연결, API 호출 등)에서 진가를 발휘하는 방식이다.
- CPU 집약적인 작업을 비동기 I/O로 처리하려면, 여러 이벤트 루프(프로세스)를 사용하거나 멀티쓰레딩과 같은 다른 방식의 동시성을 고려해야한다.
- 여러 이벤트 루프를 사용하려면?
- 하나의 이벤트 루프는 하나의 쓰레드 내에서 실행되므로, 멀티 프로세싱을 통해 여러 이벤트 루프를 동시에 실행할 수 있다.
- 예를 들어, Gunicorn에서 여러 개의 Uvicorn 워커를 설정하면, 각 워커가 별도의 프로세스로 동작하며 각각 독립적인 이벤트 루프를 사용하게 된다. 이 경우 여러 CPU 코어에서 동시에 작업이 실행되기 때문에, 더 많은 요청을 동시에 처리할 수 있다.
멀티 쓰레드로 여러 이벤트 루프를 사용하는 것이 권장되지 않는 이유
- 이벤트 루프는 싱글 쓰레드에서 작동
- Python의 비동기 라이브러리(asyncio)는 이벤트 루프가 하나의 쓰레드 내에서 작동하는 것을 기본 원칙으로 하고 있다. 여러 쓰레드에서 동시에 이벤트 루프를 실행하게 되면, 이벤트 루프가 공유하는 데이터나 상태가 예기치 않게 충돌할 수 있기 때문
- 따라서 하나의 쓰레드에 하나의 이벤트 루프만을 사용하는 것이 안정적이며, asyncio에서도 기본적으로 이 방식을 따른다.
- 멀티 쓰레드로 이벤트 루프를 사용하지 않는 이유
- 이벤트 루프는 비동기 코루틴을 매우 빠르게 전환하며 관리하는 구조이므로, 여러 쓰레드가 동시에 이벤트 루프를 조작하면 충돌이 발생할 가능성이 크다.
- 또한, Python의 GIL(Global Interpreter Lock)은 여러 쓰레드에서 동시에 Python 코드의 실행을 제한하기 때문에, 쓰레드를 여러 개 생성하더라도 실제로는 하나의 쓰레드만 활성화가 된다. 이는 멀티쓰레드에서 여러 이벤트 루프를 사용할 실질적 이유가 줄어드는 요인이 됩니다.
- 여러 이벤트 루프를 사용하고 싶다면? 멀티 프로세싱이 대안
- 멀티 프로세싱 방식을 사용하여, 여러 프로세스에서 각기 독립적인 이벤트 루프를 실행할 수 있다. Gunicorn처럼 여러 워커 프로세스를 생성해 각각이 독립적인 이벤트 루프를 가지도록 하는 방법이 좋은 대안이 된다.
- 각 프로세스가 독립된 메모리 공간과 CPU 코어를 사용할 수 있기 때문에, 멀티코어 CPU 환경에서 병렬로 이벤트 루프를 실행할 수 있는데 이 방식은 FastAPI와 같은 비동기 애플리케이션을 고성능으로 운영하는 데 효과적이다.
- 멀티 쓰레드와 이벤트 루프를 함께 사용할 수 있는 경우
- 일부 제한된 경우에 멀티쓰레드와 이벤트 루프를 함께 사용하는 방식이 가능하기도 하다. 예를 들어, 메인 이벤트 루프에서 실행되는 코드가 다른 서브 쓰레드를 생성하여 별도의 작업을 처리하도록 할 수 있다.
- 다만, 이러한 방식은 이벤트 루프가 쓰레드별로 동작하는 것이 아니라, 하나의 이벤트 루프가 쓰레드 외부에서 동작하는 다른 작업을 제어하는 방식이므로, 여러 쓰레드에서 이벤트 루프가 동시에 실행되는 것은 아니다.
# main.py
from fastapi import FastAPI
import os
app = FastAPI()
@app.get('/')
async def root():
print(os.getpid())
while True:
pass
uvicorn을 이용하여 실행
$ uvicorn main:app --port 8000
INFO: Started server process [47072]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
localhost:8000/으로 요청하면 서버는 해당 fastapi로 실행중인 프로세의 PID를 출력한다. 우리가 작성한 코드는 무한루프가 동작하기 때문에 응답을 받지 못한 상태에서 localhost:8000/을 새로운 브라우저를 열거나 다시 요청하면 서버가 블록킹 된 현상를 볼 수 있다.

from fastapi import FastAPI
import os
app = FastAPI()
@app.get('/')
def root():
print(os.getpid())
while True:
pass
$ uvicorn main:app --port 8000
INFO: Started server process [48225]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
두개의 브라우저를 이용하여 localhost:8000으로 접속하면 다음과 같이 서버는 블록킹이 되지 않는 현상을 볼 수 있다.

여기서 해당 함수가 어디서 동작하냐가 관건으로 요청이 발생하면 def로 정의된 API 핸들러는 fastapi가 새롭게 생성한 쓰레드에서 동작한다.
from fastapi import FastAPI
import os
app = FastAPI()
@app.get('/')
async def root():
print(os.getpid())
while True:
pass
$ uvicorn main:app --port 8000 --workers 3
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started parent process [50884]
INFO: Started server process [50901]
INFO: Started server process [50903]
INFO: Waiting for application startup.
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Application startup complete.
INFO: Started server process [50902]
INFO: Waiting for application startup.
INFO: Application startup complete.
$ ps -ef | grep 50884
501 50884 5839 0 2:17PM ttys000 0:00.15 /Users/jeongtaepark/.pyenv/versions/3.8.10/bin/python3.8 /Users/jeongtaepark/.pyenv/versions/3.8.10/bin/uvicorn main:app --port 8000 --workers 3
501 50900 50884 0 2:17PM ttys000 0:00.05 /Users/jeongtaepark/.pyenv/versions/3.8.10/bin/python3.8 -c from multiprocessing.resource_tracker import main;main(4)
501 50901 50884 0 2:17PM ttys000 0:00.46 /Users/jeongtaepark/.pyenv/versions/3.8.10/bin/python3.8 -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=8) --multiprocessing-fork
501 50902 50884 0 2:17PM ttys000 0:00.46 /Users/jeongtaepark/.pyenv/versions/3.8.10/bin/python3.8 -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=10) --multiprocessing-fork
501 50903 50884 0 2:17PM ttys000 0:00.46 /Users/jeongtaepark/.pyenv/versions/3.8.10/bin/python3.8 -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=12) --multiprocessing-fork
이 경우는 3개의 프로세스가 동작하기 때문에 블록킹이 된 서버가 있더라도 블록킹 되지 않은 서버가 요청을 받는다.