supervisord+nginx部署fastapi应用

supervisord+nginx部署fastapi应用

本地测试与线上生产环境部署的区别

通常在写一个fastapi的应用时,我们会直接使用uvicorn来运行,uvicorn可以很方便的开启一个本地的服务,并会在我们代码更新的时候重载,可以说是很方便了:

1
uvicorn main:app --host=0.0.0.0 --port=8000 --reload

其中main是我们项目的入口代码文件的名称,我的项目中这个文件叫做main.py,里面是fastapi的应用app,所以写成了main:app。

但是uvicorn用于线上部署会有一些缺点:

  • 线上服务器如果部署了多个网站,使用多个域名来匹配,肯定会需要使用nginx之类的软件
  • uvicorn可以让应用跑起来,但是应用如果因为一些原因挂了,或者出现了异常,需要有一个daemon进程来监控,uvicorn是不能胜任的
  • 我们希望线上部署的时候,应用是在后台运行,不需要在控制台输出

因此,在线上部署的时候,我们往往会使用其他的软件来做这件事情。

生产环境部署的步骤

先说一句,我使用的系统是ubuntu20.04,其他linux系统可能会有所出入

部署nginx

在我的服务器上,有多个网站在运行,所以我使用了nginx来做所有网站的访问入口:

Nginx(“engine x”)是一款是由俄罗斯的程序设计师Igor Sysoev所开发高性能的 Web和 反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。

nginx的安装我一直都是使用lnmp工具来进行的,具体安装过程就不赘述了,lnmp网站链接:https://lnmp.org/install.html

nginx监听了80和443端口,然后根据请求的具体domain,来将请求进一步转发到内部程序。

也就是说,fastapi应用其实监听的端口是其他端口,比如8000,nginx监听80和443端口,再将请求转发到8000端口。

nginx需要添加一个反代fastapi应用的配置:

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 {
# 监听 80 端口
listen 80;
autoindex on;
server_name 你的域名;
return 301 https://$http_host$request_uri;
access_log /home/wwwlogs/你的域名.log;
}

server {
# 你听的端口号
listen 443 ssl http2;
# 服务器
server_name 你的域名;
access_log /home/wwwlogs/你的域名.log;
charset utf-8;
client_max_body_size 2000M;

# ssl设置
ssl_certificate /usr/local/nginx/conf/ssl/你的域名/fullchain.cer;
ssl_certificate_key /usr/local/nginx/conf/ssl/你的域名/你的域名.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;

location / {
proxy_pass http://127.0.0.1:5082; # fastapi的监听端口
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

其中较为关键的是proxy_pass这一句,这里告诉了nginx要把请求转发到哪一个端口。

proxy_set_header下面的内容也比较关键,这里会让fastapi通过headers知道用户的真实ip地址、用户请求的域名、请求的方法(http还是https),否则在fastapi内部就会看到所有的请求都是来自127.0.0.1,都是http请求,这岂不是很糟糕?

这里还通过nginx为我们的访问添加了一层ssl,将http强制转换成https:

  • return 301 https://$http_host$request_uri; 这一句就是把80端口接收到的http请求转发到443端口的https。
  • listen 443 ssl http2; 这一句一是加上了ssl,二是指定了http版本为http2,http2版本与http1相比有一些改进
  • ssl_certificate和ssl_certificate_key是ssl配置最关键的ssl证书私钥和公钥,我的网站大部分都是使用免费申请的ssl证书,下一篇再来讲如何使用工具申请免费的ssl证书
  • ssl还有很多其他的参数可以配置,这里我也没有太细究,日后再进行学习

部署我们的项目代码

fastapi是一个python应用,python应用通常再python版本、各个应用的包的版本之间都会有区分,通常本地测试的时候用的是哪写版本,生产环境就也用同样的版本,避免版本不一致带来各种潜在的问题。在我的项目中都会有一个requirements.txt文件记录这些信息:

1
2
3
4
5
6
7
8
9
fastapi~=0.77.1
colorlog~=6.6.0
starlette~=0.19.1
sqlalchemy~=1.4.36
oss2~=2.15.0
pydantic~=1.9.1
httpx~=0.22.0
python-multipart==0.0.5
pyyaml~=6.0

线上部署的时候安装这些版本的软件包,如果有其他python项目也在,那么建议新建一个虚拟环境来部署:

1
2
3
4
5
6
7
8
9
10
11
# 在项目文件夹下面建立一个虚拟环境,虚拟环境所处的文件夹名称与虚拟环境名称是一致的
# 这里还要注意,python3也有版本之间的区别,这里用的哪一个版本建立虚拟环境,虚拟环境的python版本就是那个版本,要建立不同的python版本,就要先安装指定版本的python
python3 -m venv 虚拟环境名称
# 方法2,使用conda,也许会更简单?
conda create -n 虚拟环境名称 python=python版本比如3.8

# 激活虚拟环境
source 虚拟环境路径/bin/activate

# 使用虚拟环境下面的python安装指定的安装包
/path/to/python -m pip install -r requirements.txt

部署gunicorn

gunicorn与fastapi的配合还是比较好的,gunicorn也有文档讲有哪些方式来部署:https://docs.gunicorn.org/en/latest/deploy.html

gunicorn文档
1
/path/to/python -m pip install gunicorn

现在我们就可以使用gunicorn来运行我们的项目了:

1
/path/to/gunicorn main:app -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:5082 

这里的-k uvicorn.workers.UvicornWorker是fastapi官方说明中指定的,–bind则告诉gunicorn要监听哪一个端口,其实目前为止gunicorn和uvicorn基本是一样的角色。

当然,为了gunicorn日志等其他配置也都可以管理,不要每次都写这么长的参数,我们建立一个gunicorn.py来把gunicorn的配置放在里面:

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
debug = False
daemon = False
bind = '127.0.0.1:5082' # 绑定ip和端口号
backlog = 512 # 监听队列
timeout = 180 # 超时
# worker_class = 'gevent' #使用gevent模式,还可以使用sync 模式,默认的是sync模式
work_class = 'uvicorn.workers.UvicornWorker'

workers = 2 # 进程数
threads = 2 #指定每个进程开启的线程数
loglevel = 'debug' # 日志级别,这个日志级别指的是错误日志的级别,而访问日志的级别无法设置
access_log_format = '%(t)s %(p)s %(h)s "%(r)s" %(s)s %(L)s %(b)s %(f)s" "%(a)s"' # 设置gunicorn访问日志格式,错误日志无法设置
chdir = '项目目录'

# pidfile = None

"""
其每个选项的含义如下:
h 远程地址
l '-'
u 当前为“-”,或者版本中的用户名
t 请求日期
r 状态栏 (e.g. ``GET / HTTP/1.1``)
s 状态
b 响应长度 或 '-'
f referer
a user agent
T 请求时间(秒)
D 请求时间(微秒)
L 请求时间(十进制秒)
p 进程ID
"""
accesslog = "项目文件夹/log/gunicorn_access.log" # 访问日志文件
errorlog = "项目文件夹/log/gunicorn_error.log" # 错误日志文件

这个配置文件中有几个地方也很重要:

  • debug,生产环境部署我们一般不开启debug模式,防止把代码细节泄露出去
  • bind,指定监听的主机和端口,这里指定127.0.0.1来避免其他机器也可以通过端口号直接访问服务,指定的端口5082一定要和nginx端口一致
  • daemon,我们还要使用supervisor来管理进程的开启,包括实现开机自启等,就不需要gunicorn再来开启一个监控进程了,让supervisor来做这件事情
  • work_class,fastapi官方指定的参数,这是为了让fastapi和gunicorn兼容的做法,一定不要省略
  • workers,按照你服务器的cpu数量来确定,通常是2*cpu数量+1,当然也可以是别的值,我的应用比较小,只是自己用用,2个就足够了,开启太多了也浪费资源
  • pidfile,我们使用supervisor来管理的时候,一定不能指定pidfile,否则supervisor监控会失效!!!不指定就不会创建pid文件
  • chdir,在启动之间切换到项目文件夹,避免找不到main这样的事情发生

有了配置文件,我们开启gunicorn就更简单一些了:

1
/path/to/gunicorn -c /path/to/gunicorn.py main:app 

部署supervisor

这里之所以选择supervisor而不是systemctl主要是因为对systemctl用起来老出错,一会儿报安全错误,一会儿找不到路径之类的,太烦了,后来选择了supervisor,总的还是比较省心的,也比较简单。

Supervisor is a client/server system that allows its users to control a number of processes on UNIX-like operating systems. It was inspired by the following:

Convenience

It is often inconvenient to need to write rc.d scripts for every single process instance. rc.d scripts are a great lowest-common-denominator form of process initialization/autostart/management, but they can be painful to write and maintain. Additionally, rc.d scripts cannot automatically restart a crashed process and many programs do not restart themselves properly on a crash. Supervisord starts processes as its subprocesses, and can be configured to automatically restart them on a crash. It can also automatically be configured to start processes on its own invocation.

supervisor文档

首先安装:

1
apt intall supervisor

虽然官方文档也可以使用pip安装,但是pip安装之后感觉只是一个残次的,supervisor不能开机启动,因此建议不要用pip安装!用pip安装的也将其卸载了吧:

1
pip3 uninstall supervisor

用apt或者yum安装之后,就会出现一个文件夹/etc/supervisor,下面包含这些内容:

1
2
supervisord.conf
conf.d # 一个文件夹

supervisord.conf是supervisord的配置文件,我们需要对这个文件做一下修改,或者直接粘贴这些内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[unix_http_server]
file=/var/run/supervisor.sock ; the path to the socket file

[supervisord]
logfile=日志文件路径 ; main log file; default $CWD/supervisord.log
logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10 ; # of main logfile backups; 0 means none, default 10
loglevel=info ; log level; default info; others: debug,warn,trace
pidfile=/var/run/supervisord.pid ; supervisord pidfile; default supervisord.pid
nodaemon=false ; start in foreground if true; default false
silent=false ; no logs to stdout if true; default false
minfds=1024 ; min. avail startup file descriptors; default 1024
minprocs=200 ; min. avail process descriptors;default 200

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket

[include]
files=conf.d/*.conf

我这里粘贴出来的内容是必须要有的!

其他还有一些配置,可以导出sample看看添加:

1
echo_supervisord_conf > /etc/supervisor/supervisord.sample.conf

然后要告诉supervisor我们的gunicorn程序,因此在conf.d文件夹里面新建一个文件项目名称.conf

加入以下内容:

1
2
3
4
5
6
7
[program:gunicorn]  # 这里的gunicorn就是告诉supervisor我们的应用名称叫做什么
process_name=%(program_name)s
command=/path/to/gunicorn -c /path/to/gunicorn.py main:app
directory=/项目绝对路径
user=root
autostart=true
autorestart=true

其中的路径都推荐使用绝对路径!

现在就可以开启supervisor了:

1
2
3
systemctl enable supervisor.service
systemctl start supervisor.service
systemctl status supervisor.service

不出意外的话,我们的supervisor就已经运行起来了,gunicorn也已经运行起来了

然后可以通过supervisorctl来查看我们的应用状态:

1
supervisorctl status

应该会看到以下内容:

1
gunicorn                         RUNNING   pid 5188, uptime 0:01:13

gunicorn已经在运行了。

supervisorctl还有一些其他用法:

1
2
3
supervisorctl reread && supervisorctl update # 如果修改了配置文件要重新读取并更新
supervisorctl start 应用名称 # 开启我们的应用
supervisorctl stop 应用名称 # 关闭我们的应用

大功告成了

完成了以上工作,fastapi总算是部署完成了,挺费劲吧

在这个过程中其实我一开始摸索的时候有很多地方走了弯路,可以说都是经验教训,比如说:

  • systemctl start supervisor提示找不到service,因为是用了pip安装supervisor
  • supervisor总是提示找不到socket文件(unix:///tmp/supervisor.sock no such file)或者access denied,也是因为用了pip安装supervisor,需要pip卸载后,再apt安装,再重新进入shell环境(这一步也很重要),才能使用
  • supervisorctl start找不到应用:ERROR (no such process) ,是因为我把配置文件放在/etc/supervisord.conf而不是/etc/supervisor/supervisord.conf

还有很多一开始尝试systemctl来让gunicorn开启自启的,配置太麻烦了。

希望我的经验可以给后来人一些启示吧,少走弯路。

还有再网上看到一些人在mian.py中有这样的内容:

1
2
if __name__ == "__main__":
uvicorn.run...

这样也会导致出错(端口占用),把这些内容删了吧。

作者

Haoran

发布于

2022-06-11

更新于

2022-06-11

许可协议

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×