Gunicorn (Green Unicorn) — WSGI-сервер для Python, который держит нагрузку production-сайта и при этом остаётся простым в настройке. Он принимает HTTP-запросы, раскладывает их по процессам-воркерам и передаёт приложению Flask, Django или FastAPI через WSGI-интерфейс.

Как устроена модель pre-fork

Gunicorn запускает один мастер-процесс, который читает конфигурацию, открывает слушающий сокет и только потом создаёт дочерние процессы. Мастер не обслуживает запросы: его задача — следить за воркерами, перезапускать упавшие и принимать сигналы управления. Сокет наследуется дочерними процессами, поэтому новый воркер начинает принимать соединения сразу, без повторного bind и без гонки за порт.

Такая схема даёт предсказуемость: каждый запрос живёт в одном процессе, разделяемого состояния между воркерами нет, а падение одного из них не роняет остальные. Обратная сторона — память: каждый воркер держит свою копию приложения, поэтому её считают до запуска.

  • sync — один запрос на процесс, простой и надёжный вариант по умолчанию.
  • gthread — пул потоков внутри воркера, помогает на операциях ввода-вывода.
  • gevent — зелёные потоки, требует аккуратности с блокирующими библиотеками.

Gunicorn работает только на Unix: ему нужны fork и сигналы; на Windows используют waitress или контейнер.

Установка и первый запуск

Ставить сервер лучше в виртуальное окружение проекта, чтобы версия не зависела от системного Python. Пакет не тянет за собой фреймворк, поэтому приложение устанавливается отдельно.

python3 -m venv /srv/project/.venv
. /srv/project/.venv/bin/activate
pip install gunicorn flask
gunicorn --version

Минимальный запуск выглядит так: указываем адрес, число воркеров и точку входа приложения. Для Flask это обычно app:app, для Django — project.wsgi:application.

cd /srv/project
gunicorn --bind 127.0.0.1:8000 --workers 2 app:app
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/

Слушать 0.0.0.0 без прокси не стоит: TLS и статику всё равно отдаёт Nginx, а прямой доступ к порту обходит логирование.

Файл конфигурации

Длинные командные строки быстро становятся нечитаемыми, поэтому параметры выносят в gunicorn.conf.py. Это обычный Python-файл, где имена переменных совпадают с опциями.

bind = "127.0.0.1:8000"
workers = 4
worker_class = "gthread"
threads = 4
timeout = 30
graceful_timeout = 30
max_requests = 1000
max_requests_jitter = 100
accesslog = "-"
errorlog = "-"
loglevel = "info"

Запуск с конфигом: gunicorn -c gunicorn.conf.py app:app. Параметр max_requests заставляет воркер перезапуститься после указанного числа запросов — это дешёвая защита от медленных утечек памяти. Разброс max_requests_jitter не даёт всем воркерам перезапускаться одновременно.

Сколько нужно воркеров

Классическая формула — 2 × число ядер + 1. Для четырёхъядерной машины это девять процессов, но если приложение занимает 300 МБ, получится почти три гигабайта только на копии кода. Поэтому сначала считают память, потом ядра.

Для приложений, которые много ждут базу или внешние API, выгоднее меньше процессов с потоками: workers = 4 и threads = 4 дают до шестнадцати одновременных запросов при скромном потреблении памяти. Для тяжёлых вычислений на CPU наоборот — только sync-воркеры и число по ядрам.

Юнит systemd

Ручной запуск годится для отладки, в бою процессом управляет systemd. Юнит задаёт пользователя, рабочий каталог и корректную команду перезагрузки.

[Unit]
Description=Gunicorn for project
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/srv/project
Environment="PATH=/srv/project/.venv/bin"
ExecStart=/srv/project/.venv/bin/gunicorn -c gunicorn.conf.py app:app
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
PrivateTmp=true

[Install]
WantedBy=multi-user.target

После сохранения в /etc/systemd/system/project.service выполняем systemctl daemon-reload, затем systemctl enable --now project. Проверка — systemctl status project и journalctl -u project -n 50.

Nginx перед Gunicorn

Nginx забирает TLS, сжатие, статику и медленные клиенты. Приложению остаётся только динамика. Важно передать схему и адрес клиента, иначе Django будет считать соединение HTTP, а в логах появится IP прокси.

upstream project {
    server 127.0.0.1:8000 fail_timeout=0;
}
server {
    listen 80;
    server_name example.com;
    client_max_body_size 20m;
    location /static/ { alias /srv/project/static/; }
    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        proxy_pass http://project;
    }
}

Со стороны приложения нужен доверенный прокси. В Django это SECURE_PROXY_SSL_HEADER, во Flask — ProxyFix из werkzeug.middleware.proxy_fix. Без этого генерация абсолютных ссылок и проверка HTTPS работают неправильно.

Логи и наблюдаемость

При accesslog = "-" записи уходят в stdout и попадают в journald. Это удобнее файлов: ротацию и хранение берёт на себя система. Если нужен отдельный файл, укажите путь и не забудьте про logrotate.

journalctl -u project -f
journalctl -u project --since "1 hour ago" | grep -i timeout
curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' http://127.0.0.1:8000/healthz
ss -ltnp | grep 8000

Отдельный эндпоинт /healthz должен отвечать быстро и не ходить в тяжёлые зависимости.

Плавная перезагрузка

Мастер понимает сигналы: HUP перечитывает конфиг и запускает новые воркеры, USR2 готовит бинарный апгрейд, QUIT завершает работу аккуратно, TERM — быстро. Для деплоя достаточно отправить HUP: старые воркеры дождутся текущих запросов.

systemctl reload project
kill -HUP $(systemctl show -p MainPID --value project)

Диагностика типичных ошибок

Ошибка 502 от Nginx означает, что прокси не смог соединиться с приложением: процесс не слушает порт, упал или сокет лежит в недоступном каталоге. Сначала смотрим systemctl status, затем ss -ltnp, затем журнал.

Ошибка 504 — это таймаут. Проверьте, не расходятся ли proxy_read_timeout в Nginx и timeout в Gunicorn, и найдите медленный запрос, а не просто увеличивайте числа. Сообщение Worker timeout в журнале почти всегда указывает на блокирующий вызов: синхронный запрос к базе, чтение большого файла, ожидание внешнего API.

Если воркеры постоянно перезапускаются, смотрите dmesg на предмет OOM-killer и считайте реальное потребление через ps -o rss,cmd -C gunicorn.

Чек-лист перед выкладкой

  • Приложение слушает только localhost, порт закрыт файрволом снаружи.
  • Воркеры подобраны по памяти и проверены нагрузочным тестом.
  • Настроены max_requests, timeout и graceful_timeout.
  • Юнит systemd включает Restart=on-failure и корректный ExecReload.
  • Nginx передаёт X-Forwarded-Proto и X-Forwarded-For, приложение им доверяет.
  • Логи уходят в journald, есть health-эндпоинт и алерт на 5xx.

Итог

Gunicorn решает одну задачу — надёжно держать WSGI-приложение под нагрузкой, и решает её предсказуемо. Мастер-процесс, понятная модель воркеров, конфиг на Python и сигналы для деплоя дают минимум магии и максимум контроля. Связка с Nginx и systemd закрывает TLS, статику, автозапуск и логи. Начните с двух-четырёх воркеров, измерьте задержки и память, и только потом увеличивайте числа.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте