优化

Introduction:引言

默认配置会做出很多妥协。它对任何一种情况都不是最优的,但对大多数情况都足够好。

可以根据特定的使用情形应用优化。

优化可以应用于运行环境的不同属性,包括执行任务所需的时间、使用的内存量或高负载时的响应能力。

Ensuring Operations

在《Programming Pearlsopen in new window》一书中,Jon Bentley 通过提问提出了粗略计算概念;

“密西西比河一天流出多少水?”

本练习的目的是表明系统可以及时处理的数据量是有限度的。 粗略计算可以用作提前计划的一种方法。

该章节可在以下网址免费阅读:The back of the envelopeopen in new window。这本书是一本经典著作。强烈推荐。

在Celery中,如果一个任务需要花费十分钟才能完成,而每分钟有10个新任务,那么队列永远不可能是空的。这就是监控队列长度之所以很重要的原因。

一种办法是使用Munin。你还应该设置告警,以便队列长度变的不可接受时能尽快知道。这样,您就可以采取适当的操作,例如添加新的工作节点或撤消不必要的任务。

General Settings:一般设置

Broker Connection Pools:Broker连接池

2.5之后默认开启Broker连接池。

You can tweak the broker_pool_limitsetting to minimize contention, and the value should be based on the number of active threads/green-threads using broker connections.

Using Transient Queues:使用临时队列

默认情况下,Celery创建的队列是持久的。这意味着broker将把消息写入磁盘,以确保即使重新启动broker,任务也会执行。

但在某些情况下,信息丢失也没关系,所以不是所有的任务都需要持久性。

您可以为这些任务创建临时队列以提高性能:

from kombu import Exchange, Queue

task_queues = (
    Queue('celery', routing_key='celery'),
    Queue('transient', Exchange('transient', delivery_mode=1),
          routing_key='transient', durable=False),
)

或者通过使用task_routes:

task_routes = {
    'proj.tasks.add': {'queue': 'celery', 'delivery_mode': 'transient'}
}

delivery_mode 更改了发送到此队列的消息的方式。 值 1 表示消息不会写入磁盘,值 2(默认)表示消息可以写入磁盘。

要将任务定向到新的临时队列,您可以指定 queue 参数(或使用 task_routes 设置):

task.apply_async(args, queue='transient')

更多信息参考路由指南

Worker Settings

Prefetch Limits

预取是从AMQP继承而来的一个术语,但它经常被用户误解。

预读限制指的是一个worker可以给自己预先准备的任务(消息)的数量。如果是0,work将持续消费消息,不考虑可能有其他可用的worker可能能够更快地处理它们,或者消息甚至可能不适合内存。

RabbitMQ 和其他broker轮询传递消息,因此这不适用于活动系统。 如果没有预取限制并且您重新启动集群,则启动节点之间会有时间延迟。 如果有 3 个离线节点和 1 个活动节点,则所有消息都将传递到活动节点。

RabbitMQ and other brokers deliver messages round-robin, so this doesn’t apply to an active system. If there’s no prefetch limit and you restart the cluster, there will be timing delays between nodes starting. If there are 3 offline nodes and one active node, all messages will be delivered to the active node.

默认的预读数量是worker_prefetch_multiplier设置的数量乘以并行数量(进程/线程/绿色线程)

这里有并行设置:worker_concurrency以及celery worker -c选项

如果您有许多持续时间较长的任务,则希望乘数值为1:这意味着它一次只能为每个worker进程保留一个任务。(为1的话就是关闭预读了)

但是 – 如果您有许多短期运行的任务,并且吞吐量/往返延迟对您很重要,那么这个数字应该很大。 当消息被读取到内存中后,worker每秒可以处理更多任务。 您可能需要进行试验才能找到最适合您的价值。 在这些情况下,像 50 或 150 这样的值可能有意义。 或者 64 或 128。

如果长时间任务和短时间任务都有,最好的方法是运行两个节点,通过路由将它们分开。(查看任务路由)

Reserve one task at a time

只有在确认open in new window任务后,才会从队列中删除任务消息,因此,如果worker在确认任务之前崩溃,则可以将其重新交给另一个worker(or the same after recovery)。

当使用默认的 early acknowledgment 而且 prefetch multiplier 设置为 1,意味着 worker 最多会为每个 worker 进程保留一个额外的任务:或者换句话说,如果 worker 以 -c 10open in new window 启动,worker 可以保留最多 20 个任务(10 个已确认的任务正在执行,10 个未确认的保留任务)。

Often users ask if disabling “prefetching of tasks” is possible, but what they really mean by that, is to have a worker only reserve as many tasks as there are worker processes (10 unacknowledged tasks for -c 10open in new window)

That's possible, but not without also enabling late acknowledgmentopen in new window. Using this option over the default behavior means a task that’s already started executing will be retried in the event of a power failure or the worker instance being killed abruptly, so this also means the task must be idempotentopen in new window

另外:

Notes at Should I use retry or acks_late?open in new window

您可以使用下列组态选项来启用此行为:

task_acks_late = True
worker_prefetch_multiplier = 1

Memory Usage

如果你的一个worker内存占用很高,首先,您需要确定Celery主进程上是否也出现了此问题。Celery主进程的内存使用量在启动后不应继续急剧增加。如果遇到了这种情况,可能是内存写了的BUG,应将其报告给Celery。

如果只有子进程的内存使用率较高,则表明您的任务存在问题。

请记住,Python 进程内存使用量有一个"high watermark",并且在子进程停止之前不会将内存返还给操作系统。这意味着单个高内存使用任务可能会永久增加子进程的内存使用量,直到它重新启动。解决此问题可能需要向您的任务添加分块逻辑以减少峰值内存使用量。

Keep in mind, Python process memory usage has a "high watermark" and will not return memory to the operating system until the child process has stopped. This means a single high memory usage task could permanently increase the memory usage of a child process until it’s restarted. Fixing this may require adding chunking logic to your task to reduce peak memory usage.

由于子进程中的"high watermark" 和/或 内存泄漏,Celery workers有两种主要方法可以帮助减少内存使用:worker_max_tasks_per_child 和 worker_max_memory_per_child 设置。

这些设置不能太低,否则worker可能会浪费大量时间在重启上而不是处理任务。比如,将worker_max_tasks_per_child设置为1,并且子进程启动花费1s,那么这个子进程每分钟做多只能处理60个任务(假设任务立即运行)。当任务总数总是超过worker_max_memory_per_child时,可能会出现类似问题。