На небольшом сервере правильное число воркеров gunicorn - обычно один или два, а не (2 x cores) + 1 из документации. Дальше важны настройки, которые мало кто задаёт: timeout, соответствующий вашему самому медленному честному запросу, max_requests с jitter, чтобы убирать протекающие воркеры, preload_app - только если ваш код переживает fork, и keepalive, подходящий к прокси перед приложением. У uvicorn настроек меньше, но форма та же: --workers для процессов, --timeout-keep-alive, --limit-max-requests. Эта статья о настройке и эксплуатации. Сколько памяти занимает каждый воркер - отдельный вопрос, он разобран в статье сколько RAM нужно Python-приложению.
Всё здесь написано для контейнера с жёстким лимитом памяти и жёсткой долей CPU - так выглядит Python-тариф за $4-$7, - но подходит и для любого небольшого VPS. Если вы ещё разбираетесь с самым началом, то есть как выложить приложение, сначала прочитайте деплой FastAPI или Flask или деплой Django в продакшен и возвращайтесь.
Что на самом деле запускают gunicorn и uvicorn#
Gunicorn - это менеджер процессов, который заодно говорит по HTTP. Есть один мастер-процесс, который никогда не трогает запросы. Он слушает сокет, порождает воркеры, следит за ними и заменяет тех, кто умер или завис. Каждый воркер - полноценный интерпретатор Python со своей копией вашего приложения, своими импортированными модулями, своими подключениями к базе и своими кэшами. Между воркерами общий только слушающий сокет, поэтому счётчик в переменной уровня модуля считает отдельно в каждом воркере, а кэш в памяти - это на самом деле N отдельных кэшей.
Uvicorn - сервер для ASGI-приложений (FastAPI, Starlette, Django с asgi.py). Один процесс uvicorn запускает один цикл событий и обслуживает много запросов одновременно, пока ваш код использует await, а не блокируется. С --workers N он превращается в небольшой супервизор вроде мастера gunicorn: запускает N процессов, у каждого свой цикл, и перезапускает любой, который завершился.
Из этого следуют два вывода, на которых держатся все решения ниже. Во-первых, каждый воркер стоит полной копии приложения в памяти, поэтому на тарифе с лимитом памяти число воркеров - это бюджет, а не ручка настройки. Во-вторых, конкурентность внутри воркера зависит от класса воркера: sync-воркер обрабатывает ровно один запрос за раз, потоковый - по одному на поток, асинхронный - столько, сколько его цикл событий способен чередовать.
Команда gunicorn по умолчанию слушает 127.0.0.1:8000 и запускает один sync-воркер. Внутри контейнера этот адрес недоступен снаружи, поэтому первое изменение на любом хостинге - --bind 0.0.0.0:<port>, где порт берётся из окружения. На Python-сервере RE:NODE выделенный порт показан на вкладке Startup; в соседних статьях для него используется SERVER_PORT, так что проверьте имя на своей вкладке, прежде чем копировать строку.
Как выбрать число воркеров#
Формула (2 x cores) + 1 из документации предполагает ядра, которые принадлежат вам, и память, которую вы не считали. На небольшом тарифе не верно ни то, ни другое. Доля CPU - это жёсткое ограничение: тариф с половиной ядра даёт всему дереву процессов половину ядра времени, сколько бы воркеров вы ни запустили. Лишние воркеры не ускоряют приложение, они делят те же полядра на большее число частей и умножают счёт за память.
Отталкивайтесь по очереди от двух лимитов.
- CPU. Для приложения, упирающегося в процессор, воркеры сверх
ceil(cpu share)ничего не дают. Для приложения, которое в основном ждёт базу или внешний API, важнее не число воркеров, а число запросов в работе на воркер - для этого и нужны потоки и async. - Память. Измерьте резидентный размер одного воркера после реального трафика, умножьте на число воркеров, прибавьте мастер (несколько десятков МБ) и оставьте свободной не меньше четверти лимита на всплески запросов и собственный рост интерпретатора.
$ ps -o pid,rss,cmd -C gunicorn PID RSS CMD 4121 31204 gunicorn: master [app.main:app] 4128 96740 gunicorn: worker [app.main:app] 4129 97112 gunicorn: worker [app.main:app]RSS указан в килобайтах. Запускайте команду после нескольких сотен запросов, а не сразу после старта: воркер, который ещё не импортировал лениво загружаемые модули и не прогрел кэши, выглядит меньше, чем будет через час.
Отправная точка для Python-линейки в предположении около 100 МБ на воркер, что типично для приложения на FastAPI или Flask с ORM и драйвером базы (у вас может быть вдвое больше):
| Тариф | Память / доля CPU | Начальная конфигурация |
|---|---|---|
| Starter | 1 GB / 0.5 core | 1 воркер, 4 потока или async |
| Standard | 2 GB / 1 core | 2 воркера, по 2-4 потока |
| Advanced | 4 GB / 1.5 cores | 2-3 воркера |
| Pro | 6 GB / 2 cores | 3-4 воркера |
Все ступени перечислены на странице тарифов Python, от $4 в месяц. Обратите внимание, что на старших строках первым лимитом редко бывает память - им становится доля CPU. После двух-трёх воркеров на однозначной доле ядер вы в основном покупаете переключения контекста.
Есть ещё одна причина предпочесть два воркера одному, если это по карману: единственный воркер - единственная точка зависания. Если он упёрся в медленный запрос, паузу сборщика мусора или перезапуск, сервис недоступен всё это время. Два воркера означают, что второй продолжает отвечать, пока первый восстанавливается. На тарифе в 1 GB такой компромисс часто недоступен, и именно тогда потоки или async оправдывают себя.
Классы воркеров: sync, gthread и uvicorn#
worker_class в gunicorn определяет, как воркер обрабатывает запросы, и это главный рычаг после их числа.
| Класс воркера | Конкурентность на воркер | Для чего |
|---|---|---|
sync (по умолчанию) | 1 запрос | Flask или Django, упор в CPU, много воркеров |
gthread | threads запросов | Flask или Django в ожидании базы |
uvicorn_worker.UvicornWorker | Цикл событий | FastAPI, Starlette, Django ASGI |
`sync` прост и предсказуем, и именно ему сильнее всего нужен буферизующий прокси впереди: sync-воркер занят всё время, пока клиент досылает свой запрос, так что медленное соединение занимает воркер целиком. За прокси, который читает запрос полностью, прежде чем переслать его, это не проблема.
`gthread` даёт каждому воркеру пул потоков. Четыре потока в одном процессе обслуживают четыре запроса, которые все ждут базу, за память примерно одного процесса - это лучший по соотношению цены и пользы вариант для WSGI-приложения с упором в ввод-вывод на небольшом тарифе:
$ gunicorn wsgi:app --bind 0.0.0.0:8000 \ --workers 2 --worker-class gthread --threads 4Следите за арифметикой ниже по течению: два воркера по четыре потока могут одновременно держать восемь подключений к базе. Размер пула и тариф базы подбирайте под это число, а не под число воркеров. Если указать --threads больше 1 при классе sync по умолчанию, в современных версиях класс сам переключится на gthread, но явная запись избавляет от догадок.
Воркеры uvicorn под gunicorn дают надзор gunicorn вместе с циклом событий uvicorn:
$ gunicorn app.main:app -k uvicorn_worker.UvicornWorker \ --bind 0.0.0.0:8000 --workers 2Для этого нужен пакет uvicorn-worker. Старый uvicorn.workers.UvicornWorker, который поставлялся внутри uvicorn, объявлен устаревшим и в закреплённой у вас версии может уже отсутствовать, поэтому перед копированием строки из старого руководства проверьте pip show uvicorn. Можно и вовсе обойтись без gunicorn:
$ uvicorn app.main:app --host 0.0.0.0 --port ${SERVER_PORT:-8000} --workers 2Что выбрать? Собственные --workers у uvicorn - на одну зависимость меньше, и для небольшого приложения этого вполне достаточно. Gunicorn добавляет зрелый набор эксплуатационных возможностей: max_requests с jitter, таймауты на воркер, которые убивают зависший процесс, хуки post_fork, давно устоявшийся протокол сигналов и более тонкую настройку логов. Uvicorn в последних релизах сокращает отставание (см. раздел про перезагрузку), но если нужна уверенность, что зависший воркер будет убит, gunicorn пока надёжнее как супервизор.
Правило для async-воркеров: никогда не блокируйте цикл событий. Синхронный вызов базы или requests.get внутри обработчика async def останавливает все остальные запросы в этом процессе, пока он не вернётся. Заблокированный цикл не вызывает ошибки. Он проявляется как задержки, а под gunicorn в итоге как WORKER TIMEOUT, потому что цикл же ещё и отправляет heartbeat воркера мастеру.
Таймауты, которые убивают запросы#
timeout в gunicorn (по умолчанию 30 секунд) понимают неверно чаще всего. Это не таймаут запроса. Это максимальное время, в течение которого воркер может не сообщать мастеру, что он жив. У sync-воркеров heartbeat происходит между запросами, поэтому на практике он ведёт себя как лимит на запрос: запрос на 31 секунду стоит воркеру жизни. Строка в логе ни с чем не спутается:
[2026-10-17 09:14:02 +0000] [4121] [CRITICAL] WORKER TIMEOUT (pid:4128)[2026-10-17 09:14:03 +0000] [4128] [INFO] Worker exiting (pid: 4128)[2026-10-17 09:14:03 +0000] [4140] [INFO] Booting worker with pid: 4140У gthread и async-воркеров heartbeat идёт из другого места, поэтому медленный запрос сам по себе таймаут не вызывает; его вызывает заблокированный главный поток или цикл событий. Эта разница важна при отладке: WORKER TIMEOUT на async-воркере означает, что цикл что-то заморозило, а не что один запрос шёл долго.
Какие таймауты участвуют и кому они принадлежат:
| Настройка | По умолчанию | Что ограничивает |
|---|---|---|
gunicorn timeout | 30 | Молчание воркера до его убийства |
gunicorn graceful_timeout | 30 | Сколько останавливающийся воркер может доделывать запросы |
gunicorn keepalive | 2 | Простой keep-alive-соединения в секундах |
uvicorn --timeout-keep-alive | 5 | Простой keep-alive-соединения в секундах |
uvicorn --timeout-graceful-shutdown | нет | Ожидание соединений при остановке |
Поднять timeout до 120 из-за одного медленного отчёта - распространённый неверный ответ. Это же позволяет любому по-настоящему зависшему воркеру простаивать две минуты на тарифе, где их всего два. Лучшие варианты по порядку:
- Сделайте медленное быстрым: индекс, запрос поменьше, кэш.
- Вынесите это из запроса: верните
202 Accepted, выполните работу в фоновой задаче, затем опрашивайте результат. - Если оно обязано быть синхронным, поднимите
timeoutи напишите рядом комментарий, почему, и оставьте потоки или лишний воркер, чтобы один долгий запрос не забирал с собой всё приложение.
У uvicorn вообще нет таймаута запроса. Если внешний вызов завис, завис и обработчик, поэтому задавайте таймауты на каждом исходящем клиенте (httpx.AsyncClient(timeout=10), statement_timeout базы), а не ждите, что вас спасёт сервер. У прокси перед приложением тоже есть свой таймаут чтения, обычно больше, чем по умолчанию у gunicorn; если прокси сдастся первым, клиент увидит 504, а воркер будет занят ещё несколько секунд.
Наконец, worker_tmp_dir. Gunicorn пишет файлы heartbeat во временный каталог, а в контейнерах, где он лежит на медленной или оверлейной файловой системе, эти записи могут блокироваться и вызывать ложные таймауты. Документированное решение - каталог в памяти:
$ gunicorn app.main:app --worker-tmp-dir /dev/shmPreload, хуки и ловушка fork#
По умолчанию каждый воркер сам импортирует ваше приложение уже после fork. С --preload (или preload_app = True) мастер импортирует его один раз до порождения воркеров. Отсюда следуют две вещи.
Что вы получаете. Ошибки запуска всплывают сразу в мастере, а не в виде цикла неудачных стартов каждого воркера. Воркеры стартуют быстрее, а страницы с импортированным кодом могут разделяться между процессами по принципу copy-on-write, так что суммарная память меньше, чем у N независимых импортов.
На что не стоит рассчитывать. Подсчёт ссылок в Python пишет в объекты при каждом использовании, а это пачкает страницы и со временем прекращает их совместное использование. Экономия настоящая, но частичная: ожидайте, что она уменьшается по мере старения воркеров. Прежде чем решать, стоит ли это усложнения, измерьте резидентную память с --preload и без него.
Чем вы платите. Всё, что создано при импорте, создаётся один раз и наследуется каждым воркером. Для конфигурации это безвредно, а для подключений - катастрофа:
- Движок базы или подключение, открытое при импорте, теперь делят несколько процессов через один и тот же сокет. Запросы перемешиваются, и вы получаете ошибки, которые проявляются только под нагрузкой и лишь иногда.
- Потоки, запущенные при импорте, существуют только в мастере. Воркеры их не получают.
- Зерна случайных чисел и прочее состояние процесса копируются, поэтому воркеры ведут себя одинаково там, где вы ждали разного.
Решение - создавать подключения после fork, в хуке. Конфигурационный файл gunicorn - обычный Python, поэтому хуки - это функции:
def post_fork(server, worker): # runs in each worker after the fork, before it serves anything from app.db import engine engine.dispose(close=False)engine.dispose(close=False) - способ SQLAlchemy 2.x выбросить пул, унаследованный от мастера, не закрывая подключения, которыми мастер ещё владеет. Для любой другой клиентской библиотеки схема та же: выбросьте то, что создал мастер, и дайте каждому воркеру открыть своё. Если ваше приложение открывает подключения лениво при первом использовании, как большинство ORM, preload обычно безопасен как есть.
У preload есть ещё одно взаимодействие, о которое спотыкаются при деплоях, оно разобрано в следующих разделах: при --preload сигнал HUP перечитывает конфиг, но не импортирует код заново, потому что код загружен в мастере, а мастер не перезапускался.
Keepalive и прокси перед приложением#
HTTP keep-alive позволяет клиенту использовать одно TCP-соединение для многих запросов. Для небольшого приложения важно соединение между прокси и вашим сервером. На тарифах для приложений и веб-хостинга RE:NODE входит слот обратного прокси, который терминирует TLS и пересылает обычный HTTP на ваш порт, передавая реальный адрес клиента в X-Forwarded-For. Общую картину см. в статье что делает обратный прокси.
Две настройки:
- `keepalive` в gunicorn, по умолчанию 2 секунды, применяется только к классам воркеров
gthreadи async. Воркерsyncзакрывает соединение после каждого ответа, потому что не может позволить себе ждать простаивающее соединение, когда он единственный, кто обслуживает трафик. - `--timeout-keep-alive` в uvicorn, по умолчанию 5 секунд.
Классическая ошибка - гонка. Прокси держит простаивающее соединение к вашему приложению и отправляет по нему запрос в тот же миг, когда ваш сервер решает, что оно простояло достаточно, и закрывает его. Прокси видит сброс и возвращает 502 на запрос, который до вашего кода так и не дошёл. Это происходит эпизодически, чаще при малом трафике, чем при большом, и в логе приложения ничего нет. Правило такое: keep-alive-таймаут приложения должен быть больше, чем таймаут простоя прокси в сторону приложения. Узнайте, как долго ваш прокси держит соединения вверх по потоку в простое; значение для слота прокси эта статья назвать не может, поэтому измеряйте. Если вы видите редкие 502 при чистом логе приложения, поднимите keepalive примерно до 10 как эксперимент и посмотрите, прекратятся ли они.
У слишком большого значения есть цена: каждое простаивающее соединение занимает место. В воркере gthread удерживаемое соединение занимает поток. При четырёх потоках и щедром keep-alive четыре простаивающих соединения могут оставить воркер без свободных рук для пятого клиента. Держите значение скромным, от 5 до 10 секунд, а число потоков - выше числа соединений прокси, которое вы ожидаете.
Вторая половина - доверие к пересылаемым заголовкам. Оба сервера верят X-Forwarded-For и X-Forwarded-Proto только с доверенных адресов: gunicorn через forwarded_allow_ips (переменная окружения FORWARDED_ALLOW_IPS), uvicorn через --forwarded-allow-ips. По умолчанию у обоих 127.0.0.1, что не совпадает с прокси, подключающимся с другого адреса. В итоге каждый запрос выглядит пришедшим от прокси и по обычному HTTP, поэтому URL редиректов строятся с http://, а в логах виден один посетитель. Значение * это исправляет, но допустимо только если порт приложения недоступен иначе как через прокси. Если вы не уверены, что это так, впишите в список адрес прокси и посмотрите, какой адрес пира пишет ваш сервер.
max-requests и медленные утечки#
max_requests велит gunicorn отправлять воркер на покой после обработки заданного числа запросов и заменять его свежим. Это грубый, но действенный инструмент против медленной утечки в вашем коде или в зависимости, которую вы не контролируете, и против постепенной фрагментации, из-за которой долгоживущие процессы Python со временем растут. Всегда сочетайте его с jitter:
max_requests = 1000max_requests_jitter = 100Jitter добавляет к лимиту каждого воркера случайное число от 0 до 100, чтобы они не упирались в него на одном и том же запросе. Без него на тарифе с двумя воркерами оба могут перезапуститься почти одновременно, и возникает окно, в котором обслуживать некому. Заменяемый воркер сначала дорабатывает текущий запрос и только потом выходит, поэтому пользователи не видят ошибки; в худшем случае они видят чуть более медленный ответ, пока загружается новый воркер.
Выбирайте число по собственному графику памяти, а не по блогу. Понаблюдайте за графиком памяти на вкладке Console в течение суток. Если резидентный размер воркера стабильно растёт вместе с числом запросов, задайте max_requests так, чтобы воркер уходил на покой задолго до точки, где тарифу перестаёт хватать запаса. Если память ровная, max_requests не нужен, а перезапуски просто выбрасывают тёплые кэши и проделанную работу по импорту.
В uvicorn то же самое называется --limit-max-requests N: процесс завершается после N запросов. Под надзором gunicorn он порождается заново. С собственными --workers uvicorn новые релизы тоже перезапускают его, а старые нет, и после каждого перезапуска в сервисе оставалось бы меньше процессов. Проверьте закреплённый у вас релиз, прежде чем на это полагаться, и предпочтите gunicorn, если не можете это подтвердить.
max_requests прячет утечку, а не чинит её. Если вы к нему прибегли, запишите зачем и найдите причину, когда появится время. Тему памяти продолжает статья лимиты памяти Node о том, как это выглядит в Node, а утечки памяти игровых серверов описывает приёмы отличия утечки от нормального роста.
Перезагрузка без потери запросов#
Мастер gunicorn реагирует на сигналы, и это весь его эксплуатационный интерфейс:
| Сигнал | Действие |
|---|---|
HUP | Перечитать конфиг, запустить новых воркеров, мягко убрать старых |
TERM | Мягкая остановка, ждёт до graceful_timeout |
INT, QUIT | Немедленная остановка |
TTIN, TTOU | Добавить или убрать одного воркера |
USR1 | Переоткрыть лог-файлы (после ротации) |
USR2 | Запустить новый мастер рядом со старым |
HUP - сигнал для деплоев. Новые воркеры стартуют с новым кодом и конфигом до того, как старым велят остановиться, а старые доделывают текущее, так что запросы в работе завершаются. На собственной машине:
$ gunicorn app.main:app -c gunicorn.conf.py --pid /run/myapp.pid$ kill -HUP $(cat /run/myapp.pid)Под systemd, а это разумный способ запускать всё на VDS, оформите это как действие reload:
[Service]User=appWorkingDirectory=/srv/myappExecStart=/srv/myapp/.venv/bin/gunicorn app.main:app -c gunicorn.conf.pyExecReload=/bin/kill -s HUP $MAINPIDKillMode=mixedTimeoutStopSec=40Тогда systemctl reload myapp отправит HUP. TimeoutStopSec должен превышать graceful_timeout, иначе systemd убьёт процесс, пока воркеры ещё дорабатывают. Прежде чем на это полагаться, нужно знать несколько оговорок:
- При `preload_app` сигнал `HUP` не подхватывает новый код. Код живёт в мастере. Чтобы выкатить новый код при preload, перезапустите сервис или используйте передачу через
USR2(запустить новый мастер, затем отправитьWINCHиQUITстарому), что достаточно запутанно, поэтому большинство небольших установок выбирают перезапуск. - Сломанный новый релиз может положить сервис. Если новый код не импортируется, новые воркеры не загрузятся (
Worker failed to boot), а старые будут остановлены. Выполнитеpython -c "import app.main"илиgunicorn --check-configдо отправки сигнала. - Новые зависимости надо установить заранее.
HUPне запускаетpip. См. requirements и virtualenv в Python. - Изменения схемы базы - ваша забота. Старые и новые воркеры несколько секунд работают рядом, так что миграция, ломающая старый код, сломает его.
В многопроцессном режиме uvicorn появилось похожее поведение: недавние релизы перезапускают воркеры по одному по SIGHUP и добавляют или убирают воркеры по SIGTTIN и SIGTTOU. Как и с другими деталями uvicorn, подтвердите это в заметках к релизу той версии, которую вы закрепили.
На хостинге с панелью картина другая. Кнопка Restart в панели - это полная остановка и запуск процесса сервера; это чистый, простой и безопасный способ деплоя, и он оборвёт соединения на те несколько секунд, которые приложению нужны для загрузки. Консоль панели вводит текст в процесс, а не отправляет ему сигналы, поэтому HUP оттуда ждать не стоит; возможность отправки сигнала мы не подтверждали. Для деплоя без паузы на небольшом тарифе прочитайте деплой без простоя на небольшом сервере, а о том, что процесс, который останавливается, должен своим клиентам, - мягкая остановка и health-проверки. Если включён деплой из Git, пуш перезапускает сервер, так что действует та же пауза на загрузку.
Полный конфиг для небольшого тарифа#
Положите настройки в gunicorn.conf.py в рабочем каталоге приложения, где gunicorn ищет его по умолчанию: команда запуска останется короткой, а настройки будут проходить ревью в git, как и всё остальное. Этот подходит для приложения на FastAPI на тарифе Standard (2 GB, 1 core):
import osbind = f"0.0.0.0:{os.environ.get('SERVER_PORT', '8000')}"workers = int(os.environ.get("WEB_CONCURRENCY", "2"))worker_class = "uvicorn_worker.UvicornWorker"timeout = 30graceful_timeout = 30keepalive = 10max_requests = 1000max_requests_jitter = 100worker_tmp_dir = "/dev/shm"forwarded_allow_ips = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1")accesslog = "-"errorlog = "-"loglevel = "info"capture_output = TrueКоманда запуска тогда просто gunicorn app.main:app -c gunicorn.conf.py. Логи идут в стандартный вывод и вывод ошибок, откуда их читает вкладка Console; ротировать нечего. Для приложения на Flask или Django на том же тарифе замените на worker_class = "gthread", добавьте threads = 4 и уберите строку с uvicorn.
Проверяйте файл до запуска сервера и тогда, когда он не стартует:
$ gunicorn app.main:app -c gunicorn.conf.py --check-config$ gunicorn app.main:app -c gunicorn.conf.py --print-config | grep -E "^(workers|timeout)"Первая команда ловит опечатки в именах настроек и ошибки импорта; вторая печатает то, что gunicorn реально получил после применения переменных окружения и командной строки. Флаги командной строки перекрывают файл, и именно так вы потом отлаживаете настройку, которая «явно есть в конфиге» и не действует.
Диагностика проблем#
`WORKER TIMEOUT` повторяется. Воркер молчал дольше timeout. У sync-воркеров найдите медленный запрос (пишите длительность запроса через %(D)s в access_log_format). У async-воркеров ищите блокирующие вызовы внутри async def. Если это случается только при загрузке, приложение импортируется дольше, чем timeout; помогает preload, а также увеличение timeout только на время старта.
`Worker (pid:N) was sent SIGKILL! Perhaps out of memory?` Что-то убило воркера без участия gunicorn, а на тарифе с лимитом памяти первый подозреваемый - лимит памяти. При достижении лимита ядро останавливает процессы, и контейнер чисто перезапускается, а не уходит в swap. Решение - меньше воркеров, меньше потоков или приложение поменьше, именно в таком порядке. Повторные перезапуски замечаются: три за час - это предупреждение на странице сервера и тикет, шесть - приостановка сервера.
`Worker failed to boot`, и мастер завершается. Импорт не удался. Выполните тот же импорт вручную интерпретатором из venv и прочитайте трассировку. Без --preload это появляется по разу на каждого воркера, что шумно, но безвредно.
Редкие 502 от прокси при чистом логе приложения. Гонка keep-alive, описанная выше. Поднимите keepalive или --timeout-keep-alive и посмотрите.
Всё медленно при 100% CPU и куче свободной памяти. Вас ограничивает CPU. Больше воркеров только ухудшит дело. Профилируйте приложение, кэшируйте или переходите на тариф с большей долей CPU. Сервер на 100% CPU медленный, но его никогда не приостанавливают за это.
Ошибки базы только под нагрузкой. Либо подключения были открыты до fork (--preload), либо произведение воркеров, потоков и размера пула превышает то, что допускает база. Посчитайте: воркеры умножить на потоки умножить на размер пула.
Редиректы ведут на `http://`. Сервер не доверяет X-Forwarded-Proto. Задайте forwarded_allow_ips, как описано выше, а в Django - SECURE_PROXY_SSL_HEADER.
FAQ#
Сколько воркеров gunicorn запускать на тарифе в 1 GB?
Один, с несколькими потоками или async-классом воркера, если только измерение не показало, что ваше приложение занимает меньше примерно 200 МБ на воркер в сумме. Второй воркер даёт устойчивость к одиночному зависанию, но при 1 GB и 0.5 core он нередко стоит больше памяти и борьбы за CPU, чем экономит.
Достаточно ли uvicorn с --workers или нужен gunicorn?
Для небольшого приложения достаточно одного uvicorn, и зависимость у него на одну меньше. Выбирайте gunicorn, когда нужен более сильный надзор: таймаут, убивающий зависшего воркера, max_requests с jitter, хуки и давно устоявшийся интерфейс сигналов.
Нужно ли включать preload?
Только если запуск тяжёлый или вы хотите видеть ошибки импорта в мастере, и только убедившись, что ничто не открывает подключение и не запускает поток при импорте. Измерьте память с ним и без него; выигрыш от copy-on-write в Python меньше, чем обычно ждут.
Какой max_requests разумен?
Тот, что отправляет воркер на покой до того, как его память дойдёт до вашего запаса. Тысяча с jitter в сто - обычная отправная точка. Если график памяти ровный, он вам вообще не нужен.
Почему gunicorn убивает запрос, который идёт 31 секунду?
Потому что timeout по умолчанию равен 30, и sync-воркер, не ответивший к этому моменту, считается зависшим. Ускорьте запрос или вынесите его в фоновую задачу, прежде чем поднимать лимит, и поднимайте его лишь на то время, которое нужно.
Можно ли перезагрузить gunicorn без простоя на хостинге с панелью?
Не сигналом, который мы подтвердили. Restart - это чистая остановка и запуск с паузой в несколько секунд. Если пауза недопустима, запускайте два экземпляра за чем-то, что умеет выводить один из ротации, или переходите на VDS, где systemd в ваших руках и можно отправить HUP.


