因為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還有很多很多配置。。除了寫一個配置文件的啟動方式之外,還有命令行的啟動方式,這里就不多說了。請需要的自己百度。?!颈浮?/p>
此外上面也說到這次碰到的一個坑,就是關于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/"則不行。