当前有个Python项目,需要实现某个核心算法并提供api接口给其他部门调用;经过相关经验和测试分析,首选了sanic作为发布服务的框架(和flask差不多,不过sanic调用uvloop底层C性能更好)。另外,我们的核心算法需要小时级别的cpu密集型耗时计算,需要考虑api响应设计和多进程多核编程,提高核心算法的并发能力。
对于无法立即响应的api接口,首先请求方是无法一直在请求等待,http一定会是超时的。所以,为了避免这个问题,一般采用两种方案:
@blue.route('/', methods=['POST'])
async def playground(request):
"""
资源竞争调用测试与示例, 共享消息队列名为“request_queue”; 结果查询id为 sr = "{0}#{1}#{2}".format(guid, tb_name, func_name)
Request:
{
‘guid’:此次任务的唯一id。
'label_name': '[]' , (列表,是用户要分析的目标字段,包含字段名称、别名名称、字段类型)
'feature_name': '[]' , (列表,是用户选择的除目标字段外的其它字段,包含字段名称、别名名称、字段类型)
'table_name': '[]', (列表,是用户分析数据表名称、表别名)
'meta_data': '{}', (字典,包含数据连接的信息:IP、端口、数据库名、用户名、密码等)
‘select_func’: ‘’ 选择要运行的功能。单一任务
}
:return:
"""
# 请求参数解析
req = sanic_request_para(request)
guid = req.get('guid')
tb_name = req.get('table_name')
func_name = req.get('func_name') # 功能代号,请求方法名
label_name = req['label_name']
feature_name = req['feature_name']
# 测试代码, 自助创建唯一id
guid = str(guid) + label_name[0]
# 初始化返回值
data = []
resp = {'code': 400, 'msg': '异常', 'data': data} # 初始化
# 请求任务处理
dispatcher_id = "{0}#{1}#{2}".format(guid, tb_name[0], func_name)
# 查看结果列
if base_redis.exists(dispatcher_id): # 存在这个任务
status = base_redis.hget(name=dispatcher_id, key='status')
logs.info('任务「{0}」存在,当前状态为「{1}」'.format(dispatcher_id, status))
if status is not None and status == 'running':
resp = {'code': 201, 'msg': '当前已存在处理进程且在处理中', 'data': data}
elif status is not None and status == 'complete':
dt = base_redis.hget(name=dispatcher_id, key='data')
data.append(dt)
resp = {'code': 200, 'msg': '结果已返回到data区', 'data': data}
elif status is not None and status == 'error':
dt = base_redis.hget(name=dispatcher_id, key='data')
data.append(dt)
resp = {'code': 402, 'msg': '任务执行ERROR,在data区中查看错误信息', 'data': data}
else:
resp = {'code': 400, 'msg': '未知问题,请检查', 'data': data}
else:
# 没人在做,则生产任务消息
r = {'guid': guid, 'table_name': tb_name, 'label_name': label_name, 'feature_name': feature_name, 'func_name': func_name}
json_r = json.dumps(r)
request_queue = request.app.config.get('request_queue')
request_queue.put('abc')
# base_redis.lpush('request_queue', json_r)
resp = {'code': 202, 'msg': '已添加到任务队列', 'data': data}
response_json = sanic_json(resp)
return response_json
2. 设计两个接口,一个接口用来接收外部来的请求并生成任务计算,同时收到并返回一个回调地址,当任务完成时,通过回调地址主动上报通知执行状态和结果。另一个接口用于查询任务状态。
from flask import Flask, jsonify, request
import requests
app = Flask(__name__)
def long_running_task():
# 执行长时间运行的计算任务
result = {'result': '计算完成'}
return result
@app.route('/api/calculate', methods=['POST'])
def calculate():
# 启动异步任务
task = long_running_task()
# 生成回调地址
callback_url = request.args.get('callback_url')
# 发送HTTP请求,通知任务已完成
requests.post(callback_url, json=task)
# 返回任务ID和回调地址
return jsonify({'task_id': 1, 'callback_url': callback_url})
@app.route('/api/task_status', methods=['GET'])
def task_status():
# 查询任务ID
task_id = request.args.get('task_id')
# 根据任务ID从数据库或缓存中获取任务状态
status = '运行中'
if status == '完成':
# 任务完成,获取结果并返回
result = {'result': '计算完成'}
return jsonify(result)
else:
# 任务未完成,返回状态
return jsonify({'status': status})
@app.middleware("request")
def cors_middle_req(request: Request):
"""路由需要启用OPTIONS方法"""
if request.method.lower() == 'options':
allow_headers = [
'Authorization',
'content-type'
]
headers = {
'Access-Control-Allow-Methods':
', '.join(request.app.router.get_supported_methods(request.path)),
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Headers': ', '.join(allow_headers),
}
return HTTPResponse('', headers=headers)
@app.middleware("response")
def cors_middle_res(request: Request, response: HTTPResponse):
"""跨域处理"""
allow_origin = '*'
response.headers.update(
{
'Access-Control-Allow-Origin': allow_origin,
}
)
1> 多线程使用threading模块,如 t1 = threading.Thread(target=worker);t1.start()等方式,当然也可以用线程池。
2> 使用轻量级的线程(协程),通过asyncio模块实现,并配合await关键字。
两者场景大致如下:
1> 使用multiprocess模块或concurrent.futures。Python执行的 main 主函数一般不是守护进程(daemon=True),而在守护进程中创建子进程是会报错的。
2> Python3.8以前的版本中,多进程池不支持设置进程为非守护进程。因此生成进程池对象的代码需要放到main主进程再引用传递到其他进程,或者是在非守护子进程中生成进程池对象。
main.py示例一
# main.py示例一
if __name__ == '__main__':
multiprocessing.set_start_method('spawn') # 选择从父进程复制资源而非继承。
process = multiprocessing.Process(target=sanic_app, daemon=False)
process.start()
manager = multiprocessing.Manager()
shared_mem = manager.dict()
share_lock = manager.Lock()
task_pool = multiprocessing.Pool(processes=3)
Dispatcher.worker(task_pool, shared_mem, share_lock)
process.close()
logs.info('主程序退出!')
main.py示例二
# main.py 示例代码二
task_pool = multiprocessing.Pool(processes=3)
msg_queue = multiprocessing.Queue()
# 共享内存
manager = multiprocessing.Manager()
shared_primary = manager.dict() # parquet读入的缓存数据
shared_result = manager.dict() # 返回的处理结果
# sanic注册消息
app.config['task_result'] = shared_result
app.config['request_queue'] = msg_queue
loop_process = multiprocessing.Process(target=Dispatcher.worker, args=((task_pool, msg_queue, shared_primary, shared_result),))
loop_process.start()
# 注册蓝图
app.blueprint(blue)
if __name__ == '__main__':
logs.info('run sanic workers num: %d', 1)
# 多进程启动服务,因为需要大数据量的读写,会导致进程之间的资源无法共享读写,也没有统一的读写锁。而tomcat也是单进程,靠代码实现多进程。
app.run(host='0.0.0.0', port=9905, workers=1, single_process=True, debug=False) # 生产模式
loop_process.close()
logs.info('主程序退出!')
3> 示例代码一中,使用了multiprocessing.set_start_method(‘spawn’),一般来说都是设置成spawn,防止不同系统带来的区别。
4> 一般来说,我们写的api需要发布成服务,比如用flask、sanic等web框架发布成restful接口。这里以sanic举例,因为sanic会利用多核进行多进程发布,当我们的算法func1和func2采用了多进程,而func1\func2之间还有竞争和依赖,比如多个不同请求其实输出同一份结果,那需要共享结果对象,防止重复计算。这个时候,sanic的多进程发布将会导致每个进程存在多个重复的结果对象。
5> 一般来说,多进程之间的协作和通信,我们可以使用多进程管道、消息队列等方式实现数据传递。
6>因为我们写的api需要发布成服务,所以要在更高一层抽象出公共对象,才能保证程序中的多进程和sanic中的服务进行通信。例如把sanic服务发布放到另一个进程中启动,用消息队列保证信息的收发。在用另一个进程中启动进程池,保证cpu密集型计算功能在进程池中根据消息队列进行消费。
在工程中,启动sanic服务的文件中,基本不会放路由代码,而是通过蓝图在其他py文件中写路由。我们经常会使用app.config[‘abc’]来传递对象,但发现传递进去的多进程对象一直为None。原来是因为蓝图注册动作app.blueprint(blue),必须放在app.config之后,包括要传递的多进程对象。
main.py文件:
import multiprocessing
from multiprocessing import Process, Queue, Pool
from queue import Empty
from sanic import Sanic
from example.multi_process.fun import bp
app = Sanic(__name__)
def consumer(msg_queue):
# 持续处理消息的代码
while True:
try:
msg = msg_queue.get(timeout=1)
# 处理消息的代码
print(f'processing msg: {msg}')
except Empty:
pass
msg_queue = Queue()
p1 = Process(target=consumer, args=(msg_queue,))
p1.start()
p = Pool(processes=3)
print(p)
app.config['task_pool'] = p
app.config['msg_queue'] = msg_queue
app.blueprint(bp)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, workers=1)
route.py文件:
from sanic import Blueprint
from sanic.response import text
bp = Blueprint('my_blueprint')
def fc(msg):
print('功能1')
print(msg)
@bp.route('/add_msg')
async def add_msg(request):
msg = request.args.get('msg')
if msg:
msg_queue = request.app.config.get('msg_queue')
msg_queue.put(msg)
task_pool = request.app.config.get('task_pool')
print(task_pool)
task_pool.apply_async(fc, args=(msg,))
return text('msg added to queue')
else:
return text('no msg provided')