众所周知python在机器学习实践中的应用广泛深入,而在我们业务中的应用集中在提供线上实时风控输出服务,比如国内业务的模型在线服务架构和海外业务的后台决策引擎架构。这两种应用的结合就要求我们考虑如何高效安全便捷地来实现模型的在线部署,为上游提供服务。
在我们的考虑中,无论是代码复杂程度和业务场景,还是语言本身的特点,模型部署都有趋于向微服务架构转型的趋势和需要。一方面,需要进行代码分离来明确责任分工提高开发效率和容错性。另外一个方面,python在CPU密集型的应用中表现是无法令人满意的。为了使用协程来提高异步性从而处理更多的并发请求,最直接地就是将CPU密集转化为IO密集,因为Python天生就适合IO密集型的网络应用。
因此,我们生产中将模型计算抽取为model_lib代码库,并且通过微服务online_model进行交互。这里我们调研过两种模型部署的方式,最终选择了第一种。
Flask是一个轻量级的可定制框架,具有灵活、轻便且高效的特点,并且是标准的wsgi接口框架,易于扩展和维护。
1) Uwsgi搭配nginx性能快,内存占用低,高度可定制,自带详尽日志功能,支持平滑重启。
2) Flask完全兼容了wsgi标准; 微框架,扩展性强; 完全基于unicode,不需处理编码问题;自带服务可独立做单元测试及开发。
3) 我们客户端采用了tornado协程,已经实现了将cpu计算转为io操作,服务端完全是CPU密集的模型计算,不会释放进程,异步框架保持大量文件描述符状态耗费内存,因此不适用异步IO框架。
部署方式采用nginx+uwsgi+flask的方式,uwsgi可直接接受socket而不是http请求提高性能,再将服务转发给flask框架,这里注意flask此类wsgi标准接口的服务框架比如djangoweb.py在生产中一般不使用自带服务,而是在上层部署uwsgi或者gunicorn作为服务器来进行服务转发,上层再用nginx来做负载均衡,这样可以提高服务稳定性和性能。
uwsgi服务配置:
[uwsgi] # 监听端口 socket=127.0.0.1:8200 # 进程数 processes=20 ;async=4 ;threads=2 ;enable-threads = true # 运行的目录 chdir = /home/rong/www/online_model # wsgi文件 wsgi-file = model_main.py callable=app # 是否要有主进程 master = true # 后台运行及其打印的日志 daemonize = /home/rong/www/log/uwsgi.log # 主进程pid文件 pidfile = /home/rong/www/log/online_model/pid/uwsgi.pid # 日志切割大小 log-maxsize = 5000000 # 不记录请求信息的日志。只记录错误以及uWSGI内部消息到日志中。 disable-logging = false # 超时时间 http-timeout= 3 # 传输数据大小限制 buffer-size = 1048576 # 每个进程单独加载 lazy-apps = true
flask服务关键代码:
import importlib import json import cProfile import pstats import StringIO import time import traceback from flask import Flask, request from common import rong_logger from common.global_variable import StaticCacheClass import autopath # 不能去掉 app = Flask(__name__) # 这里是模型代码库的统一入口,模型代码库中是通过抽象类实现的规范化的模型代码实例,通过此服务提供调用,也通过离线调度进行跑批任务。保证线上线下模型调用一致性 online_model_main = importlib.import_module('online_model_main') MUST_PARAMS = ['resource_id', 'feature_dict'] SUCCESS_STATUS = 0 ERROR_STATUS = 1 # 路由函数只允许post请求 @app.route("/", methods=['POST']) def model_main(): uniq_id = '[%s][%s]' % (request.form.get('resource_id', ''), request.url) try: status, msg, params = _check_params(request) if status != SUCCESS_STATUS: rong_logger.error(uniq_id + 'params error, detail: %s' % msg) status, msg, result = status, msg, None else: resource_id, feature_dict = params['resource_id'], json.loads(params['feature_dict']) status, msg, result = online_model_main.main(resource_id, feature_dict, request, rong_logger) rong_logger.info(uniq_id + '[%s][%s][%s]' % (status, msg, result)) except Exception as e: rong_logger.error(uniq_id + 'error: %s, detail: %s' % (str(e), traceback.format_exc())) status, msg, result = 5, 'online_model_error:' + str(e), None return _get_response(status, msg, result)
模型代码库模型实例:
其中 XgboostExecutor类是基于xgb模型抽象类实现的类,通过它来实例化一个模型对象,给flask应用提供调用。具体我们不再深究,有兴趣可以再写一个专题来介绍模型代码库的部署。
# -*- coding:utf-8 -*- # !/usr/bin/python import logging import os from executor.src.load_helper import read_cur_path from executor.xgboost_model_executor import XgboostExecutor logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(message)s') [model_path, features_path,feature_importance_path] = map( read_cur_path, ["xgb_model", "feature_list","feature_importance"] ) model = XgboostExecutor(model_path, features_path, feature_check_white_list=["n21_score"], white_or_weight=False, feature_check_weight_limit= 1, feature_importance_path=feature_importance_path, manager="qutianhang@xx.com", developer="qutianhang@xx.com", correlation="negative")
微服务改造后20并发请求模型:
微服务改造前20并发请求模型:
本机测试并发性能就提高了20%,但注意这是在高并发的情况下,就单条请求来看,微服务并不能显著提高性能。
在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,能够更容易地创建分布式应用和微服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够执行在服务端实现的一样的方法(这个方法就类似于接口)
部署方式采用nginx+grpc,要求nginx支持http2.0。在客户端将json特征字典转为protobuf。(https://github.com/NextTuesday/py-pb-converters/blob/master/pbjson.py 这里附上json和protobuf互相转化的脚本。)
客户端:
服务端:
1) grpc使用protbuf更加复杂,需要在客户端服务端均保留protbuf文件并做校验,而flask只需要做好统一的接口标准规范即可。
2) grpc使用http2.0更适用移动端的在线模型预测,或者基于tensorflowd的大规模线上模型部署和预测,flask更适用后端面向服务的手动模型部署和预测。
3) grpc节省数据空间,但与python交互需要做json和protbuf数据转换,flask兼容wsgi标准,适用于RESTful类服务,但数据传输占用空间较大。