理解Celery的worker

5 篇文章 0 订阅
1 篇文章 0 订阅

Celery是一个异步任务队列系统,利用它,可以将繁重的工作分配到多台服务器上执行,使水平扩展处理能力成为可能。worker是Celery的核心的执行模型,对其进行比较全面的理解,对于更加有信心地使用Celery会有很大的好处。

worker的类型

顾名思义,worker就是做具体工作的实体。为了便于理解,可以将worker同一台服务器对应(也就是在一台服务器上运行一个worker。当然,这里只是简化,一台服务器上可能运行多个worker)。在Celery中,worker有多种类型,针对的是不同特点的任务场景。

1. prefork(默认):worker会开启多个进程来执行具体的任务实例(task instance),适合于CPU密集型应用;这会开启一个worker主进程,和一组工作进程(如果并行度设置为2,当使用ps -ef | grep celery的时候,会看到3个进程,多出来的一个就是主进程)。

celery -A djangogo worker -P prefork -c 2

2. eventlet:适用于I/O密集型应用;底层使用epoll或者libevent来驱动多路复用。要注意不要在这样的worker中运行CPU密集型的任务实例。

celery -A djangogo worker -P eventlet -c 20000

3. gevent:类似于eventlet,基于libev或者libuv事件循环。

celery -A djangogo worker -P gevent -c 20000

4.solo:接收控制指令同运行任务实例在同一个进程里执行,如果任务实例执行时间较长会阻塞控制指令请求的响应,客户端需要适度增加超时时间设置。(一般不使用)

celery -A djangogo worker -P solo

5. threads:任务实例在线程中执行。这里的线程就是我们通常认知上的线程,线程维护通常要明显大于协程,所以并行度的设置也需要考虑到维护的代价。

celery -A djangogo worker -P threads -c 200

worker的并行度

在上面的命令中的"-c"选项是用来制定worker的并行度的,这个并行度是指worker在某一时刻能够同时处理的任务实例的数量,如果提交的任务的实例的数量超过了这个worker的并行度限制,那么就需要排队等候了,当有任务实例执行结束的时候,排队的任务实例就被闲下来的worker处理进程(或线程/协程)来执行。针对不同类型的worker类型,对其指定并行度意味着什么呢?我们就用上面的例子来说一下。

prefork:并行度意味着开启几个进程来执行任务实例。多个进程可以在一台服务器多个物理CPU上执行,所以这个对于充分利用CPU的能力是非常合适的,适合于CPU密集型的任务实例。对其设置要充分考虑系统CPU的核心数量,防止大量的上下文切换开销。其默认值就是系统CPU的个数。

eventlet/gevent:其并行度意味着可以同时处理多少路I/O任务,对于I/O密集型的任务实例,并且I/O能力有富余的情况下,提升并行度可以充分利用系统的I/O能力,比如对于HTTP请求的转发。但是,也有前提条件,因为使用这种worker类型需要对普通函数执行monkey patch以避免阻塞事件循环,所以对于那些不能进行monkey patch的函数或者模块(C语言实现)就要仔细研究是否有替代方案;另外计算密集型的运算也会阻塞事件循环。对于I/O场景也要做对应的区分,比如对于高吞吐的I/O场景,可能并行度就要适度降低;对于高实时的I/O场景,可以将这个值相应扩大。

solo:没有并行度的概念。但是要注意这个worker运行任务实例的时候,会阻塞控制命令的执行。

threads:并行度就是开启的普通线程的数量。这个比较容易理解,但是其维护要比协程代价高,由于GIL的限制,适合于I/O密集型的任务实例。

worker关闭时如何避免当前正在执行的任务丢失?

如果任务的实现进行了逻辑上的修改,更新代码之后,需要先关闭worker进程,再次启动worker以加载新的代码,用新的逻辑处理任务实例。那么关闭时如果有正在执行的任务,这个任务会不会被丢失掉呢?worker的主进程(管理进程)提供了几个关闭worker的方式,其中一个就是Warm shutdown,就是一种比较优雅的关闭方式,它会等待当前工作进程(或者线程/协程)中的任务实例执行完毕之后,才完成关闭。不同的关闭方式,worker主进程会有不同的输出:

 当执行

kill -INT [WorkerMainProcessID]  #  worker主进程的进程ID需要通过ps -ef | grep celery获取;或者通过 celery -A app inspect stats的输出结果获取

或者

kill -TERM [WorkerMainProcessID]  #  worker主进程的进程ID需要通过ps -ef | grep celery获取;或者通过 celery -A app inspect stats的输出结果获取

时,worker主进程都会有如下的输出:

[2020-04-29 10:37:43,736: WARNING/MainProcess] /Users/Felix/PycharmProjects/djangogo/venv/lib/python3.6/site-packages/celery/fixups/django.py:203: UserWarning: Using settings.DEBUG leads to a memory
            leak, never use this setting in production environments!
  leak, never use this setting in production environments!''')

worker: Warm shutdown (MainProcess)

其中的Warm shutdown表示,这是一次优雅的关闭,会先将当前正在执行中的任务处理结束之后,再关闭worker。如果以如下方式关闭worker:

kill -QUIT [WorkerMainProcessID]  #  worker主进程的进程ID需要通过ps -ef | grep celery获取;或者通过 celery -A app inspect stats的输出结果获取

则会输出:

rning: Using settings.DEBUG leads to a memory
            leak, never use this setting in production environments!
  leak, never use this setting in production environments!''')

worker: Cold shutdown (MainProcess)

其中的Cold shutdown表示会立即关闭worker进程。但是这样就意味着任务丢掉了吗?这要视具体情况而定。先用demo任务来看一下实际的效果,首先提交demo任务(每个任务挂起30秒后结束):

# python manage.py shell
Python 3.6.10 (default, Apr 29 2020, 11:29:00) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.13.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from demoapp.tasks import demoapp_task                                                                                   

In [2]: demoapp_task.apply_async(kwargs={"num": 30})                                                                             
Out[2]: <AsyncResult: 669df96e-da61-4816-874c-36a6b8ebb32c>

In [3]: demoapp_task.apply_async(kwargs={"num": 30})                                                                             
Out[3]: <AsyncResult: 51ea5e1b-6acd-47b3-80d3-f2baec2941ab>

In [4]: demoapp_task.apply_async(kwargs={"num": 30})                                                                             
Out[4]: <AsyncResult: 7a270543-7cef-4a48-b732-255fdd43e7bb>

然后对worker进程(启动命令为:celery -A djangogo worker -P prefork -c 2)进行Cold Shutdown,然后观察worker进程的日志输出为:

[2020-04-29 11:41:14,157: WARNING/ForkPoolWorker-2] Busy....
[2020-04-29 11:41:14,168: WARNING/ForkPoolWorker-1] Busy....
[2020-04-29 11:41:15,158: WARNING/ForkPoolWorker-2] Busy....
[2020-04-29 11:41:15,169: WARNING/ForkPoolWorker-1] Busy....
[2020-04-29 11:41:16,160: WARNING/ForkPoolWorker-2] Busy....
[2020-04-29 11:41:16,171: WARNING/ForkPoolWorker-1] Busy....
[2020-04-29 11:41:17,161: WARNING/ForkPoolWorker-2] Busy....
[2020-04-29 11:41:17,172: WARNING/ForkPoolWorker-1] Busy....

worker: Cold shutdown (MainProcess)
[2020-04-29 11:41:18,760: WARNING/MainProcess] Restoring 2 unacknowledged message(s)

发现worker在打印了Colde shutdown之后,重新收回了2个未确认的消息,也就是当前正在执行的任务实例又被回收了,当worker进程再次启动的时候,还会执行这两个回收的任务实例(这里要注意幂等问题,比如任务产生了部分副作用的问题)。

这里为什么会回收没有完成的任务呢?主要和一个设置有关(djangogo/settings.py):

CELERY_TASK_ACKS_LATE = True

这个参数如果设置为True,那么任务实例的执行会在任务实例执行完毕之后才会确认;否则只要开始执行任务实例就确认了。也就是如果将上述的配置设置为False,那么就会丢掉两个任务实例。当然,不论怎样都需要注意计算的幂等问题。

所以,综合以上的情况,可以看到通过使用Warm shutdown和任务实例执行完毕后确认的机制,就可以从一定程度上保证任务至少被执行一次(不考虑断电等极端情况下)。

理解worker的任务预取机制和其注意点

每个worker(除了solo类型的worker)都有一个并行度的设置,这是一个数字,用来表示这个worker可以同时并发处理的任务实例的个数。但是对于每个worker的多个进程(线程/协程)每结束一个就去backend(比如Redis)里去取一个任务吗?这种模式自然是没有问题的,但是为了减少对backend的压力,是否可以对每个工作进程(线程/协程)都先多取几个任务?是的,调整对每个进程(线程/协程)预先读取多少任务的的参数就是(djangogo/settings.py):

CELERY_WORKER_PREFETCH_MULTIPLIER = 1

这个预取乘数设置的默认值是4,也就是说,如果你的worker有N个工作进程(线程/协程),那么一个worker一次获取的任务数量就是4*N个。

至于这个任务应该是设大还是设小,这取决于多方面的因素。首先是任务的提交密度,如果任务提交密度非常大,所有worker的工作线程都处于满负荷工作状态,那么适当提高这个值可以增加任务分发的性能,提高吞吐量;如果任务提交密度相对于worker数量来说并不大,举个极端的例子:3台服务器,每台都运行一个worker,每个worker的并行度都是2;同时一次性提交了10个任务,预取乘数如果是默认的4,那么第一个取任务的worker就会预取2*4=8个;第二个取任务的worker则拿到剩下的2个;第三个worker就空闲了,这样就导致了第三个worker的资源浪费。另外,如果要在提交任务之前做一层优先级控制(优先执行某些任务实例),将这个预取成数设置成1是合理的,否则高优先级的任务可能并不会在有工作进程(线程/协程)空闲出来时,立即被执行(而是执行预先取到的,还没有执行的任务实例)。

以上是Celery worker的一些注意的点,需要根据不同的应用场景来做针对性的调整,本文相关代码位于Github

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值