Планирование тысяч разовых (не повторяющихся) задач для почти одновременного выполнения через Django-celery
некоторый контекст: я создаю приложение Django, которое позволяет пользователю предварительно сохранить действие и запланировать точную дату/время в будущем, которое они хотят выполнить. Например, планирование сообщения, которое будет программно перенесено на стену Facebook на следующей неделе в 5: 30.
Я ищу систему планирования задач, которая могла бы обрабатывать тысячи экземпляров одноразовой задачи, все настроены на выполнение почти одновременно (погрешность плюс или минус минута).
Я рассматриваю Django-сельдерей / Rabbitmq для этого, но я заметил сельдерей docs не решают задачи, предназначенные для одноразового использования. Является ли Django-celery правильным выбором здесь (возможно, путем подкласса CrontabSchedule) или моя энергия лучше тратится на исследование какого-то другого подхода? Возможно, взламывая что-то с помощью Модуль Sched и Cron.
2 ответов
Edit 2:
по какой-то причине моя голова изначально застряла в области повторяющихся задач. Вот более простое решение.
все, что вам действительно нужно, это определить одну задачу для каждого действия пользователя. Вы можете пропустить сохранение задач, которые будут выполняться в вашей базе данных-вот для чего здесь сельдерей!
повторное использование вашего примера Facebook post снова и снова, предполагая, что у вас есть функция post_to_facebook
где-то, который принимает пользователя и некоторый текст, делает некоторая магия, и публикует текст на facebook этого пользователя, вы можете просто определить, что это такая задача:
# Task to send one update.
@celery.task(ignore_result=True)
def post_to_facebook(user, text):
# perform magic
return whatever_you_want
когда пользователь готов запросить такой пост, вы просто скажите сельдерею, когда запускать задачу:
post_to_facebook.apply_async(
(user, text), # args
eta=datetime.datetime(2012, 9, 15, 11, 45, 4, 126440) # pass execution options as kwargs
)
это все подробно здесь, среди целого ряда доступных вариантов вызова: http://docs.celeryproject.org/en/latest/userguide/calling.html#eta-and-countdown
Если вам нужен результат вызова, вы можете пропустить ignore_result param в определении задачи и получить объект AsyncResult обратно, а затем проверить его для результатов вызова. Подробнее здесь: http://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html#keeping-results
некоторые из ответов ниже по-прежнему актуальна. Вы все еще хотите задачу для каждого действия пользователя, вы все еще хотите думать о дизайне задачи и т. д., но это гораздо более простой способ сделать то, что вы просили о.
оригинальный ответ с использованием повторяющихся задач:
у Даннироа есть правильная идея. Я немного на этом остановлюсь.
Edit / TLDR: Ответ да, сельдерей подходит для ваших нужд. Возможно, Вам просто придется пересмотреть свое определение задачи.
Я предполагаю, что вы не позволяете своим пользователям писать произвольный код Python для определения своих задач. Короче говоря, вам придется предопределить некоторые действия пользователи могут планировать, а затем позволяют им планировать эти действия по своему усмотрению. Затем вы можете просто запустить одну запланированную задачу для каждого действия пользователя, проверяя записи и выполняя действие для каждой записи.
одно действие пользователя:
используя Ваш пример Facebook, вы будете хранить обновления пользователей в таблице:
class ScheduledPost(Model):
user = ForeignKey('auth.User')
text = TextField()
time = DateTimeField()
sent = BooleanField(default=False)
затем вы будете запускать задачу каждую минуту, проверяя записи в этой таблице, которые планируется разместить в в последнюю минуту (на основе поля ошибки, о котором вы упомянули). Если очень важно, чтобы вы попали в окно "одна минута", вы можете планировать задачу чаще, скажем, каждые 30 секунд. Задача может выглядеть так (в myapp/tasks.py):
@celery.task
def post_scheduled_updates():
from celery import current_task
scheduled_posts = ScheduledPost.objects.filter(
sent=False,
time__gt=current_task.last_run_at, #with the 'sent' flag, you may or may not want this
time__lte=timezone.now()
)
for post in scheduled_posts:
if post_to_facebook(post.text):
post.sent = True
post.save()
конфигурация может выглядеть так:
CELERYBEAT_SCHEDULE = {
'fb-every-30-seconds': {
'task': 'tasks.post_scheduled_updates',
'schedule': timedelta(seconds=30),
},
}
дополнительные действия пользователя:
для каждого действия пользователя в дополнение к публикации в Facebook вы можете определить новую таблицу и новую задача:
class EmailToMom(Model):
user = ForeignKey('auth.User')
text = TextField()
subject = CharField(max_length=255)
sent = BooleanField(default=False)
time = DateTimeField()
@celery.task
def send_emails_to_mom():
scheduled_emails = EmailToMom.objects.filter(
sent=False,
time__lt=timezone.now()
)
for email in scheduled_emails:
sent = send_mail(
email.subject,
email.text,
email.user.email,
[email.user.mom.email],
)
if sent:
email.sent = True
email.save()
CELERYBEAT_SCHEDULE = {
'fb-every-30-seconds': {
'task': 'tasks.post_scheduled_updates',
'schedule': timedelta(seconds=30),
},
'mom-every-30-seconds': {
'task': 'tasks.send_emails_to_mom',
'schedule': timedelta(seconds=30),
},
}
скорость и оптимизация:
чтобы получить больше пропускной способности, вместо того, чтобы перебирать обновления для публикации и отправлять их последовательно во время post_scheduled_updates
вызов, вы можете создать кучу подзадач и делать их параллельно (учитывая достаточно работники). Затем призыв к post_scheduled_updates
работает очень быстро и планирует целую кучу задач-по одной для каждого обновления fb-для запуска как можно скорее. Это будет выглядеть примерно так это:
# Task to send one update. This will be called by post_scheduled_updates.
@celery.task
def post_one_update(update_id):
try:
update = ScheduledPost.objects.get(id=update_id)
except ScheduledPost.DoesNotExist:
raise
else:
sent = post_to_facebook(update.text)
if sent:
update.sent = True
update.save()
return sent
@celery.task
def post_scheduled_updates():
from celery import current_task
scheduled_posts = ScheduledPost.objects.filter(
sent=False,
time__gt=current_task.last_run_at, #with the 'sent' flag, you may or may not want this
time__lte=timezone.now()
)
for post in scheduled_posts:
post_one_update.delay(post.id)
код, который я опубликовал, не тестируется и, конечно, не оптимизирован, но он должен вывести вас на правильный путь. В вашем вопросе вы подразумевали некоторую озабоченность пропускной способностью, поэтому вам нужно будет внимательно изучить места для оптимизации. Одним из очевидных является массовое обновление вместо итеративного вызова post.sent=True;post.save()
.
Подробнее:
дополнительная информация о периодических задачах: http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html.
раздел о стратегиях проектирования задач: http://docs.celeryproject.org/en/latest/userguide/tasks.html#performance-and-strategies
здесь есть целая страница об оптимизации сельдерея:http://docs.celeryproject.org/en/latest/userguide/optimizing.html.
эта страница о подзадачах также может быть интересной: http://docs.celeryproject.org/en/latest/userguide/canvas.html.
на самом деле, я рекомендую прочитать все документы сельдерея.
Что я сделаю, это создать модель под названием ScheduledPost.
Я буду PeriodicTask, который ходит каждые 5 минут или так.
задача проверит таблицу ScheduledPost для любого сообщения, которое нужно нажать на Facebook.