通过Django-celery安排数千个一次性(非重复)任务,以便近乎同步执行

时间:2012-08-27 18:56:09

标签: python django cron celery django-celery

某些上下文:我正在构建一个Django应用程序,允许用户预先保存操作,并在将来希望执行该操作时安排确切的日期/时间。例如,安排一个帖子在下周早上5:30以编程方式推送到Facebook墙上。

我正在寻找一个可以处理一次性任务的一千个实例的任务调度系统,所有这些都设置为几乎同时执行(错误边际加上或减去一分钟)。

我正在考虑使用Django-celery / Rabbitmq,但我注意到Celery docs没有解决一次性使用的任务。 Django-celery在这里是正确的选择(也许是通过继承CrontabSchedule)或者我的能量更好地用于研究其他方法吗?也许与Sched Module和Cron。

混在一起

2 个答案:

答案 0 :(得分:9)

编辑2:

出于某种原因,我的头脑最初被困在重复任务的领域。这是一个更简单的解决方案。

您真正需要的是为每个用户操作定义一个任务。您可以跳过存储要在数据库中执行的任务 - 这就是芹菜的用途!

再次重用你的facebook帖子示例,并再次假设你在某个地方有一个功能post_to_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参数并返回AsyncResult对象,然后检查调用结果。更多信息:http://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html#keeping-results

以下部分答案仍然具有现实意义。您仍然希望每个用户操作都有一个任务,您仍然需要考虑任务设计等,但这是一个更简单的方法来做你所要求的。

使用重复性任务的原始答案如下:

Dannyroa有正确的想法。我会在这里建立一点。

编辑/ 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调用期间以串行方式发布和发送它们,您可以生成一堆子任务并行并行执行(给定足够workers )。然后,对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

事实上,我建议阅读所有芹菜文档。

答案 1 :(得分:0)

我要做的是创建一个名为ScheduledPost的模型。

我将有一个每5分钟左右运行一次的PeriodicTask。

该任务将检查ScheduledPost表中是否有任何需要推送到Facebook的帖子。