django 服务部署教程
docker 部署
# 基于 Debian9
FROM python:3.12
LABEL authors="熊大"
USER root
ENV PYTHONUNBUFFERED 1
# 标记为生产环境
ENV PRODUCTION 1
# 启动端口
ENV PROJECT_PORT 19028
RUN mkdir -p /root/project/
WORKDIR /root/project/
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/
# RUN pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple
RUN pip install gunicorn gevent
ADD requirements.txt /root/project/
RUN pip install -r requirements.txt
ADD . /root/project/
EXPOSE ${PROJECT_PORT}
#CMD python manage.py runserver 0:${PROJECT_PORT}
CMD gunicorn --worker-connections=1000 --workers=$((`grep processor /proc/cpuinfo | wc -l`*2+1)) --worker-tmp-dir /dev/shm -b 0.0.0.0:${PROJECT_PORT} --access-logfile - conf.wsgi
Gunicorn 部署
本套部署方案采用了:
- Gunicorn来运行wsgi应用,采用unix domain socket服务模式
- gevent作为worker,采用epoll监听模型
- Supervisor作为守护进程管理程序
- nginx作为反向代理web服务器
Gunicorn对静态文件的支持不太好,所以生产环境下常用Nginx作为反向代理服务器
Gunicorn概述
Gunicorn是pre-fork worker model
架构
- worker model:意味着这个模型有一个master进程,来管理一组worker进程
- fork:意味着worker进程是由master进程fork(复刻)出来的
- pre-:意味着在任何客户端请求到来之前,就已从master进程fork出了多个worker进程,坐等请求到来
- master进程:不管理也不处理http请求,只负责管理worker进程,对worker进程的创建、销毁、以及根据负载情况增减
- worker进程:所有worker共用一组listener(Gunicorn支持绑定多个socket),worker启动时为每个监听器创建一个WSGI Server,实例化了django app,监听端口、接受和解析http请求,调用web app处理,得到处理结果后,再整理成HTTP Response,通过TCP返回给客户端
Gunicorn工作模式
- 默认的work_class是sync(同步阻塞的网络模型),
- 一个worker进程一次只处理一个请求,后面的请求会堵塞,性能不佳
- 一个请求一个进程,并发时是非常消耗CPU和内存的,只能适合在访问量不大、CPU密集而非I/O的情形
- 优点是即使一个worker的进程瘫痪了也只影响到一个请求,不会影响其他的请求
- 如果是CPU密集型的web app,那么并发请求数量就不重要了,重要的是并行请求数量,可以将worker数改为CPU核数,最大并行请求数量就是核心数,这时候适合sync工作模式
- gevent(异步模式,基于Greenlet协程+libev快速事件循环实现的,利用python协程实现)
- gevent最好的地方在于,当你的web app是同步处理请求时,而你又需要赋予它异步的能力,不需要改代码,只需要打个猴子补丁(monkey patch),Gevent就会帮你改造Python标准库和一些第三方库,使具备异步处理请求的能力
- gevent虽然只有一个线程,同时只能处理一个请求,但每个请求的连接是一个Greenlet协程(函数级线程),可以在IO等待时主动yield出控制权而不阻塞其他请求
- gevent的线程会变成基于Greentlet的task伪线程,线程数量配置参数threads无效
- gevent在不同请求间不断切换从而实现并发的方式,很适合于外部IO密集型(访问数据库、访问第三方API)的web app
- 使用该工作模式需安装:
pip install gevent
- eventlet(异步模式,基于Greenlet协程实现的)
- eventlet的线程会变成基于Greentlet的task伪线程,线程数量配置参数threads无效
- 使用该工作模型需安装:
pip install eventlet
- tornado(利用Tornado框架实现)
- 使用该工作模式需安装:
pip install tornado
- 使用该工作模式需安装:
- gthread(采用多线程工作模式)
- gthread是一种全线程worker,worker与线程池保持连接,线程会等待接受请求,一个请求一个线程。可以配置进程数和线程数(threads参数)来控制
- 应用程序会在每个worker上都加载一次,每个worker上的每个线程都会共享一些内存,但回消耗额外的CPU资源,如果不确定app的内容占用,可以使用gthread模式
- 使用该工作模式需安装:
pip install gthread
- gaiohttp(利用aiohttp库实现异步I/O,支持web socket)
Gunicorn使用
- 在项目根目录建立配置文件
gunicorn.conf.py
import multiprocessing
chdir = '/var/www/demo' # 运行前切换工作目录,好处是命令行后面可以写app的相对路径
bind = '0.0.0.0:8000' # 绑定服务的IP和端口
workers = multiprocessing.cpu_count() * 2 + 1 # 进程数量
worker_class = "gevent" # 进程的工作类型,包括sync(默认)、gevent、eventlet、tornado、gthread、gaiohttp
worker_connections = 1000 # 每个worker的协程最大并发请求数,gevent和eventlet的特殊配置,默认值是1000
daemon = 'false' # 是否以守护进程启动,交给supervisor管理
proc_name = 'demo_gunicorn' # 进程名称,默认是gunicorn
errorlog = '/var/log/gunicorn/gunicorn_error.log' # 错误日志文件存放路径
import logging
import logging.handlers
from logging.handlers import WatchedFileHandler
import os
import multiprocessing
bind = '127.0.0.1:8000' #绑定ip和端口号
worker_class = 'gevent' #使用gevent模式,还可以使用sync 模式,默认的是sync模式
workers = multiprocessing.cpu_count() * 2 + 1 # 进程数
threads = 2 # 指定gthread模式下每个worker进程开启的线程数。如果指定该参数,工作模式自动变成gthread
backlog = 2048 # 未结连接的最大数量,即等待服务的客户数量,默认是2048个,一般不修改
max_requests = 5000 # 在重启worker进程前,限制能处理的最大请求数,帮助限制内存泄漏最简单方法。默认为0,代表禁用worker自动重启功能。
timeout = 30 # 在同步模式下,worker处理单个请求超时没有响应将被杀死重启,默认30秒。异步worker则不收此参数影响
keepalive = 2 # 在keep-alive连接上等待请求的秒数,默认2秒超时,一般设置1-5秒。
limit_request_line = 4094 # 限制HTTP请求行的最大大小,默认为4094,最大可设置为8190。此参数可以防止任何DDOS攻击
limit_request_fields = 101 # 限制HTTP请求头字段数量,默认为100,最大可设置为32768,此参数可以防止DDOS攻击
limit_request_field_size = 8190 # 限制HTTP请求头大小,默认为8190,当为0时代表不限制
# debug = True # 当代码变动时会自动重启
# reload= True # 当代码变动时会自动重启,用于开发环境
pythonpath='/home/your_path/venv/bin/python3' # 设置python虚拟环境
raw_env = 'APE_API_ENV=DEV' # 设置环境变量
chdir = '/var/www/demo' # 加载应用程序前,切换工作目录
pidfile = '/run/gunicorn.pid' # pid文件存放路径
loglevel = 'info' # 错误日志级别(访问日志的级别无法设置)
access_log_format = '%(t)s %(p)s %(h)s "%(r)s" %(s)s %(L)s %(b)s %(f)s" "%(a)s"' # 设置访问日志格式,错误日志无法设置
# access_log_format = '%({X-Real-IP}i)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
accesslog = "/var/log/gunicorn/gunicorn_access.log" # 访问日志文件路径
errorlog = "/var/log/gunicorn/log/gunicorn_error.log" # 错误日志文件路径
官方配置说明:进入官方文档
- 运行Gunicorn服务
gunicorn -c gunicorn.conf.py project_name.wsgi:application
提示
上述命令后面的wsgi.py
文件路径,请在项目中查找并与之对应
application
是wsgi.py
文件的一个变量,变量里面装的是web应用
app变量名称如果是application也可以不写到命令,命令选项缺省情况下自动查找该变量
Supervisor 守护进程管理器
概述
Supervisor组成部分:
- /etc/supervisord.conf:服务配置文件
- supervisord: 服务守护进程
- supervisorctl:命令行客户端
- Web Server:Web管理客户端界面,默认监听在9091上
- XML-RPC Interface:XML-RPC接口
安装:
apt install supervisor
systemctl enable supervisord
systemctl start supervisord
配置文件选项
编辑项目内的文件:project_name/devops/supervisor_gunicorn.conf
[include]
files=/etc/supervisord.conf # 包含全局配置文件
[program:gunicorn_app] # 应用程序进程名称
directory=/var/www/project_name # 应用根目录
command=gunicorn -c /var/www/project_name/devops/gunicorn.conf.py project_name.wsgi:application # 应用启动命令
编辑配置文件: /etc/supervisor/supervisord.conf
[unix_http_server]
file=/tmp/supervisor.sock # (选填)socket文件的路径,用XML_RPC通信就是通过它进行,默认为none。
chmod=0700 # (选填)就是修改上面的socket文件权限,默认为0700
chown=user:group # (选填)修改上面的socket文件的属组为user.group,默认为启动supervisord进程的用户及属组
username=user # (选填)使用supervisorctl连接的时候认证的用户,默认为不需要用户
password=123 # (选填)和上面的用户名对应的密码,可以直接使用明码,也可以使用SHA加密,例:{SHA}82ab876d1387bfafe46cc1c8a2ef074eae50cb1d
# -------------------------------------------------------------------------
[inet_http_server] # (选填)侦听在TCP上的socket,Web Server和远程的supervisorctl都要用到他,默认为不开启
port=127.0.0.1:9001 # (可填)这个是侦听的IP和端口,侦听所有IP用 :9001或*:9001。只要上面的[inet_http_server]开启了,就必须设置它
username=user # (选填)同上
password=123 # (选填)同上
# -------------------------------------------------------------------------
[supervisord] # (必填)supervisord服务端进程参数配置
logfile=/tmp/supervisord.log # (选填)supervisord主进程的日志路径(注意与子进程日志无关),默认路径$CWD/supervisord.log,$CWD是当前目录
logfile_maxbytes=50MB # (选填)上面日志文件最大的大小,默认超过50M的时自动生成新的日志文件。当设置为0表示不限制文件大小
logfile_backups=10 # (选填)日志文件保持的数量。默认为文件数量大于10时,最初的老文件被新文件覆盖,当设置为0表示不限制文件的数量。
loglevel=info # (选填)日志级别有critical, error, warn, info, debug, trace, or blather等,默认为info
pidfile=/tmp/supervisord.pid # (选填)supervisord的pid文件路径。 默认为$CWD/supervisord.pid
nodaemon=false # (选填)如果是true,supervisord进程将在前台运行。默认为false代表后台以守护进程运行
minfds=1024 # (选填)这个是最少系统空闲的文件描述符(/proc/sys/fs/file-max),低于这个值supervisor将不会启动。默认为1024
minprocs=200 # (选填)最小可用的进程描述符(ulimit -u),低于这个值supervisor也将不会正常启动。默认为200
umask=022 # (选填)进程创建文件的掩码,默认022
user=chrism # (选填)设置一个非root用户,当我们以root用户启动supervisord之后。这里设置的用户,也可以对supervisord进行管理,默认情况是不设置
identifier=supervisor # (选填)supervisord的标识符,主要是给XML_RPC使用。当你有多个supervisor的时候,而且想调用XML_RPC统一管理,就需要为每个supervisor设置不同的标识符了,默认是supervisord
directory=/tmp # (选填)启动supervisord作为守护进程运行前,会先切换到这个目录,默认不设置
nocleanup=true # (选填)当为false的时候,启动supervisord时清除子进程以前的日志文件(路径为AUTO的情况下)清除掉。如果想看历史日志请设置为true, 默认是false
childlogdir=/tmp # (选填)当子进程日志路径为AUTO的时候,子进程日志文件的存放路径。默认路径为 python -c "import tempfile;print tempfile.gettempdir()"
environment=KEY="value" # (选填)设置额外环境变量的,默认继承了linux的环境变量,注意父进程和启动额子进程都会有。 例:environment=name="haha",age="hehe"
strip_ansi=false # (选填)这个选项如果设置为true,会清除子进程日志中的所有ANSI序列(\n,\t等),默认为false
# -------------------------------------------------------------------------
[rpcinterface:supervisor] #(可填)这个选项是给XML_RPC使用的,当然你如果想使用supervisord或者web server这个选项必须要开启的
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
# -------------------------------------------------------------------------
[supervisorctl] # 这个主要是针对supervisorctl命令行的一些配置
serverurl=unix:///tmp/supervisor.sock # (选填)supervisorctl命令连接本地supervisord的时候的UNIX socket路径,注意这个是和前面的[unix_http_server]对应的,默认值就是unix:///tmp/supervisor.sock
serverurl=http://127.0.0.1:9001 # (选填)这个是supervisorctl远程连接supervisord的时候用到的TCP socket路径,注意这个和前面的[inet_http_server]对应,默认就是http://127.0.0.1:9001
username=chris # (选填)用户名,默认为空
password=123 # (选填)密码,默认为空
prompt=mysupervisor # (选填)输入用户名密码时候的提示符,默认supervisor
history_file=~/.sc_history # (选填)shell中的history类似,我们可以用上下键来查找前面执行过的命令,默认不保持命令历史,想用查找历史命令为必填
# -------------------------------------------------------------------------
[program:theprogramname] # 要管理的子进程了的名字,建议命名和实际进程有点关联。可配置多个,一个program就是一个要被管理的子进程
command=/bin/cat # (必填)子进程启动命令,注意仅能管理在终端运行的进程,不可是守护进程
process_name=%(program_name)s # 这个是进程名,当numprocs=1时,无需填写,默认值为%(program_name)s。当numprocs大于1时则需要填写,不可能每个进程都用同一个进程名。
numprocs=1 # (选填)启动进程的数目。当不为1时,就是进程池的概念,注意process_name的设置。默认为1
directory=/tmp # (选填)进程运行前,会前切换到这个目录,默认不设置
umask=022 # (选填)进程掩码,默认none
priority=999 # (选填)子进程启动关闭优先级,优先级越低越优先(最先启动,最后关闭),默认为999
autostart=true # (选填)子进程将在supervisord启动后被自动启动,默认时true
autorestart=unexpected # 子进程挂掉后自动重启的情况,false为不重启,unexpected代表只有当进程的退出码不在下面的exitcodes里面定义的时才会被自动重启。true代表无条件重启,默认值为unexpected
startsecs=1 # (选填)这个选项是子进程启动后坚持多少秒的running状态则认为启动成功,默认为1
startretries=3 # (选填)当进程启动失败后,最大尝试启动的次数。默认值为当超过3次后,supervisor将把此进程的状态置为FAIL
exitcodes=0,2 # 注意和上面的的autorestart=unexpected对应,exitcodes里面的定义的退出码是expected的。
stopsignal=QUIT # (选填)进程停止信号,可以为TERM, HUP, INT, QUIT, KILL, USR1, or USR2等信号,默认为TERM。当用设定的信号去干掉进程,退出码会被认为是expected
stopwaitsecs=10 # (选填)当我们向子进程发送stopsignal信号后,到系统返回信息给supervisord,所等待的最大时间。超过这个时间,supervisord会向该子进程发送一个强制kill的信号,默认为10秒
stopasgroup=false # (选填)如果supervisord管理的子进程,这个子进程本身还有子进程。那么我们如果仅仅干掉supervisord的子进程的话,子进程的子进程有可能会变成孤儿进程。配置该选项可以干掉该子进程的整个进程组。 设置为true的话,一般killasgroup也会被设置为true。 需要注意的是,该选项发送的是stop信号,默认为false
killasgroup=false # 这个和上面的stopasgroup类似,不过发送的是kill信号
user=chrism # 如果supervisord是root启动,这里设置非root用户可以用来管理该program,默认不设置
redirect_stderr=true # (选填)如果为true,则stderr的日志会被写入stdout日志文件中,默认为false
stdout_logfile=/a/path # 子进程的stdout的日志路径,可以指定路径,AUTO,none等三个选项。 设置为none的话将没有日志产生。设置为AUTO的话,将随机找一个地方生成日志文件,而且当supervisord重新启动的时候,以前的日志文件会被清空。当redirect_stderr=true的时候,sterr也会写进这个日志文件
stdout_logfile_maxbytes=1MB # 日志文件最大大小,和[supervisord]中定义的一样。默认为50
stdout_logfile_backups=10 # 和[supervisord]定义的一样。默认10
stdout_capture_maxbytes=1MB # (选填)这个东西是设定capture管道的大小,当值不为0的时候,子进程可以从stdout发送信息,而supervisor可以根据信息,发送相应的event。 默认为0表示关闭管道
stdout_events_enabled=false # (选填)当设置为ture的时候,当子进程由stdout向文件描述符中写日志的时候,将触发supervisord发送PROCESS_LOG_STDOUT类型的event,默认为false
stderr_logfile=/a/path # (选填)这个东西是设置stderr写的日志路径,当redirect_stderr=true。这个就不用设置了,设置了也是白搭。因为它会被写入stdout_logfile的同一个文件中。默认为AUTO,也就是随便找个地存,supervisord重启被清空
stderr_logfile_maxbytes=1MB # 同上
stderr_logfile_backups=10 # 同上
stderr_capture_maxbytes=1MB # 同上,和stdout_capture一样。 默认为0表示关闭状态
stderr_events_enabled=false # 同上,默认为false
environment=A="1",B="2" # 这个是该子进程的环境变量,和别的子进程是不共享的
serverurl=AUTO #
# -------------------------------------------------------------------------
[eventlistener:theeventlistenername] # 这个东西其实和program的地位是一样的,也是suopervisor启动的子进程,不过它干的活是订阅supervisord发送的event。他的名字就叫listener了。我们可以在listener里面做一系列处理,比如报警等等
command=/bin/eventlistener # 同上,表示listener的可执行文件的路径
process_name=%(program_name)s # 同上,进程名,当下面的numprocs为多个的时候才需要。否则就按默认为0
numprocs=1 # 相同的listener启动的个数
events=EVENT # event事件的类型,也就是说,只有写在这个地方的事件类型。才会被发送
buffer_size=10 # 这个是event队列缓存大小。当buffer超过10的时候,最旧的event将会被清除,并把新的event放进去。默认值为10
directory=/tmp # 进程执行前,会切换到这个目录下执行,默认为不切换
umask=022 # 掩码,默认为none
priority=-1 # 启动优先级,默认-1
autostart=true # 是否随supervisord启动一起启动,默认true
autorestart=unexpected # 是否自动重启,和program一个样,分true,false,unexpected等,注意unexpected和exitcodes的关系
startsecs=1 # 也是一样,进程启动后跑了几秒钟,才被认定为成功启动,默认1
startretries=3 # 失败最大尝试次数,默认3
exitcodes=0,2 # 期望或者说预料中的进程退出码,
stopsignal=QUIT # 干掉进程的信号,默认为TERM,比如设置为QUIT,那么如果QUIT来干这个进程,那么会被认为是正常维护,退出码也被认为是expected中的
stopwaitsecs=10 # max num secs to wait b4 SIGKILL (default 10)
stopasgroup=false # send stop signal to the UNIX process group (default false)
killasgroup=false # SIGKILL the UNIX process group (def false)
user=chrism #设置普通用户,可以用来管理该listener进程。 默认为空
redirect_stderr=true # 为true的话,stderr的log会并入stdout的log里面,默认为false
stdout_logfile=/a/path # 同上
stdout_logfile_maxbytes=1MB # 同上
stdout_logfile_backups=10 # 同上
stdout_events_enabled=false # 同上
stderr_logfile=/a/path # 同上
stderr_logfile_maxbytes=1MB # 同上
stderr_logfile_backups # 同上
stderr_events_enabled=false # 这个是错的,listener不能发送event
environment=A="1",B="2" # 这个是该子进程的环境变量,默认为空
serverurl=AUTO # override serverurl computation (childutils)
# -------------------------------------------------------------------------
[group:thegroupname] # 给programs分组,划分到组里面的program。我们就不用一个一个去操作了,我们可以对组名进行统一的操作。 注意:program被划分到组里面之后,就相当于原来的配置从supervisor的配置文件里消失了。supervisor只会对组进行管理,而不再会对组里面的单个program进行管理了
programs=progname1,progname2 # 组成员,用逗号分开,这个是必填项
priority=999 # 优先级,相对于组和组之间说的,默认999
# -------------------------------------------------------------------------
[include] # 当我们要管理的进程很多的时候可以分开写,然后在这里导入配置
files = relative/directory/*.ini
官方配置说明:点击进入
supervisorctl命令
# 启动supervisord
supervisord -c project_name/devops/supervisor_gunicorn.conf
# 关闭supervisord
supervisorctl shutdown
# 关闭supervisord
systemctl stop supervisord
supervisorctl reload # 重启服务并重新加载配置文件,更新配置后执行可生效(命令好用)
supervisorctl update # 重新加载配置文件,更新配置后执行可生效
# 应用服务管理
supervisorctl start/stop/restart [program_name]/all # 注意这里不会重新加载配置文件
supervisorctl status # 查看所子进程状态
如果没有配置-c
选项,则会按照以下指定顺序查找supervisord.conf
文件,并将使用它找到的第一个文件。
- ../etc/supervisord.conf(相对于可执行文件)
- ../supervisord.conf(相对于可执行文件)
- $CWD/supervisord.conf
- $CWD/etc/supervisord.conf
- /etc/supervisord.conf
- /etc/supervisor/supervisord.conf(从 Supervisor 3.3.0 开始)
报错指南
BACKOFF Exited too quickly (process log may have details)
Supervisor只能管理前台应用程序,如果应用程序是通过fork方式实现的daemon服务, 像Apache
Tomcat
Nginx
服务默认按daemon方式启动(systemctl start nginx),则不能被管理。
Permission权限问题
有时候提示往/tmp文件夹写入文件提示PermissionErr,检查supervisor配置文件,可以设置为user=root。
exit status 127; not expected
127表示命令没找到,首先可能是directory写错,用cd命令看一下,然后可能是command命令写错,复制命令在控制台试试是否gunicorn无法使用。
WSGI配置
>>> vim wsgi.py
import os
import sys
from django.core.wsgi import get_wsgi_application
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
#os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
os.environ["DJANGO_SETTINGS_MODULE"] = "project.settings"
application = get_wsgi_application()
Nginx web服务器配置
反向代理配置
排坑指南
上传文件时,中文名报错
解决 修改http服务器配置,使用UTF-8字节码:vim /etc/sysconfig/httpd
,LANG=en_US.UTF-8
gevent模式与multiprocess多进程库兼容问题
使用gevent模式时,系统会使用monkey patch,系统部分函数会被修改,有些库会兼容gevent的类型, 例如,任务调度的库apscheduler,web socket需要socketio的库等,需要专门选择gevent的函数。 而有些库则直接无法使用,例如多进程multiprocess。 例如,在一个api请求中,如果需要使用多核cpu资源,采用multiprocess进行多进程计算。则会出现卡死的问题。gevent中,不能使用multiprocess库。