celery 队列的划分
一.为什么要分队列?
往往在生产环境中可能有大量的异步任务需要执行,不同的任务所需要的资源和时间不一样的。为了防止一些非常占用资源或耗时的任务阻塞任务队列导致一些简单任务也无法执行,这时候就需要对任务队列Queue进行合理的划分,将不同任务交由不同的Queue处理。
二. 具体代码实现思路
下例定义了三个Queue队列,default执行普通任务,insert_data, push执行重型任务。
from kombu import Exchange, Queue
default_exchange = Exchange("default", type="direct")
INSERT_DATA_ROUTING = {"queue": "insert_data", "routing_key": "insert_data_key"}
PULL_ROUTING = {"queue": "push", "routing_key": "push_key"}
CELERY_ROUTES = {
# insert_data
"home_application.tasks.insert_data_task": INSERT_DATA_ROUTING,
# insert_multi_data
"home_application.tasks.insert_multi_data_task": INSERT_DATA_ROUTING,
# push_tracking
"home_application.tasks.push_tracking": PULL_ROUTING,
# push_tracking
"home_application.tasks.push_weight": PULL_ROUTING,
}
CELERY_QUEUES = (
# keep old queue to process message left in broker, remove on next version
Queue("default", default_exchange, routing_key="default"),
Queue("insert_data", default_exchange, routing_key="insert_data_key"),
Queue("push", default_exchange, routing_key="push_key"),
)
CELERY_DEFAULT_QUEUE = "default" # 默认队列
CELERY_DEFAULT_EXCHANGE = "default" # 默认 exchange
CELERY_DEFAULT_ROUTING_KEY = "default" # 默认routing key
根据业务场景,在 CELERY_ROUTES配置中指定 多 个 Task 对应的 routing,然后根据场景需要划分了3个队列"default", "insert_data", "push",定义路由这些队列的规则,其中非默认队列的路由规则为 INSERT_DATA_ROUTING, PULL_ROUTING,默认队列的路由直接定义CELERY_DEFAULT_ROUTING_KEY,并将该路由规则加入到
CELERY_ROUTES` 中以生效(在此忽略默认的路由规则,不用添加)。这样设计的目的是为了不同场景彼此之间互不影响。
三. 分队列的其他方式:
除了以上场景的普通方式外,根据不同的场景,分队列的方式还有如下:
1.在之前划分的场景下进一步划分队列
在根据业务场景粗略划分后,对于某个场景,可能需要更细致的划分,例如在向上游推送时(tasks.push_开头的任务),为了避免一个上游的阻塞影响向其他上游推送,需要做到不同上游彼此之间互不影响。所以需要针对不同上游使用不同队列,例如:
# 增加一个exchange
retry_exchange = Exchange("retry", type="direct")
# 增加两个routing
PUSH_TRACKING_RETYY = {"queue": "push_tracking_retry_{0}", "routing_key": "retry_tracking_key"}
PUSH_WEIGHT_RETRY = {'queue': "push_weight_retry_{0}", "routing_key": "retry_weight_key"}
CLIENT_CELERY_ROUTES = {
# {0} 为 client 的占位符,在 ClientRouter 中进行格式化
'home_application.tasks.push_tracking_retry': PUSH_TRACKING_RETYY,
'home_application.tasks.push_weight_retry': PUSH_WEIGHT_RETRY,
}
CELERY_ROUTES = {
# insert_data
"home_application.tasks.insert_data_task": INSERT_DATA_ROUTING,
# insert_multi_data
"home_application.tasks.insert_multi_data_task": INSERT_DATA_ROUTING,
# push_tracking
"home_application.tasks.push_tracking": PULL_ROUTING,
# push_tracking
"home_application.tasks.push_weight": PULL_ROUTING,
"ClientRouter"
}
CELERY_QUEUES = (
# keep old queue to process message left in broker, remove on next version
Queue("default", default_exchange, routing_key="default"),
Queue("insert_data", default_exchange, routing_key="insert_data_key"),
Queue("push", default_exchange, routing_key="push_key"),
"ClientQueue"
)
class ClientRouter(object):
def route_for_task(self, task, args=None, kwargs=None):
if task not in CLIENT_CELERY_ROUTES:
return None
client_id = kwargs("client_id")
# 根据 client_id 获取队列名
queue_name = CLIENT_CELERY_ROUTES[task]["queue"].format(client_id)
return {"queue": queue_name, "routing_key": CLIENT_CELERY_ROUTES[task]["routing_key"]}
class ClientQueue(object):
def queue_for_task(self, task, args=None, kwargs=None):
if task not in CLIENT_CELERY_ROUTES:
return None
client_id = kwargs("client_id")
# 根据 client_id 获取队列名
queue_name = CLIENT_CELERY_ROUTES[task]["queue"].format(client_id)
return Queue(queue_name, retry_exchange ,routing_key=CLIENT_CELERY_ROUTES[task]["routing_key"])
在 CLIENT_CELERY_ROUTES
中指定了需要根据 Client (客户端)隔离队列的 Task 和其对应的 Queue 名称格式,队列名中含有一个占位符,为的是根据不同 Client 得到不同的队列名。
接着实现了一个路由器 ClientRouter
,其中定义了 router_for_task
方法,其作用是为 task
指定对应的队列名。可看出其中的逻辑是如果 task
在 CLIENT_CELERY_ROUTES
中,将会用 kwargs
中的 client_id
格式化队列名,得到最终发送消息的队列名,达到根据入参 client_id
来决定具体使用的队列,从而起到隔离不同 Client 使用不同队列的效果。
除了在 Client 的维度上划分,若需要在其他维度进一步划分队列以达到隔离的效果,也可参考该方法来设计路由规则。ClientQueue也是同理。
2.优先队列
假如有一个 taskA 去处理一个队列 A 中的信息,一个 taskB 去处理队列 B 中的数据,然后起了 x 个 worker 去处理队列 A ,其他的 worker 去处理队列 B。而这时也可能会出现队列 B 中一些 task 急需处理,而此时堆积在队列 B 中的 tasks 很多,需要耗费很长时间来处理队列 B 中的 task。此时就需要定义优先队列来处理紧急的 task。
celery中broker 常用的是rabbitmq,rabbitmq 3.5版本之后支持优先级队列,消息具有不同的优先级,同一个队列中优先级高的先得到处理。rabbitmq 支持这种特性的话,celery就可以实现优先级队列,毕竟celery是从broker中获取msg的。
celery还可以通过rabbitmq中的Consumer Priorities 的特性支持队列之间的优先级。A和B两个队列,A队列的consumer优先级位10,B队列的consumer优先级位1.那么之后当A队列的consumer都处于阻塞状态的时候,B队列的consumer才能从rabbitmq中获取消费信息。
-
优先级队列实现:
跟多队列相比,优先级队列相对简单一些,只需要将其中一个队列设置为优先级队列即可。这里只是多出了一个queue_arguments={'x-max-priority': 10}参数。他代表了当前队列的优先级为10(数字越大,优先级就越高),如果有多个优先级队列的话需要注意,低优先级的队列只有当高优先级队列完全阻塞的时候才会进行消费。
CELERY_QUEUES = (
# keep old queue to process message left in broker, remove on next version
Queue("default", default_exchange, routing_key="default"),
Queue("itsm_publish_ticket_priority", default_exchange, routing_key="publish_ticket_priority"),
# priority queues
Queue(
"pipeline_priority",
default_exchange,
routing_key="pipeline_push_priority",
queue_arguments={"x-max-priority": 9},
),
Queue(
"service_schedule_priority",
default_exchange,
routing_key="schedule_service_priority",
queue_arguments={"x-max-priority": 5},
),
Queue(
"pipeline_additional_task_priority",
default_exchange,
routing_key="additional_task_priority",
queue_arguments={"x-max-priority": 2},
),
)
3.动态队列
再来说说动态队列,其本质是预备队列,其目的是为了在线上环境减轻某些队列消息堆积的压力,起到快速支援的作用。通过配置来定义动态队列需要支援哪些队列,例如当 push 队列的压力较大,可配置 json 文件 如下,将 push_tracking 和 push_weight 两个 Task 路由到预备的动态队列中。
dynamic_router_config 或配置成(home_application.celery.dynamic_router_config.py)
{
"home_application.tasks.push_tracking": {
"dynamic_queue": [1,2],
"dynamic_percentage": 0.7,
},
"home_application.tasks.push_weight": {
"dynamic_queue": [3,4],
"dynamic_percentage": 0.7,
}
}
上述配置的作用是将 70% 的 home_application.tasks.push_tracking
Task 路由到动态队列 1、2 上,70% 的 home_application.tasks.push_weight
Task 路由到动态队列 3、4 上。
动态队列的路由器 DynamicRouter
大致实现如下:
class DynamicRouter(object):
def route_for_task(self, task, args=None, kwargs=None):
# 获取配置
task_config = get_conf_dict('dynamic_router_config').get(task, None)
# task如果没在配置中,则直接返回
if not task_config:
return None
# 获取task对应的动态队列配置
dynamic_queue = task_config.get('dynamic_queue', [])
dynamic_percentage = task_config.get('dynamic_percentage', 0.0)
# 将一定比例的task路由到动态队列中
if random.random() <= dynamic_percentage:
# 决定使用哪个动态队列
queue_name = router_load_balance(dynamic_queue, task_name)
log.data('get_router| task_name:%s, queue:%s', task_name, queue_name)
return {'queue': queue_name}
else:
return None
四.高效执行队列中的任务
为了提高celery中任务的执行效率,可以根据所在的业务场景的任务的类去别执行celery任务对应的worker不同的并发方式,提高任务执行效率。
首先区分任务的类型,分为以下两种:
CPU密集型(CPU-bound):
CPU密集型也叫计算密集型,是指I/O在很短的时间就可以完成,CPU需要大量的计算和处理,特点是CPU占用率相当高 例如:压缩解压缩、加密解密、正则表达式搜索
IO密集型(I/O bound):
IO密集型指的是系统运作大部分的状况是CPU在等I/O(硬盘/内存)的读写操作,CPU占用率任然较低。 例如:文件处理程序、网络爬虫程序、读写数据库程序,网络请求等。
celery中-c参数和-P参数
celery里面的 -c 参数指定的是并发度,而 -P 参数指定并发的实现方式。
celery是支持以下几个并发模式:
-
prefork (多进程的方式去实现并发)
-
threading
-
协程(gevent,eventlet)
接下来分析多进程、多线程、多协程的对比
多进程Process(multiprocess)
优点:可以利用多核CPU并行运算 缺点:占用资源最多、可启动数目比线程少 适用于:CPU密集型计算
多线程Thread(threading)
-优点:相比进程,更轻量级、占用资源少 -缺点: ·相比进程:多线程只能并发执行,不能利用多CPU(GIL) ·相比协程:启动数目有限制,占用内存资源,有线程切换开销 -适用于:IO密集型计算、同时运行的任务数目要求不多
多协程Coroutine(asynico):
-优点:内存开销最少、启动协程数量最多 -缺点:支持的库有限制(aiohttp vs requests)、代码实现复杂 -适用于:IO密集型计算、需要超多任务运行、但有现成库的场景
结合自身项目的场景,选择合适的并发方式启动celery,如在队列"insert_data"中的任务涉及大量访问数据库并拉取数据,那么任务属于 IO密集型 任务。那么在CPU资源仅有限利用的情况下,那么多协程的效率比多线程要高。
五.Flower监控
至此,异步任务已经配置完成。但是在生产环境下,如何快速准确的找到异常呢?这里我推荐一个工具,叫做flower。他可以完成对队列以及Worker的监控。可以实时的进行worker的分配以及增加worker的并发数量等一些操作。
(1)首先安装该模块
pip install flower -i https://pypi.douban.com/simple/
(2)然后在项目根目录输入启动命令
celery -A projectName flower --port=5555 --basic_auth=root:abc123
这里的意思为 在端口5555 启动flower . 登录flower的账号和密码为root和abc123
六.Supervistor进程管理
上边已经讲过了,celery若是使用了多队列,那么就会有多个worker启动。每个worker都会需要一条启动命令,这样就会造成很大的负担。每次上线,或者更新异步代码,都要挨个的去启动每一个worker,而且若是因为误操作,或者代码出错导致worker异常宕掉,若无法及时的重启,则可能会造成无法挽回的后果。 所以这里使用supervistor这个进程管理工具来托管每一个worker的正常存活。supervistor可以保证该进行会永久的存活下去,除非手动的杀掉suopervistor。 (1)安装
pip install supervisor -i Simple Index
(2)修改配置文件 /etc/supervisord.conf
, 在配置文件最后加上以下
在supervisor.conf中用指定协程并发的方式启动 该 worker
# default 队列 woker 配置启动 (默认的并发启动模式prefork)
[program: {{.app_code}}_celery]
command = /cache/.bk/env/bin/python {{.app_container_path}}code/manage.py celery worker -n {{.node_name}}_{{.app_code}} -c 4 -Ofair -l INFO --maxtasksperchild=128
directory = {{.app_container_path}}code/
stdout_logfile = {{.app_container_path}}logs/{{.app_code}}/celery.log
redirect_stderr = true
stopwaitsecs = 10
autorestart = true
environment = {{.environment}}
[program: {{.app_code}}_celery_home_applications]
command = /cache/.bk/env/bin/python {{.app_container_path}}code/manage.py celery worker -Q push -n {{.node_name}}_{{.app_code}}_comsumer -l INFO -c 4 --maxtasksperchild=128
directory = {{.app_container_path}}code/
stdout_logfile = {{.app_container_path}}logs/{{.app_code}}/celery.log
redirect_stderr = true
stopwaitsecs = 10
autorestart = true
environment = {{.environment}}
# client id 为 1 时的 push_weight_retry_1 队列配置的woker 启动
[program: {{.app_code}}_celery_home_applications]
command = /cache/.bk/env/bin/python {{.app_container_path}}code/manage.py celery worker -Q push_weight_retry_1 -n {{.node_name}}_{{.app_code}}_comsumer -l INFO -c 4 --maxtasksperchild=128
directory = {{.app_container_path}}code/
stdout_logfile = {{.app_container_path}}logs/{{.app_code}}/celery.log
redirect_stderr = true
stopwaitsecs = 10
autorestart = true
environment = {{.environment}}
# client id 为 1 时的 push_tracking_retry_1 队列配置的woker 启动
[program: {{.app_code}}_celery_home_applications]
command = /cache/.bk/env/bin/python {{.app_container_path}}code/manage.py celery worker -Q push_tracking_retry_1 -n {{.node_name}}_{{.app_code}}_comsumer -l INFO -c 4 --maxtasksperchild=128
directory = {{.app_container_path}}code/
stdout_logfile = {{.app_container_path}}logs/{{.app_code}}/celery.log
redirect_stderr = true
stopwaitsecs = 10
autorestart = true
environment = {{.environment}}
# client id 为 2 时的 push_weight_retry_2 队列配置的woker 启动
[program: {{.app_code}}_celery_home_applications]
command = /cache/.bk/env/bin/python {{.app_container_path}}code/manage.py celery worker -Q push_weight_retry_2 -n {{.node_name}}_{{.app_code}}_comsumer -l INFO -c 4 --maxtasksperchild=128
directory = {{.app_container_path}}code/
stdout_logfile = {{.app_container_path}}logs/{{.app_code}}/celery.log
redirect_stderr = true
stopwaitsecs = 10
autorestart = true
environment = {{.environment}}
# client id 为 2 时的 push_tracking_retry_2 队列配置的woker 启动
[program: {{.app_code}}_celery_home_applications]
command = /cache/.bk/env/bin/python {{.app_container_path}}code/manage.py celery worker -Q push_tracking_retry_2 -n {{.node_name}}_{{.app_code}}_comsumer -l INFO -c 4 --maxtasksperchild=128
directory = {{.app_container_path}}code/
stdout_logfile = {{.app_container_path}}logs/{{.app_code}}/celery.log
redirect_stderr = true
stopwaitsecs = 10
autorestart = true
environment = {{.environment}}
# 协程 并发方式启动 执行io密集型任务的 worker
[program: {{.app_code}}_celery_pipeline1]
command = /cache/.bk/env/bin/python {{.app_container_path}}code/manage.py celery worker -Q insert_data -n {{.node_name}}_{{.app_code}}_pipeline1 -l INFO -P gevent -c 128 --maxtasksperchild=128
directory = {{.app_container_path}}code/
stdout_logfile = {{.app_container_path}}logs/{{.app_code}}/celery.log
redirect_stderr = true
stopwaitsecs = 10
autorestart = true
environment = {{.environment}}
# flower 进程守护
[program: {{.app_code}}_flower]
command = /cache/.bk/env/bin/python {{.app_container_path}}code/flowerd.py --persistent=true --port=6666 --url_prefix=flower --basic_auth=admin:admin --broker_api=http://guest:guest@localhost:15672/api/
directory = {{.app_container_path}}code/
stdout_logfile ={{.app_container_path}}logs/{{.app_code}}/flower.log
redirect_stderr = true
stopwaitsecs = 10
autorestart = true
environment = {{.environment}}
stdout_logfile
为指定的日志文件存储的地方。directory
为指定命令执行的地方,command
就是在directory
目录下所执行的命令
(2)配置好后(wq保存退出),使用以下命令启动supervistor
supervistord -c /etc/supervisord.conf
(3)使用以下命令进入supervistor管理
supervistorctl
(4)常用的命令
start name 启动名为name的program
start all 全部启动
stop all 全部停止
restart all 全部重启