목차
1. 개요
FastAPI 애플리케이션을 프로덕션 환경에 배포할 때, ASGI 서버 선택은 성능과 안정성에 직접적인 영향을 미친다. 본 글에서는 Uvicorn 단일 프로세스 사용, Uvicorn 멀티 프로세스 사용, 그리고 Gunicorn을 프로세스 매니저로 사용하는 Gunicorn+Uvicorn 조합, 이 세 가지 방식을 비교 분석한다.
2. 각 방식의 특징
2.1 Uvicorn 단일 프로세스
Uvicorn을 워커 옵션 없이 실행하는 가장 기본적인 방식이다.
uvicorn main:app --host 0.0.0.0 --port 8000
주요 특징:
- 단일 프로세스에서 asyncio 이벤트 루프를 통해 비동기 요청을 처리한다.
- CPU 코어 하나만 사용한다.
- 메모리 사용량이 가장 적다.
- 개발 환경에서 자동 리로드 기능(--reload)을 제공한다.
- 설정이 단순하고 빠르게 시작할 수 있다.
2.2 Uvicorn 멀티 프로세스
Uvicorn에 --workers 옵션을 추가하여 여러 워커 프로세스를 실행하는 방식이다.
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
주요 특징:
- 내부적으로 Gunicorn을 사용하여 워커 프로세스를 관리한다.
- 여러 CPU 코어를 활용할 수 있다.
- 각 워커가 독립적으로 요청을 처리한다.
- 기본적인 프로세스 관리 기능을 제공한다.
중요한 사실: Uvicorn의 공식 문서를 보면, --workers 옵션을 사용할 때 실제로는 내부적으로 Gunicorn을 호출한다. 즉, Uvicorn 멀티 프로세스 모드는 사실상 Gunicorn을 래핑한 것이다.
2.3 Gunicorn + Uvicorn
Gunicorn을 명시적으로 사용하고 Uvicorn을 워커 클래스로 지정하는 방식이다.
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000
주요 특징:
- Gunicorn의 모든 프로세스 관리 기능을 직접 제어할 수 있다.
- 워커 수명 주기 관리가 정교하다.
- Graceful reload를 지원한다.
- 워커별 헬스 체크와 자동 재시작이 가능하다.
- 설정 파일을 통한 세밀한 튜닝이 가능하다.
3. 상세 비교
3.1 아키텍처 차이
Uvicorn 단일 프로세스:
Uvicorn (단일 프로세스)
└── asyncio 이벤트 루프
└── 비동기 요청 처리
Uvicorn 멀티 프로세스:
Gunicorn 마스터 프로세스 (Uvicorn이 내부적으로 호출)
├── Uvicorn 워커 1
├── Uvicorn 워커 2
├── Uvicorn 워커 3
└── Uvicorn 워커 4
Gunicorn + Uvicorn:
Gunicorn 마스터 프로세스 (명시적 제어)
├── UvicornWorker 1
├── UvicornWorker 2
├── UvicornWorker 3
└── UvicornWorker 4
Uvicorn 멀티 프로세스와 Gunicorn+Uvicorn의 아키텍처는 유사하지만, 후자는 Gunicorn의 기능을 직접 제어할 수 있다는 점에서 차이가 있다.
3.2 성능 비교
CPU 활용:
- Uvicorn 단일 프로세스: CPU 코어 하나만 사용한다. 4코어 서버에서도 25% CPU만 활용한다.
- Uvicorn 멀티 프로세스: 워커 수만큼 CPU 코어를 활용한다. 4개 워커 = 4개 코어 활용.
- Gunicorn + Uvicorn: 워커 수만큼 CPU 코어를 활용한다. 4개 워커 = 4개 코어 활용.
처리량 (동일 서버 환경):
단일 프로세스: 1,000 req/s
멀티 프로세스 (4): 3,500 req/s
Gunicorn+Uvicorn (4): 3,500 req/s
성능 자체는 Uvicorn 멀티 프로세스와 Gunicorn+Uvicorn이 거의 동일하다. 차이는 프로세스 관리 기능에 있다.
메모리 사용량:
- Uvicorn 단일: 약 50MB
- Uvicorn 멀티 (4워커): 약 200MB
- Gunicorn+Uvicorn (4워커): 약 200MB
3.3 프로세스 관리 기능 비교
이 부분이 세 방식의 가장 큰 차이점이다.
Uvicorn 단일 프로세스
uvicorn main:app --host 0.0.0.0 --port 8000
제공 기능:
- 기본적인 프로세스 실행만 가능하다.
- 프로세스가 죽으면 서비스가 중단된다.
- 코드 업데이트 시 서비스 중단이 불가피하다.
필요한 외부 도구:
- systemd, supervisor 등의 프로세스 매니저가 필수적이다.
Uvicorn 멀티 프로세스
uvicorn main:app --workers 4
제공 기능:
- 워커 프로세스 생성 및 기본 관리
- 워커가 죽으면 자동 재시작
- SIGTERM 시그널로 종료 가능
제한 사항:
- Graceful reload 기능이 제한적이다.
- 워커별 세밀한 제어가 어렵다.
- 설정 옵션이 제한적이다.
- 워커 수명 주기 관리 기능이 부족하다.
Gunicorn + Uvicorn
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000
제공 기능:
- Graceful reload: 무중단으로 코드 업데이트가 가능하다.
- 워커 수명 주기 관리: 메모리 누수 방지를 위해 일정 요청 수 처리 후 워커를 재시작한다.
- 헬스 체크: 응답 없는 워커를 감지하고 재시작한다.
- 시그널 처리: 다양한 시그널(SIGHUP, SIGTERM, SIGINT 등)을 지원한다.
- 설정 파일 지원: Python 설정 파일로 모든 옵션을 관리할 수 있다.
- 로깅 제어: 워커별 로그 관리가 가능하다.
3.4 실전 예제: Graceful Reload
프로덕션 환경에서 가장 중요한 기능 중 하나가 무중단 배포이다.
Uvicorn 멀티 프로세스의 한계
# 서비스 실행
uvicorn main:app --workers 4
# 코드 업데이트 후 재시작 필요
# 방법 1: 프로세스 종료 후 재시작 (서비스 중단 발생)
kill -TERM <pid>
uvicorn main:app --workers 4
# 방법 2: systemd 사용 (짧은 중단 발생)
systemctl restart myapp
두 방법 모두 서비스 중단이 발생한다.
Gunicorn + Uvicorn의 Graceful Reload
# 서비스 실행
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000
# 코드 업데이트 후 무중단 재시작
kill -HUP <gunicorn_master_pid>
동작 방식:
- Gunicorn 마스터 프로세스가 SIGHUP 시그널을 받는다.
- 새로운 워커 프로세스들을 시작한다 (업데이트된 코드로).
- 기존 워커들은 현재 처리 중인 요청을 완료한다.
- 기존 워커들이 종료된다.
- 서비스 중단 없이 업데이트가 완료된다.
3.5 워커 수명 주기 관리
Python 애플리케이션에서 메모리 누수는 흔한 문제다. 장시간 실행되는 워커는 메모리를 계속 소비할 수 있다.
Gunicorn의 워커 수명 주기 관리
# gunicorn.conf.py
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"
max_requests = 1000 # 워커가 1000개 요청 처리 후 재시작
max_requests_jitter = 50 # 랜덤하게 950~1050 사이에서 재시작
동작 방식:
- 각 워커는 설정된 요청 수를 처리한 후 자동으로 재시작된다.
- jitter를 추가하여 모든 워커가 동시에 재시작되는 것을 방지한다.
- 메모리 누수를 주기적으로 초기화할 수 있다.
Uvicorn 멀티 프로세스에는 이러한 기능이 없다.
3.6 설정 및 실행 방법
Uvicorn 단일 프로세스
# 기본 실행
uvicorn main:app --host 0.0.0.0 --port 8000
# 개발 환경 (자동 리로드)
uvicorn main:app --reload
# SSL 지원
uvicorn main:app --ssl-keyfile key.pem --ssl-certfile cert.pem
Uvicorn 멀티 프로세스
# 기본 실행
uvicorn main:app --workers 4
# 환경 변수와 함께
uvicorn main:app --workers 4 --host 0.0.0.0 --port 8000
제한 사항:
- --reload와 --workers를 동시에 사용할 수 없다.
- 설정 파일을 사용할 수 없다.
Gunicorn + Uvicorn
커맨드라인:
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--access-logfile - \
--error-logfile - \
--timeout 30 \
--graceful-timeout 30 \
--keep-alive 5
설정 파일 (gunicorn.conf.py):
import multiprocessing
# 서버 소켓
bind = "0.0.0.0:8000"
backlog = 2048
# 워커 프로세스
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
# 타임아웃
timeout = 30
graceful_timeout = 30
keepalive = 5
# 로깅
accesslog = "-"
errorlog = "-"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# 프로세스 이름
proc_name = "fastapi_app"
# 서버 훅
def on_starting(server):
print("서버 시작 중...")
def on_reload(server):
print("워커 리로드 중...")
def worker_int(worker):
print(f"워커 {worker.pid} 인터럽트됨")
실행:
gunicorn -c gunicorn.conf.py main:app
4. 시그널 처리 비교
프로덕션 환경에서는 다양한 시그널을 통해 서버를 제어한다.
4.1 Gunicorn + Uvicorn의 시그널 처리
# Graceful reload (무중단 재시작)
kill -HUP <master_pid>
# Graceful shutdown (현재 요청 완료 후 종료)
kill -TERM <master_pid>
# 즉시 종료
kill -QUIT <master_pid>
# 워커 수 증가
kill -TTIN <master_pid>
# 워커 수 감소
kill -TTOU <master_pid>
4.2 Uvicorn 멀티 프로세스의 시그널 처리
# 종료 (graceful 여부는 제한적)
kill -TERM <master_pid>
# 즉시 종료
kill -KILL <master_pid>
Uvicorn 멀티 프로세스는 기본적인 시그널만 지원한다.
5. 사용 시나리오별 권장사항
5.1 개발 환경
권장: Uvicorn 단일 프로세스
uvicorn main:app --reload --host 0.0.0.0 --port 8000
이유:
- 자동 리로드 기능으로 개발 생산성이 높다.
- 디버깅이 단순하다.
- 리소스 사용량이 적다.
5.2 소규모 프로덕션 (일일 방문자 < 1,000)
권장: Uvicorn 단일 프로세스 + systemd
트래픽이 적고 단순한 API 서버라면 단일 프로세스로도 충분하다.
systemd 설정 (/etc/systemd/system/myapp.service):
[Unit]
Description=FastAPI Application
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
Environment="PATH=/var/www/myapp/venv/bin"
ExecStart=/var/www/myapp/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
실행:
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
5.3 중규모 프로덕션 (일일 방문자 1,000 ~ 50,000)
권장: Uvicorn 멀티 프로세스 또는 Gunicorn + Uvicorn
이 규모에서는 두 방식 모두 사용 가능하다. 단, 무중단 배포가 필요하다면 Gunicorn+Uvicorn을 선택해야 한다.
Uvicorn 멀티 프로세스:
uvicorn main:app --workers 4 --host 127.0.0.1 --port 8000
Gunicorn + Uvicorn:
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 127.0.0.1:8000
5.4 대규모 프로덕션 (일일 방문자 > 50,000)
권장: Gunicorn + Uvicorn + 설정 파일
세밀한 튜닝과 안정적인 운영이 필요하다.
gunicorn.conf.py:
import multiprocessing
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
bind = "127.0.0.1:8000"
# 워커 수명 주기
max_requests = 1000
max_requests_jitter = 50
# 타임아웃
timeout = 30
graceful_timeout = 30
keepalive = 5
# 연결 설정
worker_connections = 1000
backlog = 2048
# 로깅
accesslog = "/var/log/myapp/access.log"
errorlog = "/var/log/myapp/error.log"
loglevel = "warning"
실행:
gunicorn -c gunicorn.conf.py main:app
5.5 WebSocket이 필요한 경우
권장: Gunicorn + Uvicorn
WebSocket 연결은 장시간 유지되므로, 워커 수명 주기 관리가 중요하다.
주의사항:
# gunicorn.conf.py
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"
timeout = 300 # WebSocket 연결을 위해 타임아웃 증가
graceful_timeout = 60
# WebSocket 연결이 있는 경우 max_requests 주의
# 연결 중인 워커가 재시작되면 연결이 끊어짐
max_requests = 5000
max_requests_jitter = 100
Nginx 설정:
location /ws {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 300s;
}
6. 실전 배포 예제
6.1 Nginx + Gunicorn + Uvicorn 구성
디렉토리 구조:
/var/www/myapp/
├── main.py
├── gunicorn.conf.py
├── requirements.txt
└── venv/
Nginx 설정 (/etc/nginx/sites-available/myapp):
upstream fastapi_backend {
server 127.0.0.1:8000 fail_timeout=0;
}
server {
listen 80;
server_name example.com;
# SSL 리다이렉트
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 보안 헤더
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# 정적 파일
location /static {
alias /var/www/myapp/static;
expires 30d;
}
# API 요청
location / {
proxy_pass http://fastapi_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 타임아웃
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
Gunicorn 설정 (gunicorn.conf.py):
import multiprocessing
import os
# 워커 설정
workers = int(os.getenv("WORKERS", multiprocessing.cpu_count() * 2 + 1))
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
# 바인딩
bind = "127.0.0.1:8000"
backlog = 2048
# 워커 수명 주기
max_requests = 1000
max_requests_jitter = 50
timeout = 30
graceful_timeout = 30
keepalive = 5
# 로깅
accesslog = "/var/log/myapp/access.log"
errorlog = "/var/log/myapp/error.log"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# 프로세스 이름
proc_name = "myapp"
# 보안
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
systemd 설정 (/etc/systemd/system/myapp.service):
[Unit]
Description=FastAPI Application with Gunicorn
After=network.target
[Service]
Type=notify
User=www-data
Group=www-data
RuntimeDirectory=gunicorn
WorkingDirectory=/var/www/myapp
Environment="PATH=/var/www/myapp/venv/bin"
Environment="WORKERS=4"
ExecStart=/var/www/myapp/venv/bin/gunicorn -c gunicorn.conf.py main:app
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
배포 스크립트 (deploy.sh):
#!/bin/bash
set -e
APP_DIR="/var/www/myapp"
VENV_DIR="$APP_DIR/venv"
echo "코드 업데이트 중..."
cd $APP_DIR
git pull origin main
echo "의존성 설치 중..."
$VENV_DIR/bin/pip install -r requirements.txt
echo "Graceful reload 수행 중..."
sudo systemctl reload myapp
echo "배포 완료!"
무중단 배포 실행:
./deploy.sh
6.2 성능 테스트
부하 테스트를 통해 최적의 워커 수를 찾아야 한다.
테스트 도구 설치:
pip install locust
테스트 시나리오 (locustfile.py):
from locust import HttpUser, task, between
class FastAPIUser(HttpUser):
wait_time = between(1, 3)
@task(3)
def get_items(self):
self.client.get("/items")
@task(1)
def create_item(self):
self.client.post("/items", json={"name": "test", "price": 100})
테스트 실행:
# 1000명의 동시 사용자, 초당 100명씩 증가
locust -f locustfile.py --host=https://example.com --users 1000 --spawn-rate 100
워커 수에 따른 성능 비교:
# 워커 2개
WORKERS=2 gunicorn -c gunicorn.conf.py main:app
# 결과: 2000 req/s
# 워커 4개
WORKERS=4 gunicorn -c gunicorn.conf.py main:app
# 결과: 3500 req/s
# 워커 8개
WORKERS=8 gunicorn -c gunicorn.conf.py main:app
# 결과: 4200 req/s
# 워커 16개
WORKERS=16 gunicorn -c gunicorn.conf.py main:app
# 결과: 4300 req/s (메모리 부족, 성능 저하)
최적의 워커 수는 서버 사양과 애플리케이션 특성에 따라 다르다.
7. 모니터링 및 로깅
7.1 Gunicorn 통계 확인
# 마스터 프로세스 PID 확인
cat /run/gunicorn/gunicorn.pid
# 워커 프로세스 목록
ps aux | grep gunicorn
# 워커별 메모리 사용량
ps -o pid,ppid,%mem,rss,cmd -p $(pgrep -P $(cat /run/gunicorn/gunicorn.pid))
7.2 로그 분석
접근 로그 분석:
# 가장 많이 호출된 엔드포인트
awk '{print $7}' /var/log/myapp/access.log | sort | uniq -c | sort -rn | head -10
# 평균 응답 시간
awk '{sum+=$NF; count++} END {print sum/count " ms"}' /var/log/myapp/access.log
# 상태 코드별 통계
awk '{print $9}' /var/log/myapp/access.log | sort | uniq -c | sort -rn
에러 로그 모니터링:
# 실시간 에러 확인
tail -f /var/log/myapp/error.log
# 에러 패턴 분석
grep "ERROR" /var/log/myapp/error.log | awk '{print $5}' | sort | uniq -c | sort -rn
7.3 프로메테우스 연동
Gunicorn 메트릭을 프로메테우스로 수집할 수 있다.
pip install prometheus-client
메트릭 수집 (main.py):
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
from fastapi import FastAPI, Response
app = FastAPI()
# 메트릭 정의
REQUEST_COUNT = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint', 'status'])
REQUEST_DURATION = Histogram('http_request_duration_seconds', 'HTTP request duration', ['method', 'endpoint'])
@app.middleware("http")
async def metrics_middleware(request, call_next):
with REQUEST_DURATION.labels(method=request.method, endpoint=request.url.path).time():
response = await call_next(request)
REQUEST_COUNT.labels(method=request.method, endpoint=request.url.path, status=response.status_code).inc()
return response
@app.get("/metrics")
def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
8. 트러블슈팅
8.1 워커가 자주 재시작되는 경우
원인 분석:
# 워커 재시작 로그 확인
grep "Worker" /var/log/myapp/error.log
# 메모리 사용량 확인
watch -n 1 'ps aux | grep gunicorn'
해결 방법:
# gunicorn.conf.py
# 타임아웃 증가
timeout = 60
graceful_timeout = 60
# 메모리 누수가 의심되는 경우
max_requests = 500 # 더 자주 재시작
max_requests_jitter = 50
8.2 느린 응답 시간
원인 분석:
# 느린 요청 로그 확인
awk '$NF > 1000' /var/log/myapp/access.log
해결 방법:
# gunicorn.conf.py
# 워커 연결 수 증가
worker_connections = 2000
# 워커 수 증가
workers = 8
8.3 502 Bad Gateway 에러
원인:
- Gunicorn이 응답하지 않음
- 워커가 모두 busy 상태
해결 방법:
# Gunicorn 상태 확인
sudo systemctl status myapp
# 워커 수 증가
sudo systemctl stop myapp
# gunicorn.conf.py에서 workers 증가
sudo systemctl start myapp
# Nginx 타임아웃 증가
# /etc/nginx/sites-available/myapp
proxy_read_timeout 120s;
9. 결론
9.1 선택 가이드
Uvicorn 단일 프로세스:
- 개발 환경
- 소규모 API (일일 방문자 < 1,000)
- 리소스가 제한된 환경
- 빠른 프로토타이핑
Uvicorn 멀티 프로세스:
- 중규모 API (일일 방문자 1,000 ~ 50,000)
- 무중단 배포가 필요 없는 경우
- 간단한 설정을 원하는 경우
- Gunicorn 학습 곡선을 피하고 싶은 경우
Gunicorn + Uvicorn:
- 대규모 프로덕션 환경
- 무중단 배포가 필수적인 경우
- 세밀한 프로세스 관리가 필요한 경우
- 워커 수명 주기 관리가 필요한 경우
- 엔터프라이즈 수준의 안정성이 필요한 경우
9.2 핵심 차이점 요약
성능: 세 방식 중 멀티 프로세스를 사용하는 두 방식의 성능은 거의 동일하다. 차이는 관리 기능에 있다.
프로세스 관리:
- Uvicorn 단일: 없음 (외부 도구 필요)
- Uvicorn 멀티: 기본 기능만 제공
- Gunicorn+Uvicorn: 완전한 프로세스 관리 기능
무중단 배포:
- Uvicorn 단일: 불가능
- Uvicorn 멀티: 제한적
- Gunicorn+Uvicorn: 완벽하게 지원
설정 유연성:
- Uvicorn 단일: 커맨드라인 옵션만
- Uvicorn 멀티: 커맨드라인 옵션만
- Gunicorn+Uvicorn: 설정 파일 + Python 훅
9.3 최종 권장사항
프로덕션 환경에서는 Gunicorn + Uvicorn 조합을 강력히 권장한다. 초기 설정이 약간 복잡하지만, 안정성, 관리 용이성, 무중단 배포 등의 장점이 복잡성을 상쇄하고도 남는다.
Uvicorn 멀티 프로세스는 내부적으로 Gunicorn을 사용하므로, 직접 Gunicorn을 사용하여 더 많은 제어권을 갖는 것이 합리적이다.
개발 환경에서는 Uvicorn 단일 프로세스의 자동 리로드 기능이 생산성을 크게 향상시키므로, 개발과 프로덕션 환경을 명확히 구분하여 각각에 적합한 방식을 사용하는 것이 최선이다.
'FastAPI' 카테고리의 다른 글
| [FastAPI] APIRouter (0) | 2025.01.12 |
|---|---|
| [FastAPI] 정적 파일 호스팅 (0) | 2025.01.10 |
