SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Django или Flask на VPS: сравнение потребления RAM

Узнайте реальное потребление оперативной памяти Django и Flask на 1-2 GB VPS. Мы измерили Resident Set Size для Gunicorn и рассчитали лимит рабочих процессов для вашего сервера.

Стоимость использования Django и Flask на небольшом VPS

Выбор между Django и Flask на небольшом VPS — это прежде всего вопрос потребления оперативной памяти. Django загружает свой объектно-реляционный маппер (ORM), механизмы миграций и, если вы его активируете, панель администратора в каждый запускаемый рабочий процесс (worker). Flask загружает только маршрутизатор и объект запроса. На сервере с 1 GB RAM эта разница определяет количество рабочих процессов, которые можно запустить, а количество процессов напрямую влияет на число одновременно обрабатываемых запросов.

Эти затраты оправданы для Django, если вы не планируете переписывать предоставляемый им функционал с нуля. Приложение с учетными записями пользователей, сессиями и панелью администратора требует Django: потребление RAM на каждый процесс — это цена кода, который вам не придется писать. JSON API для уже существующего хранилища данных лучше реализовать на Flask, так как большинство встроенных функций Django в этом случае не потребуются. Это вопрос соответствия инструментов задаче. Приведенные ниже измерения помогут определить, какой вариант подходит для вашего приложения.

Сколько памяти потребляет один воркер 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
  }
]

Это типичные опубликованные показатели для приложения «hello world» каждого типа на Ubuntu 24.04 с Python 3.12, тремя воркерами gunicorn и включенной опцией preload. Рассматривайте их как нижний предел, так как ваши собственные импорты добавляются к этому объему. Воркер Django с включенной панелью администратора занимает 96 МБ резидентной памяти, в то время как его пропорциональная доля памяти составляет 58 МБ. Разница между этими двумя числами — главная тема следующего раздела.

Выполните аналогичные измерения на своем сервере.

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

Установите setproctitle. При его наличии gunicorn переименовывает свои процессы в gunicorn: master [site1] и gunicorn: worker [site1], что позволяет следующим командам находить воркеры по имени, а не путем угадывания.

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

Столбец rss отображает размер резидентного набора (RSS) в килобайтах: это каждая страница памяти, которую процесс в данный момент удерживает в RAM. Суммирование этого значения для всех воркеров дает завышенный результат, так как форкнутый воркер разделяет страницы памяти с родительским процессом и другими воркерами, поэтому одна и та же страница учитывается несколько раз. Вместо этого запросите у ядра пропорциональный размер набора (PSS), который распределяет каждую разделяемую страницу между процессами, использующими её.

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

Запустите команду от имени пользователя, которому принадлежат воркеры, или с использованием sudo. PSS — это столбец, на который стоит ориентироваться при планировании ресурсов, так как PSS суммируется корректно, а RSS — нет.

Django потребляет больше памяти из-за того, что делает django.setup(). Он импортирует каждую запись в INSTALLED_APPS, строит реестр приложений и создает экземпляр каждого класса модели вместе с объектом Python для каждого поля в нем. Добавление django.contrib.admin запускает автообнаружение административной панели, которое импортирует модуль admin каждого приложения и подтягивает за собой уровни форм и шаблонов. Воркер Flask импортирует Werkzeug и Jinja2, после чего останавливается.

Важное замечание: фреймворк часто занимает лишь малую часть памяти. Воркер, который импортирует облачный SDK или библиотеки для вычислений, потребляет больше ресурсов из-за них, чем из-за Django. Измерьте показатели вашего реального приложения, прежде чем делать вывод, что проблема во фреймворке.

Copy-on-write и причины изменения количества процессов при предзагрузке

Главный процесс Gunicorn создает рабочие процессы (workers) с помощью fork. Сразу после fork() дочерний процесс использует те же страницы памяти, что и родительский, а ядро копирует страницу только в момент записи в неё одной из сторон. Таким образом, будет ли реестр моделей Django существовать в единственном экземпляре или в четырех на одной машине, зависит от того, в какой момент после fork он был создан.

Если preload_app отключен, каждый рабочий процесс импортирует приложение уже после fork, поэтому каждый из них создает собственную закрытую копию. Если он включен, главный процесс импортирует приложение один раз, а рабочие процессы наследуют эти страницы памяти.

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 работает вопреки принципу copy-on-write. Заголовок каждого объекта содержит счетчик ссылок, и обращение к объекту приводит к записи в этот заголовок. В результате общие страницы памяти копируются по одной по мере того, как сборщик мусора обходит кучу. gc.freeze() перемещает все выделенные к этому моменту объекты в постоянное поколение, которое сборщик больше не посещает, что позволяет сохранить больше страниц общими. when_ready является подходящим хуком, так как он выполняется после предзагрузки и до того, как будет создан первый рабочий процесс. Измерьте PSS до и после добавления этого параметра, так как экономия зависит от того, какой объем состояния вашего приложения формируется во время импорта.

У предзагрузки есть один нюанс, который часто становится неожиданностью при развертывании. systemctl reload отправляет HUP, а задокументированное поведение Gunicorn при получении HUP заключается в перезагрузке конфигурации и запуске новых рабочих процессов. Когда приложение предзагружено, оно не выполняет повторный импорт кода, поэтому новая версия не будет запущена, даже если процессы рабочих были обновлены. Используйте systemctl restart после внесения изменений в код или последовательность USR2, а затем WINCH, если необходимо, чтобы старые рабочие процессы завершили обработку текущих запросов.

Сколько воркеров реально может запустить VPS с 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
  }
]

Это показатели простоя на сервере, который ничего не обслуживает. Около 550 MB остается для воркеров приложения, и это еще до поступления первого запроса.

Теперь разделите, причем пессимистично. Запрос потребляет память во время выполнения: например, queryset, загружающий несколько тысяч строк, а затем рендеринг шаблона. Пиковое потребление на одного воркера обычно почти вдвое выше показателя простоя, поэтому закладывайте двойной объем. Django с панелью администратора при 58 MB в простое даст вам четыре воркера на этой машине. Flask с SQLAlchemy при 38 MB даст семь.

Рекомендация (2 x cores) + 1 для Gunicorn предполагает, что дефицитным ресурсом является CPU, а не RAM. На маленьком VPS всё наоборот. Один общий vCPU также дает вам меньше производительности, чем полноценное ядро, когда хост загружен. Это стоит понимать, прежде чем винить свой код: CPU steal time от «шумного соседа» отображается в top как показатель st.

Если ваши представления (views) в основном ожидают ответа от базы данных или внешнего API, потоки (threads) здесь эффективнее процессов. --worker-class gthread --workers 2 --threads 4 дает восемь одновременных запросов при затратах памяти как у двух воркеров, поскольку потоки используют одну загруженную копию интерпретатора и фреймворка. Global interpreter lock означает, что потоки не помогут представлению, которое интенсивно нагружает CPU.

Добавьте на машину swap. VPS с 1 GB RAM без swap превращает скачок потребления памяти в завершение процесса (OOM kill), тогда как swapfile превращает тот же скачок в медленный запрос.

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

Затем ограничьте само приложение. MemoryMax=600M в юните gunicorn означает, что ядро заберет память обратно из cgroup вашего приложения, вместо того чтобы выбирать жертву во всей системе. Таким образом, вышедший из-под контроля запрос не приведет к потере вашей SSH-сессии.

Поведение при холодном старте и перезапуске

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
  }
]

Стоимость загрузки оплачивается дважды: при каждом развертывании и при каждом автоматическом перезапуске после сбоя. Минимальное приложение на Flask готово примерно через 90 мс, а Django с включенной панелью администратора — примерно через 720 мс на том же общем vCPU. Оба значения являются типичными опубликованными показателями. Измерьте свои собственные, так как основную нагрузку создают ваши зависимости.

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

Последние строки содержат список самых медленных импортов с указанием совокупного времени в микросекундах. Для Flask запустите тот же флаг для вашего модуля: python -X importtime -c "import app".

При включенном preload мастер-процесс оплачивает эту стоимость один раз, и каждый форкнутый воркер запускается мгновенно. При выключенном preload каждый воркер оплачивает её самостоятельно, а timeout в gunicorn учитывает как время загрузки, так и время обработки запроса. Воркер, который не отправил сигнал о готовности в течение timeout секунд, завершается и заменяется, поэтому тяжелое приложение на медленном общем vCPU может попасть в цикл перезапусков и не обработать ни одного запроса. В логе будет указано:

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

Миграции должны выполняться в юните, а не в коде запуска приложения. ExecStartPre выполняется один раз до появления воркеров. Размещение migrate внутри приложения приведет к тому, что три воркера будут конкурировать друг с другом за одну и ту же блокировку схемы базы данных.

Схема развертывания практически идентична

Менеджер процессов

Оба фреймворка работают под управлением gunicorn, а gunicorn запускается через 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 создает /run/site1 при запуске и удаляет его при остановке, поэтому путь к сокету всегда существует с нужным владельцем. Строка umask = 0o007 в конфигурации gunicorn позволяет группе www-data записывать данные в этот сокет, что обеспечивает доступ для nginx.

Юнит для Flask выглядит так же, но с одним измененным параметром: ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app, и без ExecStartPre. Аргумент app:app указывает на модуль и вызываемый объект, поэтому ошибка Failed to find attribute 'app' in 'app'. означает, что в вашем модуле нет переменной с таким именем. Плановые задачи настраиваются по тому же принципу, и systemd timer заменяет cron для команды управления Django, что позволяет избежать добавления очереди задач на сервер такого размера.

Статические файлы

Django с DEBUG = False не отдает статические файлы самостоятельно. Установите STATIC_ROOT, выполните python manage.py collectstatic и укажите веб-серверу путь к выходной директории. Если пропустить этот шаг, админ-панель загрузится без стилей, а лог заполнится записями Not Found: /static/admin/css/base.css.

Существует два рациональных способа их отдачи. Блок alias в nginx не создает нагрузки на приложение. WhiteNoise, добавленный в качестве middleware, отдает файлы силами воркера и избавляет от необходимости настраивать блок в nginx, но потребляет немного ресурсов воркера на каждый файл. Flask отдает содержимое папки static/ в режиме разработки, а в продакшене вы указываете прокси-серверу на эту папку по той же причине.

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;
    }
}

При работе за прокси Django необходимо сообщить, что исходный запрос был выполнен по HTTPS, иначе проверки защиты от подделки межсайтовых запросов (CSRF) будут отклонять ваши формы.

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

Если на сервере уже запущены контейнеры, Traefik перед несколькими приложениями Docker Compose выполняет ту же задачу с помощью меток (labels) на контейнерах вместо отдельного файла конфигурации для каждого сайта.

Выбор базы данных

SQLite вполне подходит для сервера с одним приложением при интенсивности записи в несколько запросов в секунду, при этом она исключает целый демон из бюджета оперативной памяти. Включите режим write ahead logging (WAL) и задайте драйверу время ожидания (busy timeout).

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

Без этих двух параметров вы столкнетесь с django.db.utils.OperationalError: database is locked при первой же попытке одновременной записи двумя воркерами, так как стандартный режим журнала блокирует чтение во время записи, а стандартное время ожидания истекает почти мгновенно. Подробный разбор, включая момент, когда SQLite перестает быть подходящим решением, приведен в статье использование SQLite в продакшене на VPS.

PostgreSQL на том же сервере с 1 GB оперативной памяти потребует 120 MB из указанного выше бюджета, плюс по одному фоновому процессу на каждое постоянное соединение. Параметр CONN_MAX_AGE в Django удерживает одно соединение на воркер, поэтому четыре воркера означают четыре фоновых процесса. Обычно это оправданный компромисс. Просто учитывайте это перед настройкой количества воркеров. Если вы предпочитаете держать базу данных в контейнере рядом с приложением, запуск Docker на VPS потребует аналогичных затрат, но с более высоким порогом потребления, так как демон и каждый контейнер добавляют накладные расходы, которые заметны при таком объеме памяти.

Что вы получаете «из коробки» и во сколько это обходится

Дополнительные мегабайты Django — это набор компонентов, которые уже существуют и работают вместе: ORM с поддержкой миграций, система сессий и аутентификации, модель прав доступа, уровень форм с защитой от CSRF, шаблонизатор, команды управления и панель администратора. Панель администратора — это то, что часто недооценивают. Вы получаете готовый редактор базы данных для ваших моделей с поиском и фильтрами, добавив всего одну строку в INSTALLED_APPS.

Flask — это зеркальное отражение. Вы получаете маршрутизацию, объект запроса, шаблоны Jinja2 и объект конфигурации. Всё остальное — ваш выбор, что является преимуществом для небольших приложений: если вы не импортируете ORM, она не занимает память.

Ловушка кроется в промежуточном варианте. Если вы добавите SQLAlchemy для моделей, Alembic для миграций, Flask-Login для сессий, Flask-WTF для форм и CSRF, а также стороннее расширение для панели администратора, вы соберете систему с потреблением памяти как у Django, но без её целостности. У каждого компонента свой цикл релизов и свои представления о том, как должно быть устроено приложение. Именно в этот момент Django становится более выгодным решением — как с точки зрения потребления RAM, так и с точки зрения времени, затрачиваемого на обновления.

Django против Flask: критерии выбора

Используйте Django, если в приложении есть учетные записи пользователей, редактируемый контент, схема данных, которая будет постоянно меняться, и панель администратора, которой действительно будут пользоваться. Используйте Flask, если приложение представляет собой JSON-интерфейс для уже существующего хранилища данных или является обработчиком вебхуков без HTML-интерфейса.

Решающим фактором является составленный список. Выпишите все пакеты, которые вам придется установить во Flask для реализации необходимого функционала. Если в этом списке присутствуют ORM и инструмент для миграций, значит, вы фактически выбрали Django, но тратите лишнее время, чтобы прийти к этому решению медленным путем.

Один сценарий действительно делает Flask предпочтительным на маломощном оборудовании: запуск нескольких небольших сервисов на одном узле. Каждый Flask-сервис — это отдельный легковесный процесс под управлением собственного юнита. Три сайта на Django на одном VPS с 1 GB оперативной памяти означают три одновременно запущенные копии фреймворка, и приведенная выше экономия перестает работать. Если ресурсов постоянно не хватает, честным решением часто является переход на более дорогой тариф, а стоимость VPS в месяц — это более короткий разговор, чем переписывание работающего приложения.

Типовые сбои и сообщения об ошибках

Воркеры исчезают и появляются снова. Gunicorn выводит [ERROR] Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Это сообщение появляется как при принудительном завершении воркера, пропустившего heartbeat, так и при завершении процесса ядром ОС. Различить эти случаи поможет dmesg -T | grep -i "killed process". Наличие записи там указывает на нехватку памяти: уменьшите количество воркеров или добавьте swap.

Все страницы возвращают 400, а в логе указано Invalid HTTP_HOST header. Полное сообщение выглядит как Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.. Django отклоняет запрос до передачи в ваш код, так как ALLOWED_HOSTS пуст или не содержит имя хоста, переданное прокси в заголовке Host.

Формы не работают с ошибкой Origin checking failed. Страница сообщает о сбое проверки CSRF. Это происходит при использовании прокси с TLS termination: приложение видит обычный HTTP, формирует origin http:// и сравнивает его с запросом, поступившим по https://. Установите SECURE_PROXY_SSL_HEADER и CSRF_TRUSTED_ORIGINS, а также убедитесь, что прокси действительно передает заголовок X-Forwarded-Proto.

nginx немедленно возвращает 502. Причина указана в логе ошибок: connect() to unix:/run/site1/gunicorn.sock failed (2: No such file or directory) означает, что юнит не запущен, а (13: Permission denied) означает, что сокет существует, но nginx не может его открыть из-за настроек umask и группы.

В админ-панели отсутствует оформление. Команда collectstatic не была выполнена, либо путь alias не совпадает с STATIC_ROOT. В логе доступа видны ошибки 404 для пути /static/admin/.

Запись данных завершается с ошибкой при низкой нагрузке. Ошибка database is locked от SQLite означает, что режим WAL отключен или таймаут ожидания (busy timeout) слишком мал для одновременной записи двумя воркерами.

FAQ

Слишком ли Django тяжел для VPS с 1 GB RAM?

Нет. Django с небольшим количеством воркеров, nginx в качестве фронтенда и SQLite в качестве базы данных стабильно работает на 1 GB. Проблемы начинаются, если добавить на тот же сервер PostgreSQL с настройками по умолчанию, кэш, фоновые задачи и Docker. Оцените размер резидентной памяти (proportional set size) одного воркера, удвойте его для учета пиковых нагрузок и сравните полученное значение с объемом памяти, оставшимся после запуска операционной системы и базы данных.

Сколько воркеров gunicorn запускать на одном vCPU?

Начните с трех и проведите замеры. Обычно на небольших VPS ограничивающим фактором является память, поэтому разделите объем RAM, оставшийся после ОС и базы данных, на удвоенный размер памяти одного воркера. Если ваши представления (views) в основном ожидают ответа от базы данных или внешнего API, переключитесь на класс воркеров gthread с небольшим количеством процессов и несколькими потоками в каждом. Потоки используют одну загруженную копию фреймворка и потребляют значительно меньше памяти, чем дополнительные процессы.

Нужен ли мне PostgreSQL или достаточно SQLite?

SQLite подходит для одного сервера приложений с умеренной интенсивностью записи, к тому же это избавляет от необходимости держать в памяти отдельный демон базы данных. Включите режим write ahead logging и установите busy timeout, иначе параллельные операции записи будут завершаться с ошибкой database is locked. Переходите на PostgreSQL, если запись должна осуществляться с нескольких машин или если вам требуются функции, отсутствующие в SQLite, например, работа с большим количеством параллельных процессов записи или контроль доступа на уровне ролей.

Стоит ли запускать uvicorn вместо gunicorn?

Только если у вас есть асинхронные представления и реальные задачи, требующие ожидания. Flask — это WSGI-приложение, поэтому асинхронное представление запускается в новом цикле событий внутри потока воркера и завершается до начала обработки следующего запроса, что не дает прироста конкурентности. Асинхронным представлениям Django для работы требуется ASGI-сервер. В последних релизах uvicorn класс воркера для gunicorn был вынесен в отдельный пакет, поэтому изучите актуальную документацию uvicorn, вместо того чтобы копировать устаревшие флаги классов воркеров из старых руководств.

Почему мой воркер исчез без записи в трассировке?

Процесс, завершенный механизмом OOM killer ядра из-за нехватки памяти, получает сигнал SIGKILL и не успевает ничего записать в лог, поэтому логи приложения просто обрываются. Gunicorn фиксирует этот разрыв и выводит сообщение Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Подтвердите это с помощью dmesg -T | grep -i "killed process". Решение заключается в уменьшении количества воркеров или создании swap-файла, чтобы скачок потребления памяти приводил к замедлению запроса, а не к гибели процесса.

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