#python #django #multithreading #gunicorn #gevent
#python #django #многопоточность #gunicorn #gevent
Вопрос:
недавно мы перешли на Gunicorn, используя рабочий инструмент gevent.
На нашем веб-сайте у нас есть несколько задач, выполнение которых занимает некоторое время. Дольше 30 секунд.
Преамбула
Мы уже сделали всю работу с сельдереем, но эти задачи выполняются так редко, что просто невозможно постоянно запускать сельдерей и redis. Мы просто этого не хотим. Мы также не хотим запускать celery и redis по требованию. Мы хотим избавиться от этого. (Прошу прощения за это, но я хочу предотвратить ответы, подобные: «Почему бы вам не использовать сельдерей, это здорово!»)
Задачи, которые мы хотим запускать асинхронно
Я говорю о задачах, которые выполняют 3000 SQL-запросов (вставок), которые должны выполняться один за другим. Это делается не слишком часто. Мы также ограничились одновременным выполнением только 2 из этих задач. Они должны занять около 2-3 минут.
Подход
Итак, то, что мы делаем сейчас, — это используем преимущества gevent worker и gevent.spawn
задачи и возвращаем ответ.
Проблема
Я обнаружил, что порожденные потоки фактически блокируют. Как только возвращается ответ, задача запускается, и никакие другие запросы не обрабатываются, пока задача не перестанет выполняться. Задача будет завершена через 30 секунд, gunicorn timeout
. Чтобы предотвратить это, я использую time.sleep()
после каждого другого SQL-запроса, поэтому сервер получает возможность отвечать на запросы, но я не чувствую, что в этом суть.
Настройка
Мы запускаем gunicorn, django и используем gevent. Описанное поведение происходит в моей среде разработки и с использованием 1 рабочего gevent. В производстве мы также будем запускать только 1 рабочий (на данный момент). Кроме того, запуск 2 рабочих, похоже, не помог в обслуживании большего количества запросов, пока задача блокировалась.
TLDR
- Мы считаем целесообразным использовать поток gevent для нашей 2-минутной задачи (над сельдереем)
- Мы используем gunicorn с gevent и задаемся вопросом, почему поток, созданный с помощью gevent.spawn, блокирует
- Предназначена ли блокировка или наша настройка неправильная?
Спасибо!
Комментарии:
1. Простое выполнение кода в greenlet не делает код неблокирующим . На самом деле вы должны выполнять вызовы асинхронных API, чтобы зеленый список не блокировался. Например, вызовы, которые вы выполняете
time.sleep
, все равно будут блокироваться внутри greenlet. Вы должны использоватьgevent.sleep
для выполнения неблокирующего режима ожидания. Ваши вызовы базы данных, вероятно, тоже блокируются, если вы не используете исправление gevent monkey.2. @дано Поскольку я использую gunicorn с работником gevent, я позаботился об исправлении обезьяны.
time.sleep
позволяет изменять поток, поэтому он не блокируется. Возможно, исправление обезьяны исправило это. Но я ожидал передать задачу работнику, который затем позаботится об этом. Таким образом, он может блокировать все, что хочет, пока он выполняется параллельно. Но я думаю, что гринлеты нельзя запускать параллельно?3. гринлеты могут запускаться одновременно, но они по-прежнему однопоточны; только один из них может использовать процессор одновременно. Итак, если один greenlet находится в блокирующем вызове ввода-вывода или
gevent.sleep
, может быть запущен другой greenlet. Но если один гринлет обрабатывает числа или анализирует XML (или любую другую операцию на базе процессора), никакие другие гринлеты не будут запущены. Зеленый список также заблокирует другие зеленые списки, если он выполняет операцию ввода-вывода, которая не является асинхронной, что означает, что это не monkey, исправленная gevent или иным образом подключенная к циклу событий gevent.4. Вы говорите, что делаете вставки, но позже говорите, что используете django ORMS. Так что вы, вероятно, делаете больше, чем вставки (в противном случае вы могли бы сделать 3000 вставок за один вызов sql). Поскольку он использует процессор, он будет блокироваться, это так просто. Прекратите это нытье о нежелании запускать celery и redis. Redis легкий, и вы можете использовать сельдерей в автоматическом масштабе от 0 до 1. Скорее всего, вы будете использовать redis для гораздо большего.
5. @dalore и иногда вы не являетесь хозяином этих решений и вынуждены использовать что-то другое. кроме того, нигде не было указано, что я буду использовать Django ORM. мое решение, приведенное ниже, работает нормально уже несколько месяцев, но спасибо за ваше время
Ответ №1:
Один из способов запустить задачу в фоновом режиме — это fork
родительский процесс. В отличие от Gevent, он не блокируется — вы запускаете два совершенно разных процесса. Это медленнее, чем запуск другого (очень дешевого) greenlet, но в данном случае это хороший компромисс.
Ваш процесс разделяется на две части, родительскую и дочернюю. В родительском, return
ответ на Gunicorn точно так же, как в обычном коде.
В дочернем элементе выполните свою длительную обработку. В конце выполните очистку, выполнив специализированную версию exit
. Вот некоторый код, который обрабатывает и отправляет электронные письма:
if os.fork():
return JsonResponse({}) # async parent: return HTTP 200
# child: do stuff, exit quietly
ret = do_tag_notify(
event, emails=emails, photo_names=photo_names,
)
logging.info('do_tag_notify/async result={0}'.format(ret))
os._exit(0) # pylint: disable=W0212
logging.error("async child didn't _exit correctly") # never happens
Будьте осторожны с этим. Если в дочернем элементе возникло исключение, даже синтаксическая ошибка или неиспользуемая переменная, вы никогда не узнаете об этом! Родительский элемент с его протоколированием уже исчез. Будьте подробны с протоколированием и не делайте слишком много.
Использование fork
— полезный инструмент — получайте удовольствие!
Комментарии:
1. Несмотря на ощущение, что это не может сработать, я попробовал это, и это действительно не работает. Когда достигается код, выполняющий форк, веб-сайт, который должен быть загружен, зависает. Таким образом, ответ, похоже, не выходит.
Ответ №2:
Похоже, никто здесь не ответил на ваш вопрос.
Предназначена ли блокировка или наша настройка неправильная?
Что-то не так с вашей настройкой. Запросы SQL почти полностью связаны с вводом-выводом и не должны блокировать какие-либо гринлеты. Вы либо используете библиотеку SQL / ORM, которая не поддерживает gevent, либо что-то еще в вашем коде вызывает блокировку. Вам не нужно использовать многопроцессорную обработку для такого рода задач.
Если вы явно не выполняете join
для гринлетов, то ответ сервера не должен блокировать.
Комментарии:
1. Привет, спасибо за ваш ответ. Я использую то, что поставляется с Django, что
MySQLdb
или что-то в этом роде2. @user2033511 Это, скорее всего, источник вашей проблемы. Используйте драйвер, подобный этому: github.com/esnme/ultramysql
3. Спасибо! Я попробую это на этой или следующей неделе, и если это сработает, вы получите мощную зеленую галочку! Но на данный момент я предполагаю, что это не может быть использовано в качестве драйвера в Django 🙂
Ответ №3:
Я остановился на использовании synchronous
(стандартного) рабочего и использовании multiprocessing
библиотеки. На данный момент это кажется самым простым решением.
Я также внедрил глобальный пул, злоупотребляющий memcached
кешем, обеспечивающим блокировки, поэтому могут выполняться только две задачи.
Комментарии:
1. Все это, и вы могли бы просто использовать сельдерей / redis
2. Это не отвечает на вопрос.