Uvicorn workers: почему четыре процесса съели вчетверо больше памяти

API работает на одном процессе, но под нагрузкой начинает отвечать медленнее. Первое желание — поставить workers=4. После перезапуска скорость почти не изменилась, зато контейнер стал упираться в память. Особенно быстро это заметно, если при старте загружается модель, большой справочник или индекс.

Воркеры — отдельные процессы. У каждого свой Python, приложение, настройки и ресурсы, созданные при старте. Увеличение числа процессов меняет не только число обработчиков запросов.

Увидеть процессы своими глазами

Сохраните main.py, установите python -m pip install fastapi uvicorn:

import os
from fastapi import FastAPI

app = FastAPI()

@app.get("/pid")
def process_id():
    return {"pid": os.getpid()}
python -m uvicorn main:app --workers 4

Сделайте несколько запросов, желательно через новые соединения, и сравните pid с логами запуска. Не обязательно каждый следующий запрос попадёт в другой процесс: соединения и распределение запросов не обещают строгое чередование.

Бюджет ресурсов

Если один прогретый процесс использует около 300 МБ, четыре потребуют существенно больше 300 МБ. Точное значение зависит от приложения и разделяемых страниц памяти; для планирования измеряйте расход в вашем окружении после прогрева и под нагрузкой, а не только сразу после старта.

То же с соединениями. Пул базы на 10 соединений в каждом из четырёх процессов допускает до 40 соединений. Две реплики приложения удвоят этот предел. HTTP-клиенты и фоновые потоки тоже создаются по процессам, если вы инициализируете их в lifespan.

Не запускайте в lifespan задание, которое должно выполняться ровно один раз на весь сервис: четыре процесса могут запустить его четыре раза. Для такой работы нужна отдельная точка запуска или согласование между экземплярами.

Как выбирать число, а не угадывать

Возьмите один характерный сценарий и сравните один, два и четыре процесса при одинаковом входящем потоке. Запишите задержку, долю ошибок, CPU, память и число подключений к базе. Если задержка упирается в медленный SQL, больше процессов могут лишь увеличить очередь у базы.

Для локальной разработки reload удобен, но это другой режим работы. Не используйте его как способ обновления боевого сервера и не рассчитывайте совместить его с моделью нескольких production-воркеров.

Если рост числа процессов упирается в PostgreSQL, продолжите с бюджетом пулов соединений. Если приложение уже контейнеризировано, полезно понимать и проверку OOM после Exit 137.

Собрать следующий этап проекта

Чтобы запуск проекта не держался на наборе команд в вашей памяти, соберите его в Docker Compose. В курсе по Docker вы упакуете HTTP-сервис, подключите PostgreSQL и разберёте сети, тома, обновление приложения и восстановление данных.

Посмотреть программу: Docker и Docker Compose: контейнеризация приложений

Проверить детали: FastAPI: процессы и память, Uvicorn: настройки.

Продолжить на практике

  • Docker и Docker Compose: контейнеризация приложений ↗

    Упакуйте приложение в Docker и запустите его с PostgreSQL через Compose. Образы, сети, тома, диагностика поломок, обновление версий и восстановление данных — с практикой и итоговым проектом.

  • DevOps-инженер с нуля: полный курс ↗

    Практический курс по DevOps для начинающих: от настройки Linux и сетей до сборки контейнеров, автоматизации в GitLab CI/CD, развёртывания в Kubernetes, мониторинга и восстановления данных на собственном компьютере.