Короткий ответ
Для CPU-bound нагрузки число воркеров выбирают близким к числу ядер (формула 2*CPU+1 как отправная точка), а для I/O-bound асинхронного приложения важнее, что один event loop может обслуживать много соединений без блокировки, поэтому большое число воркеров не ускоряет ожидание внешних сервисов.
Как это работает подробнее
каждый UvicornWorker — отдельный процесс с собственным event loop; GIL не даёт параллельно исполнять байткод Python в потоках одного процесса, поэтому параллелизм для CPU-bound ограничен числом процессов.
Пример кода
Команды терминала
gunicorn app:app -k uvicorn.workers.UvicornWorker --workers 5 --bind 0.0.0.0:8000
# для 4-ядерной машины: 2*4+1 = 9 как отправная точка для CPU-boundЧто использовать на практике
- Рекомендация. начинают с
workers = CPU_coresи нагрузочно тестируют с реалистичным профилем; для смешанной нагрузки тяжёлые вычисления выносят в отдельные процессы (Celery), а не раздувают число ASGI-воркеров.
Подводные камни
- Риск. ставить огромное число воркеров на машине с малым числом ядер — приводит к избыточному потреблению памяти и конкуренции за CPU; не учитывать, что блокирующий вызов внутри async-обработчика блокирует весь event loop одного воркера независимо от общего числа воркеров.