SSD Nodes Learn 🎉 VPS od $5.50/mies.
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-13

Django czy Flask na VPS: zużycie RAM i liczba workerow

Analiza zuzycia pamieci RAM przez pojedynczy proces Gunicorn dla Django i Flask. Sprawdz, ile workerow obsluzy serwer 1 GB i czy Twoja aplikacja wymaga pelnego frameworka.

Koszty Django i Flask na małym serwerze VPS

Wybór między Django a Flask na małym serwerze VPS to przede wszystkim kwestia zużycia pamięci RAM. Django ładuje swój obiektowo-relacyjny model danych (ORM), mechanizm migracji oraz – jeśli zostanie włączony – panel administracyjny do każdego uruchomionego procesu roboczego (worker). Flask ładuje jedynie router i obiekt żądania. Na serwerze z 1 GB pamięci RAM różnica ta determinuje liczbę procesów roboczych, które można uruchomić, a ich liczba bezpośrednio wpływa na to, ile żądań można obsługiwać jednocześnie.

Ten koszt obciąża Django tylko wtedy, gdy nie wykorzystuje się dostarczonych przez niego gotowych rozwiązań. Aplikacja wymagająca obsługi kont użytkowników, sesji oraz panelu administracyjnego wymaga Django: zużycie pamięci RAM na proces roboczy jest ceną za kod, którego nie trzeba pisać samodzielnie. Proste API JSON przed istniejącą bazą danych lepiej obsłuży Flask, ponieważ zbędne moduły nie zostaną załadowane. Jest to kwestia dopasowania narzędzia do potrzeb. Poniższe pomiary wskazują, które rozwiązanie jest odpowiednie dla danej aplikacji.

Ile pamięci zużywa jeden proces roboczy gunicorn?

ChartMemory per gunicorn worker, minimal app, three workers with preload on
The data behind this chart
[
  {
    "label": "Bare Python 3.12 process",
    "rss_mb": 14,
    "pss_mb": 9
  },
  {
    "label": "Flask, one route",
    "rss_mb": 42,
    "pss_mb": 26
  },
  {
    "label": "Flask + SQLAlchemy",
    "rss_mb": 58,
    "pss_mb": 38
  },
  {
    "label": "Django, admin disabled",
    "rss_mb": 78,
    "pss_mb": 47
  },
  {
    "label": "Django, admin enabled",
    "rss_mb": 96,
    "pss_mb": 58
  }
]

Są to typowe, opublikowane wartości dla prostej aplikacji typu "hello world" w każdym wariancie, działającej na Ubuntu 24.04 z Python 3.12, trzema procesami roboczymi gunicorn i włączoną opcją preload. Należy traktować je jako wartość minimalną, ponieważ własne importy zwiększają to zużycie. Proces roboczy Django z włączonym panelem admina wykazuje 96 MB pamięci rezydentnej (RSS), podczas gdy jego proporcjonalny udział w pamięci wynosi 58 MB. Różnica między tymi dwiema wartościami jest głównym tematem następnej sekcji.

Przeprowadź ten sam pomiar na własnym serwerze.

sudo apt update && sudo apt install -y python3-venv
python3 -m venv /srv/site1/.venv
/srv/site1/.venv/bin/pip install django gunicorn setproctitle

Zainstaluj setproctitle. Dzięki temu narzędziu gunicorn zmienia nazwy swoich procesów na gunicorn: master [site1] oraz gunicorn: worker [site1], co pozwala poniższym poleceniom na odnalezienie procesów roboczych po nazwie, zamiast zgadywania ich identyfikatorów.

pgrep -af gunicorn
ps -o pid,rss,args -p $(pgrep -d, -f 'gunicorn: worker')

Kolumna rss oznacza rozmiar zbioru rezydentnego (RSS) w kilobajtach: jest to każda strona pamięci, którą proces aktualnie przechowuje w RAM. Sumowanie tych wartości dla wszystkich procesów roboczych daje wynik zawyżony, ponieważ rozwidlony (forked) proces współdzieli strony pamięci z procesem nadrzędnym oraz procesami siostrzanymi, przez co ta sama strona jest liczona wielokrotnie. Należy zapytać jądro systemu o proporcjonalny rozmiar zbioru (PSS), który dzieli każdą współdzieloną stronę pomiędzy procesy, które ją mapują.

for pid in $(pgrep -f 'gunicorn: worker'); do awk -v p="$pid" '/^Pss:/ {printf "%s  %d MB\n", p, $2/1024}' /proc/$pid/smaps_rollup; done

Uruchom polecenie jako użytkownik będący właścicielem procesów roboczych lub z użyciem sudo. PSS jest kolumną, na której należy opierać budżet pamięci, ponieważ PSS sumuje się poprawnie, a RSS nie.

Django zajmuje więcej pamięci ze względu na działanie django.setup(). Importuje ono każdy wpis w INSTALLED_APPS, buduje rejestr aplikacji i tworzy instancję każdej klasy modelu wraz z obiektem Python dla każdego pola. Dodanie django.contrib.admin uruchamia automatyczne wykrywanie panelu admina, co importuje moduł admin każdej aplikacji i pociąga za sobą warstwy formularzy oraz szablonów. Proces roboczy Flask importuje Werkzeug oraz Jinja2 i na tym kończy.

Ważne zastrzeżenie: framework często stanowi niewielką część zużycia. Proces roboczy importujący SDK chmurowe lub biblioteki numeryczne zużywa więcej pamięci z tego tytułu niż z powodu samego Django. Zmierz zużycie swojej rzeczywistej aplikacji, zanim uznasz, że problemem jest framework.

Copy on write oraz przyczyny zmiany liczby procesów przy preload

Proces nadrzędny Gunicorn tworzy procesy potomne (worker) poprzez fork. Bezpośrednio po fork() proces potomny współdzieli każdą stronę pamięci z procesem nadrzędnym, a jądro kopiuje stronę tylko wtedy, gdy jedna ze stron dokona zapisu. Zatem to, czy rejestr modeli Django istnieje w systemie raz, czy cztery razy, zależy od tego, po której stronie operacji fork został on utworzony.

Przy wyłączonym preload_app każdy proces potomny importuje aplikację po wykonaniu forka, więc każdy z nich buduje własną, prywatną kopię. Przy włączonym preload_app proces nadrzędny importuje aplikację jednokrotnie, a procesy potomne dziedziczą te strony pamięci.

import gc

bind = "unix:/run/site1/gunicorn.sock"
umask = 0o007
workers = 3
timeout = 30
preload_app = True
max_requests = 500
max_requests_jitter = 50

def when_ready(server):
    gc.freeze()

CPython działa w sposób sprzeczny z mechanizmem copy on write. Nagłówek każdego obiektu zawiera licznik referencji, a odwołanie się do obiektu powoduje zapis w tym nagłówku. W rezultacie współdzielone strony są kopiowane jedna po drugiej, gdy mechanizm garbage collector przeszukuje stertę. gc.freeze() przenosi wszystkie dotychczas przydzielone obiekty do stałej generacji, której garbage collector już nie odwiedza, co pozwala zachować współdzielenie większej liczby stron. when_ready stanowi właściwy punkt zaczepienia (hook), ponieważ uruchamia się po załadowaniu wstępnym (preload), a przed utworzeniem pierwszego procesu potomnego. Należy zmierzyć wartość PSS przed i po dodaniu tego ustawienia, ponieważ oszczędność pamięci zależy od tego, jak duża część aplikacji jest inicjowana w czasie importu.

Preload wiąże się z jednym kosztem, który często zaskakuje podczas wdrażania. systemctl reload wysyła HUP, a udokumentowane zachowanie Gunicorn po otrzymaniu HUP polega na przeładowaniu konfiguracji i uruchomieniu nowych procesów potomnych. Gdy aplikacja jest ładowana wstępnie, kod nie jest importowany ponownie, więc nowa wersja nie jest uruchamiana, mimo że procesy potomne są nowe. Po zmianie kodu należy użyć systemctl restart lub sekwencji USR2, a następnie WINCH, jeśli wymagane jest wcześniejsze zakończenie pracy starych procesów potomnych.

Ilu pracowników (workers) może obsłużyć VPS z 1 GB RAM?

ChartWhere a 1 GB VPS goes, typical idle figures before any traffic
The data behind this chart
[
  {
    "label": "Ubuntu 24.04 base",
    "ram_mb": 190
  },
  {
    "label": "nginx",
    "ram_mb": 12
  },
  {
    "label": "PostgreSQL, default config",
    "ram_mb": 120
  },
  {
    "label": "Headroom you must leave",
    "ram_mb": 150
  },
  {
    "label": "Left for gunicorn workers",
    "ram_mb": 550
  }
]

Są to wartości dla systemu w stanie spoczynku, nieobsługującego żadnego ruchu. Na potrzeby procesów roboczych aplikacji pozostaje około 550 MB, jeszcze zanim nadejdzie pierwsze żądanie.

Należy wykonać obliczenia, przyjmując pesymistyczne założenia. Żądanie zużywa pamięć w trakcie przetwarzania: dotyczy to zarówno zbioru wyników (queryset) ładującego kilka tysięcy wierszy, jak i renderowania szablonu. Szczytowe zużycie na jednego pracownika jest zazwyczaj bliskie dwukrotności wartości w spoczynku, dlatego należy przyjąć taki margines. Django z panelem administracyjnym przy 58 MB w spoczynku pozwala na uruchomienie czterech pracowników na tej maszynie. Flask z SQLAlchemy przy 38 MB pozwala na siedmiu.

Sugestia (2 x cores) + 1 w Gunicorn zakłada, że zasobem deficytowym jest procesor, a nie pamięć RAM. Na małym VPS jest odwrotnie. Jeden współdzielony vCPU oferuje również mniej mocy obliczeniowej niż pełny rdzeń, gdy host jest obciążony. Warto to zrozumieć, zanim wina zostanie przypisana kodowi: czas kradzieży CPU przez sąsiedni proces widoczny jest w top jako wartość st.

Jeśli widoki aplikacji głównie oczekują na bazę danych lub zewnętrzny interfejs API, wątki sprawdzają się lepiej niż procesy. --worker-class gthread --workers 2 --threads 4 zapewnia osiem współbieżnych żądań przy koszcie pamięci dwóch pracowników, ponieważ wątki współdzielą jedną załadowaną kopię interpretera i frameworka. Globalna blokada interpretera (GIL) oznacza, że wątki nie przyspieszą widoku, który intensywnie wykorzystuje CPU.

Należy skonfigurować swap na serwerze. VPS z 1 GB RAM bez swapu w przypadku skoku zużycia pamięci doprowadzi do zabicia procesu, podczas gdy plik wymiany zamieni ten sam skok w wolniejsze żądanie.

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system

Następnie należy ograniczyć samą aplikację. MemoryMax=600M w jednostce gunicorn oznacza, że jądro odzyska pamięć z cgroup aplikacji, zamiast wybierać ofiarę w całym systemie. Dzięki temu niekontrolowane żądanie nie spowoduje utraty sesji SSH.

Zachowanie podczas zimnego startu i restartu

ChartImport to ready, minimal app, one shared vCPU
The data behind this chart
[
  {
    "label": "Flask, one route",
    "cold_start_ms": 90
  },
  {
    "label": "Flask + SQLAlchemy",
    "cold_start_ms": 260
  },
  {
    "label": "Django, admin disabled",
    "cold_start_ms": 480
  },
  {
    "label": "Django, admin enabled",
    "cold_start_ms": 720
  }
]

Koszt uruchomienia ponoszony jest dwukrotnie: przy każdym wdrożeniu oraz przy każdym automatycznym restarcie po awarii. Minimalna aplikacja Flask jest gotowa w około 90 ms, a Django z włączonym panelem administracyjnym potrzebuje około 720 ms na tym samym współdzielonym vCPU. Obie wartości są typowymi danymi publikowanymi dla tych frameworków. Należy przeprowadzić własne pomiary, ponieważ czas uruchomienia zależy głównie od użytych zależności.

cd /srv/site1
DJANGO_SETTINGS_MODULE=site1.settings /srv/site1/.venv/bin/python -X importtime -c "import django; django.setup()" 2>&1 | tail -20

Ostatnie linie wskazują najwolniejsze importy wraz z łącznym czasem w mikrosekundach. W przypadku Flask należy użyć tej samej flagi dla własnego modułu: python -X importtime -c "import app".

Przy włączonym preload, proces główny ponosi ten koszt raz, a każdy rozwidlony proces roboczy (worker) uruchamia się natychmiast. Przy wyłączonym preload, każdy worker ponosi ten koszt samodzielnie, a parametr timeout w gunicorn obejmuje zarówno czas uruchomienia, jak i obsługę żądania. Worker, który nie zgłosi gotowości w ciągu timeout sekund, zostaje ubity i zastąpiony nowym. W rezultacie ciężka aplikacja na wolnym, współdzielonym vCPU może utknąć w pętli restartów, nie obsługując żadnego ruchu. Dziennik zdarzeń zawiera wówczas komunikat:

[2026-08-09 09:14:02 +0000] [981] [CRITICAL] WORKER TIMEOUT (pid:1004)

Migracje powinny być częścią jednostki (unit), a nie kodu startowego aplikacji. ExecStartPre uruchamia się raz, zanim powstanie jakikolwiek worker. Umieszczenie migrate wewnątrz aplikacji powoduje, że trzy workery rywalizują ze sobą o tę samą blokadę schematu bazy danych.

Schemat wdrożenia jest niemal identyczny

Menedżer procesów

Oba frameworki działają pod kontrolą gunicorn, a gunicorn jest uruchamiany przez systemd.

[Unit]
Description=gunicorn for site1
After=network.target

[Service]
User=site1
Group=www-data
WorkingDirectory=/srv/site1
RuntimeDirectory=site1
Environment="PATH=/srv/site1/.venv/bin"
ExecStartPre=/srv/site1/.venv/bin/python manage.py migrate --noinput
ExecStart=/srv/site1/.venv/bin/gunicorn -c /srv/site1/gunicorn.conf.py site1.wsgi:application
Restart=on-failure
RestartSec=3
MemoryMax=600M

[Install]
WantedBy=multi-user.target

RuntimeDirectory=site1 tworzy /run/site1 przy starcie i usuwa go przy zatrzymaniu, dzięki czemu ścieżka do gniazda zawsze istnieje z odpowiednim właścicielem. Linia umask = 0o007 w konfiguracji gunicorn sprawia, że gniazdo jest zapisywalne dla grupy www-data, co umożliwia nginx dostęp do niego.

Jednostka Flask jest tym samym plikiem z jedną zmienioną linią: ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app, bez ExecStartPre. Argument app:app to moduł oraz obiekt wywoływalny, więc błąd Failed to find attribute 'app' in 'app'. oznacza, że moduł nie definiuje zmiennej o tej nazwie. Zadania zaplanowane realizuje się w ten sam sposób, a timer systemd zastępuje cron dla komendy zarządzającej Django bez konieczności dodawania kolejki zadań na serwerze tej wielkości.

Pliki statyczne

Django z DEBUG = False nie serwuje żadnych plików statycznych. Ustaw STATIC_ROOT, uruchom python manage.py collectstatic i wskaż serwerowi WWW katalog wyjściowy. Pominięcie tego kroku spowoduje załadowanie panelu administratora bez stylów, a dziennik zapełni się błędami Not Found: /static/admin/css/base.css.

Istnieją dwa rozsądne sposoby ich serwowania. Blok alias w nginx nie obciąża aplikacji. WhiteNoise, dodany jako middleware, serwuje pliki z poziomu procesu roboczego i pozwala uniknąć bloku w nginx, kosztem niewielkiego zużycia czasu procesora na każdy plik. Flask serwuje własny folder static/ w środowisku programistycznym, a w środowisku produkcyjnym proxy wskazuje się na ten folder z tego samego powodu.

Reverse proxy

server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /srv/site1/static/;
        expires 30d;
    }

    location / {
        proxy_pass http://unix:/run/site1/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Za każdym proxy Django musi otrzymać informację, że oryginalne żądanie było typu HTTPS, w przeciwnym razie mechanizmy ochrony przed fałszerstwem żądań między witrynami (CSRF) odrzucą własne formularze.

ALLOWED_HOSTS = ["example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Jeśli na serwerze działają już kontenery, Traefik przed kilkoma aplikacjami Docker Compose wykonuje to samo zadanie przy użyciu etykiet na kontenerze, zamiast osobnego pliku dla każdej witryny.

Wybór bazy danych

SQLite jest w pełni wystarczający dla pojedynczego serwera aplikacji z częstotliwością zapisu rzędu kilku operacji na sekundę i pozwala wyeliminować cały demon z budżetu pamięci RAM. Włącz tryb write ahead logging (WAL) i ustaw limit czasu oczekiwania (busy timeout) dla sterownika.

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
        "OPTIONS": {
            "timeout": 20,
            "init_command": "PRAGMA journal_mode=WAL;",
        },
    }
}

Bez tych dwóch opcji napotkasz django.db.utils.OperationalError: database is locked, gdy dwóch pracowników spróbuje zapisać dane jednocześnie, ponieważ domyślny tryb dziennika blokuje czytelników podczas zapisu, a domyślny limit czasu kończy operację niemal natychmiast. Szersza argumentacja, w tym informacje o tym, kiedy SQLite przestaje być odpowiednim rozwiązaniem, znajduje się w uruchamianiu SQLite w środowisku produkcyjnym na VPS.

PostgreSQL na tym samym serwerze z 1 GB RAM kosztuje 120 MB z powyższego budżetu, plus jeden proces backendu dla każdego trwałego połączenia. Ustawienie CONN_MAX_AGE w Django utrzymuje jedno otwarte połączenie na proces roboczy, więc czterech pracowników oznacza cztery backendy. Zazwyczaj jest to korzystny kompromis. Należy go jednak uwzględnić przed ustawieniem liczby pracowników. Jeśli wolisz przechowywać bazę danych w kontenerze obok aplikacji, uruchamianie Docker na VPS wiąże się z tym samym kompromisem przy wyższym progu wejścia, ponieważ demon i każdy kontener dodają narzut, który przy tej skali ma znaczenie.

Co zyskujemy i jaki jest koszt

Dodatkowe megabajty Django to zestaw gotowych i współpracujących ze sobą komponentów: ORM z obsługą migracji, system sesji i uwierzytelniania, model uprawnień, warstwa formularzy z ochroną CSRF, silnik szablonów, polecenia zarządzania oraz panel administracyjny. Panel administracyjny jest elementem często niedocenianym. Stanowi on gotowy edytor bazy danych dla modeli, wyposażony w wyszukiwarkę i filtry, dostępny po dodaniu jednej linii w INSTALLED_APPS.

Flask jest przeciwieństwem tego podejścia. Otrzymujemy routing, obiekt żądania, szablony Jinja2 oraz obiekt konfiguracji. Każdy inny element jest kwestią wyboru, co stanowi realną wartość przy małych aplikacjach, ponieważ żaden ORM nie jest ładowany, jeśli nie zostanie zaimportowany.

Pułapką jest próba znalezienia złotego środka. Dodanie SQLAlchemy do obsługi modeli, Alembic do migracji, Flask-Login do sesji, Flask-WTF do formularzy i CSRF oraz rozszerzenia administracyjnego do obsługi zaplecza powoduje stworzenie systemu o profilu pamięciowym Django, ale pozbawionego jego spójności. Każdy z tych elementów posiada własny cykl wydawniczy i odmienną koncepcję integracji z aplikacją. W tym punkcie Django staje się tańszym rozwiązaniem, zarówno pod względem zużycia pamięci RAM, jak i czasu poświęcanego na aktualizacje.

Django a Flask: zasada wyboru

Django należy wybrać, gdy aplikacja obsługuje konta użytkowników, edytowalne treści, schemat danych podlegający częstym zmianom oraz wymaga panelu administracyjnego. Flask jest odpowiedni, gdy aplikacja stanowi interfejs JSON dla istniejącego magazynu danych lub odbiornik webhooków niewymagający warstwy HTML.

Rozstrzygającym kryterium jest sporządzenie listy. Należy wypisać wszystkie pakiety, które trzeba zainstalować we Flasku, aby uzyskać wymagany zestaw funkcji. Jeśli lista zawiera ORM oraz narzędzie do migracji, oznacza to, że wybór padł na Django, a użytkownik ponosi dodatkowy koszt, próbując powoli osiągnąć ten sam rezultat.

Jeden przypadek faktycznie przemawia za Flaskiem na słabym sprzęcie: kilka małych usług na jednym serwerze. Każda usługa Flask działa jako osobny, lekki proces w ramach własnej jednostki. Trzy witryny oparte na Django na jednym VPS z 1 GB RAM oznaczają trzy instancje frameworka działające jednocześnie, co sprawia, że powyższa kalkulacja przestaje być korzystna. Jeśli zasoby nadal okazują się niewystarczające, uczciwym rozwiązaniem jest zazwyczaj wybór większego planu, a rzeczywisty koszt VPS miesięcznie jest krótszą dyskusją niż przepisywanie działającej aplikacji.

Tryby awarii i towarzyszące im komunikaty

Procesy robocze znikają i pojawiają się ponownie. Gunicorn wyświetla [ERROR] Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Komunikat ten pojawia się zarówno wtedy, gdy proces roboczy przekroczył limit czasu (timeout), jak i wtedy, gdy został przerwany przez jądro systemu. Przyczynę można rozróżnić za pomocą dmesg -T | grep -i "killed process". Wpis w tym miejscu oznacza brak pamięci, co wymaga zmniejszenia liczby procesów roboczych lub dodania przestrzeni swap.

Każda strona zwraca błąd 400, a dziennik zawiera Invalid HTTP_HOST header. Pełny komunikat to Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.. Django odrzuca żądanie przed przekazaniem go do kodu aplikacji, ponieważ ALLOWED_HOSTS jest puste lub nie zawiera nazwy hosta przekazanej przez proxy w nagłówku Host.

Formularze zwracają błąd Origin checking failed. Strona informuje o nieudanej weryfikacji CSRF. Problem występuje za proxy z terminacją TLS: aplikacja widzi zwykłe HTTP, tworzy źródło http:// i porównuje je z żądaniem, które dotarło przez https://. Należy ustawić SECURE_PROXY_SSL_HEADER oraz CSRF_TRUSTED_ORIGINS i potwierdzić, że proxy faktycznie przesyła nagłówek X-Forwarded-Proto.

nginx natychmiast zwraca błąd 502. Dziennik błędów wskazuje przyczynę: connect() to unix:/run/site1/gunicorn.sock failed (2: No such file or directory) oznacza, że jednostka nie jest uruchomiona, natomiast (13: Permission denied) oznacza, że gniazdo istnieje, ale nginx nie może go otworzyć, co wynika z ustawień umask oraz grupy.

Panel administracyjny nie posiada stylów. Polecenie collectstatic nie zostało wykonane lub ścieżka alias nie odpowiada STATIC_ROOT. Dziennik dostępu wykazuje błędy 404 dla zasobów w /static/admin/.

Operacje zapisu kończą się niepowodzeniem przy niskim obciążeniu. Błąd database is locked z SQLite oznacza, że tryb WAL jest wyłączony lub limit czasu oczekiwania (busy timeout) jest zbyt krótki dla dwóch procesów roboczych próbujących zapisać dane w tym samym momencie.

FAQ

Czy Django jest zbyt ciężkie dla VPS z 1 GB RAM?

Nie. Django z niewielką liczbą procesów roboczych (workers), Nginx jako proxy i SQLite jako bazą danych działa stabilnie na 1 GB RAM. Zasoby kończą się po dodaniu PostgreSQL z domyślnymi ustawieniami, pamięci podręcznej, procesów w tle oraz Docker na tym samym serwerze. Należy zmierzyć rozmiar PSS (proportional set size) pojedynczego procesu roboczego, podwoić go w celu uwzględnienia szczytów obciążenia i porównać wynik z ilością pamięci pozostałą po uruchomieniu systemu operacyjnego oraz bazy danych.

Ile procesów roboczych gunicorn uruchomić na jednym vCPU?

Należy zacząć od trzech i monitorować wydajność. Na małym VPS zazwyczaj ograniczeniem jest pamięć RAM, więc należy podzielić ilość pamięci pozostałą po systemie i bazie danych przez dwukrotność PSS jednego procesu roboczego. Jeśli widoki aplikacji głównie oczekują na odpowiedź bazy danych lub zewnętrznego API, warto przejść na klasę gthread z mniejszą liczbą procesów i większą liczbą wątków, ponieważ wątki współdzielą załadowany framework i zużywają znacznie mniej pamięci niż dodatkowe procesy.

Czy potrzebuję PostgreSQL, czy SQLite wystarczy?

SQLite jest wystarczający dla jednego serwera aplikacji o umiarkowanej częstotliwości zapisu i pozwala zaoszczędzić pamięć, eliminując konieczność utrzymywania osobnego demona bazy danych. Należy włączyć tryb WAL (write ahead logging) i ustawić busy timeout, w przeciwnym razie operacje zapisu współbieżnego zakończą się błędem database is locked. Na PostgreSQL należy przejść, gdy zapis musi być realizowany z więcej niż jednej maszyny lub gdy wymagane są funkcje niedostępne w SQLite, takie jak obsługa wielu intensywnych procesów zapisu lub kontrola dostępu na poziomie ról.

Czy powinienem używać uvicorn zamiast gunicorn?

Tylko w przypadku korzystania z widoków asynchronicznych i posiadania zadań wymagających oczekiwania. Flask jest aplikacją WSGI, więc widok asynchroniczny uruchamia się w nowej pętli zdarzeń wewnątrz wątku roboczego i kończy przed rozpoczęciem kolejnego żądania, co nie zwiększa współbieżności. Widoki asynchroniczne Django wymagają serwera ASGI, aby uzyskać jakiekolwiek korzyści. Nowsze wydania uvicorn przeniosły klasę procesu roboczego gunicorn do osobnego pakietu, dlatego należy zapoznać się z aktualną dokumentacją uvicorn, zamiast kopiować przestarzałe flagi klasy procesu roboczego ze starszych poradników.

Dlaczego proces roboczy zniknął bez śladu w dzienniku?

Proces zabity przez mechanizm OOM (out of memory) killera otrzymuje sygnał SIGKILL i nie może zapisać żadnych informacji przed zamknięciem, dlatego dziennik aplikacji po prostu się urywa. Gunicorn zauważa lukę i wypisuje Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Należy to potwierdzić za pomocą dmesg -T | grep -i "killed process". Rozwiązaniem jest zmniejszenie liczby procesów roboczych lub utworzenie pliku wymiany (swapfile), dzięki czemu nagły skok zużycia pamięci spowoduje jedynie spowolnienie żądania, a nie nagłe przerwanie procesu.

#django#flask#python#gunicorn#deployment#vps