что такое воркеры python
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. Наиболее общий пример того, что вы можете увидеть:
Что происходит, если обе задачи будут размещены в одной очереди, если иное не определено в файле celeryconfig.py. Я полностью пониманию чем может оправдывать подобный подход, у вас есть один декоратор, который создает удобные фоновые задачи. Здесь я хотел бы обратить внимание, что taskA и taskB, находясь в одной очереди могут делать совершенно разные вещи и таким образом одна из них может быть куда важнее другой, так почему они находятся все в одной корзине? Даже, если у вас один воркер, то представьте такую ситуацию что менее важная задача taskB окажется настолько массовой, что более важной задаче taskA воркер не сможет уделить необходимого внимания.Это приводит нас к к следующему пункту.
No.3: Используйте приоритеты воркеров
Путем решения проблемы, указанной выше является размещение задачи taskA в одной очереди, а taskB в другой и после этого присвоить x воркеров обработке очередь Q1, а остальных на обработку Q2, так как в нее приходит больше задач. Таким образом вы можете быть уверены, что задача taskB получит достаточно воркеров, а остальные тем временем будут обрабатывать менее приоритетную задачу, когда она придет, не провоцируя длительного ожидания и обработки. Потому, определите ваши очереди сами:
И ваши роутеры, которые определять куда направлять задачу:
Это позволит выполнять воркеры для каждой задачи:
No.4: используйте механизмы Celery для обработки ошибок
Большинство задач, которые я видел не имеют механизмов обработки ошибок. Если в задаче произошла ошибка, то она просто падает. Это может быть удобно для некоторых задач, однако большинство задач, которые я видел взаимодействовали с внешними API и падали из-за некоторых видов сетевых ошибок или иных проблем «доступности ресурса». Самый простой подход к обработке таких ошибок перевыполнить код задачи, так как, возможно, проблемы взаимодействия с внешним API были уже устранены.
Я люблю определять по умолчанию для задачи время ожидания, которое она будет ждать прежде чем попытается выполниться снова и как много попыток перевыполнения она предпримет прежде чем окончательно выбросить ошибку(параметры default_retry_delay и max_retries соответственно). Это наиболее простая форма обработки ошибок, которую я могу представить, но я видел, что и она практически не применяется. Разумеется Celery имеет и более сложные методы обработки ошибок, они описаны в документации Celery.
No.5: используйте Flower
No.6: Отслеживайте статус задачи, только если вам это необходимо
Статус задачи это информация о том успешно или нет завершилась задача. Она может быть полезна для некоторых статистических показателей. Важная вещь, которую следует понимать в данном случае: статус задачи это не результирующие данные и той работы, которая она выполняла, такая информация наиболее похожа на неявные изменения, записываемые в базу данных (такие, например, как изменения списка друзей пользователя).
Что такое воркеры python
A worker is a Python process that typically runs in the background and exists solely as a work horse to perform lengthy or blocking tasks that you don’t want to perform inside web processes.
Starting Workers
To start crunching work, simply start a worker from the root of your project directory:
Workers will read jobs from the given queues (the order is important) in an endless loop, waiting for new work to arrive when all jobs are done.
Each worker will process a single job at a time. Within a worker, there is no concurrent processing going on. If you want to perform jobs concurrently, simply start more workers.
You should use process managers like Supervisor or systemd to run RQ workers in production.
Burst Mode
By default, workers will start working immediately and will block and wait for new work when they run out of work. Workers can also be started in burst mode to finish all currently available work and quit as soon as all given queues are emptied.
This can be useful for batch work that needs to be processed periodically, or just to scale up your workers temporarily during peak periods.
Worker Arguments
New in version 1.8.0.
Inside the worker
The Worker Lifecycle
The life-cycle of a worker consists of a few phases:
Performance Notes
Basically the rq worker shell script is a simple fetch-fork-execute loop. When a lot of your jobs do lengthy setups, or they all depend on the same set of modules, you pay this overhead each time you run a job (since you’re doing the import after the moment of forking). This is clean, because RQ won’t ever leak memory this way, but also slow.
A pattern you can use to improve the throughput performance for these kind of jobs can be to import the necessary modules before the fork. There is no way of telling RQ workers to perform this set up for you, but you can do it yourself before starting the work loop.
To do this, provide your own worker script (instead of using rq worker ). A simple implementation example:
Worker Names
Retrieving Worker Information
Updated in version 0.10.0.
Worker instances store their runtime information in Redis. Here’s how to retrieve them:
If you only want to know the number of workers for monitoring purposes, Worker.count() is much more performant.
Worker with Custom Serializer
When creating a worker, you can pass in a custom serializer that will be implicitly passed to the queue. Serializers used should have at least loads and dumps method. An example of creating a custom serializer class can be found in serializers.py (rq.serializers.JSONSerializer). The default serializer used is pickle
or when creating from a queue
Queues will now use custom serializer
Worker Statistics
If you want to check the utilization of your queues, Worker instances store a few useful information:
Better worker process title
Worker process will have a better title (as displayed by system tools such as ps and top) after you installed a third-party package setproctitle :
Taking Down Workers
If, at any time, the worker receives SIGINT (via Ctrl+C) or SIGTERM (via kill ), the worker wait until the currently running task is finished, stop the work loop and gracefully register its own death.
If, during this takedown phase, SIGINT or SIGTERM is received again, the worker will forcefully terminate the child process (sending it SIGKILL ), but will still try to register its own death.
Using a Config File
If you’d like to configure rq worker via a configuration file instead of through command line arguments, you can do this by creating a Python file like settings.py :
The example above shows all the options that are currently supported.
Alternatively, you can also pass in these options via environment variables.
Custom Worker Classes
There are times when you want to customize the worker’s behavior. Some of the more common requests so far are:
Round Robin and Random strategies for dequeuing jobs from queues
In some other circumstances, when a worker is listening to multiple queues, it can be useful to pull jobs from the different queues randomly. The custom class rq.worker.RandomWorker implements this strategy. In fact, whenever a job is pulled from any queue, the list of queues is shuffled, so that no queue has more priority than the other ones.
Custom Job and Queue Classes
Don’t forget to use those same classes when enqueueing the jobs.
Custom DeathPenalty Classes
When a Job times-out, the worker will try to kill it using the supplied death_penalty_class (default: UnixSignalDeathPenalty ). This can be overridden if you wish to attempt to kill jobs in an application specific or ‘cleaner’ manner.
DeathPenalty classes are constructed with the following arguments BaseDeathPenalty(timeout, JobTimeoutException, job_id=job.id)
Custom Exception Handlers
Sending Commands to Worker
New in version 1.6.0.
Starting in version 1.6.0, workers use Redis’ pubsub mechanism to listen to external commands while they’re working. Two commands are currently implemented:
Shutting Down a Worker
send_shutdown_command() instructs a worker to shutdown. This is similar to sending a SIGINT signal to a worker.
Killing a Horse
send_kill_horse_command() tells a worker to cancel a currently executing job. If worker is not currently working, this command will be ignored.
Stopping a Job
New in version 1.7.0.
You can use send_stop_job_command() to tell a worker to immediately stop a currently executing job. A job that’s stopped will be sent to FailedJobRegistry.
It is open sourced under the terms of the BSD license.
Worker-ы и shared worker-ы
Во всех популярных языках есть потоки (threads). В браузерном javascript для параллельной обработки используются worker-ы.
Под катом рассказ о том, как ими пользоваться, какие ограничения есть в воркерах и об особенностях взаимодействия с ними в разных браузерах.
Что такое worker
Создание worker-а
Worker создаётся из отдельного скрипта:
Shared worker идентифицируется по URL. Чтобы создать второй воркер из одного файла, можно добавить какой-нибудь параметр в URL (worker.js?num=2).
Worker можно создать и без отдельного файла. Например, так создать его из текста функции:
Создать worker из worker-а можно только в Firefox. В Chrome можно создать shared worker из странички и передать его порт другому worker-у (об этом ниже).
Ограничения worker-ов
В worker-е нельзя использовать DOM, вместо window глобальный объект называется self. Нельзя получить доступ к localStorage и рисовать на canvas. Такие же ограничения обычно есть во всех десктопных API: доступ к окнам только из UI-треда.
Доступ к объектам
Из worker-ов нельзя вернуть объект. В javascript нет lock-ов и других возможностей потокобезопасности, поэтому из worker-ов нельзя передавать объекты по ссылке, всё отправленное в worker или из него будет скопировано.
Пока что worker-ы не поддерживают CORS, создать worker можно только загрузив его со своего домена.
Размер стека
Для worker-ов выделяется меньший размер стека, иногда это имеет значение:
Chrome/osx | Firefox/osx | Safari/osx | Chrome/win | Firefox/win | IE11/win | |
---|---|---|---|---|---|---|
web | 20 800 | 48 000 | 63 000 | 41 900 | 51 000 | 63 000 |
worker | 5 300 | 43 300 | 6 100 | 21 300 | 37 000 | 30 100 |
console
До недавнего времени не было, но обычно сейчас уже есть. В некоторых браузерах консоли в worker-ах нет, поэтому перед обращением лучше проверить её доступность.
Взаимодействие с worker-ом
После создания worker-а ему можно отправить сообщение:
Подписаться на сообщение в worker-е так:
Аналогично и обратно, из worker-а можно вызвать или self.postMessage, или port.postMessage для shared worker-ов.
Transferables
Передавать по ссылке кое-что таки можно. Для этого существует второй параметр в postMessage, transferList:
В transferList можно передать список объектов, которые будут перемещены. Поддерживаются только ArrayBuffer и MessagePort. В вызывающем контексте объект будет очищен (neutered): у ArrayBuffer будет нулевая длина, и попытка его повторной отправки приведёт к ошибке:
Взаимодействие двух worker-ов
В Firefox можно создать worker из worker-а (стандарт определяет subworker-ы).
Сейчас в хроме нельзя создать worker из worker-а, а иногда worker-ам надо взаимодействовать между собой. Самый простой способ — сделать передачу сообщений от одного к другому через код страницы. Но это неудобно, потому что: 1. надо писать дополнительный код, 2. в 2 раза увеличивает количество взаимодействий и копирования данных, 3. требует выполнения кода в UI-контексте.
Worker можно научить общаться с shared worker-ом, передав ему порт shared worker-а, при этом передаваемый порт в UI-контексте мы теряем; если он нужен, надо будет переподключиться к shared worker-у, создав его заново. Передача порта выглядит так:
Правда для синхронизации всё равно движком V8 используется UI-контекст, в чём можно убедиться, завесив страничку на какое-то время: worker-ы продолжают работать, а postMessage между ними не ходят, ожидая особождения UI-контекста.
Производительность postMessage
Убийство worker-а
К сожалению, сейчас нет штатного способа отследить закрытие worker-а или страницы, его использующей.
Учёт ресурсов в shared worker-ах в Chrome
SharedWorker живёт процессе в страницы, создавшей его. На неё учитывается и показывается в task manager CPU и память, которые потребляет worker. Если страничку закроют, её процесс с worker-ом отдаст память, используемую страницей (не сразу, через некоторое время после закрытия) и останется жить, пока другие страницы используют этот worker. Интересно, что при этом такой процесс полностью исчезнет из статистики хрома: ни память, ни CPU пользоваель не сможет отследить в его внутреннем task manager-е. Это неприятно, т.к. пользователь скорее всего не догадается, почему браузер стал потреблять так много ресурсов.
Отладка worker-ов
В chrome shared worker-ы доступны на страничке chrome://inspect/#workers:
Именно туда пишется вывод console из worker.
Dedicated worker в хроме и IE отлаживается в страничке, на которой он выполняется:
В других браузерах с отладкой worker-ов пока что плохо.
Can I Use.
Поддержка разных worker-ов на Can I Use. Коротко, применительно к сегодняшнему вебу: worker есть на современных браузерах, sharedworker — на продвинутых десктопных браузерах, serviceworker — пока что рано.
Всё написанное актуально на лето 2015 года, не забывайте, что веб быстро меняется.
Python-RQ: очередь задач на базе Redis
Не так давно я рассказывал о том как лучше всего работать с очередью задач Celery. Бесспорно, Celery является наиболее продвинутым инструментом в экосистеме Python с массой возможностей для работы и управления выполнением ваших задач, включая кастомизацию брокеров и бэкэндов. Но Python мир не им един.
В этой заметке я расскажу об инструменте под названием Python RQ. Аббревиатура RQ обозначает Redis Queue. Из названия можно догадаться, что инструмент создан вокруг системы Redis. Она в данном случае выступает в качестве брокера и бэкэнда. RQ следует философии UNIX, то есть является маленьким инструментом, решающим одну задачу очень хорошо.
Установка
Тут всё просто и обыденно. Устанавливаем в отдельное виртуальное окружение:
Для любителей попробовать самое свежее следует устанавливать прямо из master ветки репозитория с кодом:
Для работы с очередью задач, также необходимо в систему установить redis-server (RQ требует Redis версии >= 2.6.0):
Под Windows RQ работать не будет, так как полагается на механизм fork для порождения процессов, выполняющих задачи. Если вы используете Windows, то можно воспользоваться Vagrant для выполнения кода на виртуальной машине под управлением, например, Ubuntu 16.04 LTS.
Механизм работы RQ следующий:
Кодинг
Я решил не заморачиваться с названиями файлов, поэтому буду придерживаться практики с Celery. Задачи буду хранить в файле tasks.py:
tasks.py (пример кода взят с официальной документации python-rq)
Код считает количество слов на заданном url. Из кода можно заметить, что нам также необходимо установить пакет requests. О том как работать с HTTP в Python я ранее уже писал.
Запускаем первый worker из корневой директории с кодом, чтобы он мог импортировать модули без ошибок импорта:
В другом окне терминала выполняем наш код с приложением:
И видимо вот такой результат:
Задача успешно выполнена воркером.
Функциональные возможности
Сохранение результата
По умолчанию результат, возвращаемый функцией-таском, хранится в Redis 500 секунд. Это можно изменить, передав значение в секундах в именованный аргумент result_ttl.
Таймаут
Таймаут на выполнение можно задать как на уровне очереди, так и конкретно для задачи:
В данном случае мы задали 30 секунд на выполнение для задач, попадающих в очередь с названием low.
Отложенные задачи
К сожалению (или к счастью, для кого как), в стандартном пакете RQ нет возможности откладывать выполнение задачи до наступления определенного времени (конкретной даты или спустя какое-то время). Но умельцы разработали расширение под названием RQ Scheduler.
Вот как можно его заюзать:
Для того, чтобы задачи корректно были переданы воркерам, необходимо запустить демон rqscheduler
Я задал выполнение задачи каждые 10 секунд 5 раз подряд.
Интеграция с веб-фреймворками
Большой необходимости в специальной интеграции с python веб-фреймворками нет, но для тех, кто любит иметь всё в одном место, умельцы разработали плагин для работы с Django. Имя ему django-rq, разработкой занимался автор RQ Scheduler. Отдельно этот пакет описывать я думаю смысла нет, так как настроить его совсем просто.
Мониторинг
В статье про Celery я описывал, что в production среде очень важно иметь наглядную картину выполнения ваших задач, так как это помогает выявлять проблемы на ранних стадиях работы (а чем раньше мы выявим баг, тем дешевле он обойдётся). Аналогом Flower для RQ является RQ Dashboard. Это небольшое, но очень полезное веб-приложение на Flask, позволяющее мониторить выполнение задач в режиме реального времени. Установка и настройка его тривиальна (см. ссылку в конце статьи).
Заключение
Целью данной заметки являлось обратить ваше внимание на столь маленький и гибкий инструмент как Python RQ и показать, что для решения небольших задач он порой подходит куда лучше Celery. Несомненно, RQ не обладает широчайшим функционалом присущим монструозному Celery. Например, RQ не умеет выполнять задачи в группе параллельно, дожидаясь результата от всех тасков (group в Celery), к нему невозможно прикрутить сторонний брокер, скажем RabbitMQ. В своей практике я использовал RQ на production, но, к сожалению, мне не доводилось его проверять под большими нагрузками.
Если вас заинтересовал инструмент, то более подробную информацию можно узнать по ссылкам, приведённым ниже.
Полезные ссылки
💌 Присоединяйтесь к рассылке
Понравился контент? Пожалуйста, подпишись на рассылку.
Celery: начинаем правильно
В этой статье мне хотелось бы поделиться с читателями своим опытом работы с таким замечательным инструментом в Python как Celery. Celery это ничто иное как распределённая очередь заданий, реализованная на языке Python. На момент написания этой статьи, самой последней версией является 3.1.20. Неосведомлённый читатель может не знать для чего вообще нужна система очередей задач наподобие Celery, поэтому кратко поясню этот момент.
Что такое Celery и зачем оно нам?
Часто ли вам приходилось сталкиваться с типовыми задачами в веб-приложениях вроде отправки электронного письма посетителю или обработки загруженных данных. Чаще всего такого рода манипуляции не требуют участия конечного пользователя вашего проекта, то есть их можно выполнять в фоновом режиме. Те из нас, кто реализует выполнение этих задач в одном из процессов веб-сервера, «тормозят» тем самым его работу, увеличивая время отклика и ухудшают user experience.
В данной заметке я опущу вводную информацию по установке и настройке Celery в вашем проекте. Кстати, Celery из коробки умеет работать с Django. Ранее был отдельный python пакет, соединяющий Django и Celery,именовался он django-celery. Сейчас он заброшен, так как последнее обновление было более года назад. Стоит отметить, что django-celery не работает Django 1.9 из-за изменений в работе cache backend. Исправленную версию можно посмотреть в моём форке. Одной из удобных фич django-celery является интеграция с Django Admin по части управления periodic tasks.
Советы по работе с Celery
Не используйте базу данных в качестве broker/backend
Разделяйте задачи по очередям
Это очень важный момент. По мере развития вашего приложения, в проекте будут появляться критичные для выполнения задачи: проверка статуса платежа, формирование отчёта, отправка электронных писем и так далее. Терять их недопустимо. Если все задачи складировать в одну очередь, то в один прекрасный момент она может забиться, поставив под угрозу выполнение критически важного кода. Мой подход: разделяйте очереди по приоритетам.
Несомненно очередей может быть больше, тут всё на усмотрение разработчика и архитектуры его приложения.
В базовых настройках Celery это выглядит следующим образом:
В данном конкретном примере объявлена очередь по-умолчанию под названием normal. То есть задачи явно не указанные в списке будут автоматически распределены в эту очередь. В high попадает задача под названием check_payment_status, а в low задача close_session.
Запускать исполнителей Celery для этих очередей необходимо следующим образом:
Здесь мы явно задаём имена исполнителей и названия очередей в которых необходимо мониторить задачи на исполнение.
ВАЖНО! Если вы явно указали для задачи очередь в которую ей нужно будет падать, и при этом запустили одного из исполнителей Celery без явного указания очереди, например вот так:
То при наступлении ситуации, когда все исполнители очереди high будут заняты, Celery автоматически перенаправит новую задачу исполнителям без конкретной очереди. Поэтому при использовании раздельных очередей задач, не запускайте исполнителей без указания для них явного наименования очереди.
Логгируйте ошибки
Важной опцией здесь является наличие CELERYD_HIJACK_ROOT_LOGGER = False. По-умолчанию значение этой переменной является True, что позволяет celery «перекрывать» все ранее объявленные кастомные обработчики logging.
При указанном выше подходе нет необходимости дополнительно в коде задач (task) логгировать ошибки/исключения отдельно. О том что такое Sentry, для чего оно используется и как его настроить я напишу отдельную статью немного позже.
Пишите задачи маленькими
При написании задач старайтесь придерживаться принципа минимализма кода. То есть не нужно в самом celery task описывать бизнес логику задачи. Например, если вам необходимо генерировать и отправлять отчёт, то не нужно в самом task писать код генерации и отправки. Разбейте его на 3 части:
Это, во-первых, позволит легче читать код (есть явное разделение на подзадачи). Во-вторых, тестировать такой код намного легче (привет модульным тестам!). В-третьих, отлавливать ошибки также будет намного легче и прозрачнее.
«Гасите» задачи вовремя
Явно указывайте лимит на выполнение задачи. Это можно сделать несколькими способами:
Указание таймлимита очень важно, так как в некоторых случаях его отсутствие попросту приведёт к «зависанию» исполнителя при выполнении неоднозначных задач (требующих длительного времени, коннект к внешнему сервису и так далее).
Не храните результаты исполнения без необходимости
В большинстве случаев результат выполнения вашей задачи вам не нужен (например, если происходит отправка письма). В такой ситуации вам нет необходимости хранить что-то. Если ваши задачи полностью попадают в эту категорию, то в настройках Celery можно задать глобальный параметр CELERY_IGNORE_RESULT = True, который будет игнорировать результат исполнения всех ваших task-функций.
Используйте Flower для мониторинга исполнения задач
Всегда используйте Flower при работе с Celery. Всегда! Данный инструмент это небольшое веб приложение, написанное с использованием микрофреймворка Flask, а также Tornado для поддержки веб-сокетов. Flower позволяет вам всегда быть в курсе того как исполняются ваши задачи. Немного скриншотов:
Не поленитесь и потратьте время на его изучение. Оно окупится многократно!
Не передавайте ORM объекты в качестве аргументов
Я пару раз попадался на этом хитром трюке, который потрепал мне изрядно нервы. Рассмотрим вот такой код:
Не самый лучший пример для демонстрации побочного эффекта при передаче ORM объекта, но всё же. В данной ситуации код, описанный в send_notification, сохранит объект, изменив лишь notified = True, но activated останется по-прежнему равен False. Лучшим решением будет передача идентификатора объекта в базе данных, а в самой task функции необходимо непосредственно обращаться к объекту через его id.
BROKER_TRANSPORT_OPTIONS и visibility_timeout
При использовании Celery нередко приходиться прибегать к помощи отложенных задач, используя apply_async и передавая аргументы eta или countdown. Но делать это нужно осторожно, так как даже здесь нас поджидают «подводные камни». О чём речь? Очень часто у разработчиков, начинающих использовать очередь задач вроде Celery, происходят аномалии вроде выполнения одного и того же таска несколькими воркерами одновременно. Согласитесь, нежелательный сценарий. Так может происходить по причине того, что время, через которое должна выполниться задача, превышает visibility_timeout. По умолчанию для Redis этот параметр равен 1 часу. То есть если вы укажете выполнение задачи через 2 часа, то демон celery подождёт 1 час, поймёт, что никто из доступных воркеров не откликнулся и насильно назначит всем воркерам её выполнение при наступлении дедлайна (eta/countdown). Поэтому не забывайте про этот параметр, если вы собираетесь использовать механизмы eta/countdown/retry, задайте visibility_timeout равным самому длительному eta/countdown в вашем проекте. Подробнее можно почитать тут.
UPD: С недавних пор у блога появился свой Telegram канал, где я стараюсь делиться со своими подписчиками интересными находками из сети на тему разработки программного обеспечения и смежных с этой областью материалов.
Long-running tasks
Старайтесь не использовать Celery для выполнения долгих задач. На этот аргумент есть ряд причин:
Если нет возможности использовать что-то другое, то при работе с long-running tasks в Celery знайте следующее:
По-умолчанию 1 воркер процесс будет забирать из очереди 4 задачи за раз. Это особенно актуально знать, если Celery масштабируется на кластере через центрального брокера. То есть, если у вас 3 отдельные машины и на каждой крутится по 10 воркеров на очередь, то каждая машина будет забирать по 40 задач. Отсюда очевидно возникает проблема равномерного распределения задач по кластеру. Такое поведение оправдано в некоторых случаях, т.к. оно уменьшает количество обращений к брокеру, увеличивая производительность при выполнении небольших тасков. Чтобы изменить это, переопределите параметр CELERYD_PREFETCH_MULTIPLIER. Например:
Долгоживущие процессы имеют тенденцию к пожиранию памяти, но вот назад её зачастую не возвращают, поэтому в контексте использования Celery с ними иногда имеет смысл перезагружать воркеры после выполнения заданного количества тасков. За это отвечает параметр CELERYD_MAX_TASKS_PER_CHILD
Настройка выше будет перезагружать воркер-процесс после выполнения 1 таска.
Полезные ссылки
💌 Присоединяйтесь к рассылке
Понравился контент? Пожалуйста, подпишись на рассылку.