关键词:REST API | Flask | Gunicorn | WSGI | Conda | Git | 运维
本文玩具项目的代码地址。
前言
之前已经写过一篇文章,介绍如何用Flask开发Python项目的REST API,最近在另一个数据分析的纯后端项目中了解了更多工程方面的知识,本文将沿用基于前文使用的玩具项目进行总结,比较适用于纯后端的中小型Python API项目。
本文内容分为三个方面:REST API、WSGI服务器部署、环境与代码管理。因为同时作为自己的备忘,所以比较trivial的细节也写了不少。本文不涉及的内容:持续集成、自动部署、前端代理相关(比如使用Nginx)、性能优化、大规模并行计算。
玩具项目的实验环境为ubuntu 16.04 LTS,这里先把最终的目录结构印出来:
toy_project/
├── logs
│ └── .placeholder
├── requirements.txt
├── restart.sh
└── toy
├── gunicorn_conf.py
├── logic_a.py
└── toyapi.py
REST API
基于前文,在API开发方面增加几点细节上的补充。
返回Response
之前的toy.py
里有三个API Route(路由),return的都是string,而在真正的工程中,返回的一般是标准的HTTP Response(响应),所以需要把JSON对象进一步包成Response:
data = {"word_str": word_str, "word_num": word_num}
js = json.dumps(data)
response = Response(js, status=200, mimetype='application/json') # 包为Response
return response
这里status
是状态码,一般约定200是OK,500是Error等;mimetype告诉client(客户端)返回文件的类型是JSON。
路由设置
注意路由设置时,末尾/
的有无将影响client的访问。也就是说/cut/json/
和/cut/json
会被视为两个不同的地址。经过调查,选用后一种无/
方式的人较多。
测试API
之前的测试方法是在toy.py
里写入一个POST方法,当我们访问5001(Client)的/test/post/
时,它会POST出一个HTTP请求到port 5000(Server)的/cut/json/
,并得到port 5000返回的结果。修改并运行代码未免繁琐,所以建议使用图形化的REST API Client发送请求来测试。
这样的免费Client其实很多,这里推荐两个:
- (收回推荐)
Advanced REST client:基于Chrome的可离线应用,设计易用,支持请求的保存与备份,只不过需要翻墙到Chrome Store(更新:现在有原生应用了,基于Chrome的应用之后会被扔掉) - Postman:不需要翻墙,设计也不错,但需要注册,功能看起来比前者更强大,支持OAuth,支持请求列表的同步、共享和导出
WSGI部署
在项目的功能及API开发完成以后,我们要将整个应用运行起来,这时就需要了解服务器部署的部分。这里涉及到一个概念:WSGI(Web Server Gateway Interface),这个通用接口用于连接基于网络框架(如Flask)的Python应用和服务器程序(如Apache、Twisted、Gunicorn)。
Flask内置服务器
之前的文章例子里使用了Flask内置的server进行部署:
export FLASK_APP=toyapi.py # 即前文中的`toy.py`,其中包含了Flask的应用实例`app`
flask run --port=5000
然而,Flask的内置server其实并不适用于正式的生产环境,存在性能低下、无法进行更复杂的配置等问题,因此最好将Flask应用部署到专门的服务器程序上。
Gunicorn
我在实际工作时选用了Gunicorn,主要原因是配置和运行都非常方便。
1.配置
我们使用配置文件的形式对Gunicorn进行配置。新建gunicorn_conf.py
作为Gunicorn的配置文件:
workers = 5 # 可以理解为进程数,会自动分配到你机器上的多CPU,完成简单并行化
worker_class = 'eventlet' # worker的类型,如何选择见:http://docs.gunicorn.org/en/stable/design.html#choosing-a-worker-type (后续实践发现eventlet有一定概率存在兼容问题,如发现Gunicorn无法启动,可以先注释掉)
bind = '0.0.0.0:5000' # 服务使用的端口
pidfile = '../gunicorn.pid' # 存放Gunicorn进程pid的位置,便于跟踪
accesslog = '../logs/gunicorn.log' # 存放访问日志的位置,注意首先需要存在logs文件夹,Gunicorn才可自动创建log文件
errorlog = '../logs/gunicorn.log' # 存放错误日志的位置,可与访问日志相同
reload = False # 如果应用的代码有变动,work将会自动重启,适用于开发阶段
daemon = True # 是否后台运行
timeout = 5 # server端的请求超时秒数
注意:如果设置了Gunicorn的日志存放路径,如上面的../logs
,那么在运行项目前必须确保存在该路径,否则项目无法正常运行,并且因为没有日志,也无法获得任何错误信息。
某些其他情况下Gunicorn也会出现运行异常但没有报错的现象,这时排查的第一步可以是测试Flask内置服务器是否能正常跑起来、不报错,如果是那么基本可以肯定Gunicorn的配置有问题。
2.运行
gunicorn -c gunicorn_conf.py toyapi:app # 详见:http://docs.gunicorn.org/en/stable/run.html
与Flask内置服务器的部署类似,Gunicorn须知道要运行的Flask应用的位置,以上命令中的toyapi
指的其实是toyapi.py
,app
指的是toyapi.py
中创建的Flask实例app
。
这里补充一点额外知识,假如我们将整个项目组织成Package,那么$MODULE_NAME
可以不是toyapi
这样的文件名,而是Package的名字,但是相应的你必须把创建app
的那个Python文件改名为__init__.py
,这样Gunicorn才能在Package中找到app
。之后会在新文章中介绍。
3.重启
因为每个Gunicorn worker都会开启一个进程,所以每次重启前都要逐一手动杀掉,未免繁琐。我们可以写一个简单的脚本restart.sh
来自动化重启过程:
# 用于寻找进程的关键字
PROCESS=toyapi:app
# 通过"toy:app"找到并杀掉之前的进程
for pid in $(ps aux | grep $PROCESS | awk '{print $2}')
do
if [ "$pid" != "" ]
then
echo "killing $pid"
kill -9 $pid
fi
done
# 启动新进程
gunicorn -c gunicorn_conf.py toyapi:app
运行脚本:
./restart.sh
或者
bash restart.sh
环境与代码管理
对环境/依赖与项目代码的良好管理也可以简化部署工作。
环境/依赖管理 - Conda
(更新)经过一段时间的实践,发现Conda更适合在开发阶段使用,强大但比较占空间,使用docker做部署时也会生成很大的image,因此生产环境使用pip进行环境管理是更好的选择。这些新发现和改进方法之后也会在新的文章中详细介绍。
使用Conda(Anaconda自带)建立环境虚拟机,以确保测试、生产等环境的一致性。
1.创建requirements.txt
name: toyenv # 虚拟机的名字
dependencies: # 依赖
- Python=3.5
- Flask=0.12
- requests=2.13.0
- gcc # 用于eventlet的build
- pip: # 无法通过`conda install`安装的部分,使用Conda内部的pip进行安装
- eventlet==0.20.1
- gunicorn==19.1.0
- jieba==0.38
注意:在有Conda的环境下需要小心使用pip install
安装依赖,少数情况下两个包管理系统会有冲突,可能导致整个环境坏掉。一般来说,在conda找不到相应库时,再用pip是安全的(比如eventlet)。
2.创建虚拟机
conda env create --file requirements.txt
3.激活虚拟机环境
source activate toyenv
激活命令应放入前述restart.sh
最前面,确保启动时环境一致。
4.改动虚拟机
更新:
conda env update --file requirements.txt # 更新
重建:
rm -rf CONDA_PATH/conda/envs/toyenv # 移除Conda中的虚拟机
conda env create --file requirements.txt # 重新创建
代码管理 - Git
- 利用Git的branch和merge request控制开发过程
- 利用Git的pull功能将项目的release版本整个拉到生产环境
- 利用
.gitignore
忽略测试时留下的log文件 - 为保持目录结构的一致性,像是
logs
这样的空文件夹,可以通过在其中置入空隐藏文件(.[filename]
,如.placeholder
),并add
到Git,以实现Git对空文件夹的跟踪 - 如果环境使用了Docker,可以通过路径映射同步容器的内外(从image创建container的时候做),这样只需要在外部设置好Git就可以了,注意一下container、Gunicorn的端口设置