Gunicorn, Django, Gevent: порожденные потоки блокируются

недавно мы перешли на Gunicorn с помощью работника gevent.

На нашем веб-сайте у нас есть несколько задач, выполнение которых требует времени. Дольше 30 секунд.

Преамбула

Мы уже сделали все, что связано с сельдереем, но эти задачи выполняются так редко, что просто невозможно постоянно поддерживать сельдерей и redis в рабочем состоянии. Мы просто этого не хотим. Мы также не хотим запускать сельдерей и редис по запросу. Мы хотим от этого избавиться. (Прошу прощения, но я не хочу, чтобы ответы были такими: «Почему бы тебе не использовать сельдерей, это здорово!»)

Задачи, которые мы хотим запускать асинхронно

Я говорю о задачах, которые выполняют 3000 SQL-запросов (вставок), которые должны выполняться один за другим. Это делается не слишком часто. Мы также ограничились запуском только двух из этих задач одновременно. Это займет примерно 2–3 минуты.

Подход

Теперь то, что мы делаем сейчас, - это использование gevent worker и gevent.spawn задачи и возвращение ответа.

Эта проблема

Я обнаружил, что порожденные потоки на самом деле блокируются. Как только ответ возвращается, задача запускается, и никакие другие запросы не обрабатываются до тех пор, пока задача не прекратит выполнение. Задание будет убито через 30 секунд, пулемет timeout. Чтобы предотвратить это, я использую time.sleep() после каждого второго SQL-запроса, чтобы сервер мог ответить на запросы, но я не думаю, что это главное.

Установка

Мы запускаем gunicorn, django и gevent. Описанное поведение происходит в моей среде разработки и при использовании одного работника gevent. В производстве мы также будем запускать только 1 рабочего (пока). Кроме того, запуск двух воркеров, похоже, не помогал в обслуживании большего количества запросов во время блокировки задачи.

TL; DR

  • Мы считаем возможным использовать поток gevent для нашей двухминутной задачи (над сельдереем)
  • Мы используем gunicorn с gevent и задаемся вопросом, почему поток, порожденный gevent.spawn, блокирует
  • Блокировка предназначается или наши настройки неверны?

Спасибо!


person enpenax    schedule 02.07.2014    source источник
comment
Простое выполнение кода в зеленой оболочке не делает код неблокирующим. Вы должны действительно выполнять вызовы асинхронных API, чтобы гринлет не блокировался. Например, вызовы, которые вы делаете на time.sleep, будут по-прежнему блокироваться внутри гринлета. Вы должны использовать gevent.sleep, чтобы сделать неблокирующий сон. Ваши вызовы базы данных, вероятно, тоже блокируются, если вы не используете исправление обезьяны gevent.   -  person dano    schedule 02.07.2014
comment
@dano Поскольку я использую gunicorn с работником gevent, исправление обезьяны заботится обо мне. time.sleep допускает изменение потока, поэтому это не блокирует. Может быть, заплатка обезьяны исправила это. Но я ожидал передать задачу работнику, который затем позаботится о ней. Таким образом, он может блокировать все, что хочет, пока работает параллельно. Но я думаю, что гринлеты нельзя запускать параллельно?   -  person enpenax    schedule 02.07.2014
comment
гринлеты можно запускать одновременно, но они по-прежнему однопоточны; только один из них может использовать ЦП одновременно. Таким образом, если один гринлет находится в вызове блокирующего ввода-вывода или gevent.sleep, другой гринлет может работать. Но если один гринлет обрабатывает числа или анализирует XML (или любую другую операцию на базе процессора), никакие другие гринлеты работать не будут. Гринлет также будет блокировать другие гринлеты, если он выполняет операцию ввода-вывода, которая не является асинхронной, то есть это не обезьяна, исправленная gevent или иным образом подключенная к циклу событий gevent.   -  person dano    schedule 02.07.2014
comment
Вы говорите, что делаете вставки, но позже скажете, что используете ORM django. Таким образом, вы, вероятно, делаете больше, чем просто вставки (в противном случае вы могли бы сделать эту вставку 3000 за один вызов sql). Поскольку он использует процессор, он будет блокироваться, это так просто. Перестаньте ныть о том, что не хотите запускать сельдерей и redis. Redis легкий, и у вас может быть сельдерей с автомасштабированием от 0 до 1. Скорее всего, вы будете использовать Redis для гораздо большего.   -  person dalore    schedule 19.05.2015
comment
@dalore и иногда ты не хозяин, над этими решениями вынужден катиться с чем-то другим. плюс нигде не было сказано, что я буду использовать Django ORM. мое решение ниже работает нормально уже несколько месяцев, но спасибо за ваше время   -  person enpenax    schedule 20.05.2015


Ответы (3)


Один из способов запустить задачу в фоновом режиме - fork родительский процесс. В отличие от Gevent, он не блокируется - вы запускаете два совершенно разных процесса. Это медленнее, чем запуск другого (очень дешевого) гринлета, но в данном случае это хороший компромисс.

Ваш процесс делится на две части: родительскую и дочернюю. В родительском 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 - полезный инструмент - получайте удовольствие!

person johntellsall    schedule 02.07.2014
comment
Несмотря на то, что у меня было ощущение, что это не сработает, я попробовал, и это действительно не сработало. Когда достигается код, выполняющий форк, сайт, который должен быть загружен, зависает. Так что ответ, кажется, не выходит. - person enpenax; 02.07.2014

Похоже, никто здесь не дал актуального ответа на ваш вопрос.

Блокировка предназначается или наши настройки неверны?

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

Если вы явно не делаете join на гринлетах, ответ сервера не должен блокировать.

person Anorov    schedule 15.07.2014
comment
Привет, спасибо за ответ. Я использую то, что поставляется с Django, это MySQLdb или что-то в этом роде - person enpenax; 16.07.2014
comment
@ user2033511 Это, скорее всего, источник вашей проблемы. Используйте такой драйвер: github.com/esnme/ultramysql - person Anorov; 16.07.2014
comment
Спасибо! Я попробую это сделать на этой или следующей неделе, и если это сработает, вы получите мощную зеленую галочку! Но пока предполагаю, что это нельзя использовать в качестве драйвера в Django :) - person enpenax; 16.07.2014

Я решил использовать synchronous (стандартный) воркер и использовать multiprocessing библиотеку. На данный момент это кажется самым простым решением.

Я также реализовал глобальный пул, злоупотребляя memcached кешем, обеспечивающим блокировки, поэтому могут выполняться только две задачи.

person enpenax    schedule 03.07.2014
comment
Все это, и вы могли бы просто пойти сельдерей / редис - person dalore; 19.05.2015
comment
Это не отвечает на вопрос. - person Marco Lavagnino; 16.11.2017