利用Gunicorn和Nginx在服务器上部署一个Flask API
徐徐 抱歉选手

在过去,我们部署一个应用的时候,几乎总是要分布在多台机器的。比如,4台HTTP服务器把动态请求分发到两台Application服务器上,并且它们都访问一个数据库服务器。但是随着机器的能力在增强,而互联网应用的覆盖面从业务逻辑极其复杂的银行业电信业到了送盒饭选泡面的小行业,越来越多的Application服务器和Web服务器合体了(以django圈子举例,有httpd+mod_wsgi或者Nginx+mod_uwsgi)。而且很多时候这种小应用的数据库也host在同一台机器上。

Nginx是什么

Nginx是一个处理用户请求、并返回响应的HTTP服务器。

在网络不稳定的情况下,从客户端发来的请求可以被Nginx缓存,一并发送给Gunicorn。

在存在多个应用服务器(就是具体处理业务、生成相应内容的服务器,比如Django框架、Flask框架)的情况下,Nginx的反向代理功能让用户客户端不必知道他们真正访问的是哪一个服务器。

Gunicorn是什么

Gunicorn is a Python Web Server Gateway Interface(WSGI) HTTP server.

WSGI是什么?它的主要工作是什么?WSGI简介

WSGI是一个规范,定义了Web服务器如何与Python应用程序进行交互,使得使用Python写的Web应用程序可以和Web服务器对接起来。

WSGI的主要目的有两个:

  1. 让Web服务器知道如何调用Python应用程序,并且把用户的请求告诉应用程序。
  2. 让Python应用程序知道用户的具体请求是什么,以及如何返回结果给Web服务器。

Gunicorn不需要直接处理用户客户端发送过来的请求。Gunicorn首先接受来自Nginx的动态请求;Gunicorn调用应用服务器中的逻辑生成相应内容,返回给Nginx;Nginx把数据再返回给用户。

三者关系

借用一个简单又形象的回答

Nginx面向来自外部的请求,并且Nginx持有当前服务器文件系统中的静态资源文件。然而,Nginx不会直接和Python应用程序有数据交互,此二者之间要有一个桥梁,这个桥梁就是Gunicorn。Gunicorn运行Python应用程序,将来自外部的请求交付Python应用程序处理,并返回数据。

那么Gunicorn怎么完成这些任务呢?他会生成一个Unix Scoket,利用WSGI协议把Python应用程序得出的响应数据传递给Nginx。

1
The outside world <-> Nginx <-> The socket <-> Gunicorn

部署步骤

先确定服务器系统上安装了必要的依赖内容,比如合适版本的gcc/g++,最新的动态库,git,Nginx,Python版本管理工具与虚拟环境管理工具(如pyenv、pipenv、virtualenv等自行选择)。

现在本地PyCharm的虚拟环境中完成代码开发,虚拟环境导出必要的库到requirements.txt,使用SFTP将代码部分上传到服务器,在服务器上创建虚拟环境并进入,安装requirements.txt下的依赖。

Gunicorn配置

进入虚拟环境,在项目根目录下,安装gunicorn,pipenv install gunicorn

在整个项目的启动文件,也就是app = Flask(__name__)所在Python文件的同一目录下,创建一个名为gunicorn.conf.py的Gunicorn的配置文件。

注意gunicorn.conf运行会报错,官网文档要求以.py结尾。

The third source of configuration information is an optional configuration file gunicorn.conf.py searched in the current working directory or specified using a command line argument.

vim gunicorn.conf.py配置文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
# 并行工作线程数
workers = 4
# 监听内网端口8080【按需要更改】
bind = '127.0.0.1:8080'
# 设置守护进程【关闭连接时,程序仍在运行】
daemon = True
# 设置超时时间120s,默认为30s。按自己的需求进行设置
timeout = 120
# 设置访问日志和错误信息日志路径,在项目启动文件夹下提前创建一个logs文件夹,否则后续运行可能会报错
accesslog = './logs/acess.log'
errorlog = './logs/error.log'

关于workers配置的血与泪的教训。如果自己的服务子配置不咋地,像我一样1核2G内存,项目里还有预训练模型要调用的,强烈建议不要多线程……不然运行起来卡到ssh连接建立都要一分钟,无语!

要让Gunicorn处理这个Python应用就在项目启动文件所在目录下执行下面的命令。

1
gunicorn -c gunicorn.conf.py  flask_server:app

Gunicorn命令的参数解释:

-c:按照指定的Gunicorn配置文件运行。

flask_server:app:项目的启动文件,项目启动文件中flask app的名字(一般都默认叫app,也可以自己定义)。

当然,配置文件中的参数也可以放在命令行中执行,只是每一次写一长串命令行会很麻烦。如gunicorn -D -w 3 -b 127.0.0.1:8080 main:app中-D 表示后台运行,-w 决定线程个数,-b指定ip和端口。

查看运行的Gunicorn的进程号,并关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 请求gunicorn相关的进程树
pstree -ap|grep gunicorn

# 返回的结果
(myChatbot) [root@VM-4-13-centos api]# pstree -ap|grep gunicorn
|-gunicorn,21545 /root/.local/share/virtualenvs/myChatbot-Bk5svUYU/bin/gunicorn -cgunic
| |-gunicorn,21548 /root/.local/share/virtualenvs/myChatbot-Bk5svUYU/bin/gunicorn -cgunic
| | |-{gunicorn},21578
| | |-{gunicorn},21579
| | |-{gunicorn},21580
| | |-{gunicorn},21589
| | |-{gunicorn},21590
| | |-{gunicorn},21591

kill -9 21545

Nginx配置

上一篇博客中,记录一些Nginx配置相关的内容。/etc/nginx/nginx.conf中对http模块中的server模块进行如下更改。因为我们在服务器上既放了web前端的代码(基于Vanilla JS),以及后端api,所以要有多个location分别给予前端和后端。

经过多次尝试发现以下配置是可行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name 121.4.67.30;


# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;


# 这是前端Web页面的路径,里面就css/js/html各一个,两张图
location / {
root /home/myChatbotWebFrontend/QABotFrontend/;
index index.html index.htm;
}

# 这是后端Flask API的路径,Nginx会把请求转发给Gunicorn
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# Web项目下静态资源的查找
location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js|pdf|txt){
root /home/myChatbotWebFrontend/QABotFrontend/;
}

# 默认的错误页面,可要可不要
error_page 404 /404.html;
location = /404.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}

变化就是把root项给删除了,不然会一直显示Welcome to CentOS的欢迎页面。添加了三个location,分别用于指向Web代码,Python代码,以及静态资源(虽然不知道有没有用,但是不想再删了试了)。

在location项中添加了一个代理配置,将访问server_name121.4.67.30的流量都代理到proxy_pass本机的8080端口,因为我们gunicorn的配置文件中决定了我们的Python应用程序在本机127.0.0.1的8080端口。

做好这些工作后重启nginx即可。systemctl restart nginx

关于Nginx中location和proxy_pass中斜杠的问题

这主要涉及Nginx匹配的路径与云服务器上多个项目存放路径的问题。

首先是location的匹配规则,如下。Nginx中的一个location,可以被prefix string,或者被一个regular expression定义。

为了匹配从用户客户端发来的一个请求地址,nginx首先会校验prefix string,选择最长匹配项目。其次,nginx进行正则匹配。regular expression的匹配是按照它们在配置文件中定义的先后顺序进行的;正则匹配方式一旦匹配到了路径,就不会再继续匹配下去。一边下来发现正则匹配没有匹配的项目,那么nginx就会返回prefix string匹配的项目。查看Nginx-Httpcore官方文档,与文档的中文解读

1
location [=|~|~*|^~] /uri/ { ... }
  • ~*代表使用正则匹配,不区分大小写匹配。
  • ~代表使用正则匹配区分大小写匹配。
  • =代表使用精确匹配,一定情况下会加速匹配。
  • ^~代表如果符号后的字符是最佳匹配,就采用改方式,不再进行查找。

对于 URL 中的尾部 / 则是,当有 / 时表示目录,没有时表示文件。当有 / 是服务器会自动去对应目录下找默认文件,而如果没有/ 则会优先去匹配文件,如果找不到文件才会重定向到目录,查默认文件。

location中uri是否有斜杠与proxy_pass是否有斜杠,决定了能否正确匹配到路径。关于斜杠的提示,来自这篇配置教程

1
2
3
4
5
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

我的理解是,nginx首先会基于server_name 121.4.67.30;,把当前拥有的待匹配对象作为http://121.4.67.30/api/。我从浏览器中输入的请求地址类似于http://121.4.67.30/api/test?ques=你好,nginx会将这个请求uri与带匹配对象做比较,并用proxy_pass中的内容http://127.0.0.1:8080/替换为http://127.0.0.1:8080/test?ques=你好,这样就能顺利访问到本地执行的、由Gunicorn管理的Python Flask API了。

我们希望Nginx把目标uri转变为实际上我们Python运行的flask api:http://127.0.0.1:8080/test?ques=你好。我们不希望本地访问的地址中有/api/。

如果proxy_pass中的内容http://127.0.0.1:8080,且location /api/,那么nginx会保留location中路径的部分,替换成了http://127.0.0.1:8080/api/test?ques=你好

或有location /apihttp://127.0.0.1:8080,替换成http://127.0.0.1:8080/api/test?ques=你好,无法成功搜索。

proxy_pass中的内容http://127.0.0.1:8080被成为proxy_pass withput uri(i.e. without path after server:port)。就是单纯的ip地址和端口的组合。

或者location /api,且proxy_pass http://127.0.0.1:8080/;,那么nginx会使用alias的替换方式对请求的url进行替换,如将请求地址http://121.4.67.30/api/test?ques=你好变为http://127.0.0.1:8080//test?ques=你好

关于Nginx中proxy_pass的斜杠问题,这篇文章中写的很清楚,可以参考。

无法访问或报错

遇到无法访问或者报错的情况,去gunicorn配置的logs目录下看日志!一般终端执行这些命令的时候不会有return value。

比如一开始不知道gunicorn的main:app命令参数代表什么意思,自己和教程乱写,结果怎么也出不了页面,error.log中都记录了下来,才发现是根本没有main模块,别人的main模块在我这边的名称是flask_server。

image-20210329183317198

参考

AI画家第四弹——利用Flask发布风格迁移API

AI画家第五弹——从0到1部署你的RESTful API

falsk中gunicorn的使用

使用Nginx和Gunicorn在服务器上部署Flask项目

  • 本文标题:利用Gunicorn和Nginx在服务器上部署一个Flask API
  • 本文作者:徐徐
  • 创建时间:2021-03-29 15:41:47
  • 本文链接:https://machacroissant.github.io/2021/03/29/gunicorn-nginx-flask-api/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论