【Flask】 利用uWSGI和Nginx发布Flask应用

因为Flask比较容易上手,之前也拿flask写过几个小项目,不过当时天真地以为只要在服务器上nohup跑一个python脚本就算是成功发布了这个flask项目。实际上这还面临很多问题,比如并发性不好,不支持异步(虽然也可以在run里面加上threaded之类的参数来解决,但终究不是正途)等等。真正通用的做法应该是用某些web容器来启动项目。接下来说明做法,整个过程主要参考了这篇文章(https://segmentfault.com/a/1190000004294634)

  我测试部署的系统是CentOS7 x86_64,环境搭建部分(包括安装python,安装flask以及flask相关依赖)的工作就跳过了。从安装uWSGI开始讲起。

■  uwsgi的安装和配置

  uWSGI是一个由python实现的web容器,可以兼容性比较好地发布Django,Flask等pythonweb框架的应用。因为本质上来说uwsgi是python的一个模块,所以可以用pip install uwsgi直接来安装它。

  安装完成之后可以在一个合适的目录建立一个uwsgi服务器的配置文件。比如我选择在项目的根目录建立了一个uwsgiconfig.ini的文件。顺便一提,除了ini格式的配置,uwsgi还支持json,xml等多种多样的配置格式。这里以ini格式为例。

  一个典型的配置文件如下:

复制代码

[uwsgi]

socket = 127.0.0.1:5051

pythonpath = /home/wyz/flask

module = manage

wsgi-file = /home/wyz/flask/manage.py

callable = app

processes = 4

threads = 2

daemonize = /home/wyz/flask/server.log

复制代码

  依次解释一下这些配置项。socket指出了一个套接字,相当于为外界留出一个uwsgi服务器的接口。需要注意的是,socket不等于http。换句话说用这个配置起来的uwsgi服务器是无法直接通过http请求成功访问的,这一点后面还会提到,是遇到的一个坑。

  pythonpath指出了项目的目录,module指出了项目启动脚本的名字而紧接着的wsgi-file指出了真正的脚本的文件名。callable指出的是具体执行.run方法的那个实体的名字,一般而言都是app=Flask(__name__)的所以这里是app。processes和threads指出了启动uwsgi服务器之后,服务器会打开几个并行的进程,每个进程会开几条线程来等待处理请求,显然这个数字应该合理,太小会使得处理性能不好而太大则会给服务器本身带来太大负担。daemonize项的出现表示把uwsgi服务器作为后台进程启动,项的值指向一个文件表明后台中的所有输出都重定向到这个日志中去。

  以上这些配置项都是一些最为常见的配置项,实际上uwsgi还有很多很多配置。。除了写一个配置文件的启动方式之外,还有命令行的启动方式,这里就不多说了。请需要的自己百度。。【抱歉】

  此外上面也说到这次碰到的一个坑,就是关于socket和http的差别。从概念上来说,socket本身不是协议而是一种具体的TCP/IP实现方式,而HTTP是一种协议且基于TCP/IP。具体到这个配置这里来,如果我只配了socket = 127.0.0.1:5051的话,通过浏览器或者其他HTTP手段是无法成功访问的。而在uwsgi这边的日志里会提示请求包的长度超过了最大固定长度。另一方面,如果配置的是http = 127.0.0.1:5051的话,那么就可以直接通过一般的http手段来访问到目标。但这会引起nginx无法正常工作。正确的做法应该是,如果有nginx在uwsgi之前作为代理的话应该配socket,而如果想让请求直接甩给uwsgi的话那么就要配http。

  配置完成之后就可以键入 uwsgi 配置文件.ini来启动uwsgi,再查看日志(如果配置了daemonize的话)如果最终没有报错,ps也能看到processes指定个数的uwsgi进程在跑的话说明成功启动。如果直接把uwsgi作为留给外部的连接接口发布应用的话当然也可以,但是一般而言我们肯定还要在uwsgi前面再加上一个nginx。nginx的好处在于可以进行安全过滤,防DDOS攻击,多台机器的负载均衡等工作。

  关于uwsgi服务器的停止,官方文档说可以uwsgi -HUP之类的命令操作,但是这需要找到这个uwsgi的pid,目前为止我都还是很粗暴地killall -9 uwsgi了。。

■  nginx的安装和配置

  最开始用yum install nginx装了好多此还是报缺少libpcre.so.0的错,网上搜了一通发现可能是因为我用的是CentOS7版本的系统而yum源中还是适用于CentOS6的包。所以不如去网上找个rpm包或者直接下个源码包来编译安装。。。

  nginx常用命令:

  nginx  启动nginx

  nginx -s stop/reload  停止nginx/重载配置文件

  nginx -v  查看版本

  nginx -t  测试配置文件是否有语法上的错误等

  安装完成后默认的nginx的配置文件位于/etc/nginx/conf.d/default.conf,我直接修改了这个文件。在修改之前可以考虑先备个份。如果需要指定配置文件开启nginx可以加入-c参数。其实nginx默认读取的文件是/etc/nginx/nginx.conf,打开这个文件看看可以看到在其http块中有些include /etc/nginx/conf.d/*.conf,所以在那里的default.conf可以直接写server块。

  之前也了解过一点关于nginx的配置问题,其要义大概就是nginx的配置文件格式比较要紧,比如要有大括号,句尾有分号等等。另外以#开头的行都是注释,都可以不用管。在nginx的这个配置中我们主要修改以下内容:

复制代码

    server {

        listen      80;        //默认的web访问端口

        server_name  xxxxxx;    //服务器名

        #charset koi8-r;

        access_log  /home/wyz/flask/logs/access.log;    //服务器接收的请求日志,logs目录若不存在需要创建,否则nginx报错

        error_log  /home/wyz/flask/logs/error.log;        //错误日志

        location / {

            include        uwsgi_params;    //这里是导入的uwsgi配置

            uwsgi_pass    127.0.0.1:5051;  //需要和uwsgi的配置文件里socket项的地址

                                            //相同,否则无法让uwsgi接收到请求。

            uwsgi_param UWSGI_CHDIR  /home/wyz/flask;    //项目根目录

            uwsgi_param UWSGI_SCRIPT manage:app;    //启动项目的主程序(在本地上运行

                                                    //这个主程序可以在flask内置的

                                                    //服务器上访问你的项目)

}

}

复制代码

  这样配置完后,当外部有一个80端口的请求送到本机时,先让nginx开始处理。nginx进行一些处理之后转发给这里配置的uwsgi_pass地址,刚好传送给uwsgi处理。再由uwsgi来调用项目中的代码处理请求返回。再来回味一下上面那个坑,如果当时仅仅配了一个http项而没有配置socket的话,就会导致一切容器启动都顺利,但是当我把请求发送给80端口的时候迟迟不来响应,直到超时。

  * 经网友提醒,这其实是一个Nginx和uWSGI之间配置协同的一个问题。如果uWSGI直接通过HTTP方式对外提供服务,那么nginx中需要配置proxy_pass,指出HTTP服务具体套接字,从而实现请求的转发(参考zabbix安装时的nginx配置就是这样的)。而如果将uWSGI配置为socket,通过socket对外提供服务(由于socket不涉及具体的协议,外部没法直接通过uWSGI端口访问服务也更加安全一些。比如可以在nginx中配置一些URL的拒接防止sql注入之类的),那么nginx配置就应该得是uwsgi_pass来实现请求的转发。 proxy_pass配置的时候写http://,即表示是走http协议的;uwsgi_pass的时候未指出协议,表示走socket。

  当应用开始运行起来之后,我的这个项目根目录的结构是这样的;

  其中access.log和error.log分别记录了送到nginx处的请求的记录以及nginx部分中发生的错误的记录。项目的入口app.run被写在manage.py中,server.log记录的则是uwsgi服务器的运行状况。

  以上项目还是一个非常简单的flask项目,不知道随着代码变复杂起来这么做来发布flask应用会不会遇到各种各样的问题。。总之前途还是险阻呐。

■  部署websocket项目时的坑

  不久前做了一个带websocket的小flask项目,然而部署时历经各种问题。。最后都还是没能完全解决。

  首先是一个,因为要带websocket所以我们需要在uwsgi启动的配置文件中写上合适的配置项,比如像下面这个一样:

复制代码

[uwsgi]

project = /root/ICManage

pythonpath = /root/ICManage

wsgi-file = /root/ICManage/manage.py

chdir = %(project)

module = manage

callable = app

master = true

processes = 1

#threads = 2

socket = 127.0.0.1:5050

chmod-socket = 664

#buffer-size = 32768

http-websockets = 1

gevent = 1000

async = 30

daemonize = /home/hips/ICManage/uwsgi/logs/server.log

复制代码

  project指出了项目目录,%(project)是对已配置项project进行一个取值,设置master是首先开启一个uwsgi的管理进程,然后由它开启若干个worker子进程,当子进程挂掉的时候还会自动重启。这些其实是对上面一般性配置描述的一个补充,并不是决定websocket特性的。

  决定websocket特性的则是http-websockets,gevent,async这些配置项,他们指出了通过这个配置文件启动的uwsgi进程是支持websocket的(uwsgi版本在2.0之后才开始支持websocket)。另外还有一个很重要的改动:processes改成了1,并且注释去掉了threads配置。如果不去掉threads,这会和gevent冲突,导致的现象就是通过nginx访问uwsgi程序时总会返回502 bad gateway。如果processes设置大于1,那么导致的现象就是socket通信总是迟缓且没有规律。这主要是因为websocket的通信是要基于一个sessionid的,而每个进程接受请求时给出的sessionid都不同。uwsgi在做均衡的时候可能把发向某一个进程的请求发给了另一个进程,而那个进程显然没有处理这个请求的上下文,导致返回400 bad request,所以在socket通信时总是会涌现出大量的400和502错误。

  把processes改成1显然不是一个万全之策,如此,性能上就出现了问题,这个要如何解决还有待研究。

  然后贴出改造成兼容websocket之后的nginx配置,至少我是这么配置启动之后可以正常运行:

复制代码

server {

    listen      80;

    server_name  192.168.1.101;

    #charset koi8-r;

    access_log  /var/log/nginx/access.log;

    error_log  /var/log/nginx/error.log;

    location / {

        include uwsgi_params;

        uwsgi_pass 127.0.0.1:5050;

        proxy_http_version 1.1;

        proxy_set_header Upgrade $http_upgrade;

        proxy_set_header Connection "upgrade";

    }

}

复制代码

■  在一个nginx下部署多个应用的location配置简单说明

  上述location配置可以保证我们直接访问这个IP(端口默认是80)就可以看到web应用响应的界面。但是有一个问题,如果这个机器上有好多应用呢?此时应该考虑在nginx的配置中体现出多应用的方法。一个简单的办法就是多加几条location配置来把指向不同URI的访问路由到不同的应用上去。

  然而这个过程并没有说说的这么简单。比如沿用上面的例子,假如在这个nginx上我们还要部署一个到zabbix的路由,那么可以把配置文件改成这样:(只写location部分):

复制代码

location ^~ / {

    include uwsgi_param;

    uwsgi_pass 127.0.0.1:5050;

    proxy_http_version 1.1;

    proxy_set_header Upgrade $http_upgrade;

    proxy_set_header Connection "upgrade";

}

location ^~ /zabbix/ {

    proxy_pass http://127.0.0.1:8881/zabbix/;

    proxy_redirect default;

    proxy_set_header HOST $host;

    proxy_set_header X-Real-IP $remote_addr;

    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

}

复制代码

  把location /中间加上一个^~,是指出了URI从头开始匹配“/”的将全部转发到这个路由,当然/zabbix/开头的URI由于下面还配置了一条^~ /zabbix/,所以会转发到zabbix下面去。这个匹配和转发的详细规则可以学习下nginx的配置明细,就不再多说。

  上面的location配置中,使用了include uwsgi_param,所以紧跟的配置项是uwsgi_pass,注意这个配置项无需也不能写出http://和后面的URI,这也就意味着,原生请求的URI只能一一对应到uwsgi_pass设置的值的这个根URL上去。考虑的这边下面配置了^~ /zabbix/,所以综合来看,除了http://xxxx:xx/zabbix/以及其他zabbix开头的URI之外都会路由到5050端口的那个web应用中去,并且请求URI不会被nginx做任何加工,比如原生请求指向http://xxxx:xx/a/b/c/ 那么最终路由到的地址就是127.0.0.1:5050/a/b/c/。这看起来似乎理所当然,但是如果改成location ^~ /fullpack/ 呢,此时如果原生请求是http://xxxx:xx/upload/,那么最终路由到的是127.0.0.1:5050/fullpack/upload还是127.0.0.1:5050/upload/呢?答案是后者,也就是说nginx未对URI做任何加工。

  相反的,看通过proxy_pass方法配置的location。在下面的配置中如果原生请求是http://xxxx:xx/zabbix/a/b/c/,那么最终请求路由到的是127.0.0.1:8881/zabbix/a/b/c/,可以看成将原生的URI,去掉了开头的/zabbix/,然后再把剩余部分拼接到127.0.0.1:8881/zabbix/后面,虽然这里凑巧两边都是/zabbix/,但是如果把location的换成/zbx/,那么就可以发现,原生的/zbx/a/b/请求将会路由到8881端口的/zabbix/a/b/请求。这证明了nginx对proxy_pass方式的配置收到的URI是有处理的。

■  通过nginx访问时自动加末尾斜杠的问题

  在上面的实验中,其实我遇到了一个小坑。就是配置完nginx之后访问每次都是404,经过原因排查,发现是这么回事:

  在后端代码中,我写的是@app.route('/info',methods=['GET','POST'])这样的。当不使用uwsgi+nginx部署,而是用flask自带的web服务器进行测试时,我访问xxxx:xx/info,可以访问到界面。但是通过nginx访问时,nginx会把所有末尾不带斜杠的非文件类请求都加上斜杠,并且给出301回应,然后重定向到有斜杠的URL下。这可能是因为其他一些比较经典的WEB开发语言中请求往往是一个文件如.php,.aspx,.html等,而python的框架实际上是把一个“目录”节点作为一个html文件给出了。这就使得末尾要加上一个斜杠,才能让nginx知道这是一个指向目录的请求。

  解决的办法也很简单,通过浏览器直接发起GET请求的页面(也就是一定要经过nginx访问的),路由设置时记得加上末尾的斜杠就好了。因为不同过键盘打到浏览器地址栏这种方式的GET请求(比如页面的一个超链接的href值,或者AJAX发起指向的URL)都是不会自动补齐斜杠的,所以其他那些页面也都不会受影响。另外加了斜杠的设置也可以估计没加斜杠的请求,比如我改成@app.route('/info/')之后,浏览器地址栏里打/info会自动补齐成/info/,而点击页面上href="/info"或者通过程序手段如requests.get('xxxx/info')也都可以访问到那个页面的。要是反过来,route('/info')而href="/info/"则不行。

推荐阅读更多精彩内容