Если у вас Django проект работает на синхронных воркерах и вы периодически их перезапускаете (например, в gunicorn это опция --max-requests), полезно было бы знать, что по-умолчанию после каждого перезапуска воркера, первый запрос к нему обрабатывается гораздо дольше, чем последующие. В этой статье расскажу как я решил эту и другие проблемы, вызывающие аномальные задержки на случайных запросах. В статье будут приведены примеры для gunicorn wsgi сервера. Но они актуальны при любых способах запуска проекта на синхронных воркерах. Тоже самое актуально и для uWSGI и mod_wsgi. Недавно перенесли наш Django проект в Kubernetes кластер. Там есть readiness/liveness probes, которые могут дёргать каждый запущенный экземпляр wsgi сервера (в нашем случае это gunicorn) за указанную http ручку. У нас это /api/v1/status: Так вот, до переезда в Kubernetes у нас стоял Zabbix, который каждую минуту делал запрос на /api/v1/status через loadbalancer. И этот health check особо никогда не фэйлился. Но после переезда, когда проверки стали выполняться для каждого отдельного gunicorn инстанса и с большей частотой, вдруг оказалось, что иногда мы не укладываемся в таймаут 5 секунд. Тем не менее все работало нормально, проблем у пользователей не было. Поэтому особо внимание этому не уделял, но поставил себе фоновую задачу все таки разобраться в чем же дело. И вот что удалось выяснить: По-умолчанию gunicorn запускает master процесс, который форкает количество процессов указанное аргументом --workers. Причем модуль wsgi, переданный gunicorn в качестве основного аргумента, загружается каждым воркером уже после форка. Но есть опция --preload. Если её задать, модуль wsgi будет загружен ДО форка. Отсюда правило: Повторюсь, что большая часть этих оптимизаций имеет смысл, если у вас Django проект работает на синхронных воркерах и вы периодически их перезапускаете (--max-requests). Тем не менее, удалось выяснить, что использование --preload недостаточно, и первый запрос к свежезапущеному воркеру все равно занимает больше времени чем последующие. Трейс показал, что предзагрузка wsgi мало что даёт, и большая часть Django инициализируется лишь во время первого запроса. Поэтому родилось решение "в лоб": Например, я добавил в wsgi.py следующее: В результате воркеры стали инициализироваться на порядок быстрее, т.к. уже форкались полностью готовыми к следующему запросу. По трейсам проблемы с инициализацией прекратились… почти. К своему стыду я не знал об этой особенности. Оказывается, по-умолчанию, Django при каждом запросе переподключается к БД. За это отвечает настройка CONN_MAX_AGE, которая только лишь(?) по историческим причинам заставляет ваше Django приложение работать как php скрипт из нулевых. Так что правило: Я бы даже и не заметил этого. Но, по какой-то причине, вызов psycopg2.connect иногда подвисает ровно на 5 секунд. Вот в этом я до конца не разобрался. Параллельно запущенный скрипт, который вызывает эту функцию раз в 10 секунд работал стабильно и подключался к БД быстрее чем за секунду за все время пока был запущен (пару недель). Но эти два правила конфиликтуют друг с другом, т.к. перед форком в основном процессе создаются подключения к БД и кешу. Дочерние процессы наследуют открытые сокеты основного процесса. В результате, это приводит к неопределённому поведению, когда несколько процессов будут одновременно работать с одним сокетом. Поэтому, перед форком, нужно закрыть все подключения: Т.о. при использовании --preload и WSGI_FULL_INIT, нужно еще задать WSGI_FULL_INIT_CLOSE_CONNECTIONS. В результате, аномальные задержки были полностью исключены. Но есть пара крайних случаев, когда они все же могут возникнуть: Если все воркеры начнут перезапускаться одновременно. Это вполне вероятная ситуация, т.к. если запросы между воркерами распределяются приблизительно равномерно, то и max-requests наступит приблизительно в одно и то же время. Поэтому: Также задержка может возникнуть во время первого запроса, когда создаются подключения к БД и другим внешним системам. Это можно решить, но у меня нет идей как написать код независимый от используемого wsgi сервера. В gunicorn можно добавить обработчик для post_worker_init и там еще раз вызвать make_init_request(), тогда воркер перед получением первого запроса будет готов на 100%. Чтобы не усложнять, было решено пока что обойтись без этого, ведь мы и так добились того, что больше задержек на практике не наблюдается.