CELERY WORKER 简易示例1
celery 使用版本限制:
python3.9.10
celery 4.4.7
rabbitmq
celery使用目标:
大量的异步数据从上层逻辑传输过来,如心跳里的数据,上报的数据等等。这些数据每次上报到服务器时候直接做异步处理是允许的,但是某些应用场景下,如客户端集中大量上报,这时服务器会收到大量的客户端的数据,且还是每次上报只有一条的场景,当这些数据传递到后端的数据库插入或者更新的业务里,会造成为每一条数据单独写一个mysql语句,直接造成后端数据库压力大增。
完善的处理方式是在接收到数据后,先进行数据汇总,然后发送给数据库插入业务,这样就可以将单条sql语句,写成批量sql语句。比如客户端每次上报一条业务数据,先不直接将数据推送给数据库业务,而是先在本进程中缓存一段时间,等到了一百或者一千条,然后发送给数据库插入业务,这样数据库的压力就会直线下降。
这样做就引入了两个业务处理问题:1、数据汇总;2、当上报的业务数据,不足以凑足一百或者一千条时,需要定时推送到数据库业务,避免数据不能及时写入。
这样做同时也引入了数据风险:
1、rabbitmq是可靠的,但是当在本进程中做了数据缓冲后,就不再是可靠的。一旦缓冲数据的进程发生意外死亡,就会有数据丢失。
遇到的问题:
1、参考网上的一些说法,在java中使用rabbitmq时,通过设置worker_prefetch_multiplier (我使用的是CELERY开头的配置项,旧设置项)可以实现一次获取多条数据,这多条数据是由java的中间件负责将所有数据组合成一个数组。但是在python的celery中无法实现类似功能,实际使用时,发现无论该值为什么,获取到的数据都是单条数据,而不是一个列表,这样就无法通过一次获取多条数据,实现数据缓冲。
2、
实现数据缓冲的方式:
1、利用全局变量做数据缓冲。celery worker启动后,只要从rabbitmq中获取到数据就会执行celery 的task函数,此时只要worker进程不死亡,全局变量就会一直存在。celery支持通过设置某些设置项,在task任务执行一定次数后,关闭掉worker,以避免内存泄漏,注意避开这个坑。
2、利用时间信号做定时推送。由于celery是异步任务,在任务压力大的情况下,会有多个celery worker参与某一个task,这样就必须让所有参与该任务的worker都将缓存的数据推送到数据库业务中。之前打算用celery beat来实现这个功能,后来发现celery beat将数据推送到队列后根本无法保证所有指向该业务的worker全部将缓存数据推送到数据库处理业务(这里有一个很扯淡的现象,当有两个worker处理同一个celery task时,其中一个worker会不断抢这个celery task的标准处理逻辑,而另外一个celery worker会不断的抢beat发送的信息,测试的celery beat消息是celery task逻辑中的一个if),于是想到在每个worker中添加定时器,让定时器来实现定时推送任务。
3、上面提到了使用全局变量做为数据缓冲来实现数据汇总,以便将多条数据汇合成一条SQL语句。这里提一个不得不说的问题,那就是值传递和地址传递(变量地址空间)的问题。由于数据是在celery task的任务函数中生成的,因此在收集(缓存)这些数据时,必须想到是否会跟着函数的调用结束而消失。
代码内容:
下面仅仅会记录部分伪代码内容
celery启动运行参数:
-Ofair参数:
在Celery 3.1中,默认的调度机制只是将任务发送到可写的第一个队列,并使用一些启发式方法确保我们在它们之间进行循环,以确保每个子进程都能接收相同数量的任务。这意味着在默认调度策略中,工作人员可以将任务发送到已经执行任务的同一子进程。如果该任务长时间运行,则可能会长时间阻止等待任务。更糟糕的是,即使有子进程可以自由地工作,数百个短期运行的任务也可能被困在长期运行的任务之后。添加了-Ofair调度策略以避免这种情况,并且在启用时添加了规则,即不应将任务发送到已执行任务的子进程。如果您只有短期运行任务,那么公平调度策略可能会稍微差一些。
加上-O fair时,celery将取消预取机制
--without-mingle
选项在启动时禁用工作程序同步
--without-gossip
--without-heartbeat
关闭celery master与celery worker之间的心跳检测
--autoscale
当worker不足时,自动启动多个worker
celery 启动入口:代码方式启动
三部分启动:
1、实现celery beat启动
1、实现celery beat启动
2、实现celery worker启动
3、关闭celery worker
# celery beat启动入口
@cli_utils.action_logging
def celery_beat(args):
from celery.bin import beat as bin_beat
# 配置日志输出位置,pid文件位置等
pid_file_path, stdout, stderr, log_file = setup_locations(
process=BEAT_PROCESS_NAME,
pid=args.pid,
stdout=args.stdout,
stderr=args.stderr,
log=args.log_file,
)
handle = setup_logging(log_file)
beat_instance = bin_beat.beat(app=celery_app)
options = {
'optimization': 'fair',
'O': 'fair',
'queues': args.queues,
#'autoscale': autoscale,
'hostname': args.celery_hostname,
#celery 日志输出等级
'loglevel': logging.INFO,
'pidfile': pid_file_path,
'without_mingle': args.without_mingle,
'without_gossip': args.without_gossip,
}
# 以Daemon进程方式运行程序
if args.daemon:
# 重定向标准输出和错误输出
with open(stdout, 'w+') as stdout_handle, open(stderr, 'w+') as stderr_handle:
if args.umask:
umask = args.umask
ctx = daemon.DaemonContext(
files_preserve=[handle],
umask=int(umask, 8),
stdout=stdout_handle,
stderr=stderr_handle,
)
with ctx:
beat_instance.run(**options)
#非Daemon形式运行程序
else:
beat_instance.run(**options)
# celery worker启动入口
@cli_utils.action_logging
def celery_msgdata(args):
autoscale = args.autoscale
skip_serve_logs = False #args.skip_serve_logs
# Setup locations
pid_file_path, stdout, stderr, log_file = setup_locations(
process=WORKER_PROCESS_NAME,
pid=args.pid,
stdout=args.stdout,
stderr=args.stderr,
log=args.log_file,
)
handle = setup_logging(log_file)
if hasattr(celery_app.backend, 'ResultSession'):
try:
session = celery_app.backend.ResultSession()
session.close()
except sqlalchemy.exc.IntegrityError:
pass
# Setup Celery worker
worker_instance = worker_bin.worker(app=celery_app)
options = {
'optimization': 'fair',
'O': 'fair',
'queues': "msgdata,broadcast_msgdata",#args.queues,
'concurrency': 3, #args.concurrency,
#'autoscale': autoscale,
'hostname': args.celery_hostname,
'loglevel': logging.INFO,
'pidfile': pid_file_path,
'without_mingle': args.without_mingle,
'without_gossip': args.without_gossip,
# "heartbeat_interval": 10,
# "beat": True,
}
if args.daemon:
# Run Celery worker as daemon
with open(stdout, 'w+') as stdout_handle, open(stderr, 'w+') as stderr_handle:
if args.umask:
umask = args.umask
ctx = daemon.DaemonContext(
files_preserve=[handle],
umask=int(umask, 8),
stdout=stdout_handle,
stderr=stderr_handle,
)
with ctx:
#sub_proc = _serve_logs(skip_serve_logs)
worker_instance.run(**options)
else:
# Run Celery worker in the same process
#sub_proc = _serve_logs(skip_serve_logs)
worker_instance.run(**options)
# 关闭celery worker入口
@cli_utils.action_logging
def stop_celery_msgdata(args): # pylint: disable=unused-argument
"""读取PID文件中记录,关闭进程"""
pid_file_path, _, _, _ = setup_locations(process=WORKER_PROCESS_NAME)
pid = read_pid_from_pidfile(pid_file_path)
# Send SIGTERM
if pid:
worker_process = psutil.Process(pid)
worker_process.terminate()
# Remove pid file
remove_existing_pidfile(pid_file_path)
数据格式校验入口
Celery 初始配置文件
WORKER_UMASK = conf.get('celery', 'worker_umask') or '0o077'
sync_parallelism = conf.get('celery', 'sync_parallelism') or 0
DEFAULT_QUEUE = conf.get('celery', 'default_queue') or 'default'
WORKER_CONCURRENCY = conf.get('celery', 'worker_concurrency') or 16
broker_url = conf.get('celery', 'broker_url') or 'amqp://admin:admin@127.0.0.1:5672/kvnet-web-rabbitmq'
result_backend = conf.get('celery', 'result_backend') or 'db+mysql://root:root@127.0.0.1:33060/lslkvdata'
#result_backend = 'redis://127.0.0.1:6379/0'
# broker_url = 'amqp://admin:admin@192.168.6.210:5672/kvnet-rabbitmq'
# result_backend = 'db+mysql://root:root@192.168.0.115:3306/kvnet'
task_adoption_timeout = 600
task_publish_max_retries = 3
operation_timeout = 1.0
task_track_started = True
worker_prefetch_multiplier = 1
broker_transport_options = {}
if 'visibility_timeout' not in broker_transport_options:
broker_transport_options['visibility_timeout'] = 21600
TASK_QUEUES = {
DEFAULT_QUEUE: {
"exchange": DEFAULT_QUEUE,
"exchange_type": "direct",
"routing_key": DEFAULT_QUEUE
},
"scheduler_queue": {
"exchange": "scheduler",
"exchange_type": "direct",
"routing_key": "scheduler"
},
"common_queue": {
"exchange": "common",
"exchange_type": "direct",
"routing_key": "common"
}
}
TASK_ROUTES = {
}
DEFAULT_CELERY_CONFIG = {
# 数据格式
'accept_content': ['json'],
'event_serializer': 'json',
'worker_prefetch_multiplier': 1,
# 执行完毕发送ack
'task_acks_late': True,
# 默认的任务队列
'task_default_queue': DEFAULT_QUEUE,
# 默认的交换机 默认策略为direct
'task_default_exchange': DEFAULT_QUEUE,
# task状态跟踪
'task_track_started': task_track_started,
'broker_url': broker_url,
'broker_transport_options': broker_transport_options,
# 结果存储
'result_backend': result_backend,
'worker_concurrency': WORKER_CONCURRENCY,
}
result_backend = DEFAULT_CELERY_CONFIG['result_backend']
# if 'amqp://' in result_backend or 'redis://' in result_backend or 'rpc://' in result_backend:
# # log.warning(
# # "You have configured a result_backend of %s, it is highly recommended "
# # "to use an alternative result_backend (i.e. a database).",
# # result_backend,
# # )
# print(
# "You have configured a result_backend of %s, it is highly recommended "
# "to use an alternative result_backend (i.e. a database).",
# result_backend,
# )
celery Task的代码入口
import time
import signal
from typing import Dict, List, Union
from celery import Celery
from kombu import Exchange, Queue
from kombu.common import Broadcast
from kvnet.exceptions import MsgDataException
from kvnet.celery_settings import DEFAULT_CELERY_CONFIG
from kvnet.utils.configuration import conf
from kvnet.server.module.multi_data import MultiData
from kvnet.server.module.check_model import check
from kvnet.tools.singleton import Singleton
from kvnet.tools.define.sqldatadefine import SqlDataType
from kvnet.tools.server_log.error_dec import try_except
from kvnet.tools.server_log.log_collect import LogCollect
log = LogCollect()
CELERY_MSGDATA_NAME = 'kvnet.server.celery_msgdata' #conf.get("celery", "celery_heartbeat_name") or 'kvnet.celery_heartbeat'
msgdata_worker_concurrency = conf.get("celery", "msgdata_worker_concurrency") or 4
QUEUE = conf.get("celery", "msgdata_queue") or "msgdata"
QUEUE2 = "broadcast_msgdata"
msgdata_settings = {
"kvnet.server.celery_msgdata.msgdataTask": { # 为每个celery task配置默认参数,这里配置好,就不需要在函数装饰器里指定,效果都是一样的
"queue": QUEUE,
"routing_key": QUEUE
}
}
# 自定义创建queue和exchange
broadcast_msgdata = Broadcast('broadcast_msgdata')
task_queues = (
#参数中,依次是队列名,exchange名,路由关键字
Queue(QUEUE, Exchange("default"), routing_key=QUEUE),
Queue(QUEUE2, Exchange("lsl", type='fanout'), routing_key=QUEUE2),
# 如下使用方式一直报错,放弃使用。celery官网说支持广播方式,但不清楚为什么一直报错
#Queue(QUEUE2, exchange=broadcast_msgdata, routing_key=QUEUE2)
)
# 该字典用于配置生成
beat_settings = {
# 以下为测试数据,测试可用,只是后来放弃了使用celery beat心跳来通知worker推送缓冲数据给celery 的数据库业务
# "kvnet.server.celery_msgdata.msgdataTask_beat": {
# "task": "kvnet.server.celery_msgdata.msgdataTask",
# "schedule": 10.0,
# "args": ({
# "table": "node_file_viruses",
# "types": SqlDataType.TYPE_BEAT,
# "op": SqlDataType.OP_BEAT,
# "data": {}
# },),
# "options": {
# "exchange": "lsl",
# #"exchange": "broadcast_msgdata",
# "routing_key": QUEUE2,
# "queue": QUEUE2,
# }
# },
# 仍然保留的部分,用于模拟测试数据
"kvnet.server.celery_msgdata.msgdataTask": { # 此字段名,可以任意指定
"task": "kvnet.server.celery_msgdata.msgdataTask", #celery task的任务名,默认情况下,task函数名即为任务名。该名字还必须带上项目路径
"schedule": 2.0, # 每隔2秒 由celery beat生成一次请求
"args": ({ # celery task函数的参数
"table": "node_file_viruses",
"types": SqlDataType.TYPE_MULTI,
"op": SqlDataType.OP_INSERT,
"data": {
"nid": 1,
"virus_name": "virus_name",
"idsamp": 1,
"vlib_version": 1234,
"virus_findby": 1,
"virus_op": 1,
"virus_type": 1,
"filepath": "/user/lsl",
"md5": "md5",
"scan_type": 1,
"find_time": int(time.time()),
"create_time": "2021-9-18 10:10:10"
}
},),
"options": { # celery task参数
"exchange": "default",
"routing_key": QUEUE,
"queue": QUEUE,
}
},
}
# 配置入口,字典中的key,参考celery官网的配置项
# 指定并发数,worker的个数
DEFAULT_CELERY_CONFIG['worker_concurrency'] = msgdata_worker_concurrency
# 指定默认队列
DEFAULT_CELERY_CONFIG['task_default_queue'] = QUEUE
# 指定任务配置参数,如执行速率,队列,交换机
DEFAULT_CELERY_CONFIG['task_annotations'] = msgdata_settings
# 创建任务队列,交换机,交换机交换方式
DEFAULT_CELERY_CONFIG['task_queues'] = task_queues
# 指定定时任务
DEFAULT_CELERY_CONFIG['beat_schedule'] = beat_settings
# 指定记录定时任务的位置
DEFAULT_CELERY_CONFIG['beat_schedule_filename'] = "/tmp/kvnet/celerybeat-schedule"
# 指定定时的休息间隔
DEFAULT_CELERY_CONFIG['beat_max_loop_interval'] = 5
app = Celery(CELERY_MSGDATA_NAME, config_source=DEFAULT_CELERY_CONFIG)
class MsgData(Singleton):
FLAG = None
def __init__(self):
#super(MsgData).__init__(args, kwargs)
if MsgData.FLAG == None:
MsgData.FLAG = True
self.dic: Dict[str, Union[None, MultiData]] = {} #
self.time = int(time.time()) #
self.interval = 30 # 秒
self.limit = 10 # 一百条数据
self.times_limit = 5 # 两分钟一次
self.times = 0 # 计数
self.initDict()
# 这里先定义一个空值,即引用源。将task函数中的数据添加到这里的初始值里,就不会出现数据随着task函数消失而消失
# 之前将MultiData定义为namedtuple,后来发现不可以。
def initDict(self):
for table, obj in check.table.items():
self.dic[table] = MultiData("", [])
def work(self, taskid: str, args: Dict):
# 数据格式校验 -- 这里跳过即可
msg_table = "msg_data_args"
obj = check.table[msg_table]
#print(check.table)
#print(args)
args = obj.load(args)
# 获取数据内容
table = args['table']
types = args['types']
op = args['op']
data = args['data']
# 准备获取sql语句
table_sql = check.table[table]
# 检查mysql操作类型
# 单条mysql操作
if types == SqlDataType.TYPE_SINGLE:
if op == SqlDataType.OP_INSERT:
sqlstring = table_sql.insert(data)
elif op == SqlDataType.OP_UPDATE:
cond = data["cond"]
value = data["value"]
sqlstring = table_sql.update(cond, value)
else:
raise MsgDataException("op code error!")
# sql语句数据好后,直接通过celery task发送出去
print(sqlstring)
print("celery task --")
pass
elif types == SqlDataType.TYPE_MULTI:
# 准备初始值
# if types not in self.dic.keys():
# self.dic[table] = None
# 准备数据
if op == SqlDataType.OP_INSERT:
values = table_sql.manyInsertData(data)
md = self.dic[table]
print("multi -- op_insert ->", id(md))
if md.sqlstring == "" and md.data == []:
sqlstring = table_sql.manyInsert(data)
#datalist = []
#datalist.append(values)
#global new_md
#new_md = MultiData(sqlstring, datalist)
#self.dic[table] = new_md
md.sqlstring = sqlstring
md.data.append(values)
else:
md.data.append(values)
elif op == SqlDataType.OP_UPDATE:
cond = data["cond"]
value = data["value"]
value.update(cond)
cond = list(cond.keys())[0]
md = self.dic[table]
if md.sqlstring == "" and md.data == []:
# datalist = []
# datalist.append(cond)
# datalist.append(value)
# new_md = MultiData("", datalist)
# self.dic[table] = new_md
md.data.append(cond)
md.data.append(value)
else:
md.data.append(value)
else:
raise MsgDataException("op code error!")
print("dicdata--> multi : ", self.dic[table])
# 是否满足条件1
length = len(self.dic[table].data)
if length > self.limit:
print("multi--> length")
md = self.dic[table]
sqlstring = md.sqlstring
datalist = md.data
if sqlstring == "":
cond = datalist[0]
value = datalist[1:]
sqlstring = table_sql.manyUpdate(cond, value)
datalist = None
message = MultiData(sqlstring, datalist)
print("celery task--", message)
#del self.dic[table]
self.dic[table].sqlstring = ""
self.dic[table].data.clear()
self.times = 0
else:
raise MsgDataException("types code error!")
# 定时任务的使用方式二:使用装饰器实现定时任务,以下的配置方式是可用的,但是因为后来采用了配置文件方式配置,就注释了以下内容
# @app.on_after_configure.connect
# def setup_periodic_tasks(sender, **kwargs):
# sender.add_periodic_task(2.0, msgdataTask.s(({
# "table": "node_file_viruses",
# "types": SqlDataType.TYPE_BEAT,
# "op": SqlDataType.OP_BEAT,
# "data": {}
# },)).set(exchange="lsl", routing_key=QUEUE2))
#
# sender.add_periodic_task(2.0, msgdataTask.s({
# "table": "node_file_viruses",
# "types": SqlDataType.TYPE_MULTI,
# "op": SqlDataType.OP_INSERT,
# "data": {
# "nid": 1,
# "virus_name": "virus_name",
# "idsamp": 1,
# "vlib_version": 1234,
# "virus_findby": 1,
# "virus_op": 1,
# "virus_type": 1,
# "filepath": "/user/lsl",
# "md5": "md5",
# "scan_type": 1,
# "find_time": int(time.time()),
# "create_time": "2021-9-18 10:10:10"
# }
# }).set(exchange="default", routing_key=QUEUE))
# 单实例类,全局变量
md_g = MsgData()
# 时间终端处理函数,用于推送数据到数据库处理业务部分,因为数据库处理业务暂时接口没有获取到,直接Print以下意思意思。
def handel_timeout(signum, frame):
global md_g
print("clear info")
print("md.dic --> ", md_g.dic)
log.info("celery msgdata: ")
if 1==1:
print("for--- start", md_g.dic.items())
for key, value in md_g.dic.items():
print("table name = {}".format(key))
print("table info = {}".format(value))
print(id(value))
md = value
if md.sqlstring == "" and md.data == []:
print("value error===> continue")
continue
else:
sqlstring = md.sqlstring
datalist = md.data
print("data ={0} {1}".format(sqlstring, datalist))
if sqlstring == "":
cond = datalist[0]
value = datalist[1:]
table_sql = check.table[key]
sqlstring = table_sql.manyUpdate(cond, value)
datalist = None
print("None")
message = MultiData(sqlstring, datalist)
print("BEAT: celery task--", message)
# del self.dic[table]
md_g.dic[key].sqlstring = ""
md_g.dic[key].data.clear()
signal.signal(signal.SIGALRM, handel_timeout)
@app.task(bind=True, CELERYD_PREFETCH_MULTIPLIER=10)
@try_except
def msgdataTask(self, args: Dict):
signal.alarm(5)
taskid = str(self.request.id)
print("args->", args)
md_g.work(taskid, args)

浙公网安备 33010602011771号