组件介绍

  1. Nginx: 高性能Web服务器,负责反向代理;

  2. gunicorn: (Green Unicorn,绿色独角兽)高性能 uWSGI 服务器;

  3. gevent: 将Python同步代码转换为异步的协议库;

  4. supervisor: 监控服务流程的工具;

版本信息

gunicorn --version

输出

gunicorn (version 19.9.0)

安装 gunicorn 和 gevent

pip install gunicorn
pip install gevent

pip 安装的 gunicorn 无法找到路径,可以使用sudo apt install gunicorn3(Ubuntu)命令安装,或者手动编写/root/venv/bin/gunicorn(其中 venv 为你的 python 虚拟环境名称)

#!/root/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from gunicorn.app.wsgiapp import run
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(run())

之后执行chmod +x /root/venv/bin/gunicorn,然后运行gunicorn --version可以查看其版本信息

配置 gunicorn

# gun.py
import os
import gevent.monkey

gevent.monkey.patch_all()

import multiprocessing

if not os.path.exists('log'):
os.mkdir('log')
debug = True
loglevel = 'debug'
# 绑定的ip及端口号
bind = '0.0.0.0:5000'
pidfile = 'log/gunicorn.pid'
logfile = 'log/debug.log'

# 启动的进程数
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'gunicorn.workers.ggevent.GeventWorker'

x_forwarded_for_header = 'X-FORWARDED-FOR'

工作模式是通过 work_class 参数配置的值:

sync
gevent
eventlet
tornado
gaiohttp
gthread

其中默认值为sync

  • Sync Workers (sync)

    最简单的同步工作模式

  • Async Workers (gevent, eventlet)

    gevent 和 eventlet 都是基于 Greenlet 库,利用 python 协程实现的

  • Tornado Workers (tornado)

    利用 python Tornado 框架实现

  • AsyncIO Workers (gthread, gaiohttp)

    gaiohttp 利用 aiohttp 库实现异步 I/O,支持 web socket

  • gthread

    采用的是线程工作模式,利用线程池管理连接

参阅:gunicorn 多种工作模式_chengzhi0371 的博客-CSDN 博客

启动服务

gunicorn -k gevent -c gun.py runserver:app
[2019-08-07 16:00:20 +0800] [32111] [DEBUG] Current configuration:
config: gun.py
bind: ['0.0.0.0:5000']
backlog: 2048
workers: 3
worker_class: gevent
threads: 1
# ………… 省略中间部分
ciphers: TLSv1
raw_paste_global_conf: []
[2019-08-07 16:00:20 +0800] [32111] [INFO] Starting gunicorn 19.9.0
[2019-08-07 16:00:20 +0800] [32111] [DEBUG] Arbiter booted
[2019-08-07 16:00:20 +0800] [32111] [INFO] Listening at: http://0.0.0.0:5000 (32111)
[2019-08-07 16:00:20 +0800] [32111] [INFO] Using worker: gevent
[2019-08-07 16:00:20 +0800] [32115] [INFO] Booting worker with pid: 32115
[2019-08-07 16:00:20 +0800] [32116] [INFO] Booting worker with pid: 32116
[2019-08-07 16:00:20 +0800] [32117] [INFO] Booting worker with pid: 32117
[2019-08-07 16:00:20 +0800] [32111] [DEBUG] 3 workers

此时正常访问http://10.10.15.111:5000/应该可以看到首页信息提示表示连接服务正常。
注意:此处 ip 和端口均由自己设置,所以访问时需要做出相应调整。
如果无法正常访问,则需要验证防火墙是否正常:

  • CentOS 7 以下版本
    # 查询
    iptables -nL|grep 5000
    # 开放指定端口
    iptables -I INPUT -p tcp --dport 5000 -j ACCEPT
    # 再次查询
    iptables -nL|grep 5000
    ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:5000
  • CentOS 7+
    使用firewall-cmd管理防火墙端口
    firewall-cmd --query-port=5000/tcp      # no
    firewall-cmd --add-port=5000/tcp --permanent
    firewall-cmd --reload
    firewall-cmd --query-port=5000/tcp # yes
    systemctl status firewall
    systemctl status firewalld
    systemctl restart firewalld

    安装 nginx 和 supervisor

yum -y install nginx supervisor

如果提示找不到,可以使用yum install epel-release更新源之后再尝试。

配置 nginx

默认安装的 Nginx 配置文件/etc/nginx/nginx.conf内有如下配置:

include /etc/nginx/conf.d/*.conf;

即从外部目录/etc/nginx/conf.d/文件夹下还引入了其他配置文件。
这样,我们不修改默认配置,只在/etc/nginx/conf.d/目录下增加一个***.conf,来在外面增加新配置。

/etc/nginx/conf.d/增加app.conf,内容如下:

server {
listen 80; # 如果80端口被占用,可以使用其他端口,记得在防火墙中打开相应端口
server_name localhost;
charset utf-8;
access_log /var/app/access.log;
error_log /var/app/error.log;

client_max_body_size 100M;

location / {
proxy_pass http://0.0.0.0:5000;
proxy_http_version 1.1;
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
send_timeout 300;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Request-Start $msec;
}
}

启动服务

systemctl start nginx
  • 报错nginx: [emerg] getpwnam("nginx") failed in /etc/nginx/nginx.conf:5
    查看配置该行内容为user nginx;,添加用户useradd nginx

    如果是手动编译安装 nginx,可能无法使用systemctl直接管理进程,可以手动创建配置文件/usr/lib/systemd/system/nginx.service写入:

    [Unit]
    # 描述服务
    Description=The nginx HTTP and reverse proxy server
    # 描述服务类别
    After=network-online.target remote-fs.target nss-lookup.target
    Wants=network-online.target

    [Service]
    # 指定为后台运行的形式
    Type=forking
    # 进程号写入文件
    PIDFile=/run/nginx.pid
    # Nginx will fail to start if /run/nginx.pid already exists but has the wrong
    # SELinux context. This might happen when running `nginx -t` from the cmdline.
    # https://bugzilla.redhat.com/show_bug.cgi?id=1268621
    ExecStartPre=/usr/bin/rm -f /run/nginx.pid
    # 实际路径根据编译安装的路径可能有所不同
    ExecStartPre=/usr/local/nginx/sbin/nginx -t
    ExecStart=/usr/local/nginx/sbin/nginx
    ExecReload=/usr/local/nginx/sbin/nginx -s reload
    ExecStop=/usr/local/nginx/sbin/nginx -s quit
    KillSignal=SIGQUIT
    TimeoutStopSec=5
    KillMode=process
    # 表示给服务分配独立的临时空间
    PrivateTmp=true

    [Install]
    WantedBy=multi-user.target

    参阅:NGINX systemd service file | NGINX

报错

  • ERR_CONTENT_LENGTH_MISMATCH
    查看 nginx 错误日志

    tailf /var/log/nginx/error.log
    2019/08/09 03:04:21 [crit] 24616#0: *204 open() "/var/lib/nginx/tmp/proxy/2/03/0000000032" failed (13: Permission denied) while reading upstream, client: 10.10.15.199, server: localhost, request: "GET /static/js/app.1b53c809113e333c2727.js.map HTTP/1.1", upstream: "http://0.0.0.0:5000/static/js/app.1b53c809113e333c2727.js.map", host: "10.10.15.111:82"

    参考这里:ERR_CONTENT_LENGTH_MISMATCH 解决方法

  • 进入首页出现403 Forbidden

    1. nginx 启动用户和配置中的工作用户不一致(注意:如果你的nginx服务是root用户运行,则配置中user项配置为root);
    2. 配置文件中缺少 index index.html index.htm index.php 行;
    3. nginx 用户没有相应工作目录的操作权限(chown -R nginx:nginx WORK_DIR_PATH);
    4. 防火墙设置。
      参考这里:解决 Nginx 出现 403 forbidden (13: Permission denied)报错的四种方法
    5. selinux 处于开启状态
      • 临时
        setenforce 0
      • 永久禁用
        vi /etc/sysconfig/selinux
        # 配置
        SELINUX=disable
  • Address already in use
    Sep 07 10:26:07 iZ9bq05sg6feeodta154guZ nginx[17582]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
    ````
    提示端口占用,可以使用`lsof -i:80`或者`netstat -tulpn`查看被占用端口的进程,比较诡异的是我当时看到的就是 nginx 的服务,但是`systemctl status nginx`看到的状态就是 faild,后面注意到报错信息:
    ```plain
    Sep 07 10:14:48 iZ9bq05sg6feeodta154guZ systemd[1]: PID file /run/nginx.pid not readable (yet?) after start. # 此行
    Sep 07 10:16:18 iZ9bq05sg6feeodta154guZ systemd[1]: nginx.service start operation timed out. Terminating.
    Sep 07 10:16:18 iZ9bq05sg6feeodta154guZ systemd[1]: Failed to start The nginx HTTP and reverse proxy server.
    Sep 07 10:16:18 iZ9bq05sg6feeodta154guZ systemd[1]: Unit nginx.service entered failed state.
    Sep 07 10:16:18 iZ9bq05sg6feeodta154guZ systemd[1]: nginx.service failed.

    所以在配置文件/usr/local/nginx/conf/nginx.conf中找到 pid 文件指定行并修改为pid /run/nginx.pid;,之后kill -9 xxx杀死进程重新启动之后即可。理论上讲你也可以放开原配置注释,然后修改nginx.service相应配置。参阅:NGINX systemd: PID file /run/nginx.pid not readable (yet?) - Computer How To

配置 Supervisor

echo_supervisord_conf

用来生成默认的配置文件,一般生成默认文件为 supervisor.conf

首先检查是否存在配置文件,一般配置文件的路径是/etc/supervisord.conf,如果配置文件不存在,我们可以通过命令来生成:

echo_supervisord_conf > /etc/supervisord.conf

配置文件涉及的内容很多,项目配置可以参照官网文档

打开配置文件,可以看到最后一行:

;[include]
;files = /etc/supervisord/*.conf

默认一般是注释掉的,我们直接取消注释即可:
[include]
files = /etc/supervisord/*.conf

这行配置的作用也很浅显,就是导入设置的路径下的所有conf文件,这使得我们如果有多个项目可以不用都写在同一个配置文件里,可以一个项目一个配置文件,更适合管理。这里的路径也是可以按照实际需求随意更改。

注意
supervisor 配置兼容iniconf格式,例如本人安装时版本为ini格式,所以我们自写的配置也调整为相应的.ini格式即可。

[include]
files = supervisord.d/*.ini

Supervisord

可以理解成 supervisor 的服务端,运行 supervisor 时会启动一个进程 supervisord,它负责启动所管理的进程,并将所管理的进程作为自己的子进程来启动,而且可以在所管理的进程出现崩溃时自动重启

  • 手动启动 Supervisord
    # 配置文件路径根据自己实际的路径决定
    supervisord -c /etc/supervisord.conf
    在设置的路径下新建一个配置文件,命令请根据上一步设置的扩展名。
    [program:project_name]
    ;具体路径根据自己安装或者虚拟环境中的位置进行配置
    command=/usr/bin/gunicorn -c gun.py runserver:app
    ;项目绝对路径
    directory=/project_path/
    startsecs=0
    stopwaitsecs=0
    autostart=true
    autorestart=true
    project_name按照你的实际需求修改,作为你这个服务的唯一标识,用于启动停止服务时使用。
    command修改为测试gunicorn时使用的命令,建议使用绝对路径。
    directory指定了工作路径,通常设置为项目根目录,我们填写的 gun.py 和 app 都是基于这个路径的。

supervisorctl

可以理解成 supervisor 的客户端,管理Supervisor的项目是使用supervisorctl命令,我们可以启动项目试试看:

supervisorctl start {{PROJECT_NAME}}

如果没有报错,应该可以和上一步测试gunicorn一样可以正常访问项目了。
使用supervisorctl工具验证应用状态:

supervisorctl
app RUNNING pid 24639, uptime 0:20:55

设置开机自启

  • 加入 systemctl 管理服务

创建/etc/systemd/system/supervisord.service 文件,写入:

[Unit]
Description=Supervisor daemon
Documentation=http://supervisord.org
After=network.target

[Service]
# 根据实际配置和位置编写,使用which 命令查找路径
ExecStart=/usr/local/bin/supervisord -n -c /etc/supervisor/supervisord.conf
ExecStop=/usr/local/bin/supervisorctl $OPTIONS shutdown
ExecReload=/usr/local/bin/supervisorctl $OPTIONS reload
KillMode=process
Restart=on-failure
RestartSec=42s[Unit]
Description=Supervisor daemon
Documentation=http://supervisord.org
After=network.target

[Install]
WantedBy=multi-user.target

执行命令使能:
systemctl enable supervisord.service

报错记录

注意
ini 格式文件不支持行内注释!请确保你的注释为独立注释,且以;开头,而不是#(Python 中的注释符)

  • Error: Another program is already listening on a port that one of our HTTP servers is configured to use. Shut this program down first before starting supervisord. For help, use /usr/bin/supervisord -h
    # 可以查看supervisor.sock并删除 
    find / -name supervisor.sock
    找到路径
    /run/supervisor/supervisor.sock
    删除之:
    unlink /run/supervisor/supervisor.sock
    再次重新启动
    supervisord -c /etc/supervisord.conf
  • 程序启动失败
    执行裸命令/usr/bin/gunicorn -c gun.py runserver:app提示:

    Traceback (most recent call last):
    File "/root/venv/bin/gunicorn", line 5, in <module>
    from gunicorn.app.wsgiapp import run
    File "/root/venv/lib/python3.6/site-packages/gunicorn/app/wsgiapp.py", line 9, in <module>
    from gunicorn.app.base import Application
    File "/root/venv/lib/python3.6/site-packages/gunicorn/app/base.py", line 13, in <module>
    from gunicorn.config import Config, get_default_config_file
    File "/root/venv/lib/python3.6/site-packages/gunicorn/config.py", line 16, in <module>
    import ssl
    File "/usr/local/lib/python3.6/ssl.py", line 101, in <module>
    import _ssl # if we can't import it, let the error propagate
    ModuleNotFoundError: No module named '_ssl'

    如果项目使用 Python3.6,可以尝试下面的方法:
    这时可能需要重新安装 Python3,首先需要安装 ssl 依赖openssl-developenssl

    yum install openssl-devel -y

    如果是 Python3.7,建议使用下面的方法:

    2. 在类 Unix 环境下使用 Python — Python 3.10.1 文档
    Python3.7 installation (solve ssl problem) - Programmer All

    1. Tags · openssl/openssl 下载较高版本 openssl;
    2. 解压并编译安装 openssl
      cd openssl-1.1.1-pre8
      ./config --prefix=/usr/local/openssl # 新版openssl将安装在/usr/local/openssl目录下
      make && make install
    3. 备份原版 openssl
      mv /usr/bin/openssl /usr/bin/openssl.bak  # backup
      mv /usr/include/openssl/ /usr/include/openssl.bak
    4. 备份好之后为新版 openssl 配置软连接
      # 将安装好的openssl的openssl命令软连到/usr/bin/openssl
      ln -s /usr/local/openssl/include/openssl /usr/include/openssl

      # 软链到升级后的libssl.so
      ln -s /usr/local/openssl/lib/libssl.so.1.1 /usr/local/lib64/libssl.so

      # 将安装好的openssl命令软连到/usr/bin/openssl
      ln -s /usr/local/openssl/bin/openssl /usr/bin/openssl
    5. 最后再修改下系统配置即可
      # 写入openssl库文件的搜索路径
      echo "/usr/local/openssl/lib" >> /etc/ld.so.conf # 需要root权限

      # 使修改后的/etc/ld.so.conf生效
      ldconfig -v
    6. 验证 openssl 版本
      openssl version
      OpenSSL 1.1.1m 14 Dec 2021
      之后重新编译安装 Python3。
      ./configure --prefix=/usr/local/python3 --enable-shared CFLAGS=-fPIC
      带新的openssl编译成功
      make && make install
  • 直接退出
    使用一下命令查看报错信息

    supervisorctl tail {{PROJECT_NAME}} stderr    # PROJECT_NAME为项目名称
  • 修改配置之后重启应用程序
    supervisorctl reread
    supervisorctl update
    supervisorctl restart {{PROJECT_NAME}}
  • xxx: ERROR (no such process)
    提示找不到进程,需要重新读取配置(reread->update->start)
  • xxx:ERROR (spawn error)
    可能是配置写得有问题,注意对齐,如果有 app 的日志,则必须保证目录和文件存在。

Docker 容器化

以下部分供参考,部分未完成。

安装 docker

yum install docker -y
  • 编写 Dockerfile
    # 从仓库拉取 带有 python 3.6 的 Linux 环境
    FROM python:3.6

    # 设置 python 环境变量
    ENV PYTHONUNBUFFERED 1
    # 设置测试环境变量
    ENV ENV test

    # 创建 code 文件夹并将其设置为工作目录
    RUN mkdir /code
    WORKDIR /code
    # 国内换源
    RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple
    RUN pip config set install.trusted-host mirrors.aliyun.com
    # 更新 pip
    RUN pip install pip -U
    # yum 换源
    # 首先下载源 wget -O ./CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo
    ADD CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo
    # 安装依赖
    #RUN yum install zlib-devel -y
    #RUN yum install python-devel -y
    #RUN yum install openssl-devel -y

    # 将 requirements.txt 复制到容器的 code 目录
    ADD requirements.txt /code/

    # 安装库
    RUN pip install -r requirements.txt
    # 将当前目录复制到容器的 code 目录
    ADD . /code/

    安装 docker-compose

    pip install docker-compose
    如果提示没有添加到环境变量,则需要编辑~/.bashrc并写入export PATH=/usr/local/python3/bin:$PATH。当然,其中的路径应为 python 的真实路径。

    使用上述版本安装的可能运行报错,怀疑是版本不对(1.29.2),如果遇到同样错误,使用下述命令安装:

    sudo curl -L https://get.daocloud.io/docker/compose/releases/download/1.22.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose  
    sudo chmod +x /usr/local/bin/docker-compose

    此版本不会报错。

  • 编写 docker-compose.yml
    测试环境,我们直接使用 Django 的 web 服务器就好,所以 command 比较简单。
    # docker-compose.yml
    version: "3"
    services:
    app:
    restart: always
    # 指定一个包含 Dockerfile 的路径
    build: . # '点'代表当前目录
    # 容器运行时需要执行的命令
    command: "python manage.py runserver 0.0.0.0:8000"
    volumes:
    - .:/code
    ports:
    # 定义了宿主机和容器的端口映射。容器的隔离不止环境,甚至连端口都隔离起来了。但 web 应用不通过端口跟外界通信当然不行,因此这里定义将宿主机的 8000 端口映射到容器的 8000 端口
    - "8100:8000"

    启动

  • 启动容器服务
    docker-compose up
    docker ps
    CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
    96347684e5ce sz_mobile_app "python manage.py ru…" 2 hours ago Up 5 seconds 0.0.0.0:8100->8000/tcp sz_mobile_app_1
    ……
    可以看到 Docker 按照配置文件的要求,成功构建了镜像及容器,并启动了容器。

打开浏览器,输入本地 IP 端口 192.168.1.88:8100,访问请求,其中 ip 和端口需要根据个人实际情况决定。注意端口为宿主机器的实际端口,并且防火墙必须开放该端口。

Ctrl + C 即可停止开发服务器运行,停止服务器后实际上容器还存在,只是停止运行了而已。
此外还有一些指令我们可能需要使用:

  • 删除容器
    docker-compose down
  • 后台运行容器
    docker-compose up -d
  • 重新构建镜像
    docker-compose build
    如果修改了 Dockerfile,则需要docker-compose up --build重新构建
  • 启动和停止已有的容器
    docker-compose start
    docker-compose stop
    通常来说,我们在测试环境中,只需要让 Django 服务简单启动起来即可,而在生产环境,则需要 nginx 来处理静态文件和大流量请求,所以需要隔离测试环境和生产环境。
    生产配置通常与开发配置略有不同。 比如使用 Gunicorn 为 Django 应用程序提供服务,使用 NGINX 作为反向代理和静态/媒体文件服务器。

这种设置虽然有利于生产,但在开发中并不是很方便,因为开发的目标通常是快速响应。 再比如,为了稳定性我决定使用托管数据库服务,因此不需要容器化的数据库服务。

云端部署

所以我们编写生产环境中的docker-compose.prod.yml文件。

version: "3"
services:
web:
build: .
restart: on-failure
env_file:
- ./.env
command: gunicorn --bind 0.0.0.0:8080 meta.wsgi
ports:
- "8100:8080"
depends_on:
- nginx
nginx:
image: "nginx"
restart: always
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./staticfiles:/static
- ./mediafiles:/media
ports:
- "80:80"

网络相关:docker network 基础 - wadeson - 博客园

参考来源

常态化部署

  1. Huawei Cloud Centos7 Flask+Gunicorn+Gevent+Supervisor+Nginx Multi-site Production Environment Deployment
  2. Flask + Nginx + Gunicorn + Gevent 部署
  3. gunicorn+gevent+nginx 部署 flask 应用
  4. CentOS 上 Flask + uWSGI + Nginx 部署
  5. Nginx、Gunicorn 在服务器中分别起什么作用
  6. 用 gunicorn 实现 Django 高并发的解决方案_旷古的寂寞的博客-CSDN 博客
  7. Django 部署多个网站_Li-boss-CSDN 博客
  8. gunicorn 和 nginx 端口映射_qq_40472064 的博客-CSDN 博客_gunicorn 端口
  9. Gunicorn-Django 部署 - 朝朝哥 - 博客园

    Docker 部署

  10. Django-Docker 容器化部署:Django-Docker-MySQL-Nginx-Gunicorn 云端部署 - 杜赛的博客
  11. Docker 部署 Django+MySQL+Redis+Gunicorn+Nginx | Python 技术论坛
  12. 隔离测试与生产环境:Dockerizing Django in development and production