Планирование тысяч разовых (не повторяющихся) задач для почти одновременного выполнения через 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.