机器学习模型的服务化高并发部署--以Nginx+gunicorn+flask为例的docker部署方案

机器学习模型训练完成后如何让其他人使用是一个工程化的问题,也许我们的用户是没有一点机器学习的基础,我们让他们独自完成模型的部署是十分困难的,这时候我们可以考虑为他们提供一种服务,他们并不需要关心怎么实现的,只需要简单的调用我们提供的服务接口便可以实现自己的需求,这便是模型服务化的部署过程。
机器学习模型部署是一个复杂的工程化问题,本部分为了简单化模型的部署过程,主要介绍简单的Nginx+gunicorn+flask的docker部署方案。

1. flask部署机器学习模型

flask介绍

Python 现阶段有三大主流Web框架,分别是Django、Tornado、Flask :

  1. Django主要特点是大而全,集成了很多组件,例如: Models Admin Form 等等, 不管你用得到用不到,反正它全都有,属于全能型框架;
  2. Torando主要特点是原生异步非阻塞,在IO密集型应用和多任务处理上占据绝对性的优势,属于专注型框架;
  3. Flask主要特点小而轻,原生组件几乎为0, 三方提供的组件请参考Django 非常全面,属于短小精悍型框架。

在这里我们选用的是flask,这里主要介绍一下flask:
Flask 是一个 web 框架,也就是说 Flask 为你提供工具、库和技术来允许你构建一个 web 应用程序。这个 wdb 应用程序可以使一些 web 页面、博客、wiki、基于 web 的日历应用或商业网站。
Flask 属于微框架(micro-framework)这一类别,微架构通常是很小的不依赖于外部库的框架。这既有优点也有缺点,优点是框架很轻量,更新时依赖少,并且专注安全方面的 bug,缺点是你不得不自己做更多的工作,或通过添加插件增加自己的依赖列表,但是简单的部署服务也是可行的。

在模型flask部署的过程中,主要需要以下的python依赖包:

Flask
numpy
torch
torchvision
pillow

这里以resnet模型为例进行介绍,可以使用这个训练好的模型权重(权重网址(k9rb),如下式使用该模型进行推理的代码:

# -*- encoding: utf-8 -*-
import json
import torch
import numpy as np
from PIL import Image
from torchvision import transforms, models
data_trans = transforms.Compose([transforms.Resize([224,224]),
                                transforms.ToTensor(),
                                transforms.Normalize([0.485, 0.456, 0.406], 
                                                    [0.229, 0.224, 0.225])])
def thresh_sort(x, thresh):
    idx, = np.where(x > thresh)
    return idx[np.argsort(x[idx])]
# 加载模型部分
def init_model():
    resnet = models.resnet50()
    num_ftrs = resnet.fc.in_features
    resnet.fc = torch.nn.Linear(num_ftrs, 20)
    resnet.load_state_dict(torch.load('model.pth', 
                           map_location='cpu'))
    for param in resnet.parameters():
        param.requires_grad = False
    resnet.eval()
    return resnet
def make_prediction(path):
    img = Image.open(path)
    img_trans = data_trans(img).unsqueeze(0)
    output = model(img_trans)
    output = output[0].numpy().ravel()
    labels = thresh_sort(output, 0.5)
    if len(labels) == 0 :
        label_array = "No Categories"
        status = 0
    else:
        label_array = [cat_to_name[str(i)] for i in labels]
        status = 1
    return label_array, status
if __name__ == '__main__':
    # 初始化,预加载完成模型
    model = init_model()
    # 类别信息
    with open('class_name.json', 'r') as f:
        cat_to_name = json.load(f)
    path = "path/image"
    label, status = make_prediction(path)
    print(label, status)

在上述的代码中class_name.json的文件内容为:

{"0": "Aeroplane", "1": "Bicycle", "2": "Bird", "3": "Boat", "4": "Bottle", "5": "Bus", "6": "Car", "7": "Cat", "8": "Chair", "9": "Cow", "10": "Dining Table", "11": "Dog", "12": "Horse", "13": "Motorbike", "14": "Person", "15": "Potted Plant", "16": "Sheep", "17": "Sofa", "18": "Train", "19": "TV Monitor"}

flask服务化部署模型

如上是常用的模型推理的代码,现将模型改为flask方式实现模型服务化,如下所示:

# -*- encoding: utf-8 -*-
@File    :   deploy.py
@Time    :   2021/11/07 16:05:22
@Author  :   xx Xianqin 
@Version :   1.0
@Contact :   xianqin.xx@163.com
@Desc    :   None
import json
import torch
import numpy as np
from PIL import Image
from torchvision import transforms, models
from flask import Flask, request
app = Flask(__name__)
app.config["data_trans"] = transforms.Compose([transforms.Resize([224,224]),
                                transforms.ToTensor(),
                                transforms.Normalize([0.485, 0.456, 0.406], 
                                                    [0.229, 0.224, 0.225])])
def thresh_sort(x, thresh):
    idx, = np.where(x > thresh)
    return idx[np.argsort(x[idx])]
# 加载模型部分
def init_model():
    resnet = models.resnet50()
    num_ftrs = resnet.fc.in_features
    resnet.fc = torch.nn.Linear(num_ftrs, 20)
    resnet.load_state_dict(torch.load('model.pth', map_location='cpu'))
    for param in resnet.parameters():
        param.requires_grad = False
    resnet.eval()
    return resnet
# 调用服务执行的内容
@app.route('/model_predict', methods=['POST'])
def make_prediction():
    if request.method == 'POST':
        file_data = request.files.get('image')
        img = Image.open(file_data)
        img_trans = app.config["data_trans"](img).unsqueeze(0)
        output = app.config["model"](img_trans)
        output = output[0].numpy().ravel()
        labels = thresh_sort(output, 0.5)
        if len(labels) == 0 :
            label_array = "No Categories"
            status = 0
        else:
            label_array = [app.config["name"][str(i)] for i in labels]
            status = 1
        return json.dumps({"result": label_array, "status":status})
if __name__ == '__main__':
    # 初始化,预加载完成模型
    app.config["model"] = init_model()
    # 类别信息
    with open('class_name.json', 'r') as f:
         app.config["name"] = json.load(f)
    # 启动模型的flask服务
    app.run(host='0.0.0.0', port=10086, debug=True)

对比两个段代码可以看出,使用flask部署模型可以很简单的实现,仅需要修改较少的代码就可以,其他的模型参照类似的方式实现。

启动flask服务

若上述项目的python文件名为model_flask.py,则启动flask服务执行如下命令:

python model_flask.py

在进行测试时,建议选用postman(点击进入官网下载)进行测试,如下图是针对本项目启动的flask进行测试的配置页面:
在这里插入图片描述
如上图,由于我们选用的是post方式,按照本图中的相关内容进行配置即可进行测试。
注意这里是 flask 代码启动了 app.run(), 尤其注意这是用 flask 自带的服务器启动 的app服务,后边会介绍这一问题。

2. gunicorn部署flask项目

flask直接生产环境部署的问题

在启动上述的flask项目时,会出现“WARNING: Do not use the development server in a production environment.”的警告提示;这是提示不要在生产环境直接部署flask服务。
Flask的web框架内部已经有了一个 WSGI server用来接受请求,但是因为其自带的server在处理并发等情况时不够优秀,并且存在响应慢等问题,出现这种情况也是由于flask框架的重点都放在了WSGI applicaiton的层面上,因为flask只是一个web框架,并不是一个web server的容器,flask自带的werkzeug只能用于开发环境,不能用于生产环境。此外如果直接通过nginx进行反向代理,也会经常无法响应请求。因此在生产环境下,flask 自带的服务器是无法满足性能要求的。
有两个可以在生产环境中使用、性能良好且支持Flask程序的服务器,分别是Gunicorn和uWSGI,但是这两个模块不提供对window的支持。因此本部分主要介绍gunicorn部署flask服务。
常见的客户请求模式下图所示,因此主要是介绍下图模式的部署方案实现:
在这里插入图片描述

gunicorn介绍

gunicorn是一个python Wsgi http server(其中WSGI为Web Server Gateway Interface,服务器网关接口),只支持在Unix系统上运行,来源于Ruby的unicorn项目。Gunicorn使用prefork master-worker模型(在gunicorn中,master被称为arbiter),能够与各种wsgi web框架协作。
Gunicorn很容易配置,轻量级对cpu的消耗很少,且兼容性好,具有高性能,并支持了很多Worker模式,推荐的模式有以下几种:
同步Worker:也是默认模式Sync,也就是一次只处理一个请求。
异步Worker:通过Eventlet、Gevent实现的异步模式。
异步IO Worker:目前支持gthread和gaiohttp两种类型。

gunicorn依赖环境

gunicorn
supervisor

gunicorn部署flask

在安装好 gunicorn 后,需要用 gunicorn 启动 flask(不需要启动上一步的flask,不然会造成gunicorn端口号冲突),使用了 gunicorn启动flask服务,则这一过程中model_flask.py 就等同于一个库文件,被 gunicorn 调用。

启动flask(在终端输入如下命令)

gunicron -w 4 -k gevent -b 0.0.0.0:10086 model_flask:app

上述参数介绍:
-b 绑定应用的ip(0.0.0.0是任何服务器都可以访问,127.0.0.1是只能本机访问)和端口
-w work的数量,也就是同时启动的模型进程数量,官方说可以有:核心数*+1个,若是部署的机器学习模型,这样设置可能存在问题,后续会介绍。
worker_class
-k STRTING, --worker-class STRTING要使用的工作模式,默认为sync。可引用以下常见类型“字符串”作为捆绑类,主要有以下几种:
sync
eventlet:需要下载eventlet>=0.9.7
gevent:需要下载gevent>=0.13
tornado:需要下载tornado>=0.2
gthread
gaiohttp:需要python 3.4和aiohttp>=0.21.5
他们的区别可以
参考这个链接
model_flask:app是前边部署的flask文件model_flask.py的名字和固定的app
gunicorn有较多的参数,其他的参数介绍可以参考官方文档。

若要结束 gunicorn 需执行 pkill gunicorn,有时需要利用ps -ef | grep gunicorn查找到 pid 进程号才能 kill。
这样的操作有些繁琐,因此出现了supervisor,这是专门用来管理进程的工具,还可以管理系统的工具进程。

在这里我们主要利用supervisor管理gunicorn,将其当作自己的子进程启动;当gunicorn由于异常等停止运行后,supervisor可以自动重启gunicorn

supervisord启动成功后,可以通过supervisorctl客户端控制进程,启动、停止、重启
具体的使用步骤如下:

  1. 安装supervisor文件
pip install supervisor
  1. 生成supervisor的配置文件
echo_supervisord_conf > supervisor.conf

注意:可能echo_supervisord_conf不在你的环境变量目录下,可能要查找,通常在python环境的bin目录下,如果不在可以去这个目录查找。利用命令find / -name echo_supervisord_conf查找到echo_supervisord_conf路径,使用绝对路径执行上述命令

3.修改配置文件

vi supervisor.conf

在配置文件的最后添加相关的gunicorn内容

[program:our_app]
directory=/工程文件/model_flask.py/所在的绝对路径/
command=gunicorn -w 4 -k gevent -b 0.0.0.0:10086 model_flask:app

保存上述内容即可,可以看出与终端执行gunicorn是基本一致的。

  1. 启动supervisor
    以下两种方式都可以启动我们的应用:
supervisorctl start our_app 
supervisord -c supervisor.conf

其中一定要注意our_app与supervisor.conf中的[program:our_app]是相一致的。
其他supervisor的基本使用命令:

supervisord -c supervisor.conf                             通过配置文件启动supervisor
supervisorctl -c supervisor.conf status                    察看supervisor的状态
supervisorctl -c supervisor.conf reload                    重新载入 配置文件
supervisorctl -c supervisor.conf start [all]|[appname]     启动指定/所有 supervisor管理的程序进程
supervisorctl -c supervisor.conf stop [all]|[appname]      关闭指定/所有 supervisor管理的程序进程

现在访问http://IP:10086/model_predict就是利用gunicorn进行flask服务调用了,方法与前边介绍的postman测试一致。

3. Nginx部署

Nginx介绍

Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。

Nginx安装

系统安装nginx安装包
ubuntu安装nginx
sudo apt install nginx
centos安装nginx,可以
参考这个

Nginx配置

以Ubuntu系统下的nginx为例进行介绍:
sudo vi /etc/nginx/nginx.conf
如下是该配置文件的内容:

user nginx;   # 设置使用用户nginx,保持不变即可
worker_processes 2; # nginx要开启的进程数,若不设置默认为1,一般情况下不用修改,但考虑到实际情况,可以修改这个数值,以提高性能,上限为主机的CPU核数;nginx开启太多的进程,会影响主进程调度,所以占用的cpu会增高,因此该数值要适量设置,个人建议1-4即可。
error_log /var/log/nginx/error.log; # 出现错误存放的日志文件路径,保持不变即可
pid /run/nginx.pid; # 进程号的PID存在该路径中
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf; 
events {# 工作模式与连接数上限
    worker_connections 1024;  # 单个进程的最大连接数
http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"'; # 连接日志的保存格式
    access_log  /var/log/nginx/access.log  main;  # 日志的存放路径
    sendfile            on;   # 使用sendfile系统调用来传输文件,保持该默认即可
    tcp_nopush          on;   # 激活tcp_nopush参数可以允许把http response header和文件的开始放在一个文件里发布,作用是减少网络报文段的数量
    tcp_nodelay         on;   # 激活tcp_nodelay,内核会等待将更多的字节组成一个数据包,从而提高I/O性能
    keepalive_timeout   65;   # 长连接超时时间,单位是秒
    types_hash_max_size 4096;  # 为了快速处理静态数据集,例如服务器名称, 映射指令的值,MIME类型,请求头字符串的名称,nginx使用哈希表
    include             /etc/nginx/mime.types;      # 文件扩展名与类型映射表
    default_type        application/octet-stream;   # 默认文件类型
    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    # 加载模块化配置文件
    include /etc/nginx/conf.d/*.conf;
    server {
        listen 8999;  # 监听端口,为nginx的开放端口内容,默认81
        #server_name 192.168.10.88;  # 域名,没有可以为空
        location / { # 对“/”启用反向代理
                proxy_pass http://0.0.0.0:10086;   
                proxy_redirect     off;
                proxy_set_header   Host $host:8999 ; # 若listen的端口号不是81,此处一定要
                proxy_set_header   X-Real-IP            $remote_addr;
                proxy_set_header   X-Forwarded-For      $proxy_add_x_forwarded_for;
                proxy_set_header   X-Forwarded-Proto    $scheme;

上述配置文件中主要添加了server部分,其中listen 是nginx的代理端口号,默认为81,可以修改为其他的;proxy_pass 处的内容为flask的地址和端口号,proxy_set_header 处为要与listen处一致,若为81可以写成【 Host $host;】,但是若listen不为81,必须写成【Host $host:8999 ;】,与listen处一致。

启动nginx

在终端执行

sudo service nginx start 

现在访问http://IP:8999/model_predict就是利用nginx代理的flask服务调用方式了了,与前边介绍的postman测试一致。注意端口号需要改成listen处的接口。

4. docker镜像内部署nginx+gunicorn+flask的实现方案

本项目的目录结构如下:
在这里插入图片描述
具体的dockerfile文件内容如下:

#基于的基础镜像
FROM python:3.7
RUN  mkdir /code
COPY app  /code  
#并发相关配置文件
RUN apt-get install nginx -y
COPY supervisor.conf /code
COPY nginx.conf /etc/nginx/
#安装python相关环境
RUN pip install -r /code/requirements.txt  -i https://pypi.tuna.tsinghua.edu.cn/simple/ -f https://download.pytorch.org/whl/torch_stable.html
WORKDIR /code
#建立相关软连接配置
RUN ln -s /opt/python37/bin/gunicorn /usr/bin/ && ln -s /opt/python37/bin/supervisorctl /usr/bin && ln -s /opt/python37/bin/supervisord /usr/bin
RUN useradd -s /sbin/nologin -M nginx
#启动容器执行的命令,依次是启动gunicorn、启动nginx && service nginx start 
# nginx -g 'daemon off;'关闭nginx的后台,
# nginx默认是以后台模式启动的,Docker未执行自定义的CMD之前,nginx的pid是1,执行到CMD之后,nginx就在后台运行,bash或sh脚本的pid变成了1。所以一旦执行完自定义CMD,nginx容器也就退出了。为了保持nginx的容器不退出,应该关闭nginx后台运行
ENTRYPOINT ["/bin/bash", "-c", "supervisord -c /code/supervisor.conf && nginx -g 'daemon off;'"]
#端口号为Nginx反向代理的接口,与nginx.conf中的listen设置保持一致
EXPOSE 8999

依据上边工程目录结构执行dockerfile即可生成镜像。

5. 此方案问题

  1. gunicorn的-w是对应的启动服务的进程数:
    对于机器学习模型来说,尤其是深度学习模型通常比较大,启动多个进程便是加载多次模型到内存中,此处必须要考虑服务器的显存,否则会出现out of memory。例如:我们有一张显存为12G的卡,若是单个模型加载到显存内占用的显存空间为3G,若设置-w为4(需要注意-w的数量是子进程的数量,并不包括主进程,因此-w为1,则实际有2个进程),则此时刚好3G×5=15G,此时变会出现内存溢出。所以再设置gunicorn的-w时需要考虑部署的环境问题。

此方案有不妥之处欢迎大佬们多多指教!
若在部署过程中遇到问题,可以随时留言沟通,亦或者发送邮件maxianqin1112@163.com,在看到后我会第一时间回复,一起加油!

此内容可以任意转载,但请注明出处!

机器学习模型的服务化高并发部署--以Nginx+gunicorn+flask的docker部署方案欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入欢迎使用Markdown编辑器你好! 这是你
flask加apscheduler重复加载问题已经不是新问题了。 众所周知,单机运行只需要在app.run()中增加use_reloader=False即可解决 但是到服务器采用gunicorn部署时老问题又出现了。 查阅各种资料均无法解决。 gunicorn worker 使用 sync 个数为4个 解决过程如下: 1.gunicorn启动配置中增加–preload,无效。 2.编写单例模式实例...
Gunicorn“绿色独角兽”是一个被广泛使用的高性能的Python WSGI UNIX HTTP服务器,移植自Ruby的独角兽(Unicorn )项目,使用pre-fork worker模式,具有使用非常简单,轻量级的资源消耗,以及高性能等特点。 Gunicor...
用过 Flask 框架的朋友都知道,Flask 自带的 wsgi 性能低下,不支持高并发。 只适合你开发调试的时候用,所以在线上一般都使用 Nginx + gunicorn 才能获得更强的性能和更高的安全性! gunicorn 是一个 python Wsgi http server,只支持在 Unix 系统上运行,下面我们来熟悉一下以 gunicorn 的配置与使用。 一、gunicorn 的安装 注意 gunicorn 不能在 windows 环境下使用 pip install gunicorn
CentOS 下部署Nginx+Gunicorn+Supervisor部署Flask项目 Flask 处理高并发、多线程 Flask 高并发部署方案详细教程! Flask: flask框架是如何实现非阻塞并发的 Flask 高并发部署方案详细教程 应项目需求,需要把做好的深度学习算法提供给别人使用,采用Tornado web框架,查阅了很多网上的Tornado的demo,大多数的demo都是实现网页间的交互等等,跟自己的需求不太一样。在这里记录一下自己的demo,详细解释看代码注释~ API服务代码server.py import sys import os import tornado.httpserver import ...
1. 首先,在本地安装DockerDocker Compose。 2. 在本地创建一个文件夹,例如nacos,用于存放nacos的配置文件和docker-compose.yml文件。 3. 在nacos文件夹中创建一个名为docker-compose.yml的文件,内容如下: version: '3' services: nacos: image: nacos/nacos-server:latest container_name: nacos ports: - "8848:8848" volumes: - ./data:/home/nacos/data - ./logs:/home/nacos/logs - ./init.d:/home/nacos/init.d environment: - MODE=standalone 4. 在nacos文件夹中创建一个名为data的文件夹,用于存放nacos的数据。 5. 在nacos文件夹中创建一个名为logs的文件夹,用于存放nacos的日志。 6. 在nacos文件夹中创建一个名为init.d的文件夹,用于存放nacos的初始化脚本。 7. 在终端中进入nacos文件夹,执行以下命令启动nacos: docker-compose up -d 8. 等待一段时间后,可以通过浏览器访问http://localhost:8848/nacos/,即可进入nacos的管理界面。 注意:在生产环境中,需要根据实际情况修改docker-compose.yml文件中的配置,例如修改端口号、数据存储路径等。