[源码解析] 并行分布式框架 Celery 之 worker 启动 (1)

0x00 摘要

Celery是一个简单、灵活且可靠的,处理大量消息的分布式系统,专注于实时处理的异步任务队列,同时也支持任务调度。Celery 是调用其Worker 组件来完成具体任务处理。

$ celery --app=proj worker -l INFO
$ celery -A proj worker -l INFO -Q hipri,lopri
$ celery -A proj worker --concurrency=4
$ celery -A proj worker --concurrency=1000 -P eventlet
$ celery worker --autoscale=10,0

所以我们本文就来讲解 worker 的启动过程。

0x01 Celery的架构

前面我们用几篇文章分析了 Kombu,为 Celery 的分析打下了基础。

[源码分析] 消息队列 Kombu 之 mailbox

[源码分析] 消息队列 Kombu 之 Hub

[源码分析] 消息队列 Kombu 之 Consumer

[源码分析] 消息队列 Kombu 之 Producer

[源码分析] 消息队列 Kombu 之 启动过程

[源码解析] 消息队列 Kombu 之 基本架构

以及

源码解析 并行分布式框架 Celery 之架构 (2)

[源码解析] 并行分布式框架 Celery 之架构 (2)

下面我们再回顾下 Celery 的结构。Celery的架构图如下所示:

 +-----------+            +--------------+
| Producer | | Celery Beat |
+-------+---+ +----+---------+
| |
| |
v v +-------------------------+
| Broker |
+------------+------------+
|
|
|
+-------------------------------+
| | |
v v v
+----+-----+ +----+------+ +-----+----+
| Exchange | | Exchange | | Exchange |
+----+-----+ +----+------+ +----+-----+
| | |
v v v +-----+ +-------+ +-------+
|queue| | queue | | queue |
+--+--+ +---+---+ +---+---+
| | |
| | |
v v v +---------+ +--------+ +----------+
| worker | | Worker | | Worker |
+-----+---+ +---+----+ +----+-----+
| | |
| | |
+-----------------------------+
|
|
v
+---+-----+
| backend |
+---------+

0x02 示例代码

其实网上难以找到调试Celery worker的办法。我们可以去其源码看看,发现如下:

# def test_worker_main(self):
# from celery.bin import worker as worker_bin
#
# class worker(worker_bin.worker):
#
# def execute_from_commandline(self, argv):
# return argv
#
# prev, worker_bin.worker = worker_bin.worker, worker
# try:
# ret = self.app.worker_main(argv=['--version'])
# assert ret == ['--version']
# finally:
# worker_bin.worker = prev

所以我们可以模仿来进行,使用如下启动worker,进行调试。

from celery import Celery

app = Celery('tasks', broker='redis://localhost:6379')

@app.task()
def add(x, y):
return x + y if __name__ == '__main__':
app.worker_main(argv=['worker'])

0x03 逻辑概述

当启动一个worker的时候,这个worker会与broker建立链接(tcp长链接),然后如果有数据传输,则会创建相应的channel, 这个连接可以有多个channel。然后,worker就会去borker的队列里面取相应的task来进行消费了,这也是典型的消费者生产者模式。

这个worker主要是有四部分组成的,task_pool, consumer, scheduler, mediator。其中,task_pool主要是用来存放的是一些worker,当启动了一个worker,并且提供并发参数的时候,会将一些worker放在这里面。

celery默认的并发方式是prefork,也就是多进程的方式,这里只是celery对multiprocessing pool进行了轻量的改造,然后给了一个新的名字叫做prefork,这个pool与多进程的进程池的区别就是这个task_pool只是存放一些运行的worker。

consumer也就是消费者,主要是从broker那里接受一些message,然后将message转化为celery.worker.request.Request 的一个实例。

Celery 在适当的时候,会把这个请求包装进Task中,Task就是用装饰器app_celery.task()装饰的函数所生成的类,所以可以在自定义的任务函数中使用这个请求参数,获取一些关键的信息。此时,已经了解了task_pool和consumer。

接下来,这个worker具有两套数据结构,这两套数据结构是并行运行的,他们分别是 'ET时刻表' 、就绪队列。

就绪队列:那些 立刻就需要运行的task, 这些task到达worker的时候会被放到这个就绪队列中等待consumer执行。

我们下面看看如何启动Celery

0x04 Celery应用

程序首先会来到Celery类,这是Celery的应用。

可以看到主要就是:各种类名称,TLS, 初始化之后的各种signal。

位置在:celery/app/base.py,其定义如下:

class Celery:
"""Celery application.""" amqp_cls = 'celery.app.amqp:AMQP'
backend_cls = None
events_cls = 'celery.app.events:Events'
loader_cls = None
log_cls = 'celery.app.log:Logging'
control_cls = 'celery.app.control:Control'
task_cls = 'celery.app.task:Task'
registry_cls = 'celery.app.registry:TaskRegistry' #: Thread local storage.
_local = None
_fixups = None
_pool = None
_conf = None
_after_fork_registered = False #: Signal sent when app is loading configuration.
on_configure = None #: Signal sent after app has prepared the configuration.
on_after_configure = None #: Signal sent after app has been finalized.
on_after_finalize = None #: Signal sent by every new process after fork.
on_after_fork = None

对于我们的示例代码,入口是:

def worker_main(self, argv=None):
if argv is None:
argv = sys.argv if 'worker' not in argv:
raise ValueError(
"The worker sub-command must be specified in argv.\n"
"Use app.start() to programmatically start other commands."
) self.start(argv=argv)

4.1 添加子command

celery/bin/celery.py 会进行添加 子command,我们可以看出来。

这些 Commnd 是可以在命令行作为子命令直接使用的

celery.add_command(purge)
celery.add_command(call)
celery.add_command(beat)
celery.add_command(list_)
celery.add_command(result)
celery.add_command(migrate)
celery.add_command(status)
celery.add_command(worker)
celery.add_command(events)
celery.add_command(inspect)
celery.add_command(control)
celery.add_command(graph)
celery.add_command(upgrade)
celery.add_command(logtool)
celery.add_command(amqp)
celery.add_command(shell)
celery.add_command(multi)

每一个都是command。我们以worker为例,具体如下:

worker = {CeleryDaemonCommand} <CeleryDaemonCommand worker>
add_help_option = {bool} True
allow_extra_args = {bool} False
allow_interspersed_args = {bool} True
context_settings = {dict: 1} {'allow_extra_args': True}
epilog = {NoneType} None
name = {str} 'worker'
options_metavar = {str} '[OPTIONS]'
params = {list: 32} [<CeleryOption hostname>, ...... , <CeleryOption executable>]

4.2 入口点

然后会引入Celery 命令入口点 Celery。

def start(self, argv=None):
from celery.bin.celery import celery celery.params[0].default = self try:
celery.main(args=argv, standalone_mode=False)
except Exit as e:
return e.exit_code
finally:
celery.params[0].default = None

4.3 缓存属性cached_property

Celery 中,大量的成员变量是被cached_property修饰的

使用 cached_property修饰过的函数,就变成是对象的属性,该对象第一次引用该属性时,会调用函数,对象第二次引用该属性时就直接从词典中取了,即 Caches the return value of the get method on first call。

很多知名Python项目都自己实现过 cached_property,比如Werkzeug,Django。

因为太有用,所以 Python 3.8 给 functools 模块添加了 cached_property 类,这样就有了官方的实现。

Celery 的代码举例如下:

    @cached_property
def Worker(self):
"""Worker application.
"""
return self.subclass_with_self('celery.apps.worker:Worker') @cached_property
def Task(self):
"""Base task class for this app."""
return self.create_task_cls() @property
def pool(self):
"""Broker connection pool: :class:`~@pool`.
"""
if self._pool is None:
self._ensure_after_fork()
limit = self.conf.broker_pool_limit
pools.set_limit(limit)
self._pool = pools.connections[self.connection_for_write()]
return self._pool

所以,最终,Celery的内容应该是这样的:

app = {Celery} <Celery tasks at 0x7fb8e1538400>
AsyncResult = {type} <class 'celery.result.AsyncResult'>
Beat = {type} <class 'celery.apps.beat.Beat'>
GroupResult = {type} <class 'celery.result.GroupResult'>
Pickler = {type} <class 'celery.app.utils.AppPickler'>
ResultSet = {type} <class 'celery.result.ResultSet'>
Task = {type} <class 'celery.app.task.Task'>
WorkController = {type} <class 'celery.worker.worker.WorkController'>
Worker = {type} <class 'celery.apps.worker.Worker'>
amqp = {AMQP} <celery.app.amqp.AMQP object at 0x7fb8e2444860>
annotations = {tuple: 0} ()
autofinalize = {bool} True
backend = {DisabledBackend} <celery.backends.base.DisabledBackend object at 0x7fb8e25fd668>
builtin_fixups = {set: 1} {'celery.fixups.django:fixup'}
clock = {LamportClock} 1
conf = {Settings: 163} Settings({'broker_url': 'redis://localhost:6379', 'deprecated_settings': set(), 'cache_...
configured = {bool} True
control = {Control} <celery.app.control.Control object at 0x7fb8e2585f98>
current_task = {NoneType} None
current_worker_task = {NoneType} None
events = {Events} <celery.app.events.Events object at 0x7fb8e25ecb70>
loader = {AppLoader} <celery.loaders.app.AppLoader object at 0x7fb8e237a4a8>
main = {str} 'tasks'
on_after_configure = {Signal} <Signal: app.on_after_configure providing_args={'source'}>
on_after_finalize = {Signal} <Signal: app.on_after_finalize providing_args=set()>
on_after_fork = {Signal} <Signal: app.on_after_fork providing_args=set()>
on_configure = {Signal} <Signal: app.on_configure providing_args=set()>
pool = {ConnectionPool} <kombu.connection.ConnectionPool object at 0x7fb8e26e9e80>
producer_pool = {ProducerPool} <kombu.pools.ProducerPool object at 0x7fb8e26f02b0>
registry_cls = {type} <class 'celery.app.registry.TaskRegistry'>
set_as_current = {bool} True
steps = {defaultdict: 2} defaultdict(<class 'set'>, {'worker': set(), 'consumer': set()})
tasks = {TaskRegistry: 10} {'celery.chain': <@task: celery.chain of tasks at 0x7fb8e1538400>, 'celery.starmap': <@task: celery.starmap of tasks at 0x7fb8e1538400>, 'celery.chord': <@task: celery.chord of tasks at 0x7fb8e1538400>, 'celery.backend_cleanup': <@task: celery.backend_clea
user_options = {defaultdict: 0} defaultdict(<class 'set'>, {})

具体部分成员变量举例如下图:

+---------------------------------------+
| Celery |
| |
| Beat+-----------> celery.apps.beat.Beat
| |
| Task+-----------> celery.app.task.Task
| |
| WorkController+----------> celery.worker.worker.WorkController
| |
| Worker+-----------> celery.apps.worker.Worker
| |
| amqp +----------> celery.app.amqp.AMQP
| |
| control +----------> celery.app.control.Control
| |
| events +---------> celery.app.events.Events
| |
| loader +----------> celery.loaders.app.AppLoader
| |
| pool +----------> kombu.connection.ConnectionPool
| |
| producer_pool +----------> kombu.pools.ProducerPool
| |
| tasks +----------> TaskRegistry
| |
| |
+---------------------------------------+

0x05 Celery 命令

Celery的命令总入口为celery方法,具体在:celery/bin/celery.py。

代码缩减版如下:

@click.pass_context
def celery(ctx, app, broker, result_backend, loader, config, workdir,
no_color, quiet, version):
"""Celery command entrypoint.""" if loader:
# Default app takes loader from this env (Issue #1066).
os.environ['CELERY_LOADER'] = loader
if broker:
os.environ['CELERY_BROKER_URL'] = broker
if result_backend:
os.environ['CELERY_RESULT_BACKEND'] = result_backend
if config:
os.environ['CELERY_CONFIG_MODULE'] = config
ctx.obj = CLIContext(app=app, no_color=no_color, workdir=workdir,
quiet=quiet) # User options
worker.params.extend(ctx.obj.app.user_options.get('worker', []))
beat.params.extend(ctx.obj.app.user_options.get('beat', []))
events.params.extend(ctx.obj.app.user_options.get('events', [])) for command in celery.commands.values():
command.params.extend(ctx.obj.app.user_options.get('preload', []))

在方法中,会遍历celery.commands,拓展param,具体如下。这些 commands 就是之前刚刚提到的子Command:

celery.commands =
'report' = {CeleryCommand} <CeleryCommand report>
'purge' = {CeleryCommand} <CeleryCommand purge>
'call' = {CeleryCommand} <CeleryCommand call>
'beat' = {CeleryDaemonCommand} <CeleryDaemonCommand beat>
'list' = {Group} <Group list>
'result' = {CeleryCommand} <CeleryCommand result>
'migrate' = {CeleryCommand} <CeleryCommand migrate>
'status' = {CeleryCommand} <CeleryCommand status>
'worker' = {CeleryDaemonCommand} <CeleryDaemonCommand worker>
'events' = {CeleryDaemonCommand} <CeleryDaemonCommand events>
'inspect' = {CeleryCommand} <CeleryCommand inspect>
'control' = {CeleryCommand} <CeleryCommand control>
'graph' = {Group} <Group graph>
'upgrade' = {Group} <Group upgrade>
'logtool' = {Group} <Group logtool>
'amqp' = {Group} <Group amqp>
'shell' = {CeleryCommand} <CeleryCommand shell>
'multi' = {CeleryCommand} <CeleryCommand multi>

0x06 worker 子命令

Work子命令是 Command 总命令的一员,也是我们直接在命令行加入 worker 参数时候,调用到的子命令。

$ celery -A proj worker -l INFO -Q hipri,lopri

worker 子命令继承了click.BaseCommand,为。

定义在celery/bin/worker.py。

因此如下代码间接调用到 worker 命令:

celery.main(args=argv, standalone_mode=False)

定义如下:

def worker(ctx, hostname=None, pool_cls=None, app=None, uid=None, gid=None,
loglevel=None, logfile=None, pidfile=None, statedb=None,
**kwargs):
"""Start worker instance. Examples
--------
$ celery --app=proj worker -l INFO
$ celery -A proj worker -l INFO -Q hipri,lopri
$ celery -A proj worker --concurrency=4
$ celery -A proj worker --concurrency=1000 -P eventlet
$ celery worker --autoscale=10,0 """
app = ctx.obj.app
maybe_drop_privileges(uid=uid, gid=gid)
worker = app.Worker(
hostname=hostname, pool_cls=pool_cls, loglevel=loglevel,
logfile=logfile, # node format handled by celery.app.log.setup
pidfile=node_format(pidfile, hostname),
statedb=node_format(statedb, hostname),
no_color=ctx.obj.no_color,
**kwargs)
worker.start()
return worker.exitcode

此时流程如下图,可以看到,从 Celery 应用就进入到了具体的 worker 命令:

      +----------+
| User |
+----+-----+
|
| worker_main
|
v
+---------+------------+
| Celery |
| |
| Celery application |
| celery/app/base.py |
| |
+---------+------------+
|
| celery.main
|
v
+---------+------------+
| @click.pass_context |
| celery |
| |
| |
| CeleryCommand |
| celery/bin/celery.py |
| |
+---------+------------+
|
|
|
v
+----------+------------+
| @click.pass_context |
| worker |
| |
| |
| WorkerCommand |
| celery/bin/worker.py |
+-----------------------+

0x07 Worker application

此时在该函数中会实例化app的Worker,Worker application 就是 worker 的实例此时的app就是前面定义的Celery类的实例app

定义在:celery/app/base.py。

@cached_property
def Worker(self):
"""Worker application. See Also:
:class:`~@Worker`.
"""
return self.subclass_with_self('celery.apps.worker:Worker')

此时subclass_with_self利用了Python的type动态生成类实例的属性。

def subclass_with_self(self, Class, name=None, attribute='app',
reverse=None, keep_reduce=False, **kw):
"""Subclass an app-compatible class.
"""
Class = symbol_by_name(Class) # 导入该类
reverse = reverse if reverse else Class.__name__ # 判断是否传入值,如没有则使用类的名称 def __reduce__(self): # 定义的方法 该方法在pickle过程中会被调用
return _unpickle_appattr, (reverse, self.__reduce_args__()) attrs = dict(
{attribute: self}, # 默认设置app的值为self
__module__=Class.__module__,
__doc__=Class.__doc__,
**kw) # 填充属性
if not keep_reduce:
attrs['__reduce__'] = __reduce__ # 如果默认则生成的类设置__reduce__方法 return type(bytes_if_py2(name or Class.__name__), (Class,), attrs) # 利用type实诚类实例

此时就已经从 worker 命令 得到了一个celery.apps.worker:Worker的实例,然后调用该实例的start方法,此时首先分析一下Worker类的实例化的过程。

我们先回顾下:

我们的执行从 worker_main 这个程序入口,来到了 Celery 应用。然后进入了 Celery Command,然后又进入到了 Worker 子Command,具体如下图。

                                     +----------------------+
+----------+ | @cached_property |
| User | | Worker |
+----+-----+ +---> | |
| | | |
| worker_main | | Worker application |
| | | celery/app/base.py |
v | +----------------------+
+---------+------------+ |
| Celery | |
| | |
| Celery application | |
| celery/app/base.py | |
| | |
+---------+------------+ |
| |
| celery.main |
| |
v |
+---------+------------+ |
| @click.pass_context | |
| celery | |
| | |
| | |
| CeleryCommand | |
| celery/bin/celery.py | |
| | |
+---------+------------+ |
| |
| |
| |
v |
+----------+------------+ |
| @click.pass_context | |
| worker | |
| | |
| | |
| WorkerCommand | |
| celery/bin/worker.py | |
+-----------+-----------+ |
| |
+-----------------+

下面就会正式进入 worker,Celery 把 worker 的正式逻辑成为 work as a program。

我们在下文将接下来继续看后续 work as a program 的启动过程。

0xFF 参考

Celery 源码学习(二)多进程模型

celery原理初探

celery源码分析-wroker初始化分析(上)

celery源码分析-worker初始化分析(下)

celery worker初始化--DAG实现

python celery多worker、多队列、定时任务

celery 详细教程-- Worker篇

使用Celery

Celery 源码解析一:Worker 启动流程概述

Celery 源码解析二:Worker 的执行引擎

Celery 源码解析三:Task 对象的实现

Celery 源码解析四:定时任务的实现

Celery 源码解析五:远程控制管理

Celery 源码解析六:Events 的实现

Celery 源码解析七:Worker 之间的交互

Celery 源码解析八:State 和 Result

[源码解析] 并行分布式框架 Celery 之 worker 启动 (1)的更多相关文章

  1. [源码解析] 并行分布式框架 Celery 之 worker 启动 (2)

    [源码解析] 并行分布式框架 Celery 之 worker 启动 (2) 目录 [源码解析] 并行分布式框架 Celery 之 worker 启动 (2) 0x00 摘要 0x01 前文回顾 0x2 ...

  2. [源码解析] 并行分布式框架 Celery 之 Lamport 逻辑时钟 & Mingle

    [源码解析] 并行分布式框架 Celery 之 Lamport 逻辑时钟 & Mingle 目录 [源码解析] 并行分布式框架 Celery 之 Lamport 逻辑时钟 & Ming ...

  3. [源码解析] 并行分布式框架 Celery 之架构 (2)

    [源码解析] 并行分布式框架 Celery 之架构 (2) 目录 [源码解析] 并行分布式框架 Celery 之架构 (2) 0x00 摘要 0x01 上文回顾 0x02 worker的思考 2.1 ...

  4. [源码解析] 并行分布式框架 Celery 之架构 (1)

    [源码解析] 并行分布式框架 Celery 之架构 (1) 目录 [源码解析] 并行分布式框架 Celery 之架构 (1) 0x00 摘要 0x01 Celery 简介 1.1 什么是 Celery ...

  5. [源码解析] 并行分布式框架 Celery 之 容错机制

    [源码解析] 并行分布式框架 Celery 之 容错机制 目录 [源码解析] 并行分布式框架 Celery 之 容错机制 0x00 摘要 0x01 概述 1.1 错误种类 1.2 失败维度 1.3 应 ...

  6. [源码解析] 并行分布式任务队列 Celery 之 消费动态流程

    [源码解析] 并行分布式任务队列 Celery 之 消费动态流程 目录 [源码解析] 并行分布式任务队列 Celery 之 消费动态流程 0x00 摘要 0x01 来由 0x02 逻辑 in komb ...

  7. [源码解析] 并行分布式任务队列 Celery 之 Task是什么

    [源码解析] 并行分布式任务队列 Celery 之 Task是什么 目录 [源码解析] 并行分布式任务队列 Celery 之 Task是什么 0x00 摘要 0x01 思考出发点 0x02 示例代码 ...

  8. [源码解析] 并行分布式任务队列 Celery 之 多进程模型

    [源码解析] 并行分布式任务队列 Celery 之 多进程模型 目录 [源码解析] 并行分布式任务队列 Celery 之 多进程模型 0x00 摘要 0x01 Consumer 组件 Pool boo ...

  9. [源码解析] 并行分布式任务队列 Celery 之 EventDispatcher & Event 组件

    [源码解析] 并行分布式任务队列 Celery 之 EventDispatcher & Event 组件 目录 [源码解析] 并行分布式任务队列 Celery 之 EventDispatche ...

随机推荐

  1. VSCode & outline & source code

    VSCode & outline & source code Dart 源码学习 outline 速览 dart-core List class instance-methods ht ...

  2. webpack 4 & dev server

    webpack 4 & dev server proxy https://webpack.js.org/configuration/dev-server/#devserverproxy htt ...

  3. taro error

    taro error index.json 中没有申明 "component: true" 或其他异常 https://blog.csdn.net/qq_35629609/arti ...

  4. 【Python核心编程笔记】一、Python中一切皆对象

    Python中一切皆对象 本章节首先对比静态语言以及动态语言,然后介绍 python 中最底层也是面向对象最重要的几个概念-object.type和class之间的关系,以此来引出在python如何做 ...

  5. 怎么创建CSV文件和怎么打开CSV文件

    CSV(Comma Separated Values逗号分隔值). .csv是一种文件格式(如.txt..doc等),也可理解.csv文件就是一种特殊格式的纯文本文件.即是一组字符序列,字符之间已英文 ...

  6. ImageApparate(幻影)镜像加速服务让镜像分发效率提升 5-10 倍

    作者介绍 李昂,腾讯高级开发工程师,主要关注容器存储和镜像存储相关领域,目前主要负责腾讯容器镜像服务和镜像存储加速系统的研发和设计工作. 李志宇,腾讯云后台开发工程师.负责腾讯云 TKE 集群节点和运 ...

  7. 代码小知识之UUID

    1.生成UUID(UUID保证对在同一时空中的所有机器都是唯一的,UUID的唯一缺陷在于生成的结果串会比较长.UUID 来作为数据库数据表主键是非常不错的选择,保证每次生成的UUID 是唯一的) UU ...

  8. xscan的安装和使用(作业整理)

    1.将学习通上下载的xscan.rar进行解压. 2.将缺少的.dll文件粘贴到软件解压目录中. 3.点击打开软件. 3.1在运行中除了发现缺少.dll文件的问题,我电脑又出现类似问题, 采取了关闭防 ...

  9. Java数组练习(打印杨辉数组)

    打印杨辉数组 package com.kangkang.array; import java.util.Scanner; public class demo02 { public static voi ...

  10. Ch1-What is DAX?

    What is DAX? 数据分析表达式 (DAX) 是在 Analysis Services.Power BI 以及 Excel 中的 Power Pivot 使用的公式表达式语言.在第一版Powe ...