Django生产环境部署之uWsgi

今天我们来讲解如何把已经开发完成的Django应用部署到生产环境;生产环境和开发环境有哪些不一样呢?下面我们一起来看一看。其实在发布到生产环境之前我们还有几个事情要做:首先我们要把配置调整为生产环境;其次就是我们要为Django应用选择一个托管环境(比如IaaS/PaaS、阿里云、AWS、Azure等等)、在IaaS的环境中我们不需要自己去启动进程、我们只需要去定义一些配置然后把整个应用推到云上就可以了;当然除了云主机我们还可以把Django应用托管到自建的服务器中。然后就是部署前的安全检查、静态资源管理了;静态资源可以放到WEB服务中、云资源中或者是CDN上面。前面的这些准备工作完成以后我们就可以去部署Django应用了。话不多说、我们先来看看在生产环境中有哪些配置项是必须要调整的关键配置。

关键配置调整

# DEBUG在生产环境中设置为False,避免在WEB页面上显示敏感的调试跟踪和变量信息
DEBUG = False

# SECRET_KEY,用于CRSF保护的随机值
import os
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'xxxxz')

# 注:这里我们可以使用下面的命令随机生成一个SECRET_KEY把默认的KEY替换掉
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
# 注:上面的命令在Windows下无效、在Unix上面有效

# ALLOWED_HOSTS,生产环境必须设置允许访问的域名
ALLOWED_HOSTS = ['127.0.0.1', 'blog.z0ukun.com']

# 钉钉消息相关配置
DINGTALK_WEB_HOOK = 'https://oapi.dingtalk.com/robot/send?access_token=50b961d2***'

注:这里有必要强调一下、之前我们是直接将配置信息放在settings.py配置文件里面;在实际开发过程中我们可以写一个base_settings,把公共部分的配置信息提取到base_settings;然后把不同部分分别写到dev_settings和pro_settings并继承base_settings,启动项目时候通过–settings=settings.pro_settings命令指定dev_settings开发环境或者pro_settings生产环境即可。当然在代码提交的时候、我们是不把pro_settings的配置提交到代码库的。这里的配置内容不限于上面这些、还包括访问数据库的密钥、SECRET_KEY以及阿里云OSS的KEY等等信息;我们可以把这类配置信息放在工程之外的一个单独配置脚本中去引用;也可以放在操作系统的环境变量中、在启动应用的时候从环境变量里面去读取密钥。但是上面两种方案都是以明文的方式放在配置脚本中、最好的办法是我们把密钥存储在KMS(Key Management System)系统中(KMS系统我们可以自建也可以使用KMS云平台)。

现在我们按照上面的要求、在项目目录下面创建一个settings目录、然后再分别创建base_settings.py、dev_settings.py、pro_settings.py三个配置文件、我们把公共信息全部提取到base_settings.py中、然后再分别把开发环境的配置信息和生产环境的配置信息配置到dev_settings.py和pro_settings.py文件中并引入base_settings.py文件、如下:

from .base_settings import *
DEBUG = False
ALLOWED_HOSTS = ["127.0.0.1", "blog.z0ukun.com"]
......

这里因为我们修改了配置文件的路径、我们还需要去修改项目目录下面的wsgi.py、asgi.py和manage.py这三个文件的默认配置信息:

# 原配置信息
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "datasite.settings")

# 修改后的配置信息
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.dev_settings')

如果你用的IDE跟我一样也是PyCharm、那么我们还需要去修改PyCharm中的启动路径:Run–Edit Configurations–Django server:

image-20210128085033055

虚拟环境配置

前面这些工作做完以后我们来准备Django运行所需要的环境。这里我们采用virtualenvwrapper虚拟环境的方式来把不同的Django项目隔离开。为什么需要 virtualenvwrapper?而不是virtualenv?因为 virtualenv 的缺点就是:每次开启虚拟环境之前要去虚拟环境所在目录下的 bin 目录下 source 一下 activate,这就要求我们记住每个虚拟环境所在的目录。一种可行的解决方案是,将所有的虚拟环境目录全都集中起来,比如放到 ~/virtualenvs/,并对不同的虚拟环境使用不同的目录来管理,virtualenvwrapper 正是这样做的。并且它还省去了每次开启虚拟环境时候的 source 操作,使得虚拟环境更加好用。

这里我准备的是一台Centos7.6的虚拟机、我这里开发的时候用的是Python3.6.8,所以我们还需要在Centos7中安装Python3、这一块我之前也写过文章、Pyhton3安装步骤请移步:CentOS7升级Python版本。我们在服务器 /data 目录分别创建Envs(虚拟环境)和EnvsProject(项目目录)两个目录;然后我们把代码上传到EnvsProject项目目录中;

(datasite) [root@localhost data]# pwd
/data
(datasite) [root@localhost data]# ls
Envs  EnvsProject
(datasite) [root@localhost data]# 

# 代码上传之前记得使用下面的命令把环境依赖包导入到requirements.txt文件中
pip freeze > requirements.txt

现在我们来配置虚拟环境管理器:

# 安装基础开发包
yum groupinstall "Development tools" -y
yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel gcc gcc-c++  openssl-devel libffi-devel python-devel mariadb-devel -y

# 安装虚拟环境管理器
yum install python-setuptools python-devel
pip3 install virtualenvwrapper

# 创建快捷方式
ln -s /usr/local/python3/bin/virtualenv /usr/bin/virtualenv

我们按照上面的步骤安装完virtualenvwrapper之后、其实workon命令是不能使用的。我们还需要把virtualenvwrapper配置到系统环境变量中,这里我们用vi命令去 ~/.bashrc 文件中添加如下的配置内容:

# virtualenvwrapper虚拟环境配置
# 指定解释器
export VIRTUALENVWRAPPER_PYTHON=/usr/local/python3/bin/python3
# 这个可以自定义虚拟环境存放目录
export WORKON_HOME=/data/Envs
# 项目存放目录,需要自己创建(可以不设置)
export PROJECT_HOME=/data/EnvsProject
# 自动加载virtualenvmrapper
source /usr/local/python3/bin/virtualenvwrapper.sh

添加完成之后我们用 source ~/.bashrc 命令使环境变量生效、然后我们就可以来创建虚拟环境了、具体如下:

[root@localhost Envs]# mkvirtualenv datasite
created virtual environment CPython3.6.8.final.0-64 in 1145ms
  creator CPython3Posix(dest=/data/Envs/datasite, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/root/.local/share/virtualenv)
    added seed packages: pip==20.3.3, setuptools==51.3.3, wheel==0.36.2
  activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator
virtualenvwrapper.user_scripts creating /data/Envs/datasite/bin/predeactivate
virtualenvwrapper.user_scripts creating /data/Envs/datasite/bin/postdeactivate
virtualenvwrapper.user_scripts creating /data/Envs/datasite/bin/preactivate
virtualenvwrapper.user_scripts creating /data/Envs/datasite/bin/postactivate
virtualenvwrapper.user_scripts creating /data/Envs/datasite/bin/get_env_details
(datasite) [root@localhost Envs]# ls
datasite         initialize    postdeactivate  postmkvirtualenv  preactivate    premkproject     prermvirtualenv
get_env_details  postactivate  postmkproject   postrmvirtualenv  predeactivate  premkvirtualenv
(datasite) [root@localhost ~]# workon
datasite
(datasite) [root@localhost ~]#

我们用 mkvirtualenv 命令创建了一个名为 datasite 的虚拟环境、在 /data/Envs 目录中我们可以看到已经创建的 datasite 虚拟环境以及系统自动生成的virtualenv依赖包;现在我们就可以通过 workon 命令直接进入到虚拟环境下面了:

# 进入 /data/EnvsProject/datasite 项目目录
(datasite) [root@localhost datasite]# pwd
/data/EnvsProject/datasite
(datasite) [root@localhost datasite]# ls
chpa_data  db.sqlite3  manage.py  requirements.txt  static     test data.csv
datasite   logs        reque.txt  settings          templates
(datasite) [root@localhost datasite]#
# 安装项目依赖包
(datasite) [root@localhost datasite]# pip install -r requirements.txt -i https://pypi.douban.com/simple
# 启动Django应用
(datasite) [root@localhost datasite]python manage.py runserver 0.0.0.0:8000

image-20210128094711416

访问主机的8000端口、我们可以看到页面已经可以正常打开;但是现在是DEBUG模式、在正式启动Django之前我们还需要去做安全检查、静态资源收集、uWsgi部署等工作。

注:这里所需要的MySQL、Nginx环境请各位小伙伴自备。

安全检查

前面我们讲到过在部署之前我们还需要进行安全检查、来检查我们的应用是否足够安全。我们可以使用下面的命令进行安全检查:

(datasite) [root@localhost datasite]# python manage.py check --deploy --settings=settings.pro_settings
System check identified some issues:

WARNINGS:
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. If your entire site is served only over SSL, you may want to consider setting a value and enabling HTTP Strict Transport Security. Be sure to read the documentation first; enabling HSTS carelessly can cause serious, irreversible problems.
?: (security.W006) Your SECURE_CONTENT_TYPE_NOSNIFF setting is not set to True, so your pages will not be served with an 'X-Content-Type-Options: nosniff' header. You should consider enabling this header to prevent the browser from identifying content types incorrectly.
?: (security.W007) Your SECURE_BROWSER_XSS_FILTER setting is not set to True, so your pages will not be served with an 'X-XSS-Protection: 1; mode=block' header. You should consider enabling this header to activate the browser's XSS filtering and help prevent XSS attacks.
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. Unless your site should be available over both SSL and non-SSL connections, you may want to either set this setting True or configure a load balancer or reverse-proxy server to redirect all connections to HTTPS.
?: (security.W012) SESSION_COOKIE_SECURE is not set to True. Using a secure-only session cookie makes it more difficult for network traffic sniffers to hijack user sessions.
?: (security.W016) You have 'django.middleware.csrf.CsrfViewMiddleware' in your MIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True. Using a secure-only CSRF cookie makes it more difficult for network traffic sniffers to steal the CSRF token.
?: (security.W019) You have 'django.middleware.clickjacking.XFrameOptionsMiddleware' in your MIDDLEWARE, but X_FRAME_OPTIONS is not set to 'DENY'. The default is 'SAMEORIGIN', but unless there is a good reason for your site to serve other parts of itself in a frame, you should change it to 'DENY'.

System check identified 7 issues (0 silenced).
(datasite) [root@localhost datasite]# 

从上面的安全检查命令我们可以看到、他提示我们需要去配置 SECURE_HSTS_SECONDS 、SECURE_SSL_REDIRECT包括 XSS 跨站攻击等一些安全相关的配置内容;这里我们可以把相关的配置内容写进 pro_settings.py 文件中。

SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_SSL_REDIRECT = True

CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
SESSION_COOKIE_SECURE = True

注:上面我们强制开启了HTTPS、如果不配置SSL证书、Django应用将无法正常打开。

静态资源收集

Django自带了一个collectstatic工具、这个工具会把Django需要用到的内部静态资源都收集起来放到项目工程的static目录下面;这样我们就可以通过Nginx把所有的静态资源开放出去了、这样Python程序就只需要去运行动态资源而无需运行静态资源、从而提升Django应用的访问速度。collectstatic工具在收集静态资源的时候会用到几个路径、如下:STATAIC_URL:能够访问到静态文件的URL路径;STATIC_ROOT:collectstatic工具用来保存收集到的项目引用到的任何静态文件的路径;STATICFILES_DIRS:列出了Django的collectstatic工具应该搜索静态文件的其他目录;收集完成后我们就可以将这些静态文件,上到到托管文件的服务器或CDN 了。如果我们用的是CDN服务器或者是阿里云的OSS来存放静态资源、我们可以做如下配置:

# CDN服务器配置
STATIC_URL='http://icdn.ipopeit.com/static'

# 阿里云OSS配置
STATICFILES_STORAGE = 'django_oss_storage.backends.OssStaticStorage'
DEFAULT_FILE_STORAGE = 'django_oss_storage.backends.OssMediaStorage'

# 完整的配置信息如下
STATIC_URL = '/static/'
STATIC_ROOT= os.path.join(BASE_DIR, "static/")

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = 'media'

配置完成以后我们可以使用下面的命令来收集静态资源:

# 把 static 目录路径配置到pro_settings.py文件中
STATIC_URL = '/static/'
STATIC_ROOT= os.path.join(BASE_DIR, "static/")

# 静态资源收集
python manage.py collectstatic
python manage.py collectstatic --settings=settings.pro_settings

# collectstatic命令执行完后、我们在static目录下面可以看到已经收集好的静态资源
(datasite) [root@localhost static]# ls
admin  import_export  __init__.py  js
(datasite) [root@localhost static]# 

注:当我们运行collectstatic工具命令的时候、会把静态资源文件收集到static目录下面。

Django应用服务器选择

Django+uWsgi+Nginx

官方文档:https://docs.djangoproject.com/zh-hans/3.1/howto/deployment/

Django的应用服务器分为两种、分别是 WSGI(WEB服务器网关接口)和ASGI(异步网关协议接口) ;在讲WSGI和ASGI之前、我们先来看看什么是CGI。CGI(Common Gateway Interface,通用网关接口),定义客户端与Web服务器的交流方式的一个程序,例如正常情况下客户端发送过来一个请求,根据HTTP协议Web服务器将请求的内容解析出来, 经过处理会后,再将返回的内容封装好。例如服务器返回一个HTML页面,并且根据HTTP协议构建返回内容的响应格式,涉及到TCP连接、HTTP原始请求和相应格式都是由一个软件来完成,这个程序就是CGI。

WSGI(Python Web Server Gateway Interface,WSGI)Web服务器网关接口,是为Python语言定义的Web服务器和Web应用程序或框架之间的联系,从语义上理解,WSGI为了解决Web服务器与客户端之间的通信问题而产生的。并且WSGI是基于现存的CGI标准而设计的,同样是一种程序 。ASGI是异步网关协议接口,介于网络服务和python饮用应用之间的标准接口,能够处理多种通用的协议类型,包括http、http2和websocket。二者的区别在于WSGI是基于HTTP协议模式开发的,不支持WebSocket;而ASGI的诞生解决了Python中的WSGI不支持当前的web开发中的一些新的协议标准,同时ASGI支持原有模式和Websocket的扩展,即ASGI是WSGI的扩展。常用的WSGI包括:Gunicorn、uWsgi、mod_wsgi;而常用的ASGI应用服务包括:Daphne、Hypercorn、Uvicorn。

Django WSGI 应用部署有多种方式、这里我们使用uWsgi作为Django的应用服务,我们让uWsgi通过读取配置文件的方式来启动Django应用。我们先来看看如何通过uWsgi部署Django应用。那么,我们为什么要是使用uWsgi呢?因为 Django Runserver 虽然可以直接对外提供服务、但是仅限于测试环境;uWsgi可以很好的进行多线程调度、进程监控、uWsgi提供完善的请求日志处理。我们在datasite项目目录下新建一个test.py文件用于测试uWsgi应用:

# test.py
def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World"] # python3
    #return ["Hello World"] # python2

# 进入datasite项目虚拟环境
(datasite) [root@localhost datasite]# workon datasite

# 安装uWsgi
(datasite) [root@localhost datasite]# pip3 install uwsgi

# 启动uWsgi测试
(datasite) [root@localhost datasite]# uwsgi --http :8000 --wsgi-file test.py

注:http :8000:使用协议http,端口8000;wsgi-file test.py:加载指定的文件test.py。

image-20210201172232864

uWsgi测试文件启动以后、我们去访问主机的8000端口;可以看到已经正常返回了Hello World、说明此时uWsgi已经可以正常工作。现在我们就去datasite的项目目录下创建一个uwsgi.ini配置文件、具体配置内容如下:

# 定义uwsgi全局域
[uwsgi]

chdir = /data/EnvsProject/datasite  # 配置项目目录
module = datasite.wsgi  # 配置项目的wsgi模块
home = /data/Envs/datasite  # 配置虚拟环境目录

http-socket = :8000  # 配置请求方式为http-socket,端口为8000
master = True  # 是否设置为master
processes = 10  # 处理的进程数
threads = 1  # 处理的线程数
vacuum = True  # 退出的时候是否清理缓存
max-requests=5000  # 最大请求数

# backend run uwsgi
daemonize = %(chdir)/logs/uwsgi-8000.log  # 配置日志文件(uwsgi的启动停止日志)
log-maxsize = 1024*1024*1024  # 日志文件大小
pidfile = %(chdir)/pid/uwsgi-8000.pid  # 使用后台进程运行需要把进程写到一个pid文件中
# static-map=/static=/data/EnvsProject/datasite/static  # 静态文件路径
env = DJANGO_SETTINGS_MODULE=settings.pro_settings  # 配置DJANGO_SETTINGS_MODULE的配置文件为生产环境的配置文件

注:上面的配置文件准备完成之后我们需要去手动创建pid和log目录、否则程序会启动报错。更多的配置内容大家可以自行查阅uWsgi官方文档:https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html

uWsgi启动以后我们可以去看一下启动日志和uWsgi的PID进程号:

(datasite) [root@localhost datasite]# ls
chpa_data  datasite  db.sqlite3  logs  manage.py  pid  requirements.txt  settings  static  templates  uwsgi.ini
(datasite) [root@localhost datasite]# cd logs/
(datasite) [root@localhost logs]# ls
admin.info.log  datasite.log  performance.info.log  uwsgi-8000.log  uwsgi-8000.log.1612188434  uwsgi-8000.log.1612188858  uwsgi-8000.log.1612188862  uwsgi-8000.log.1612188865  uwsgi-8000.log.1612188869
(datasite) [root@localhost logs]# cd ..
(datasite) [root@localhost datasite]# ls
chpa_data  datasite  db.sqlite3  logs  manage.py  pid  requirements.txt  settings  static  templates  uwsgi.ini
(datasite) [root@localhost datasite]# cd pid/
(datasite) [root@localhost pid]# ls
uwsgi-8000.pid
(datasite) [root@localhost pid]# cat uwsgi-8000.pid 
1749
(datasite) [root@localhost pid]# 

image-20210202001131480

从上图我们可以看到、我们去访问8000端口发现静态页面没有加载出来。那是因为uWsgi部署完成以后、我们还需要用到Nginx服务器;为什么我们需要用到Nginx服务器呢?第一个作用是我们使用Nginx去处理静态资源;第二个作用是作为Django后端服务的代理。因为Django的Server(python manage.py runserver)是单进程应用;如果我们直接把Django的Server应用暴露给用户去使用的话安全性很差、效率非常低下也非常不容易扩展。所以我们需要借助Nginx来实现后端服务的路由转发以及静态资源的服务代理。这里我们有两种方案可以解决静态资源的加载问题、第一种方案是通过Nginx来加载静态资源、第二种方案是通过uWsgi来加载静态资源。如果想通过uWsgi来加载静态资源我们直接在uwsgi.ini配置文件中加上static-map即可:

# 通过uwsgi加载静态资源
static-map=/static=/data/EnvsProject/datasite/static  # 静态文件路径

现在我们一起来看看如何通过Nginx来加载静态资源、Nginx的安装过程这里我们就略过了。我们直接去修改Nginx的配置文件Nginx.conf、修改内容如下:

user  root;
worker_processes  1;
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  /data/EnvsProject/datasite/logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    #keepalive_timeout  65;

    #gzip  on;
   # 通过upstream实现负载均衡、这里我们只开启了一个8000端口、可以按需开启多个服务端口
   upstream uwsgi {
        server 127.0.0.1:8000;
   }

    server {
        listen       80;
        server_name  localhost;

        charset utf-8;

        access_log /data/EnvsProject/datasite/logs/access.log;
        error_log /data/EnvsProject/datesite/logs/error.log;

        location / {
            proxy_pass http://uwsgi;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
        # 静态资源代理
        location /static {
            autoindex on;
            alias /data/EnvsProject/datasite/static;
        }        
    }
}

Nginx配置修改完成之后我们重启Nginx、然后去访问Django应用;从下图我们可以看到、Django应用已经正常启动、静态资源也已经可以正常加载出来了。

image-20210202002026279

image-20210202002042494

这里我们如果想实现uWsgi的启动和停止,最简单的处理方式是直接在命令行中启动和kill掉uWsgi服务:

# workon进入项目虚拟环境并进入项目目录
uwsgi --ini uwsgi.ini             # 启动
uwsgi --reload uwsgi.pid          # 重启
uwsgi --stop uwsgi.pid            # 关闭

虽然上面命令行的方式也可以用来管理uWsgi,但是每次都需要先进入项目虚拟环境然后再进入项目目录进行命令才做、太过于麻烦。为了更安全、更方便的管理uWsgi服务;我们需要把uWsgi配置到systemd服务中,同时实现开启自启的功能。
注:鉴于supervisor不支持python3,所以我们并没有采用supervisor来管理uwsgi服务。

# 我们去/etc/systemd/system/目录下面创建一个名为datasite_uwsgi.service的配置文件,内容如下:
[Unit]
Description=HTTP Interface Server
After=syslog.target

[Service]
KillSignal=SIGQUIT
ExecStart=/data/Envs/datasite/bin/uwsgi --ini /data/EnvsProject/datasite/uwsgi.ini
Restart=always
Type=notify
NotifyAccess=all
StandardError=syslog

[Install]
WantedBy=multi-user.target

然后我们通过下面的命令把uWsgi加入到systemd中、然后我们就可以通过system来管理uWsgi啦。

# 把uWsgi加入到systemd中
systemctl enable /etc/systemd/system/datasite_uwsgi.service

# 关闭uwsgi服务
systemctl stop datasite_uwsgi.service
# 开启uwsgi服务
systemctl start datasite_uwsgi.service
# 重启uwsgi服务
systemctl restart datasite_uwsgi.service

注:这里需要重点强调一下、如果你在 uwsgi.ini 配置文件中配置了 daemonize = %(chdir)/logs/uwsgi-8000.log (uwsgi服务以守护进程运行) 会导致 sytemctl 启动时多次重启而导致启动失败,需改为 logto = %(chdir)/logs/uwsgi-8000.log。

image-20210202102133549

推荐文章