【轉】利用uWSGI和Nginx發佈Flask應用

轉載自:https://www.cnblogs.com/franknihao/p/7202253.html

 

因爲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/"則不行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章