Django+Tensorflow serving+Gunicorn+Nginx配置

前言,由于我是使用这个做demo,没有考虑到性能优化的各种参数问题,因此本文仅作为对Django 2.0框架和Tensorflow serving之间的通信的基本参考。


有关于Tensorflow serving的部署方法见这里
配置要求:
硬件:华为云服务器,最便宜的那档,反正是练习用的
操作系统:Linux Ubuntu 16.04
使用工具:Tensorflow-serving Docker,Django 2.0.4,python3.6,Docker compose, nginx,gunicorn
模型文件:Tensorflow生成的PB(ProtoBuf)格式模型参数文件


1.创建Django项目

Django作为web框架可以通过两种方法创建

方法一:使用命令行创建项目

pip install django
django-admin startproject 项目名称

样例中我将项目名称命名为tfs_django

方法二:使用Pycharm创建项目

file ---> new project ---- 选择Django ---> 配置路径和项目名称 ---> 配置环境(默认用系统环境) ----> 点击create(完成创建)

具体在pycharm内安装django参见:https://www.runoob.com/django/django-install.html

创建后的项目的文件结构如下:
在这里插入图片描述


2. 制作用于部署nginx和gunicorn的Dockerfile

2.1. 什么是Docker

Docker是一种创建容器的引擎工具,可以把容器想象成虚拟机一类的技术,但是相比起虚拟机,它启动快,资源利用率高(一台主机可以同时运行几千个Docker容器),占用空间也很小,虚拟机一般要几GB到几十GB的空间,Docker容器只需要MB级甚至KB级。

Dockerfile是一种用于构建images(镜像)的DSL(domain-specific language )。使用Dockerfile可以同时部署多个容器,并且在多个云服务器上快速实现同样的容器,包含同样的系统资源和文件资源。

如何通俗解释Docker是什么? - 刘允鹏的回答 - 知乎
https://www.zhihu.com/question/28300645/answer/67707287

大部分主流应用程序在Docker官方网站上都可以找到对应版本镜像并且下拉到本地,但是Django在Docker官方网站上我只找到了1.0版本镜像(包含服务器等配置),因此需要自己使用Dockerfile来制作打包了Django的Nginx和Gunicorn镜像文件。

由于需要同时启用三个容器(包含Django的Gunicorn,Nginx,postgres),我们使用docker-compose来进行配置。


2.2. 安装Docker-ce和Docker-compose

安装教程参考教程:https://zhuanlan.zhihu.com/p/44423066

2.2.1. 首先随便开一台云服务器,我买的是最便宜的那档,然后在服务器端安装Docker。在安装 docker-ce 之前,先使用 apt 命令安装所需的 docker 依赖项。

sudo apt install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common

2.2.2. 完成依赖项后,在安装docker-ce前,首先通过运行以下命令添加 docker 密钥和仓库,直接复制粘贴就可以了。

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

curl命令下载docker的安全秘钥并将通过apt-get add信任该秘钥(来源)。

2.2.3. 开始安装docker-ce:

sudo apt update
sudo apt install -y docker-ce

2.2.4. 安装完成后,启动 docker 服务并使其能够在每次系统引导时启动。

systemctl start docker
systemctl enable docker

systemctl是 Systemd 的主命令,用于管理系统。Systemd 为系统的启动和管理提供一套完整的解决方案,初衷是用于解决Linux中init命令启动慢且启动脚本复杂的问题。Systemd 的优点是功能强大,使用方便,缺点是体系庞大,非常复杂。systemctl enable docker用于设置开机启动docker服务。有关Systemd其他功能参见http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html

2.2.5. 添加一个新用户并将其添加到 docker 组,然后从root切换到该用户名下。

useradd -m -s /bin/bash 用户名
usermod -a -G docker 用户名
su 用户名

2.2.6. 输入docker run hello-world,如果看到以下输出信息说明docker安装成功
在这里插入图片描述

2.2.7. 安装docker-compose

sudo curl -L https://github.com/docker/compose/releases/download/1.21.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

uname 返回的是保存在string内的系统信息,-s 返回的是kernal的名字,例如linux,-m返回的是机器的硬件名字,例如x86_64。
调用docker-compose version查看是否安装成功
在这里插入图片描述


2.3. 配置项目环境

配置教程参考:https://zhuanlan.zhihu.com/p/44423066

项目环境分三个部分,Django项目,Dockerfile和nginx配置文件,虽然所有文件和目录位置都可以自定义,文件名也可以,但为了防止新手混淆且方便进行比对,放一下我配置完环境后的文件目录结构:
在这里插入图片描述
2.3.1. 创建各级目录

这个目录名可以随意,这里取名依照教程没有改动还是guide01。然后在该目录下创建两个新目录,同样可以随意取名,因为后面可以在制作docker容器的时候指定地址映射的时候再改。这里我取名为projectconfig

project用来存放之前建立好的Django项目,config里则存放项目配置文件,包括nginx配置文件和用于安装python依赖的包文件的requirements.txt。

mkdir -p guide01
cd guide01/
mkdir project/ config/

2.3.2. 使用vim在config目录中创建一个 requirements.txt 文件

vim是linux下的文本编辑命令,如果在命令行内输入vim显示找不到该命令,则需要先安装vim。一般云服务器都会预装vim,但是之后在docker容器内可能也需要使用vim来查看文件参数是否配置正确,所以先把安装命令放在这里。

sudo apt-get update
sudo apt-get install vim

使用命令 vim config/requirements.txt在命令行界面打开文件,将下面的python依赖包复制粘贴到文件内,然后按esc后输入:wq即保存退出文件。

Django==2.0.4
gunicorn==19.7.0
psycopg2>=2.8
requests== 2.24.0
numpy==1.18.0
scipy==1.4.1
gevent>=21.0.0

注1:需要安装的包和教程里有些不同,根据项目实际需要来添加就可以了。
注2:requirements.txt文件名不是强制的,可以随便取,但是看了很多教程似乎都用了requirements.txt,应该是个约定俗成的文件名,可以的话还是建议沿用这个名字,不容易和其他文件弄混。

2.3.3. 创建nginx虚拟主机文件 django.conf

Nginx是非同步框架的网页伺服器,也可以用作反向代理、负载平衡器和HTTP快取。
django.conf可以在任意位置,只要在docker-compose.yml把配置文件的路径映射到容器路径/etc/nginx/conf.d即可。

mkdir -p config/nginx/
vim config/nginx/django.conf

在django.conf配置文件中,粘贴以下语句,包括需要nginx监听的端口等等配置:

upstream web {
  ip_hash;
  server web:8000;
}

# portal
server {
  location / {
        proxy_pass http://web/;
  }
  listen 8000;
  server_name localhost;

  location /static {
    autoindex on;
    alias /src/static/;
  }
}

proxy_connect_timeout   3000;
proxy_send_timeout      3000;
proxy_read_timeout      3000;

最后输入:wq 保存并退出文件。

2.3.4. 创建 Dockerfile
在这里插入图片描述
图中是guide01文件夹下的文件和文件夹列表,方便用来进行对比。2.3.4和2.3.5分别用来设置Dockerfile和docker-compose.yml文件。

guide01目录下创建名为Dockerfile的文件,文件名可以自定义,该文件用于构建Docker镜像。

vim Dockerfile

将以下内容粘贴到Dockerfile内:

FROM python:3.6
ENV PYTHONUNBUFFERED 1

RUN pip install --upgrade pip

RUN apt-get -y update && \
    apt-get install -y --no-install-recommends gcc python3-dev python3-pip python3-venv && \
    apt-get install -y postgresql postgresql-contrib bash

RUN mkdir /config
ADD /config/requirements.txt /config/
RUN pip install -r /config/requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
RUN pip3 install tensorflow==2.4.0 -i https://mirrors.aliyun.com/pypi/simple/
RUN mkdir /src
WORKDIR /src

Dockerfile内定义了需要运行的安装程序,以及为项目创建新目录/src。如果需要安装其他软件的话可以在里面添加,我自己只在原教程基础上另外定义了需要安装的tensorflow和镜像。

注意:原教程中使用了python:3.5-alpine这个基础Docker镜像,虽然alpine据说是最小的linux镜像,安装速度非常快,但是不少人说其实在安装了其他软件后alpine版本的linux体积并没有优于其他镜像,而且alpine和很多软件不兼容,所以不个人不推荐。

2.3.5. 创建 Docker-compose 脚本
guide01目录下创建docker-compose.yml文件

vim docker-compose.yml

将以下内容复制并粘贴到文件内:

version: '3'
services:
  db:
    image: postgres:10.3
    container_name: postgres01
  nginx:
    image: nginx:1.13
    container_name: nginx01
    ports:
      - "8000:8000"
    volumes:
      - ./tfs_django:/src
      - ./config/nginx:/etc/nginx/conf.d
    depends_on:
      - web
  web:
    build: .
    container_name: django01
    command: bash -c "python manage.py makemigrations && python manage.py migrate && python manage.py collectstatic --noinput && gunicorn tfs_django.wsgi -b 0.0.0.0:8000"
    depends_on:
      - db
    volumes:
      - ./tfs_django:/src
    expose:
      - "8000"
    ports:
      - "8080:8000"
    restart: always

文件内定义了三个服务(容器),名称由container_name定义:postgre01包含PostgreSQL数据库服务, nginx01包含Nginx服务,和 django01包含Django服务。
Django服务中使用expose命令暴露了8000端口,并通过ports命令将容器的8000端口绑定至主服务的8080端口上。


3. 配置Django项目

在这里插入图片描述

将Django项目文件复制guide01目录下,上图是我的项目的路径,仅供参考。项目路径必须和docker-compose.yml中设置的一致,因为在docker-compose.yml中的Django01服务中可以看到以下命令:

volumes:
      - ./tfs_django:/src

该命令将主服务器下的./tfs_django目录映射到了/src下,同样nginx服务下还映射了./config/nginx:/etc/nginx/conf.d,这样容器内的nginx服务就可以读取到之前保存在config/ngxin目录下的配置文件了。

进入tfs_django目录并编辑应用程序设置settings.py

cd ~/guide01/tfs_django/
vim tfs_django/settings.py

ALLOW_HOSTS 行中,添加服务名称 *

ALLOW_HOSTS = ['*']

因为本文只是练习用,安全性没有太高要求,如果安全要求比较高可以参考原教程中将*改为web,对应docker-compose.yml文件中的服务名称。

更改数据库设置,使用PostgreSQL的数据库来运行名为db的服务,使用默认用户和密码,db对应docker-compose.yml文件中的服务名称。

DATABASES = {  
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'postgres',
        'USER': 'postgres',
        'HOST': 'db',
        'PORT': 5432,
    }
}

在文件尾端添加STATIC_ROOT配置目录:

STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

保存文件并退出。


4. 构建并运行Docker镜像

运行以下命令构建:

cd ~/guid01/
docker-compose build
docker-compose up -d

也可以使用 docker-compose up --build 直接启动 (告訴docker-compose rebuild后run),比起原教程的build之后再up简洁点。
等运行完毕后使用如下命令检查运行容器并在系统上列出docker镜像。

docker-compose ps
docker-compose images

在这里插入图片描述


5. 使用Django连接tensorflow serving

参考教程:https://blog.csdn.net/JerryZhang__/article/details/85238069
教程中使用的是Django1.0,所以前面的Docker步骤都不适用于2.0,但后面的WSGI等设置在2.0中可以通用的。
我的项目文件路径如下,屏蔽了其他自定义路径和文件,仅作参考:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210427180921614.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Jhc2llbF8yMDE5,size_16,color_FFFFFF,t_70
主要文件解释:

tfs_django:项目容器
manage.py: 一个实用的命令行工具,可让你以各种方式与该 Django 项目进行交互,启动项目的时候就是通过该文件。
tfs_django/init.py: 一个空文件,告诉 Python 该目录是一个 Python 包。
tfs_django/asgi.py: 一个 ASGI 兼容的 Web 服务器的入口,以便运行你的项目,创建项目的时候就已经设置好了不需要进行改动。
tfs_django/settings.py: 该 Django 项目的设置/配置,比如说SQL设置,根目录设置等等。
tfs_django/urls.py: 该 Django 项目的 URL 声明; 一份由 Django 驱动的网站"目录"。
tfs_django/wsgi.py: 一个 WSGI 兼容的 Web 服务器的入口,以便运行你的项目,创建项目的时候就已经设置好了不需要进行改动。。

下图是urls.py文件的内容,简单来说这个就是个转发用的文件,例如在网页中输入http://IP:端口/data,它就会自动跳转至predict文件的predict方法中(图2),predict方法中可以处理request传过来的参数并调用其他方法。
在这里插入图片描述
predict.py代码结构大致如下,删除了部分音频和tensorflow serving返回结果的处理代码,仅作参考:

from django.http import HttpResponse
from django.shortcuts import render_to_response
import base64
import requests
import json
import requests
import os
import sys
from os import path
from django.contrib.auth import authenticate, login
from django.template import RequestContext
from django.template.context_processors import csrf
from django.views.decorators.csrf import csrf_exempt
import numpy as np

@csrf_exempt #取消csrf验证,因为我这边会出现csrf验证错误,虽然并不建议这么做
def request_prediction_url(wav_url):
    '''
    :params wav_url: the web location of wav file
    '''
    print("Now let's start download the wav file from obs to loacal")
    #This will be changed to buffer afterwards
    import wget
    wget.download(wav_url,'/usr/src/wav_download/1.wav') #下载音频文件到本地,仅作测试用
    # now read in file from local direction
    import wave
    f = wave.open('/usr/src/wav_download/1.wav')
    params = f.getparams()
    nchannels, sampwidth, framerate, nframes = params[:4]
    wav_bytes = f.readframes(nframes) # bytes format

    SERVER_URL = 'http://tensorflow_serving_IP:restful_api_ports/v1/models/model_name:predict'
    HEADERS = {"content-type": "application/json"}
    # dictionary path
    VOCAB_DIR = 'dictionary_path_pre_defined'
    # download audio file
    print("Now let's start feature extraction")
    data_int = np.frombuffer(wav_bytes, np.int16)  # convert byte to int for fbank convert

    feat_data = feature_extraction_defined_by_usr(framerate, data_int)
    # change feat to list under the requirement of tensorflow serving request
    feat_list = feat_data.tolist()
    try:
        # Compose a JSON predict request
        print("Compose a JSON Predict request")
        data = json.dumps({"signature_name": "serving_default", "inputs": {"the_inputs": feat_list}})
        predict_request = data

        # Send few actual requests and report average latency.
        print("Send requests")
        total_time = 0
        num_requests = 1
        for _ in range(num_requests):
            print("now let's post and response")
            response = requests.post(SERVER_URL, data = predict_request, headers = HEADERS, timeout=5)
            # Response.raise_for_status() returns the response status
            # If any error occurs by request, the error information will print on the console
            response.raise_for_status()
            predictions = json.loads(response.text)
            for key, value in predictions.items():
                print("we do nothing in here, just want to get the received value")

            value_array = np.array(value, dtype=np.float32)
            # readin vocabulary dictionary
            p_text = convert_result_from_index_to_words
            print("p_text is: {}".format(p_text))
            return p_text
    except:
        return 'something wrong occurs'

def predict(request):
    request.encoding = 'utf-8'
    if len(request.POST)>0:
        wav_url = request.POST.get('wav_url')
        print("wav_url is: {}".format(wav_url))
        print("wav_url type is: {}".format(type(wav_url)))

        predict_result = request_prediction_url(wav_url)
        # return HttpResponse(message)
        c=csrf(request)
        c.update({'predict_result': predict_result})
        return render_to_response('predict_result.html',context=c)
    else:
        c=csrf(request)
        c.update({'predict_result': 'Please input correct wave data'})
        return render_to_response('predict_result.html',context=c)

predict方法会将返回的识别结果通过render_to_response方法传递,并通过predict_result.html显示,predict_result.html代码如下,predict_result.html里大部分是简单的html代码,重点只有里的部分,即{% csrf_token %},由于是使用post方法进行传递,因此这段代码不能省略,否则会产生403错误,通过关键字wav_url返回传递的音频路径,predict_result 来提取返回的预测值:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Speech Recognition</title>
</head>
<body>
    <strong>
        <p style="font-size:25px">This is a demo of ASR task using Django and tf-serving.</p>
    </strong>
        <p style="font-size:15px">Type you wav URL in the input box below, and click the predict button, it will parse the audio file to the backend.</p>
    <form action="/data" method="POST">
        {% csrf_token %}
        <input type="text" name="wav_url" placeholder="Type audio url here" size="80" >
        <input type="submit" value="predict">
    </form>
    <strong>
        <p style="font-size:20px"><br />predict result: <span style="font-size:20px;color:red">{{ predict_result }}</span></p>
    </strong>
</body>
</html>

最后在浏览器中输入http://ip:8000/data就可以显示predict_result.html页面了,返回的值也会在该页面显示。

6. Errors

6.1 Gunicorn Timeout解决办法

Gunicorn的CRITICAL WORKER TIMEOUT可以通过 docker-compose logs --tail 50 service_name 看到,出现这个错误的原因是Gunicorn默认的响应时间较短(好像是30秒)。解决方案当然有优化模型等等,这里提供一个在gunicorn内设置参数的解决方法,即在Django项目的manage.py同目录下新建一个gunicorn的配置文件,名字可以类似于gunicorn.conf.py,然后在配置文件中添加一条timeout=XXXX XXXX代表指定秒数之后终止。

以下是我的配置文件仅作参考

import multiprocessing
import os

# preloads
preload_app = True
# number of worker
workers = 5
# number of threads
thread = 4
# port 8000
bind = '0.0.0.0:8000'
# timeout setup
timeout = 5000
# run deamonized in the background or not
daemon = 'false'
# asyn workers
worker_class = 'gevent'
# a maximum count of active greenlets grouped in a pool that will be allowed in each process (for "greenven work class")
worker_connections = 2000
# granularity of error log outputs
loglevel = "warning"

保存完gunicorn.conf.py文件后,需要后台根据配置文件启动gunicorn,需要进入到web容器内才能运行gunicorn命令:

docker-compose exec web bash

由于是使用了Django框架,启动的代码比较简单:

nohup gunicorn 项目名称.wsgi:application -c gunicorn.conf.py&

由于需要gunicorn长期在后台运行,因此需要使用linux自带的nohup命令,项目名称在我的项目下就是tfs_django,根据自己的项目名称进行调整,gunicorn.conf.py如果不和manage.py在同目录下的话需要设置路径,其他都是默认的参数不需要调整。

如果只是需要进行单个timeout参数调整的话,应该也可以直接在通过命令行启动的时候设置

nohup gunicorn -b :$PORT 项目名称.wsgi:application --timeout 5000

参考链接:
Gunicorn+Django+Ngin设置
Gunicorn官方文档

6.2. Nginx 504超时错误

通过docker-compose exec nginx bash进入nginx容器,使用vim /etc/nginx/nginx.conf打开nginx配置文件,如下图所示,可以看到已经在最后自动添加了一个/etc/nginx/conf.d/*.conf文件。

之前我们已经在docker-compose.yml将主机的目录./config/nginx映射到了/etc/nginx/conf.d中,所以我们只需要修改主机目录下~/guide01/config/nginx/django.conf文件内参数即可更新容器内的nginx。
在这里插入图片描述

最后一段include /etc/nginx/conf.d/*.conf用于将conf.d目录下的所有配置文件都包含在里面,找了下,只需要在配置文件内添加uwsgi_read_timeout 600s;就可以解决该问题。为了避免其他问题,我还延长了响应时间。参数的用处如下:

proxy_connect_timeout: 后端服务器连接的超时时间。发起握手等候响应超时时间
proxy_read_timeout: 连接成功后,等候后端服务器响应时间。其实已经进入后端的排队之中等候处理(也可以说是后端服务器处理请求的时间)
proxy_send_timeout: 后端服务器数据回传时间,就是在规定时间之内后端服务器必须传完所有的数据
uwsgi_read_timeout: 指定接收uWSGI应答的超时时间,完成握手后接收uWSGI应答的超时时间。

在这里插入图片描述
https://serverfault.com/questions/897424/cant-change-nginx-timeout

6.3 can’t open file ‘manage.py’: [Errno 2] No such file or directory

看一下manage.py的位置,如果不在子目录下的话就会出错

6.4 No module named XXX

增加其他自定义python包的时候出现该错误,服务器端由于不像pycharm可以自动配置,因此需要在使用的python文件内添加sys.path.append(path.abspath('包的相对路径'))来说明需要的包所在的位置。添加的包需要和manage.py在同一个目录下

6.5 django.core.exceptions.ImproperlyConfigured: You’re using the staticfiles app without having set the STATIC_ROOT setting to a filesystem path

https://stackoverflow.com/questions/23215581/unable-to-perform-collectstatic

6.6 django一直处于restarting状态

停止容器运行:
docker-compose stop
重启容器:
docker-compose start
或者直接重启
docker-compose restart(没试过可不可以取消restarting状态)

6.7 Error: CSRF verification failed. Request aborted.

在这里插入图片描述

https://stackoverflow.com/questions/10388033/csrf-verification-failed-request-aborted
在post的form里添加{% csrf_token %}
例如:

<form action="/steps_count/" method="post">
    {% csrf_token %} <----------------添加到这里
    Name: <input type="text" name="Name" /><br />
    Steps: <input type="text" name="Steps" /><br />
   <input type="submit" value="Add" />
 </form>

如果还是无法解决,在settings.py文件末尾添加如下两条命令看看是否可行:

# CSRF set-up
CSRF_COOKIE_DOMAIN = None
CSRF_COOKIE_SECURE = False

6.8 Error: render_to_response() got an unexpected keyword argument ‘context_instance’

https://blog.csdn.net/PlusChang/article/details/78306667

6.9 Error: ModuleNotFoundError at /data


7. 可能用到的容器使用命令

7.1 查看容器日志的方法:

docker-compose logs the_name_of_your_service(“web” in this case)
或者仅查看最后50条输出:

docker-compose logs -t --tail 50 web

Usage: logs [options] [SERVICE…]

Options:
–no-color Produce monochrome output.
-f, --follow Follow log output.
-t, --timestamps Show timestamps.
–tail=“all” Number of lines to show from the end of the logs
for each container.

参考链接:https://stackoverflow.com/questions/53783720/an-easy-way-to-figure-out-why-a-docker-container-keep-restarting

7.2 编译并创建容器

docker-compose up --build -d

7.3 进入容器

docker-compose exec the_name_of_your_service bash

7.4 查看所有容器运行状况:

docker-compose ps


8. 其他参考资料:

https://blog.csdn.net/JerryZhang__/article/details/85238069

Docker-compose內的port与expose差异

nginx配置参数详解

https://stackoverflow.com/questions/51141248/python-program-wont-run-psycopg2-rename-warning

https://stackoverflow.com/questions/16956810/how-do-i-find-all-files-containing-specific-text-on-linux

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值