그냥 생각 없이 다들 쓰니까 쓰던 것들 자세히 알아보기
worker=4
일 때, 최대 4개의 요청을 동시에 처리 가능하고 나머지 요청은 큐에서 대기asyncio
를 사용해 비동기적으로 요청 처리Gunicorn은 주로 Django, Flask와 같이 WSGI 기반 프레임워크와 사용하고 Uvicorn은 Fastapi 처럼 ASGI 기반 프레임워크에 사용한다.
그런데 FastAPI에서 모두 동기 함수로만 개발해도 Uvicorn이 필요할까? 라는 궁금증이 들어 찾아보니
애초에 Uvicorn은 각 요청을 coroutine으로 처리하기 때문에, I/O 작업이 있을 시 자동으로 비동기 처리함.
FastAPI를 배포할 때 Uvicorn을 단독으로 사용하는 것 보다 Gunicorn과 함께 사용하는 것이 일반적으로 더 좋은 성능을 보장한다고 한다.
Uvicorn 단독 실행의 한계
Gunicorn + Uvicorn 혼합 실행의 장점
gunicorn -w 4 -k uvicorn.workers.UvicornWorker
Gunicorn은 설정 된 수 4개 만큼 Worker 프로세스를 생성하고, 각 프로세스는 uvicorn.workers.UvicornWorker
클래스를 사용해서 Uvicorn 인스턴스를 생성한다.
클라이언트 요청이 들어오면, Gunicorn은 해당 요청을 Worker 프로세스 중 하나에 할당하고, 할당 된 Worker 프로세스 내의 Uvicorn 인스턴스가 요청을 처리하고 응답을 반환한다.
Gunicorn은 Worker 프로세스 간의 로드밸런싱을 자동으로 수행행서 각 프로세스에 균등하게 요청을 분배한다.
Gunicorn의 worker 수는 서버의 CPU 코어 수를 고려하여 설정해야 한다. 일반적으로 다음 공식을 사용한다:
worker = (2 x CPU 코어 수) + 1
이 공식은 CPU 바운드 작업과 I/O 바운드 작업 사이의 균형을 맞추는데 효과적이다. 예를 들어 4코어 CPU의 경우 9개의 worker가 권장된다.
얼마나 차이날지 궁금해서 실제로 해봄
10명의 유저가 동시에 요청하도록 설정
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 5)
host = "http://127.0.0.1:8000"
@task(1)
def cpu_intensive_task(self):
self.client.get("/cpu-intensive")
@task(1)
def memory_intensive_task(self):
self.client.get("/memory-intensive")
import time
import requests
import aiohttp
from fastapi import FastAPI
app = FastAPI()
@app.get("/cpu-intensive")
def cpu_intensive_task():
"""중첩 for 루프로 CPU 사용량 증가"""
start_time = time.time()
for i in range(1000):
for j in range(100):
pass
end_time = time.time()
return {
"message": "CPU-intensive task completed",
"duration": end_time - start_time,
}
@app.get("/memory-intensive")
def memory_intensive_task():
"""대용량 리스트 생성"""
start_time = time.time()
data = [i for i in range(10000)]
end_time = time.time()
return {
"message": "Memory-intensive task completed",
"duration": end_time - start_time,
}
@app.get("/sync-google")
def request_google_sync():
"""google.com에 동기적으로 요청"""
start_time = time.time()
response = requests.get("https://www.google.com")
end_time = time.time()
return {
"message": "Sync request to google.com completed",
"duration": end_time - start_time,
}
@app.get("/async-google")
async def request_google_async():
"""google.com에 비동기적으로 요청"""
start_time = time.time()
async with aiohttp.ClientSession() as session:
async with session.get("https://www.google.com") as response:
await response.text()
end_time = time.time()
return {
"message": "Async request to google.com completed",
"duration": end_time - start_time,
}
uvicorn main:app
gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app
w 4
: worker 프로세스의 개수를 4개로 설정합니다. 이는 CPU 코어 수에 따라 적절히 조절해야 합니다.k uvicorn.workers.UvicornWorker
: worker 클래스로 UvicornWorker를 사용하도록 지정, UvicornWorker는 Gunicorn 내에서 Uvicorn을 실행하여 비동기 요청 처리를 담당응답 시간 (Response Time)
초당 처리량 (Requests per Second)
실패율 (Failure Rate)
결론적으로 Gunicorn + Uvicorn 조합이 단순 Uvicorn 설정보다 더 나은 성능을 보여주었다. 특히 CPU 집약적인 작업에서 멀티 프로세스의 이점이 더욱 두드러졌다.