Uvicorn'u Üretim Ortamında Doğru Çalıştırmak
Selamlar, bu yazımızda FastAPI ya da Starlette ile yazdığımız bir uygulamayı uvicorn main:app komutundan alıp üretim ortamına nasıl taşırız, ona bakacağız. Lokal'de tek satırla çalışan şey üretimde aynı şekilde ayakta tutulamıyor; worker yönetimi, SSL, log formatı, süreç gözetimi gibi parçalar birden devreye giriyor. Lafı uzatmadan girelim.
Uvicorn lokalde neden yetiyor, üretimde neden yetmiyor?
Uvicorn temelde tek başına bir ASGI sunucusu. uvloop ve httptools ile birlikte gerçekten hızlı, async iş yüklerini sevmesi de cabası. Ama tek başına çalıştırdığında karşılaştığın şey tek bir Python process'i. Process çökerse kimse kaldırmıyor, restart yok, graceful shutdown'a kendin sarmalıyorsun, birden fazla CPU çekirdeğini de pek kullanamıyorsun.
İşin aslı şu: üretimde Uvicorn'u doğrudan çalıştırmak da mümkün, ama pratikte çoğu ekibin tercih ettiği kombinasyon Gunicorn + UvicornWorker. Gunicorn pre-fork modeliyle worker'ları ayağa kaldırıyor, çökeni yerine yenisini koyuyor, graceful reload yapıyor; biz de Uvicorn'un async performansını kaybetmiyoruz. Bence küçük bir servis için bile bu setup zahmete değer.
Worker sayısını kafadan atmayalım
Klasik formül (2 x CPU) + 1. Bu IO-bound uygulamalar için iyi bir başlangıç; bekleyen worker'ların yerini başkaları doldursun diye CPU sayısının üstüne çıkıyoruz. Ama her uygulamayı aynı kalıba sokmak hata olur.
# worker_sayisi.py
import multiprocessing
import os
def worker_sayisi(uygulama_tipi: str = 'io_bound') -> int:
cpu = multiprocessing.cpu_count()
if uygulama_tipi == 'io_bound':
# API cagrilari, DB sorgulari: bekleme cok, worker fazla olsun
return (2 * cpu) + 1
if uygulama_tipi == 'cpu_bound':
# ML inference, gorsel isleme: cekirdek sayisi yeter
return cpu
# Karisik yuk: ortayi tut
return int(1.5 * cpu)
WORKERS = int(os.getenv('WORKERS', worker_sayisi(os.getenv('APP_TYPE', 'io_bound'))))
ML inference döndüren bir endpoint'iniz varsa (2*CPU)+1 size yarar değil zarar getirir; worker'lar CPU için kapışırlar, latency tavan yapar. Şahsi kanaatim: önce profilleyin, sonra worker sayısı verin.
Gunicorn ile Uvicorn beraber
Kurulum bir satır, asıl iş yapılandırmada:
pip install 'uvicorn[standard]' gunicorn
gunicorn app.main:app \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--workers 4 \
--timeout 120 \
--keep-alive 5 \
--graceful-timeout 30 \
--max-requests 10000 \
--max-requests-jitter 1000
Burada dikkat edeceğiniz iki nokta var. Birincisi --max-requests ve --max-requests-jitter: worker belli sayıda istek sonrası kendini yeniliyor, jitter ise tüm worker'ların aynı anda restart olmasını engelliyor (thundering herd istemeyiz). İkincisi --graceful-timeout: SIGTERM gelince worker'a kaç saniye süre tanıyoruz. 30 saniye çoğu API için makul; uzun çalışan istekleriniz varsa artırın.
SSL'i nerede sonlandırmalı?
Uvicorn doğrudan SSL yapabilir, ama production'da bunu önerimiyorum. Sertifika yenileme, HSTS, HTTP/2, OCSP stapling gibi şeyleri Nginx ya da bulut load balancer'a bırakmak hem güvenli hem de daha az iş. Uvicorn arkada düz HTTP'de kalır, proxy_headers=True ve forwarded_allow_ips ayarları ile X-Forwarded-* başlıklarına güvenir.
server {
listen 443 ssl http2;
server_name <Alanadi>;
ssl_certificate /etc/letsencrypt/live/<Alanadi>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<Alanadi>/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
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;
# WebSocket destegi
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
}
Loglar metin değil JSON olsun
Üretimde insan gözüyle log okumayı bırakın. Log toplayıcısı ne olursa olsun (Loki, ELK, Datadog), JSON satırları parse etmek kıyaslanmayacak kadar kolay. Uvicorn'un kendi logger'ını da kendi formatınıza çekebilirsiniz:
# log_kurulumu.py
import logging, sys, json
from datetime import datetime, timezone
class JsonFormatter(logging.Formatter):
def format(self, record):
veri = {
'ts': datetime.now(timezone.utc).isoformat(),
'level': record.levelname.lower(),
'logger': record.name,
'msg': record.getMessage(),
}
if record.exc_info:
veri['exc'] = self.formatException(record.exc_info)
return json.dumps(veri, ensure_ascii=False)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
for ad in ('uvicorn', 'uvicorn.error', 'uvicorn.access'):
lg = logging.getLogger(ad)
lg.handlers = [handler]
lg.propagate = False
Container'da stdout'a yazıp toplayıcıya orchestrator'ın götürmesini bırakmak en temizi. Disk'e log dosyası yazmaya kalkmayın, rotasyonla uğraşmak başlı başına bir iş.
Docker ve systemd: hangisi nerede?
Kubernetes ya da ECS gibi bir orchestrator'unuz varsa Docker imajınızda Gunicorn'u doğrudan CMD'ye yazmak yeterli. Bare-metal ya da klasik VM'de ise systemd işin doğru yeri; restart politikası, log entegrasyonu (journalctl -u <Svc>), socket activation gibi kabiliyetler hazır geliyor.
Docker tarafı için kısa bir Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN groupadd --gid 1000 app && useradd --uid 1000 --gid app app
USER app
EXPOSE 8000
CMD ["gunicorn", "app.main:app", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--access-logfile", "-", "--error-logfile", "-"]
systemd tarafında ise Type=notify, Restart=on-failure ve sık unutulan LimitNOFILE=65535 satırlarını koymadan geçmeyin; yüksek bağlantı sayılarında dosya tanıtıcı sınırı sizi şaşırtarak bulur.
Sık karşılaşılan tuzaklar
- Tek worker ile production'a çıkmak: Tek process çökerse servis düşüyor, üstelik çok çekirdekli makineyi de boşa harcıyorsunuz. En azından Gunicorn altında 2 worker ile başlayın.
access_logaçık unutmak: Yüksek RPS'de Uvicorn'un kendi access log'u CPU'yu yakar. Kapatın, access log'u uygulama middleware'inden ya da reverse proxy'den çıkarın.proxy_headersayarını yapmamak: Reverse proxy arkasındaysanızrequest.client.hosther zaman127.0.0.1görünür.forwarded_allow_ipsile beraber ayarlayın, log ve rate limit doğru IP'yi görsün.max-requestskoymamak: Bellek sızıntısı olan bir kütüphane bir gün karşınıza çıkar; periyodik worker yenilemesi bu durumun maliyetini düşürür.graceful-timeoutçok kısa: Deploy sırasında uzun süren bir istek yarıda kesilirse kullanıcı 502 yer. İstek profilinize göre 30-60 saniye arası genelde iyi.
Doğrulama
Yeni kurulumu test ederken hey ya da wrk ile yük gönderip worker sayılarını izlemek faydalı:
hey -n 5000 -c 100 http://localhost:8000/health
ps -ef | grep uvicorn | grep -v grep
journalctl -u uvicorn-api.service -f
Worker sayısı tam istediğiniz kadar mı, restart'lar düzgün mü, latency dağılımı kabul edilebilir mi - bu üçünü görmeden 'production'a hazır' demeyin.
Kapanış
Uvicorn üretimde tek başına yetmiyor; Gunicorn ile süreç yönetimi, reverse proxy ile SSL, JSON loglar ve doğru worker hesabı bir araya geldiğinde ortaya gerçekten dayanıklı bir servis çıkıyor. Bana sorarsanız, lokal komutu tek satır da olsa, üretim için her zaman bir konfigürasyon dosyası tutun; üç ay sonra niye o sayıyı seçtiğinizi siz bile hatırlamayabilirsiniz. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.
