Python API工程:REST API、WSGI部署、环境与代码管理

Gunicorn

关键词: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.pyapp指的是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的端口设置
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335