веб-разработчик на python
0,0
рейтинг
22 октября 2015 в 22:16

Разработка → Celery: лучшие практики перевод

Если вы работаете с Django, то на некотором этапе разработке вам может понадобиться фоновая обработка долго выполняющихся задач. Возможно, что для такого рода задач вы используете какой-либо инструмент для управления очередями задач. Celery — один из самых популярных проектов для решения подобных задач в мире python и Django на данный момент, но есть и другие проекты для этой цели.

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

No.1: Не используйте СУБД как ваш AMQP брокер


Позвольте мне объяснить почему я считаю это неправильным(помимо тех ограничений что описаны в документации Celery).

СУБД не разрабатывались для тех задач, которые выполняют полноценный AMQP брокер такой как RabbitMQ. Она упадет в «боевых» условиях даже на проекте с не очень большим трафиком\пользовательской базой.

Я предполагаю, что самой популярной причиной того почему люди решают использовать СУБД в том что, как правило, у них уже есть одна СУБД для веб-приложения, так почему бы не воспользоваться ей еще раз. Начать работать с таким вариантом несложно и не надо беспокоиться о других компонентах(таких как RabbitMQ).

Предположим не такой уж гипотетический сценарий: у вас есть 4 фоновых воркера для обработки, которые вы помещаете в базу данных. Это значит что вы получаете 4 процесса, которые достаточно часто запрашивают базу о новых задачах, не говоря уже о том, что каждый из них может иметь собственные конкурирующие потоки. В некоторый момент времени вы понимаете, что растет задержка при обработке задач, а потому приходит больше новых задач чем завершается, необходимо увеличивать количество воркеров. Вдруг скорость вашей базы данных начинает «проседать» из-за огромного количества запросов воркеров к базе, дисковый ввод\вывод превышает заданные лимиты, а все это начинает влиять на ваше приложение, так как воркеры, фактически, устроили DDOS-атаку вашей базе.

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

Я пойду еще дальше и скажу, что вы не должны использовать СУБД как брокера даже в процессе разработки, тогда когда есть такие вещи как Docker и множество преднастроенных образов, которые предоставляют настроенный RabbitMQ «из коробки».

No.2: Используйте больше очередей (т.е. не только одну, которая дается по умолчанию)


Celery очень легко начать использовать, и она предоставляет сразу же одну очередь по умолчанию, в которую и помещаются все задачи пока не будет явно предписано другое поведение Celery. Наиболее общий пример того, что вы можете увидеть:
@app.task()
def my_taskA(a, b, c):
    print("doing something here...")

@app.task()
def my_taskB(x, y):
    print("doing something here...")


Что происходит, если обе задачи будут размещены в одной очереди, если иное не определено в файле celeryconfig.py. Я полностью пониманию чем может оправдывать подобный подход, у вас есть один декоратор, который создает удобные фоновые задачи. Здесь я хотел бы обратить внимание, что taskA и taskB, находясь в одной очереди могут делать совершенно разные вещи и таким образом одна из них может быть куда важнее другой, так почему они находятся все в одной корзине? Даже, если у вас один воркер, то представьте такую ситуацию что менее важная задача taskB окажется настолько массовой, что более важной задаче taskA воркер не сможет уделить необходимого внимания.Это приводит нас к к следующему пункту.

No.3: Используйте приоритеты воркеров


Путем решения проблемы, указанной выше является размещение задачи taskA в одной очереди, а taskB в другой и после этого присвоить x воркеров обработке очередь Q1, а остальных на обработку Q2, так как в нее приходит больше задач. Таким образом вы можете быть уверены, что задача taskB получит достаточно воркеров, а остальные тем временем будут обрабатывать менее приоритетную задачу, когда она придет, не провоцируя длительного ожидания и обработки. Потому, определите ваши очереди сами:
CELERY_QUEUES = (
    Queue('default', Exchange('default'), routing_key='default'),
    Queue('for_task_A', Exchange('for_task_A'), routing_key='for_task_A'),
    Queue('for_task_B', Exchange('for_task_B'), routing_key='for_task_B'),
)

И ваши роутеры, которые определять куда направлять задачу:
CELERY_ROUTES = {
    'my_taskA': {'queue': 'for_task_A', 'routing_key': 'for_task_A'},
    'my_taskB': {'queue': 'for_task_B', 'routing_key': 'for_task_B'},
}


Это позволит выполнять воркеры для каждой задачи:
celery worker -E -l INFO -n workerA -Q for_task_A
celery worker -E -l INFO -n workerB -Q for_task_B

No.4: используйте механизмы Celery для обработки ошибок


Большинство задач, которые я видел не имеют механизмов обработки ошибок. Если в задаче произошла ошибка, то она просто падает. Это может быть удобно для некоторых задач, однако большинство задач, которые я видел взаимодействовали с внешними API и падали из-за некоторых видов сетевых ошибок или иных проблем «доступности ресурса». Самый простой подход к обработке таких ошибок перевыполнить код задачи, так как, возможно, проблемы взаимодействия с внешним API были уже устранены.

@app.task(bind=True, default_retry_delay=300, max_retries=5)
def my_task_A():
    try:
        print("doing stuff here...")
    except SomeNetworkException as e:
        print("maybe do some clenup here....")
        self.retry(e)

Я люблю определять по умолчанию для задачи время ожидания, которое она будет ждать прежде чем попытается выполниться снова и как много попыток перевыполнения она предпримет прежде чем окончательно выбросить ошибку(параметры default_retry_delay и max_retries соответственно). Это наиболее простая форма обработки ошибок, которую я могу представить, но я видел, что и она практически не применяется. Разумеется Celery имеет и более сложные методы обработки ошибок, они описаны в документации Celery.

No.5: используйте Flower


Flower — прекрасный инструмент для отслеживания состояния ваших задач и воркеров Celery. У инструмента есть веб-интерфейс и он позволяет такие вещи как:
  • прогресс задач
  • детали выполнения
  • статус воркеров
  • запускать новые воркеры

Полный список возможностей вы можете увидеть по приведенной ссылке.

No.6: Отслеживайте статус задачи, только если вам это необходимо


Статус задачи это информация о том успешно или нет завершилась задача. Она может быть полезна для некоторых статистических показателей. Важная вещь, которую следует понимать в данном случае: статус задачи это не результирующие данные и той работы, которая она выполняла, такая информация наиболее похожа на неявные изменения, записываемые в базу данных(такие, например, как изменения списка друзей пользователя).

В большинстве проектов, которые я видел реально не заботились о данных по статусу задачи после ее завершения, используя базу данных sqlite, которую предлагается по умолчанию или лучше того тратили время на использование больших СУБД типа PostgreSQL. Зачем просто так нагружать базу данных своего приложения? Используйте CELERY_IGNORE_RESULT = True в вашем файле настроек celeryconfig.py и отбрасывайте такие данные.

No.7: не передавайте объекты базы данных\ORM в задачу


После обсуждения вышеизложенного на встречах локальных групп python разработчиков некоторые люди предложили включить дополнительный пункт в представленный список. О чем он? Вы не должны передавать объекты базы данных, например, модель пользователя в фоновую задачу, так как в сериализованном объекте могут оказаться уже устаревшие и некорректные данные. Если вам необходимо, то передавайте в задачу ID пользователя, а в самой задаче запрашивайте базу об этом пользователе.
Перевод: Deni Bertovic
@GDApsy
карма
23,0
рейтинг 0,0
веб-разработчик на python
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (29)

  • 0
    Возможно данная статья уже публиковалась на Хабре, но я ее не нашел. Об ошибках орфографии или опечатках пишите в личку
  • +4
    На практике Redis быстрее чем RabbitMQ в роли брокера.
    • +2
      Вполне вероятно что так, но можете описать условия в которых вы это выявили и предполагаемые причины?
      • 0
        Причина, очевидно, в самом главном достоинстве и одновременно недостатке Redis – он все держит в памяти.
    • +1
      Только скорее всего он будет терять задачи ри падениях воркеров.
    • +2
      У RabbitMQ есть встроенная веб морда с метриками для очередей.
      Редис не может накапливать сообщения в очереди, то есть там нужно чтобы и подписчики и паблишеры были онлайн.
      Насколько я в курсе в редисе нету подтверждения сообщений, нету авторизации, логического разделения очередей и много чего другого. В общем это инструменты для разных нужд и нагрузок.
      Плюс про производительность хочу сказать, что у меня в работе есть очереди которые обрабатывают тысячи сообщений в секунду и сверхнагрузки на RMQ нет, а большинству людей насколько я в курсе и 10-20/секунду никогда не достичь нагрузки. Так что предлагаю вспомнить Дональда Кнута: «Предварительная оптимизация — зло».
      • 0
        В Redis-e есть авторизация и он умеет накапливать в очереди.

        У рубистов есть Resque, который использует Redis в качестве бэкенда.
        Resque сделан в GitHub и используется ими в качестве основного MQ. И он действительно быстрый и нагрузку он держит.

        Есть очень много имплементаций на других языках и очень хорошая web паель управления (в лучших традициях GitHub).
        • 0
          «В Redis-e есть авторизация и он умеет накапливать в очереди.»
          Авторизация там не для разграничения доступа к очередям, а для доступа к Redis.

          Да, накапливает, тут я неправильно сказал.

          «У рубистов есть Resque, который использует Redis в качестве бэкенда.»
          Он Redis использует в качестве брокера очередей. А Rescue по сути — воркер-враппер, для заданий и c RMQ его нельзя сравнивать потому что это совсем разные продукты.

          Я не понимаю что вы хотите доказать, я не сказал что Redis'ом не надо пользоваться как брокером очередей, я сказал что у них разные задачи и их не нужно сравнивать. Redis для очень простых очередей с небольшим количеством неважных задач. Вот у меня на RMQ крутятся сотни очередей от десятков проектов, написанные на 4 разных языках программирования, которые изолированы друг от друга авторизацией. Данные в очередях (которые durable) ПЕРЕЖИВАЮТ РЕСТАРТ RMQ, вот в редисе так точно нельзя. Если у вас возникнут какие-то проблемы с Redis, например он начнёт жрать очень много памяти, вы не сможете выяснить почему, там нет никакой возможности мониторить сложные проблемы. Также у RMQ есть ещё море всяких уникальных возможностей которые в один комментарий не засунешь, это надо целый пост писать.
          • 0
            Я не понимаю что вы хотите доказать

            Ваше изначальное утверждение, что решение основанное на Redis недостаточно гибко/масштабируемо/надежно итд.
            Я показал пример, что Redis в качестве бэкенда для MQ достаточно серьезное решение, используемое Github, где совсем не: «очень простые очереди с небольшим количеством неважных задач». К примеру Redis-Resque и RabbitMQ вполне конкурирующие системы.

            Данные в очередях (которые durable) ПЕРЕЖИВАЮТ РЕСТАРТ RMQ, вот в редисе так точно нельзя

            Конечно можно, Redis умеет складывать данные в файловую систему и подхватывать в случае рестарта/поломки. Кроме того, есть репликация.

            По функционалу — пройдитесь по списку resque плагинов и их описанию. Заодно статья за 2009 год будет полезна почему Github написали свой велосипед, перепробовав кучу других MQ:
            highscalability.com/blog/2009/11/6/product-resque-githubs-distrubuted-job-queue.html

            Я в корне не согласен с трактовкой что Redis как бэкенд для MQ (пример resque) не подходит для серьезных задач и нагрузок. И по функционалу и по скорости (~100K сообщений/секунду на ядро одинаково достижимы как для resque так и для RabbitMQ) и по надежности (миллионы сообщений в сутки не проблема вообще ни для какой MQ).
            • 0
              А master-master между двумя площадками умеет? А реплицировать между инстансами не все очереди гуртом, а только одну (которую администратор позволит)?

              Ах черт, это же про RabbitMQ и про «большинству этого не нужно» :)
      • +1
        Редис не может накапливать сообщения в очереди, то есть там нужно чтобы и подписчики и паблишеры были онлайн.
        Насколько я в курсе в редисе нету подтверждения сообщений, нету авторизации, логического разделения очередей и много чего другого. В общем это инструменты для разных нужд и нагрузок.
        Может, в редисе всего этого нет «из коробки» в механизме pub/sub, но все это точно можно реализовать средствами самого редиса.
    • +1
      Тоже использую Редис как брокер. Радует.
      • 0
        Радует, когда не нужны два брокера в разных датацентрах, которые должны быть связаны между собой. Как пример.
  • +1
    Хотел бы попросить автора описать, чем celery выгоднее и лучше чем просто использовать RabbitMQ (я про реально используемые фичи). Потому что пару раз задумывался об этом, читал доку celery, но так там ничего полезного для нас и не нашёл особо. Как я понимаю это универсальная надстройка над разными брокерами сообщений, но вот конкретно для раббита что она добавляет?
    • +1
      celery это система для выполнения задач и rabbitmq она использует по его прямому назначению — роутить сообщения
  • 0
    У меня очень негативный опыт использования celery и особенно связки celery + mongo. Производительность мне была не важна, задачь очень мало. Но их надо было не терять. Т.е. если я добавляю задачу то она, во-первых, должна когда-нибудь выполниться, а во-вторых, мне должен прийти результат выполнения задачи. Запускаю систему, она работает, но иногда задачи исчезают. Долго копаюсь в коде, а там в celery абстракция над абстракцией и абстракцией погоняет, нахожу проблему, исправляю. Заодно понимаю, что оно by design будет терять задачи: задача выгружается в память воркера и если он упадет, то попытается в expect блоке положить её обратно в очередь. Но может и не положить.
    Потом выясняется, что при смене мастера монги celery виснет. Оно исключение не обрабатывало. Пишу патч, отправляю разработчикам. Результат — мы сами не используем mongodb, поэтому не понимает что это исправление делает, так что патч принимать не будем.
    Пока они над этим думали, я написал свой велосипед который занимает на несколько порядков меньше строк кода и который рагантированно отказоустойчив.
    • 0
      А что именно в celery привлекло? Почему не чистый RMQ (там для вашей проблемы есть durable очереди + ручное подтверждение задач, своя кластеризация + один инструмент вместо двух, меньше точек отказа).
      • 0
        В проекте уже использовалась монга. Соответсвенно можно было использовать её, а можно было поднимать рядом RabbitMQ. Отделу администрирования не понравилась идея поднимать еще один сервис и обеспечивать его отказоустойчивость. Причем опыт эксплуатации mongo был значительно больше, чем и RabbitMQ.
    • 0
      В RabbitMQ такой ситуации бы не возникло — при разрыве соединения с consumer'ом Rabbit автоматически пометит задачи как снова ожидающие в очереди. И используйте ACK_LATE.
      RabbitMQ предназначен именно для работы с очередью сообщений, у него внутренний механизм это обеспечивает. Mongo — нет, это просто хранилище. Ей сказали «запиши 1 в колонку» — она и записала. И ее не волнуют ваши очереди-шмочереди. И как поддерживаемый брокер она имеет статус Experimental в celery.
      Поэтому мне кажется, что вы зря жалуетесь. Именно из вашего описания RabbitMQ — это то, что вам бы подошло. Он довольно дружелюбен и не так сложен в установке, ИМХО.
      • 0
        Поэтому мне кажется, что вы зря жалуетесь.
        Я жалуюсь на то, что патч был отклонен с формулировкой «мы не знаем как работает эта часть кода из нашго репозитория поэтому не можем понять хороший патч или плохой, вдруг он не только исправит critical багу, но и еще что-нибудь сломает».
        И еще я жалуюсь на то, что код у celery + combu очень сложный. Очень много абстракций.

        А та часть системы, для которой у меня отлично подходил RabbitMQ — это небольшая часть, деталь реализации.
        • 0
          Не соглашусь с вами по поводу сложности. Я копался в нем и, в принципе, все довольно внятно.
          Для такого большого функционала оно очень грамотно написано — без абстракций было бы много копипасты.
          В celery/kombu настраивается все что угодно — вы можете переписать/перегрузить почти любую часть с нужным вам функционалом. Посмотрите только на список брокеров — входной интерфейс один, но работает с совершенно разными движками.
          Это уже третья версия же, обычно к такой цифре автор как раз два раза понимает как убрать накопившийся говнокод и сделать «чисто».

          А можете найти ссылку на номер бага, о котором вы говорите? Просто интересно посмотреть на комментарии… Автор Ask Solem, вроде, довольно общительный и адекватный, на критику реагирует нормально…
  • +1
    Может кому пригодится
    os.environ['CELERY_CONFIG_MODULE'] = 'conf.celeryconfig'
    указать альтернативное размещение конфига
  • 0
    Вопрос. Я занимаюсь разработкой большого проекта на Джанго, и сегодня увидел эту статью. Я не могу понять зачем нам нужен Celery. Можете привести примеры использования Celery в реальной жизни? Пытаюсь что нибудь нарыть в интернете, но все источники просто твердят, что это task queue
    • 0
      А чем он еще должен быть? Celery же и проектировался как абстракция над системами очередей. Вот, например, его использует такая джанго-батарейка: habrahabr.ru/post/253445 и ее репозиторий: github.com/LPgenerator/django-db-mailer
    • 0
      Начиная от cron задач и заканчивая любыми асинхронными операциями. Например, отправка писем.
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Много чего есть, конечно. Celery иногда может быть не нужен. Если вам не нравится «дубликат проекта в Django» — загружайте celery в отдельном проекте и не импортируйте весь проект. Хотя не думаю что будет сильно большой выигрыш, только если у вас в глобальном namespace в Django не идет обработка больших строк. Не идет же? Тогда откуда " жрали память"?

          Вы можете и не использовать RabbitMQ — если у вас один сервер с двумя всего тасками, то используйте хоть sqlite как брокер или Redis.
          Можете написать скрипт, который будет запускаться через cron каждые 5 минут и выполнять таски из базы.
          Есть еще, например, Luigi как альтернатива. Или python-multitask.
    • 0
      Классический пример: социальная сеть, в которой пользователи загружают картинки в свои профайлы. После загрузки картинки должны быть сконвертированы в один и тот же формат и обрезаны до определенных размеров. После этого они должны появиться в профайле, а всем подписчикам отправлены уведомления по почте. Если делать это синхронно запросом, то сервер может не справиться в пике, да и ждать долго всех операций, а мы не хотим долгие коннекты к серверу держать.
      Как решение — мы ставим отдельный сервер (несколько серверов), на которых крутятся celery-воркеры. при загрузке картинки она пишется на временный storage и в очередь celery кладется задача с (user_id, image_name). Юзеру в браузер возвращается «200 OK» и он знает что картинка скоро появится.
      Первый освободившийся воркер подхватывает эту задачу и обрабатывает картинку, кладет ее в базу, а потом создает новую задачу в другой очереди на отправку уведомлений (которая может тоже работать довольно долго).
  • +1
    Я люблю celery за возможность строить сложные цепочки асинхронных задач, используя их примитивы
    Например так:
    # (4 + 4) * 8 * 10
    res = chain(add.s(4, 4), mul.s(8), mul.s(10))
    Результат выполнения задачи идёт в кач-ве первого аргумента для след. задачи
    Ссылка на документацию
    Бывает весьма полезно использовать web hooks, которые позволяют в кач-ве воркера использовать сторонний сервис
    Так же часто бывает полезно, когда много мелких однотипных задач, держать соединение с базой для всех выполняемых задач, а не создавать его заново в каждой новой.
    Пример

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.