Как рассчитывается количество воркеров для Django
Что проверяют
Понимаете ли вы, что число воркеров выводится из характера нагрузки и ресурсов, а не берётся из одной формулы. Следом обычно спрашивают, что изменится, если приложение большую часть времени ждёт базу или внешние API.
Как отвечать
Начните с того, кто держит воркеры: Django сам запросы не распараллеливает, этим занимается сервер приложения, чаще всего Gunicorn, uWSGI или Uvicorn для ASGI. Дальше каркас:
- стартовая точка: документация Gunicorn предлагает
(2 × ядра) + 1синхронных воркеров. Это ориентир для первого прогона, а не ответ; по умолчанию Gunicorn запускает всего один воркер классаsync; - закон Литтла: одновременных запросов в среднем столько, сколько RPS, умноженный на время ответа. При 200 RPS и 100 мс нужно около 20 параллельных обработчиков плюс запас на пики;
- тип нагрузки: CPU-bound работа упирается в ядра, процессы сверх их числа прироста не дают, а потоки не помогают из-за GIL. При ожидании ввода-вывода выгоднее
gthreadс--threadsили асинхронный стек, чем раздувать число процессов; - память: каждый процесс держит свою копию приложения. Воркеры × RSS должны влезать в RAM с запасом,
--preloadэкономит память за счёт copy-on-write, а--max-requestsс--max-requests-jitterлечит утечки; - база: воркеры × потоки дают число соединений. У PostgreSQL по умолчанию
max_connections = 100, несколько подов легко его выбирают, поэтому нужен пул вроде PgBouncer.
Финальное число подбирается нагрузочным тестом (Locust, k6) по p95 времени ответа и загрузке CPU. Если речь про Celery, там свой параметр --concurrency, по умолчанию равный числу ядер.
Где спотыкаются
Называют формулу 2n+1 законом и не могут объяснить, откуда она и когда не работает.
Не учитывают контейнеры: os.cpu_count() видит ядра хоста, а не лимит CPU пода, и на ноде с 64 ядрами воркеров оказывается 129 при лимите в два ядра.
Забывают про соединения с базой и память: воркеров добавили, а приложение упёрлось в too many clients или в OOM killer.