智能化运维平台部署(gunicorn+nginx+gevent+supervisor部署flask+vue)

2019年6月13日
[toc]

前言

  简要介绍一下,为小白普及基础知识,老司机请自行略过~
  Flask本身带着 WSGI server,但是性能差强人意,自带的web server 更多的是测试用途。线上发布时,最好使用高性能的 wsgi server或者是联合nginx做uwsgi 。
  greenlet是一个轻量级的协程库。
  gevent是基于greenlet的网络库,每次遇到io操作,需要耗时等待时,会自动跳到下一个协程继续执行。
  guincorn是支持wsgi协议的http server,只支持在Unix系统上运行,来源于Ruby的unicorn项目。gevent是它支持的模式之一 ,是为了解决django、flask这些web框架自带wsgi server性能低下的问题。它的特点是与各个web框架结合紧密,实现简单,轻量级,高性能,部署方便。
  Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。其特点是占有内存少,并发能力强,在同类型的网页服务器中表现较好,百度、京东、新浪、网易、腾讯、淘宝等网站都广泛使用nginx。
  Supervisor是一个UNIX操作系统上的进程管理工具,允许用户通过其控制许多进程,包括进程启停、优先级分配等,具有部署简单、管理集中、高效等特点。
  pipenv能够有效管理Python多个环境,各种依赖包。过去我们一般用virtualenv搭建虚拟环境,管理python版本,但是跨平台的使用不太一致,且有时候处理包之间的依赖总存在问题;过去也常常用 pip进行包的管理,pip已经足够好,但是仍然推荐pipenv,相当于virtualenv和pip的合体,更加强大,在各个平台的命令都是一样的,且使用了哈希校验,无论安装还是卸载包都十分安全。详见Python新利器之pipenv

一.环境

1.应用架构:大前端+后端API

(1)大前端框架:iview-admin套餐(vue、vue-router、vuex等)
(2)后端框架:flask套餐(flask、flask-sqlalchemy、python-crontab、sqlparse等)

2.部署架构:nginx+gunicorn+gevent+supervisor

3.服务器环境

IP:11.11.11.11
操作系统:centos7.2

4.部署目录

安装目录:/app/setup
运行目录:/app/run
nginx日志目录:/var/log/nginx
gunicorn日志目录:/var/log/gunicorn
supervisor日志目录:/var/log/supervisor/
supervisor运行目录:/var/supervisor/run/

二.安装python3.7.1

1.移除默认运行python3.6.4

vi ~/.bash_profile

export PATH
#export PATH="/root/.pyenv/bin:$PATH"
#export PYENV_ROOT="$HOME/.pyenv"
#export PATH="$PYENV_ROOT/bin:$PATH"

cd /usr/bin可知
此时默认版本为2.7.5

lrwxrwxrwx. 1 root root 7 Apr 9 2018 python -> python2
lrwxrwxrwx. 1 root root 9 Apr 9 2018 python2 -> python2.7
-rwxr-xr-x. 1 root root 7136 Nov 20 2015 python2.7

2.下载安装包

登录网址https://www.python.org/downloads/release/python-371/
下载Python-3.7.1.tgz

3.传输

传输到11.11.11.11 的/app/setup目录下

4.解压安装包

cd /app/setup
tar -xvzf Python-3.7.1.tgz

5.安装

(1)安装依赖(不管装没装,强撸)

yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel libffi-devel gcc make

(2)编译安装软件

cd  /app/setup/Python-3.7.1
./configure --prefix=/usr/python3
make && make install

6.修改profile

[root@dbmgt1 bin]# vi ~/.bash_profile 
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:$HOME/bin
export PATH
#export PATH="/root/.pyenv/bin:$PATH"
#export PYENV_ROOT="$HOME/.pyenv"
#export PATH="$PYENV_ROOT/bin:$PATH"
#eval "$(pyenv init -)"
export PYENV_ROOT="/usr/python3"
export PATH="$PYENV_ROOT/bin:$PATH"

注意:
yum依赖于/usr/bin/python,指向python2,后续还需使用yum进行依赖安装,所以不要动这个文件

7.升级pip

(1)如果没有外网

pip install --index http://11.4.76.252/simple/ --trusted-host=11.4.76.252 --upgrade pip

(2)如果有外网

pip install  --upgrade pip

8.安装pipenv

pip install pipenv

三.代码环境安装

1.从gitlab拉取代码

cd /app/run
mkdir www
cd www
git init
git pull https://gitlab.www.com.cn/88888/automation.git

输入工号密码后完成代码拉取

2.创建虚拟环境

cd /app/run/www/AutoDB
yum install postgresql-devel*
pipenv install
pipenv install gunicorn
pipenv install supervisor
pipenv install gevent

说明:由于系统用到pg数据库,需要安装psycopg2组件,安装该组件在linux环境下需要先安装postgresql-devel依赖包,否则会报错:Error: pg_config executable not found.
此外,由于部署需要,安装gevent、gunicorn、supervisor组件

3.安装证书(如果已申请证书,该步骤可忽略)

(1)安装依赖


yum install openssl

yum install openssl-devel

(2)制作证书


cd /app/run/www/AutoDB

#生成key
openssl genrsa -des3 -out server.key 1024
#生成csr
openssl req -new -key server.key -out server.csr
#生成免输入密码的key,不然网站启动时总是要求输入key
openssl rsa -in server.key -out server_nopwd.key
#生成crt
openssl x509 -req -days 3650 -in server.csr -signkey server_nopwd.key -out server.crt

4.代码运行验证(验证时防火墙可开启5000端口)

cd /app/run/www/AutoDB
pipenv shell
python start.py

浏览器访问https://11.11.11.11:5000
如果有正常返回页面,则成功

四.wsgi服务器安装部署

WSGI的全称是Web Server Gateway Interface,翻译过来就是Web服务器网关接口。具体的来说,WSGI是一个规范,定义了Web服务器如何与Python应用程序进行交互,使得使用Python写的Web应用程序可以和Web服务器对接起来,基于gunicorn的强大性能,采用gunicorn作为flask的wsgi服务器是最佳实践之一。WSGI的理解详见什么是wsgiuWSGI和Gunicorn

1.安装gunicorn

cd /app/run/www
pipenv install gunicorn

2.编写配置文件

在项目主目录下新增gunicorn.conf

import os
bind='0.0.0.0:5000'
workers=4
backlog=2048
worker_class="gevent" #sync, gevent,meinheld
debug=True
proc_name='gunicorn_www.pid'
pidfile='/var/log/gunicorn/wwwpid.log'
errorlog='/var/log/gunicorn/wwwerror.log'
accesslog='/var/log/gunicorn/wwwaccess.log'
loglevel='debug'
threads=4
worker_connections = 2000
keyfile='server.key'
certfile='server.cer'

参考官方文档

3.创建gunicorn日志目录

在/var/log目录下创建gunicorn目录

cd /var/log
mkdir gunicorn

4.验证gunicorn配置

/root/.local/share/virtualenvs/AutoDB-zUItRR7g/bin/gunicorn -c gunicorn.conf start:autodb_app

如果浏览器可以正常访问平台,说明配置成功
gunicorn的路径由pipenv shell可查看到虚拟环境路径

五.web服务器

基于nginx的强大性能,采用nginx作为web服务器,

1.安装依赖包

yum install gcc-c++
yum install -y pcre pcre-devel
yum install -y zlib zlib-devel
yum install -y openssl openssl-devel

2.下载nginx安装包

http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm

3.传输到/app/setup

4.安装nginx

rpm -Uvh nginx-release-centos-7-0.el7.ngx.noarch.rpm

或者

 yum install -y nginx

5.启动nginx

systemctl start nginx.service
systemctl enable nginx.service

6.验证是否安装成功

浏览器访问http://11.11.11.11:80,如果正常返回html页面说明安装成功

7.配置nginx

思路:
系统采用前后端分离的架构,可以将前端静态文件与后端应用程序分开部署。为实现更为安全的访问配置,通过nginx反向代理实现应用程序部署端口的隐藏。
部署方案及难点如下:
(1)项目前端采用VUE大前端架构,编译后为html、js等纯静态文件,部署在80端口。
难点:由于前端vue的路由模块采用了history模式,需要解决前端路由请求刷新后报404错误的问题;
(2)后端程序部署在5000端口,对外开放443端口,nginx监听443端口,限制只接收https请求。接到外部请求后,判断路径是静态文件还是API请求,静态文件请求直接跳转静态目录,API请求转发到5000端口。
难点:实现https认证方式的配置

(1)修改主配置文件nginx.conf

[root@dbmgt1 nginx]# cd /etc/nginx
[root@dbmgt1 nginx]# vi nginx.conf 
user root root;
worker_processes 10;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
    worker_connections 1024;
}
http {
    include /etc/nginx/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 /var/log/nginx/access.log main;
    sendfile on;
    #tcp_nopush on;
    keepalive_timeout 65;
    #gzip on;
    include /etc/nginx/conf.d/*.conf;
}

(2)创建项目配置文件

按照部署思路分为adbo_http.conf和adbo_https.conf两个文件,分别实现大前端项目的部署和后端API程序的部署
先移除默认配置

cd /etc/nginx/conf.d
mv  default.conf default.conf.bak
a.创建adbo_http.conf配置文件
server {
    listen       80;
    server_name  adbo.www.com.cn;
    location /static {
        alias /app/run/www/AutoDB/AutoDB/static;
    }
    location / {
        root /app/run/www/AutoDB/AutoDB/templates;
        index index.html;
        if (!-e $request_filename) {
            rewrite ^/(.*) /index.html last;
            break;
        }
    }
}

注:通过if条件判断,解决前端vue项目history模式的路由刷新问题,如果不加判断,刷新会出现404 详见VUE路由history模式坑记-NGINX
vue的路由模式详见Vue-router 中hash模式和history模式vue-router两种模式,到底什么情况下用hash,什么情况下用history模式呢?

b.创建adbo_https.conf配置文件
vi adbo_https.conf 
server {
    listen       443 ssl;
    server_name  adbo.www.com.cn;
    ssl_certificate  /app/run/www/AutoDB/server.cer;
    ssl_certificate_key  /app/run/www/AutoDB/server.key;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;
    location /static {
        alias /app/run/www/AutoDB/AutoDB/static;
    }

    location / {
        proxy_pass https://adbo.www.com.cn:5000;
        #add_header Access-Control-Allow-Origin 'http://adbo.www.com.cn';
        #add_header Access-Control-Allow-Methods 'GET, POST,PUT,DELETE, OPTIONS';
        #add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
    }
    # redirect server error pages to the static page /50x.html
    #
}

注:排查问题可通过nginx日志排查 详见
如果出现权限问题,执行以下命令 详见

setsebool httpd_can_network_connect on -P

8.验证配置是否成功

浏览器输入11.11.11.11 ,访问正常即成功
nginx配置https详见Nginx 配置 HTTPS 服务器
注意:基于域名的证书,必须通过域名访问才能认证成功!基于ip的访问将被拒绝!!

六.防火墙配置

开通80、443端口

[root@dbmgt1 services]#  firewall-cmd --zone=public --list-ports
[root@dbmgt1 services]# firewall-cmd --zone=public --add-port=80/tcp --permanent 
success
[root@dbmgt1 services]# firewall-cmd --zone=public --add-port=443/tcp --permanent 
success
[root@dbmgt1 services]# firewall-cmd --reload
success
[root@dbmgt1 services]# firewall-cmd --zone=public --list-ports
443/tcp 80/tcp

注:防火墙启停命令

systemctl start/stop firewalld.service
systemctl enable/disable firewalld.service

七.oracle客户端安装配置

由于程序使用cx_Oracle模块访问oracle数据库,需要安装oracle客户端,并且添加相应的环境变量,以便让cx_Oracle识别到。

1.下载客户端

下载地址:https://www.oracle.com/technetwork/topics/linuxx86-64soft-092277.html

2.传输到/app/setup

3.安装客户端

cd /app/setup
rpm -Uvh oracle-instantclient12.1-basic-12.1.0.2.0-1.x86_64.rpm

4.修改环境变量

(1)通过whereis oracle查看安装目录
(2)修改环境变量

vi ~/.bash_profile
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/bin

export PATH
#export PATH="/root/.pyenv/bin:$PATH"
#export PYENV_ROOT="$HOME/.pyenv"
#export PATH="$PYENV_ROOT/bin:$PATH"
#eval "$(pyenv init -)"
export PYENV_ROOT="/usr/python3"
export PATH="$PYENV_ROOT/bin:$PATH"
export LD_LIBRARY_PATH=/usr/lib/oracle/12.1/client64/lib:$LD_LIBRARY_PATH
export PATH=/usr/lib/oracle/12.1/client64/bin:$PATH
export PATH=/usr/lib/oracle/12.1/client64/lib:$PATH
NLS_LANG=american_america.ZHS16GBK
export NLS_LANG
export ORACLE_HOME=/usr/lib/oracle/12.1/client64

5.验证是否配置成功

cd /app/run/www/AutoDB
pipenv shell
python
import cx_Oracle
connection = cx_Oracle.Connection("HXBTEST4_SPT/Lz4_olb2@11.4.76.154:1521/zhtestdb1")

如果没有报错说明配置成功
如果报错:cx_Oracle.DatabaseError: ORA-21561: OID generation failed
通过hostname查看主机名 dbmgt1
将主机名添加到配置

vi /etc/hosts
11.11.11.11 dbmgt1

八.supervisor安装配置

1.安装supervisor

cd /app/run/www/AutoDB
pipenv install supervisor

2.初始化supervisor配置文件

pipenv shell
echo_supervisord_conf > /etc/supervisord.conf

3.修改配置

vi /etc/supervisord.conf
; Sample supervisor config file.
;
; For more information on the config file, please see:
; http://supervisord.org/configuration.html
;
; Notes:
; - Shell expansion ("~" or "$HOME") is not supported. Environment
; variables can be expanded using this syntax: "%(ENV_HOME)s".
; - Quotes around values are not supported, except in the case of
; the environment= options as shown below.
; - Comments must have a leading space: "a=b ;comment" not "a=b;comment".
; - Command will be truncated if it looks like a config file comment, e.g.
; "command=bash -c 'foo ; bar'" will truncate to "command=bash -c 'foo ".

[unix_http_server]
file=/var/supervisor/run/supervisor.sock ; the path to the socket file
;chmod=0700 ; socket file mode (default 0700)
;chown=nobody:nogroup ; socket file uid:gid owner
;username=user ; default is no username (open server)
;password=123 ; default is no password (open server)

;[inet_http_server] ; inet (TCP) server disabled by default
;port=127.0.0.1:9001 ; ip_address:port specifier, *:port for all iface
;username=user ; default is no username (open server)
;password=123 ; default is no password (open server)

[supervisord]
logfile=/var/log/supervisor/supervisord.log ; 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/supervisor/run/supervisord.pid ; supervisord pidfile; default supervisord.pid
nodaemon=false ; start in foreground if true; default false
minfds=1024 ; min. avail startup file descriptors; default 1024
minprocs=200 ; min. avail process descriptors;default 200
;umask=022 ; process file creation umask; default 022
;user=supervisord ; setuid to this UNIX account at startup; recommended if root
;identifier=supervisor ; supervisord identifier, default is 'supervisor'
;directory=/tmp ; default is not to cd during start
;nocleanup=true ; don't clean up tempfiles at start; default false
;childlogdir=/tmp ; 'AUTO' child log dir, default $TEMP
environment=PATH=/usr/lib/oracle/12.1/client64/lib:/usr/lib/oracle/12.1/client64/bin:%(ENV_PATH)s,NLS_LANG=american_america.ZHS16GBK,ORACLE_HOME=/usr/lib/oracle/12.1/client64:%(ENV_ORACLE_HOME)s,LD_LIBRARY_PATH=/usr/lib/oracle/12.1/clien
t64/lib:%(ENV_LD_LIBRARY_PATH)s ; key value pairs to add to environment
;strip_ansi=false ; strip ansi escape codes in logs; def. false

; The rpcinterface:supervisor section must remain in the config file for
; RPC (supervisorctl/web interface) to work. Additional interfaces may be
; added by defining them in separate [rpcinterface:x] sections.

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

; The supervisorctl section configures how supervisorctl will connect to
; supervisord. configure it match the settings in either the unix_http_server
; or inet_http_server section.

[supervisorctl]
serverurl=unix:///var/supervisor/run/supervisor.sock ; use a unix:// URL for a unix socket
;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
;username=chris ; should be same as in [*_http_server] if set
;password=123 ; should be same as in [*_http_server] if set
;prompt=mysupervisor ; cmd line prompt (default "supervisor")
;history_file=~/.sc_history ; use readline history if available

; The sample program section below shows all possible program subsection values.
; Create one or more 'real' program: sections to be able to control them under
; supervisor.

[program:adbo]
command=/root/.local/share/virtualenvs/AutoDB-zUItRR7g/bin/gunicorn -c gunicorn.conf start:autodb_app ; the program (relative uses PATH, can take args)
;command=/root/.local/share/virtualenvs/AutoDB-zUItRR7g/bin/python start.py
;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
numprocs=1 ; number of processes copies to start (def 1)
directory=/app/run/www/AutoDB ; directory to cwd to before exec (def no cwd)
;umask=022 ; umask for process (default None)
priority=999 ; the relative start priority (default 999)
autostart=true ; start at supervisord start (default: true)
;startsecs=1 ; # of secs prog must stay up to be running (def. 1)
startretries=10 ; max # of serial start failures when starting (default 3)
autorestart=true ; when to restart if exited after running (def: unexpected)
;exitcodes=0 ; 'expected' exit codes used with autorestart (default 0)
;stopsignal=QUIT ; signal used to kill process (default TERM)
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 ; setuid to this UNIX account to run the program
;redirect_stderr=true ; redirect proc stderr to stdout (default false)
stdout_logfile=/tmp/adbo.log ; stdout log path, NONE for none; default AUTO
stdout_logfile_maxbytes=50MB ; max # logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10 ; # of stdout logfile backups (0 means none, default 10)
;stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
;stdout_events_enabled=false ; emit events on stdout writes (default false)
;stdout_syslog=false ; send stdout to syslog with process name (default false)
stderr_logfile=/tmp/adbo_err.log ; stderr log path, NONE for none; default AUTO
stderr_logfile_maxbytes=50MB ; max # logfile bytes b4 rotation (default 50MB)
stderr_logfile_backups=10 ; # of stderr logfile backups (0 means none, default 10)
;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
;stderr_events_enabled=false ; emit events on stderr writes (default false)
;stderr_syslog=false ; send stderr to syslog with process name (default false)
;environment=A="1",B="2" ; process environment additions (def no adds)
;serverurl=AUTO ; override serverurl computation (childutils)

; The sample eventlistener section below shows all possible eventlistener
; subsection values. Create one or more 'real' eventlistener: sections to be
; able to handle event notifications sent by supervisord.

;[eventlistener:theeventlistenername]
;command=/bin/eventlistener ; the program (relative uses PATH, can take args)
;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
;numprocs=1 ; number of processes copies to start (def 1)
;events=EVENT ; event notif. types to subscribe to (req'd)
;buffer_size=10 ; event buffer queue size (default 10)
;directory=/tmp ; directory to cwd to before exec (def no cwd)
;umask=022 ; umask for process (default None)
;priority=-1 ; the relative start priority (default -1)
;autostart=true ; start at supervisord start (default: true)
;startsecs=1 ; # of secs prog must stay up to be running (def. 1)
;startretries=3 ; max # of serial start failures when starting (default 3)
;autorestart=unexpected ; autorestart if exited after running (def: unexpected)
;exitcodes=0 ; 'expected' exit codes used with autorestart (default 0)
;stopsignal=QUIT ; signal used to kill process (default TERM)
;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 ; setuid to this UNIX account to run the program
;redirect_stderr=false ; redirect_stderr=true is not allowed for eventlisteners
;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO
;stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10 ; # of stdout logfile backups (0 means none, default 10)
;stdout_events_enabled=false ; emit events on stdout writes (default false)
;stdout_syslog=false ; send stdout to syslog with process name (default false)
;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO
;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups=10 ; # of stderr logfile backups (0 means none, default 10)
;stderr_events_enabled=false ; emit events on stderr writes (default false)
;stderr_syslog=false ; send stderr to syslog with process name (default false)
;environment=A="1",B="2" ; process environment additions
;serverurl=AUTO ; override serverurl computation (childutils)

; The sample group section below shows all possible group values. Create one
; or more 'real' group: sections to create "heterogeneous" process groups.

;[group:thegroupname]
;programs=progname1,progname2 ; each refers to 'x' in [program:x] definitions
;priority=999 ; the relative start priority (default 999)

; The [include] section can just contain the "files" setting. This
; setting can list multiple files (separated by whitespace or
; newlines). It can also contain wildcards. The filenames are
; interpreted as relative to this file. Included files *cannot*
; include files themselves.

;[include]
;files = relative/directory/*.ini

注意:由于supervisor运行在pipenv虚拟环境下,为了将root用户的环境变量复制一份传递到supervisor的子进程中,需要配置environment参数变量
详见supervisor 加载系统环境变量问题Supervisor and Environment Variables

九.发布步骤

部署成功后,后续应用更新的发布步骤如下:

1.本地提交代码

git commit -m '新增功能***'
git push

2.服务器拉取代码

cd /app/run/www
git pull https://gitlab.www.com.cn/88888/automation.git
输入工号密码

3.重启应用

cd /app/run/www/AutoDB
pipenv shell
supervisorctl restart adbo

十.后记

  本次部署过程较为曲折坎坷,踩了很多坑,也收获了很多,对gunicorn、gevent、supervisor、异步同步、nginx、ssl证书、vue的路由模式等技术有了更深入的理解。
  最值得一提的是:代码中有使用threading的Thread类实现耗时业务的处理,不等处理完就返回值给前端,以提升web页面的用户体验,包括运维脚本自动化执行模块、实时查询自助服务模块等功能。这些功能在本机调试app.run()跑没有问题,但是部署上去后发现后端处理请求时会等到耗时业务执行完成后才返回,无法达到预期效果。
  反复排查gunicorn、supervisor、nginx的日志,却没有发现任何问题,代码调试也没有任何报错,百思不得其解,甚至怀疑时空是不是出现了错乱。在否定与自我否定中不断测试验证,万幸发现了一个特例:spt用户密码自助申请,这个功能也是耗时业务,但是部署上去后具有预期效果。OK,开始深扒代码逻辑
  该功能处理逻辑如下:
  1.连接数据库,执行修改数据库spt用户密码的sql脚本
  2.根据用户申请时间,time.sleep相应时间
  3.到时间后,再次连接数据库,将spt用户的密码修改为原密码
  业务逻辑本身看不出什么特别之处,只能比对分析,找了实时查询模块的例子进行对比分析
  1.连接数据库,执行查询数据库状态的sql脚本,结束
  一对比,恍然大悟!关键点就在time.sleep!!!
  分析如下:
  本地app.run采用的是flask框架自带的网络模式,是阻塞型web模式,thread调用的是threading原生的代码,原来的设计是遇到耗时任务时,新建一个线程去处理,web主线程不管结果出没出直接返回,因此可以满足不让前端用户等待的需求。
  而部署方案采用gunicorn+gevent的模式,也就是通过协程的方式实现非阻塞,此时每个网络请求都由协程去处理,由于协程与线程既不对等也不是上下级关系(具体差别详见进程、线程和协程之间的区别和联系),按照原来的设计方式,在协程A中调用新线程thread,协程A会等待该线程的函数堆栈处理完才能返回(个人理解此时协程A启动的新线程B还是在A的上下文中,不是独立的新的线程,因此如果要结束A协程,必须要等到它发起的B线程执行完毕后才能结束)。
  原理搞清楚了,问题关键在于协程调用线程,怎么解决呢?答案秒出:协程搞线程搞不定,搞协程总能搞定吧~原理不懂的继续看进程、线程和协程之间的区别和联系(Ps:为自己的机智打call)
  解决方案:由于gevent是猴子补丁的一种(不懂请看关于Monkey Patch猴子补丁),利用greenlet可以动态将运行的代码改成协程模式,查询官网及gevent源码发现,该补丁会对Socket、time等模块进行动态修改,比如将time.sleep修改为greenlet.sleep等,因此,只要代码中调用sleep函数,就会进入新的协程,这也说明了spt申请功能为何可以多协程运行而不影响主协程(实际上,spt申请功能第1步并没有进入一个新的协程,是在第2步开始才因为time.sleep进入新的协程,由于第1步执行时间短而第2步sleep时间长,因此,笔者一开始分析时误认为其完美实现耗时任务与前端及时响应相分离的需求)。
  因此,最简单的懒人方式,在脚本自动化执行的函数开头新增一行代码:time.sleep(1),欢迎进入协程的时代!重新发布并验证,完美解决问题~

  本次部署历经千辛万苦,成功的一刻真想仰天长啸!感谢坚持前进的自己,感谢我文中和文末所链接的所有文章的作者!

import time

#实时查询执行脚本
#脚本状态:等待执行/执行中/执行异常/执行成功/取消执行
#作业状态:执行中/执行成功/执行异常
def rt_doscript(jobid):
    time.sleep(1)
    returnflag=False
    jobdata=RTJOB.query.get(jobid)
    scriptlist=jobdata.scriptlist
    #遍历每个脚本,读取要执行的对象列表,遍历对象列表,连接每一个对象,执行脚本,调用rt_writestatus记录执行结果,写入日志,实现操作与日志分离
    for index,scriptdata in enumerate(scriptlist):
        #写入开始时间
        rtscript_writebegin(jobdata.id,index,datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
......

参考文档
http://docs.gunicorn.org/en/stable/settings.html#ssl
http://www.zhangdongshengtech.com/article-detials/81
https://www.cnblogs.com/songxingzhu/p/8568432.html
https://www.cnblogs.com/ameile/p/7447232.html
https://www.jianshu.com/p/a83a8f5d68dd?utm_campaign=maleskine&utm_content=note&utm_medium=writer_share&utm_source=weibo
https://blog.csdn.net/u012965373/article/details/52066580
https://www.cnblogs.com/cwp-bg/p/8780204.html
https://www.58jb.com/html/175.html
https://www.jianshu.com/p/09e522b8d64b
https://blog.csdn.net/xudailong_blog/article/details/80490137
https://blog.csdn.net/xudailong_blog/article/details/80821326
https://www.cnblogs.com/zhuzhenwei918/p/6892066.html
https://segmentfault.com/a/1190000010151973?utm_source=tag-newest
https://www.cnblogs.com/robert871126/p/10107258.html

上一篇:greenDao android开源框架数据库更新表的问题


下一篇:android自带theme