前言,由于我是使用这个做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容器的时候指定地址映射的时候再改。这里我取名为project
和config
。
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 1RUN pip install --upgrade pipRUN 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 bashRUN 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.3container_name: postgres01nginx:image: nginx:1.13container_name: nginx01ports:- "8000:8000"volumes:- ./tfs_django:/src- ./config/nginx:/etc/nginx/conf.ddepends_on:- webweb:build: .container_name: django01command: 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:- dbvolumes:- ./tfs_django:/srcexpose:- "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中可以通用的。
我的项目文件路径如下,屏蔽了其他自定义路径和文件,仅作参考:
主要文件解释:
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 afterwardsimport wgetwget.download(wav_url,'/usr/src/wav_download/1.wav') #下载音频文件到本地,仅作测试用# now read in file from local directionimport wavef = wave.open('/usr/src/wav_download/1.wav')params = f.getparams()nchannels, sampwidth, framerate, nframes = params[:4]wav_bytes = f.readframes(nframes) # bytes formatSERVER_URL = 'http://tensorflow_serving_IP:restful_api_ports/v1/models/model_name:predict'HEADERS = {
"content-type": "application/json"}# dictionary pathVOCAB_DIR = 'dictionary_path_pre_defined'# download audio fileprint("Now let's start feature extraction")data_int = np.frombuffer(wav_bytes, np.int16) # convert byte to int for fbank convertfeat_data = feature_extraction_defined_by_usr(framerate, data_int)# change feat to list under the requirement of tensorflow serving requestfeat_list = feat_data.tolist()try:# Compose a JSON predict requestprint("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 = 0num_requests = 1for _ 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 consoleresponse.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 dictionaryp_text = convert_result_from_index_to_wordsprint("p_text is: {}".format(p_text))return p_textexcept: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