Aplikace, která běží na tvém notebooku přes flask run nebo uvicorn main:app --reload, je produkci blíž, než se lidé bojí. Naváž se na 0.0.0.0 místo localhostu, vezmi port z prostředí, nahraď vývojový server gunicornem nebo uvicornem, zvol počet workerů, který tvoje paměť opravdu unese, a řekni aplikaci, že sedí za proxy. Těchto pět věcí je celá práce. Všechno níže je detail k nim: čísla, podle kterých dimenzovat workery, nastavení, která mění chování, a chyby, na které narazíš zhruba v pořadí, v jakém je potkáš.
Oba frameworky se liší v jedné věci, která rozhoduje o téměř každé další volbě. Flask je synchronní a mluví WSGI. FastAPI je asynchronní a mluví ASGI. Když tohle uhodneš, zbytek z toho vyplyne.
ASGI nebo WSGI: co tvůj framework potřebuje#
WSGI je staré synchronní webové rozhraní Pythonu: server předá aplikaci požadavek a čeká na odpověď. Jeden worker obsluhuje jeden požadavek najednou. Je jednoduché, prověřené a neumí držet otevřený websocket.
ASGI je asynchronní náhrada. Jeden proces může držet tisíce spojení najednou, pokud kód uvnitř při čekání na vstup a výstup uvolní řízení. Právě to umožňuje websockety, server-sent events a long-polling v jednom procesu.
| Framework | Rozhraní | Použitý server | Import string |
|---|---|---|---|
| Flask | WSGI | gunicorn | wsgi:app |
| Django | WSGI (ASGI volitelně) | gunicorn | myproject.wsgi:application |
| FastAPI | ASGI | uvicorn | app.main:app |
| Starlette | ASGI | uvicorn | app.main:app |
Flask podporuje view funkce async def od verze 2.0 a je snadné z toho vyčíst příliš. Flask spustí async view tak, že pro ten jediný požadavek nastartuje event loop a počká, až skončí, takže worker je po celý požadavek stále obsazený. Dostaneš syntaxi, ne souběžnost. Pokud potřebuješ, aby jeden proces obsluhoval mnoho pomalých požadavků najednou, potřebuješ ASGI framework, ne async view ve WSGI.
Opačný případ stojí za zmínku také. Pokud je tvoje aplikace hrstka CRUD endpointů před databází, Flask na gunicornu s několika vlákny ji obslouží naprosto dobře a je to méně mechaniky, ve které se dá udělat chyba. FastAPI vol kvůli validaci typů a generovanému schématu OpenAPI, ne proto, že je async na papíře rychlejší.
Requirements, virtualenv a krok instalace#
Všechno, co server spouští, pochází z připnutého souboru requirements nainstalovaného do virtuálního prostředí, které patří aplikaci. Ne do systémového Pythonu, ne to, co pip zrovna ten den vyřešil.
fastapi==0.115.0uvicorn[standard]==0.30.6gunicorn==22.0.0pydantic-settings==2.4.0psycopg[binary]==3.2.1$ python -m venv .venv$ .venv/bin/pip install --upgrade pip$ .venv/bin/pip install --no-cache-dir -r requirements.txtVolání .venv/bin/pip a .venv/bin/python přímo, místo aktivace prostředí, odstraní celou kategorii problémů typu „v mém shellu to funguje“ - není tu žádný stav shellu, ve kterém by se dalo udělat chybu. --no-cache-dir je na malém tarifu důležité: cache wheelů v pipu žije v domovském adresáři a nenápadně naroste na stovky megabajtů na disku, který může mít jen 5 GB. Python requirements a virtualenvy probírá připínání verzí, lock soubory a balíčky, které se na stroji s půl jádrem snaží kompilovat samy.
Extra [standard] u uvicornu stojí za to. Přitáhne uvloop a httptools, které nahradí event loop a HTTP parser napsané v čistém Pythonu implementacemi v C, plus knihovnu websockets. Je to nejlevnější zlepšení propustnosti, které FastAPI aplikace má, a stojí jeden řádek.
Kde instalace běží, je rozhodnutí. Dát pip install -r requirements.txt před startovací příkaz znamená, že každý restart instaluje znovu, což je pomalé, ale zaručuje, že běžící kód a deklarované závislosti si odpovídají. Jednorázová ruční instalace rychleji nabíhá, ale rozjede se při první příležitosti, kdy někdo přidá balíček a zapomene. Na hostingu, který při startu stahuje tvůj repozitář, je obvykle správná první možnost: pip přeskočí vše, co je už splněné, takže restart bez změny závislostí přidá pár sekund, ne pár minut.
Startovací příkaz: bind, porty a jedna věc, která se rozbije#
Tady selhává většina prvních nasazení a vždy je to totéž. Uvnitř kontejneru znamená 127.0.0.1 kontejner samotný. Nic zvenčí se k němu nedostane. Server se musí navázat na 0.0.0.0.
# FastAPI, single process$ uvicorn app.main:app --host 0.0.0.0 --port 8000# Flask, four gunicorn workers$ gunicorn wsgi:app --bind 0.0.0.0:8000 --workers 4# FastAPI under gunicorn, which supervises uvicorn workers$ gunicorn app.main:app -k uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8000 --workers 4Ta třetí forma je tradiční způsob, jak spustit několik workerů uvicornu, a je uprostřed migrace. Od uvicornu 0.30 je vestavěný modul uvicorn.workers zastaralý ve prospěch samostatného balíčku uvicorn-worker, jehož třída je uvicorn_worker.UvicornWorker. Než řádek zkopíruješ, zkontroluj, kterou verzi máš připnutou. Uvicorn umí také spouštět vlastní workery přes --workers 4 a gunicorn vůbec nepotřebuje, což je o jednu závislost méně za stejný výsledek.
Import string má tvar module:attribute, kde cesta k modulu používá tečky a atribut je objekt aplikace. app.main:app znamená „proměnná app uvnitř app/main.py“. Pokud tvoje Flask aplikace používá factory pattern, gunicorn ji zavolá za tebe: gunicorn "myapp:create_app()". Uvicorn chce místo toho --factory.
Port vezmi z prostředí, ne napevno, protože port, který přidělí panel nebo platforma, není vždy 8000:
$ uvicorn app.main:app --host 0.0.0.0 --port ${SERVER_PORT:-8000}Panely postavené na Pterodactylu zveřejňují přidělený port jako proměnnou prostředí na záložce Startup; jiné platformy používají PORT. Podívej se na záložku, použij název, který tam najdeš, a nech rozumnou výchozí hodnotu pro svůj vlastní počítač.
Workery, vlákna a kolik každý stojí paměti#
Dokumentace gunicornu doporučuje (2 x cores) + 1 workerů. Ta rada předpokládá stroj, jehož jádra jsou tvoje. Na kontejneru s pevným podílem CPU půl jádra vyjde číslo, které vyčerpá tvoji paměť dřív, než někomu pomůže, protože každý worker je samostatný proces operačního systému s vlastní kopií interpretu a všech importovaných modulů.
Změř to jednou na své aplikaci pomocí ps -o rss= -p PID poté, co obsloužila nějaký provoz. Jako výchozí bod: malá Flask aplikace zabere kolem 50-80 MB na worker; FastAPI aplikace s modely pydantic a databázovým ovladačem je blíž 80-150 MB; cokoli, co importuje numpy, pandas nebo knihovnu pro strojové učení, je jiná liga a dimenzuje se jedině měřením.
| Paměť tarifu | Podíl CPU | Flask (sync workery) | FastAPI (async workery) |
|---|---|---|---|
| 1 GB | 0.5 core | 1-2 | 1 |
| 2 GB | 1 core | 2-3 | 1-2 |
| 4 GB | 1.5 cores | 3-4 | 2 |
| 6-8 GB | 2-3 cores | 4-6 | 2-3 |
Tyto sloupce počítají zhruba se 150 MB na worker a nechávají rezervu pro těla požadavků, cache a příležitostnou velkou odpověď. Async worker vychází nízko, protože nepotřebuje společníky: jeden proces uvicornu obslouží stovky souběžných požadavků, když práce spočívá v čekání na databázi. Víc workerů nad podíl CPU, který sis koupil, nic nepřinese.
Tvar tohoto výpočtu mění dvě nastavení:
--worker-class gthread --threads 4dá sync workeru gunicornu čtyři vlákna. Čtyři požadavky, které všechny čekají na databázi, mohou běžet najednou za paměťovou cenu jednoho procesu. Pro Flask aplikaci vázanou na I/O je to nastavení s nejlepším poměrem cena/výkon.--max-requests 1000 --max-requests-jitter 100po zhruba tisíci požadavcích worker vyřadí a nastartuje nový. Je to tupý nástroj, a zároveň způsob, jak přežít pomalý únik paměti v závislosti, kterou nemáš pod kontrolou. Jitter zabrání tomu, aby se všechny workery recyklovaly ve stejný okamžik.
U FastAPI platí jedno pravidlo, které přebíjí všechno ostatní: nikdy neblokuj event loop. Synchronní volání databáze, requests.get nebo výpočetně náročná smyčka uvnitř handleru async def zastaví všechny ostatní požadavky v tom procesu, dokud neskončí. FastAPI tě částečně chrání tím, že obyčejné handlery def spouští v threadpoolu s několika desítkami slotů, takže synchronní endpoint je bezpečný; endpoint async def s blokujícím kódem ne.
Přehnaný počet workerů má na kontejnerových hostech specifický způsob selhání. Na RE:NODE se server, který dosáhne limitu paměti, zastaví jádrem a čistě restartuje, místo aby se nechal swapovat, takže o jednoho workera víc se projeví jako aplikace, která pod zátěží umírá, ne jako aplikace, která zpomalí. Hlídač pádů si opakovaných restartů všimne a po třech za hodinu otevře ticket, což je užitečný signál, že se počet workerů a tarif neshodují.
Za reverzní proxy: HTTPS, IP klientů a timeouty#
V produkci před tvou aplikací něco ukončuje TLS a přeposílá jí prosté HTTP. To něco nastavuje tři hlavičky - X-Forwarded-For, X-Forwarded-Proto a X-Forwarded-Host - a tvoje aplikace je všechny ignoruje, dokud jí neřekneš, ať to nedělá.
Uvicorn v současných verzích čte přeposílané hlavičky ve výchozím stavu, ale jen z adres uvedených v --forwarded-allow-ips, což je ve výchozím stavu 127.0.0.1. Pokud je tvoje proxy na jiné adrese, hlavičky se zahodí a každý klient vypadá jako proxy. Nastav tam adresu proxy, nebo *, když se k portu nedostane nikdo jiný než proxy.
Flask potřebuje middleware:
from flask import Flaskfrom werkzeug.middleware.proxy_fix import ProxyFixapp = Flask(__name__)app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)Čísla říkají, do jaké hloubky proxy věřit. Jedna proxy znamená 1. Nastavení vyšší, než je počet proxy, které skutečně provozuješ, dovolí klientovi zfalšovat vlastní adresu tím, že hlavičku pošle sám, což je standardní způsob, jak si lidé omylem vyřadí vlastní rate limiting. Řetězec hlaviček podrobněji popisuje článek co dělá reverzní proxy.
Ještě dva problémy ve tvaru proxy. Smyčky přesměrování vznikají, když aplikace věří, že požadavek byl HTTP, přesměruje na HTTPS a proxy přepošle stejný požadavek znovu - oprava hlavičky proto smyčku vyřeší. A websockety potřebují propustit upgrade hlavičky a dost dlouhý read timeout pro nečinné spojení, což je samostatné téma v článku websockety za reverzní proxy.
Tarify pro aplikace na RE:NODE obsahují proxy slot: nasměruj A záznam na zobrazenou adresu a certifikát se vystaví a obnovuje automaticky v 21denním okně, přičemž skutečná adresa klienta přichází v X-Forwarded-For. To znamená, že výše uvedená konfigurace tu není volitelná - je to jediný způsob, jak v logu uvidíš něco jiného než jednu opakující se adresu.
Proměnné prostředí a nastavení#
Konfigurace patří do prostředí, ne do repozitáře. Vzor, který přežije kontakt s týmem, je typovaný objekt nastavení, který při startu hlasitě selže, pokud něco chybí:
from pydantic_settings import BaseSettings, SettingsConfigDictclass Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env") database_url: str secret_key: str debug: bool = Falsesettings = Settings()Chybějící DATABASE_URL teď zastaví proces při importu s jasnou zprávou, místo aby o hodinu později vyrobil chybu spojení při prvním požadavku. Flask má menší verzi téhož nápadu: app.config.from_prefixed_env() načte do konfiguračního objektu každou proměnnou začínající na FLASK_.
DATABASE_URL=postgresql://app:secret@db.example.net:5432/appSECRET_KEY=change-meDEBUG=falsePYTHONUNBUFFERED=1.env drž mimo git a skutečné hodnoty nastav na serveru. Cokoli napsané na záložku Startup v panelu vidí každý, kdo může daný server otevřít, takže spolupracovníkům, kteří potřebují jen konzoli, dej roli, která ji vylučuje - proměnné prostředí a tajné údaje probírají zbytek, včetně toho, co dělat poté, co byl klíč vložen někam, kam nepatřil.
Statické soubory, uploady a databáze#
Flask servíruje vlastní složku static/. FastAPI to dělá přes mount:
from fastapi.staticfiles import StaticFilesapp.mount("/static", StaticFiles(directory="static"), name="static")Servírovat assety z Pythonu je v malém měřítku v pořádku. Přestane to být v pořádku, když je worker zablokovaný čtením 40MB souboru pro někoho na pomalém spojení, což je na tarifu s jedním nebo dvěma workery skutečná cena. Pokud máš před sebou proxy, nech ji cachovat, co jde, a u všeho, co má v názvu souboru hash, posílej dlouhé hodnoty Cache-Control.
Uploady uživatelů nesmí jít do /tmp a nesmí jít nikam, co se při nasazení nahrazuje. Vyber adresář na trvalém disku, zapisuj tam a pamatuj, že se počítá do diskového limitu tarifu - 5 GB na nejmenším tarifu pro aplikace, 50 GB na nejvyšším. Uploady jsou také to, na co lidé zapomínají při zálohování, protože nejsou v repozitáři ani v databázi.
U databáze je číslo, které sledovat, počet spojení, ne dotazů. Každý worker gunicornu si vytváří vlastní pool spojení, takže celkem je to počet workerů krát velikost poolu:
engine = create_engine( settings.database_url, pool_size=5, max_overflow=5, pool_pre_ping=True,)Čtyři workery s touto konfigurací mohou pod zátěží otevřít čtyřicet spojení, což samo stačí k vyčerpání malého serveru PostgreSQL. Pool zmenšuj tak, jak workery zvětšuješ, a než předpokládáš, že výchozí hodnota je bezpečná, přečti si connection pooly a limity. Databázové sloty v panelu přicházejí s vygenerovaným hostitelem, uživatelem a heslem; samostatná instance PostgreSQL nebo MongoDB je tarif hostingu databází.
Migrace spouštěj dřív, než server nastartuje, ne uvnitř něj:
$ .venv/bin/alembic upgrade head && .venv/bin/uvicorn app.main:app \ --host 0.0.0.0 --port ${SERVER_PORT:-8000}Logy, health checky a čisté vypnutí#
Python buffruje stdout, když není připojený k terminálu. V konzoli panelu to vypadá jako aplikace, která deset minut nic nevypíše a pak vyhodí všechno najednou, a je to nejčastější falešný poplach v hostingu aplikací. Nastav PYTHONUNBUFFERED=1 a zmizí to.
Gunicorn zapisuje chybové logy na stderr ve výchozím stavu, ale žádný access log, dokud o něj nepožádáš: --access-logfile - ho pošle na stdout. Uvicorn loguje řádky přístupu ve výchozím stavu a --no-access-log je vypne, což stojí za to udělat, pokud tě health check dotazuje každých pět sekund a zahrabává všechno ostatní.
Health endpoint by měl být levný a neměl by sahat na databázi:
@app.get("/healthz")async def healthz(): return {"status": "ok"}Důvod je ten, že liveness kontrola, která dotazuje databázi, restartuje tvoji aplikaci, když má databáze špatnou minutu, a změní malý výpadek ve smyčku pádů. Pokud chceš kontrolu pokrývající závislosti, udělej z ní druhý endpoint a její výsledek ber jako informaci, ne jako důvod k restartu.
Oba servery zvládají SIGTERM správně: přestanou přijímat nová spojení, nechají doběhnout rozpracované požadavky a skončí. Gunicorn dává workerům --graceful-timeout sekund (výchozí je 30), než je zabije. V tu chvíli se spustí lifespan shutdown FastAPI, kde zavíráš pooly a vyprazdňuješ vše, co bylo ve bufferu. Graceful shutdown a health checky obsahují celý vzor, včetně toho, proč je požadavek trvající déle než graceful timeout problémem návrhu, ne konfigurace.
Co se může pokazit#
Log říká „Uvicorn running“, ale nic se nepřipojí. Hostitel je 127.0.0.1. Změň ho na 0.0.0.0 a restartuj.
„Address already in use“ při startu. Port stále drží předchozí proces. Použij v panelu Kill, nebo ho najdi přes ss -ltnp na stroji, kde máš shell.
`ModuleNotFoundError` po nasazení, které lokálně fungovalo. Balíček, který sis před měsíci nainstaloval ručně, se nikdy nedostal do requirements.txt. Vytvoř lokálně virtualenv znovu ze souboru a uvidíš, jak selže stejně.
Všechno je v pořádku, dokud to nepoužijí čtyři lidé najednou. Jeden sync worker bez vláken. Přidej --threads 4 a pak workery, pokud to paměť dovolí.
Požadavky umírají přesně po třiceti sekundách. Za to může --timeout gunicornu, který zabije workera, jenž neskončil. Zvyš ho, pokud je endpoint oprávněně pomalý, ale třicetisekundový webový požadavek se obvykle má stát úlohou na pozadí.
Too many redirects. Aplikace si myslí, že požadavek přišel přes HTTP. Oprav zpracování X-Forwarded-Proto dřív, než sáhneš na kód přesměrování.
IP klienta je u každého požadavku stejná. Přeposílaným hlavičkám se nevěří. Nastav --forwarded-allow-ips nebo přidej ProxyFix.
Paměť roste celý den a server se přes noc restartuje. Něco uniká. --max-requests získá čas, než to najdeš; graf paměti v konzoli ukáže, jestli jde o svah, nebo schod.
FAQ#
Potřebuju gunicorn, když už používám uvicorn?
Ne. Uvicorn umí spravovat vlastní workery přes --workers a pro většinu nasazení to stačí. Gunicorn má smysl přidat, když chceš jeho dohled nad procesy, jeho formát access logu nebo nastavení jako --max-requests, která uvicorn nemá.
Kolik workerů se vejde do 1 GB?
Jeden nebo dva. FastAPI aplikace zabere typicky 80-150 MB na worker, než vůbec začne pracovat, a kontejner musí pojmout i těla požadavků a případnou cache. Na nejmenších tarifech porazí jeden worker s vlákny nebo async souběžností dva workery soupeřící o stejnou paměť.
Je async podpora ve Flasku stejná jako ve FastAPI?
Ne. Flask spouští async view uvnitř nového event loopu a worker pořád obsazuje po celý požadavek, takže dostaneš syntaxi bez souběžnosti. FastAPI běží na event loopu, který obsluhuje všechna spojení v procesu najednou.
Můžu na stejném serveru provozovat worker na pozadí?
Ano, pokud je malý: spusť ho ze stejného příkazu přes &, aby oba procesy žily ve stejném kontejneru. Sdílí CPU a paměť tarifu s webovým procesem, takže cokoli těžkého si zaslouží vlastní server. Pro periodickou práci místo fronty je obvykle levnější odpovědí plánovač běžící uvnitř aplikace.
Potřebuju před uvicornem nginx?
Ne, pokud už něco jiného ukončuje TLS a přeposílá ti provoz. Uvicorn je schopný HTTP server. Proxy přidává certifikáty, cachování statických souborů, limity velikosti požadavků a místo pro rate limiting - pokud už jednu před sebou máš, druhá přidá jen další skok a nic víc.
Proč konzole v panelu nic neukáže, dokud aplikace nespadne?
Python buffruje stdout, protože to není terminál. Nastav v prostředí PYTHONUNBUFFERED=1 nebo předej interpretu -u a výstup se objeví tak, jak se zapisuje.




Komentáře
Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.