因?yàn)镕lask比較容易上手,之前也拿flask寫過幾個(gè)小項(xiàng)目,不過當(dāng)時(shí)天真地以為只要在服務(wù)器上nohup跑一個(gè)python腳本就算是成功發(fā)布了這個(gè)flask項(xiàng)目。實(shí)際上這還面臨很多問題,比如并發(fā)性不好,不支持異步(雖然也可以在run里面加上threaded之類的參數(shù)來解決,但終究不是正途)等等。真正通用的做法應(yīng)該是用某些web容器來啟動(dòng)項(xiàng)目。接下來說明做法,整個(gè)過程主要參考了這篇文章(https://segmentfault.com/a/1190000004294634)
我測試部署的系統(tǒng)是CentOS7 x86_64,環(huán)境搭建部分(包括安裝python,安裝flask以及flask相關(guān)依賴)的工作就跳過了。從安裝uWSGI開始講起。
■ uwsgi的安裝和配置
uWSGI是一個(gè)由python實(shí)現(xiàn)的web容器,可以兼容性比較好地發(fā)布Django,F(xiàn)lask等pythonweb框架的應(yīng)用。因?yàn)楸举|(zhì)上來說uwsgi是python的一個(gè)模塊,所以可以用pip install uwsgi直接來安裝它。
安裝完成之后可以在一個(gè)合適的目錄建立一個(gè)uwsgi服務(wù)器的配置文件。比如我選擇在項(xiàng)目的根目錄建立了一個(gè)uwsgiconfig.ini的文件。順便一提,除了ini格式的配置,uwsgi還支持json,xml等多種多樣的配置格式。這里以ini格式為例。
一個(gè)典型的配置文件如下:
復(fù)制代碼
[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
復(fù)制代碼
依次解釋一下這些配置項(xiàng)。socket指出了一個(gè)套接字,相當(dāng)于為外界留出一個(gè)uwsgi服務(wù)器的接口。需要注意的是,socket不等于http。換句話說用這個(gè)配置起來的uwsgi服務(wù)器是無法直接通過http請求成功訪問的,這一點(diǎn)后面還會(huì)提到,是遇到的一個(gè)坑。
pythonpath指出了項(xiàng)目的目錄,module指出了項(xiàng)目啟動(dòng)腳本的名字而緊接著的wsgi-file指出了真正的腳本的文件名。callable指出的是具體執(zhí)行.run方法的那個(gè)實(shí)體的名字,一般而言都是app=Flask(__name__)的所以這里是app。processes和threads指出了啟動(dòng)uwsgi服務(wù)器之后,服務(wù)器會(huì)打開幾個(gè)并行的進(jìn)程,每個(gè)進(jìn)程會(huì)開幾條線程來等待處理請求,顯然這個(gè)數(shù)字應(yīng)該合理,太小會(huì)使得處理性能不好而太大則會(huì)給服務(wù)器本身帶來太大負(fù)擔(dān)。daemonize項(xiàng)的出現(xiàn)表示把uwsgi服務(wù)器作為后臺進(jìn)程啟動(dòng),項(xiàng)的值指向一個(gè)文件表明后臺中的所有輸出都重定向到這個(gè)日志中去。
以上這些配置項(xiàng)都是一些最為常見的配置項(xiàng),實(shí)際上uwsgi還有很多很多配置。。除了寫一個(gè)配置文件的啟動(dòng)方式之外,還有命令行的啟動(dòng)方式,這里就不多說了。請需要的自己百度。?!颈浮?/p>
此外上面也說到這次碰到的一個(gè)坑,就是關(guān)于socket和http的差別。從概念上來說,socket本身不是協(xié)議而是一種具體的TCP/IP實(shí)現(xiàn)方式,而HTTP是一種協(xié)議且基于TCP/IP。具體到這個(gè)配置這里來,如果我只配了socket = 127.0.0.1:5051的話,通過瀏覽器或者其他HTTP手段是無法成功訪問的。而在uwsgi這邊的日志里會(huì)提示請求包的長度超過了最大固定長度。另一方面,如果配置的是http = 127.0.0.1:5051的話,那么就可以直接通過一般的http手段來訪問到目標(biāo)。但這會(huì)引起nginx無法正常工作。正確的做法應(yīng)該是,如果有nginx在uwsgi之前作為代理的話應(yīng)該配socket,而如果想讓請求直接甩給uwsgi的話那么就要配http。
配置完成之后就可以鍵入 uwsgi 配置文件.ini來啟動(dòng)uwsgi,再查看日志(如果配置了daemonize的話)如果最終沒有報(bào)錯(cuò),ps也能看到processes指定個(gè)數(shù)的uwsgi進(jìn)程在跑的話說明成功啟動(dòng)。如果直接把uwsgi作為留給外部的連接接口發(fā)布應(yīng)用的話當(dāng)然也可以,但是一般而言我們肯定還要在uwsgi前面再加上一個(gè)nginx。nginx的好處在于可以進(jìn)行安全過濾,防DDOS攻擊,多臺機(jī)器的負(fù)載均衡等工作。
關(guān)于uwsgi服務(wù)器的停止,官方文檔說可以uwsgi -HUP之類的命令操作,但是這需要找到這個(gè)uwsgi的pid,目前為止我都還是很粗暴地killall -9 uwsgi了。。
■ nginx的安裝和配置
最開始用yum install nginx裝了好多此還是報(bào)缺少libpcre.so.0的錯(cuò),網(wǎng)上搜了一通發(fā)現(xiàn)可能是因?yàn)槲矣玫氖荂entOS7版本的系統(tǒng)而yum源中還是適用于CentOS6的包。所以不如去網(wǎng)上找個(gè)rpm包或者直接下個(gè)源碼包來編譯安裝。。。
nginx常用命令:
nginx 啟動(dòng)nginx
nginx -s stop/reload 停止nginx/重載配置文件
nginx -v 查看版本
nginx -t 測試配置文件是否有語法上的錯(cuò)誤等
安裝完成后默認(rèn)的nginx的配置文件位于/etc/nginx/conf.d/default.conf,我直接修改了這個(gè)文件。在修改之前可以考慮先備個(gè)份。如果需要指定配置文件開啟nginx可以加入-c參數(shù)。其實(shí)nginx默認(rèn)讀取的文件是/etc/nginx/nginx.conf,打開這個(gè)文件看看可以看到在其http塊中有些include /etc/nginx/conf.d/*.conf,所以在那里的default.conf可以直接寫server塊。
之前也了解過一點(diǎn)關(guān)于nginx的配置問題,其要義大概就是nginx的配置文件格式比較要緊,比如要有大括號,句尾有分號等等。另外以#開頭的行都是注釋,都可以不用管。在nginx的這個(gè)配置中我們主要修改以下內(nèi)容:
復(fù)制代碼
? ? server {
? ? ? ? listen? ? ? 80;? ? ? ? //默認(rèn)的web訪問端口
? ? ? ? server_name? xxxxxx;? ? //服務(wù)器名
? ? ? ? #charset koi8-r;
? ? ? ? access_log? /home/wyz/flask/logs/access.log;? ? //服務(wù)器接收的請求日志,logs目錄若不存在需要?jiǎng)?chuàng)建,否則nginx報(bào)錯(cuò)
? ? ? ? error_log? /home/wyz/flask/logs/error.log;? ? ? ? //錯(cuò)誤日志
? ? ? ? location / {
? ? ? ? ? ? include? ? ? ? uwsgi_params;? ? //這里是導(dǎo)入的uwsgi配置
? ? ? ? ? ? uwsgi_pass? ? 127.0.0.1:5051;? //需要和uwsgi的配置文件里socket項(xiàng)的地址
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //相同,否則無法讓uwsgi接收到請求。
? ? ? ? ? ? uwsgi_param UWSGI_CHDIR? /home/wyz/flask;? ? //項(xiàng)目根目錄
? ? ? ? ? ? uwsgi_param UWSGI_SCRIPT manage:app;? ? //啟動(dòng)項(xiàng)目的主程序(在本地上運(yùn)行
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //這個(gè)主程序可以在flask內(nèi)置的
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //服務(wù)器上訪問你的項(xiàng)目)
}
}
復(fù)制代碼
這樣配置完后,當(dāng)外部有一個(gè)80端口的請求送到本機(jī)時(shí),先讓nginx開始處理。nginx進(jìn)行一些處理之后轉(zhuǎn)發(fā)給這里配置的uwsgi_pass地址,剛好傳送給uwsgi處理。再由uwsgi來調(diào)用項(xiàng)目中的代碼處理請求返回。再來回味一下上面那個(gè)坑,如果當(dāng)時(shí)僅僅配了一個(gè)http項(xiàng)而沒有配置socket的話,就會(huì)導(dǎo)致一切容器啟動(dòng)都順利,但是當(dāng)我把請求發(fā)送給80端口的時(shí)候遲遲不來響應(yīng),直到超時(shí)。
* 經(jīng)網(wǎng)友提醒,這其實(shí)是一個(gè)Nginx和uWSGI之間配置協(xié)同的一個(gè)問題。如果uWSGI直接通過HTTP方式對外提供服務(wù),那么nginx中需要配置proxy_pass,指出HTTP服務(wù)具體套接字,從而實(shí)現(xiàn)請求的轉(zhuǎn)發(fā)(參考zabbix安裝時(shí)的nginx配置就是這樣的)。而如果將uWSGI配置為socket,通過socket對外提供服務(wù)(由于socket不涉及具體的協(xié)議,外部沒法直接通過uWSGI端口訪問服務(wù)也更加安全一些。比如可以在nginx中配置一些URL的拒接防止sql注入之類的),那么nginx配置就應(yīng)該得是uwsgi_pass來實(shí)現(xiàn)請求的轉(zhuǎn)發(fā)。 proxy_pass配置的時(shí)候?qū)慼ttp://,即表示是走h(yuǎn)ttp協(xié)議的;uwsgi_pass的時(shí)候未指出協(xié)議,表示走socket。
當(dāng)應(yīng)用開始運(yùn)行起來之后,我的這個(gè)項(xiàng)目根目錄的結(jié)構(gòu)是這樣的;
其中access.log和error.log分別記錄了送到nginx處的請求的記錄以及nginx部分中發(fā)生的錯(cuò)誤的記錄。項(xiàng)目的入口app.run被寫在manage.py中,server.log記錄的則是uwsgi服務(wù)器的運(yùn)行狀況。
以上項(xiàng)目還是一個(gè)非常簡單的flask項(xiàng)目,不知道隨著代碼變復(fù)雜起來這么做來發(fā)布flask應(yīng)用會(huì)不會(huì)遇到各種各樣的問題。??傊巴具€是險(xiǎn)阻吶。
■ 部署websocket項(xiàng)目時(shí)的坑
不久前做了一個(gè)帶websocket的小flask項(xiàng)目,然而部署時(shí)歷經(jīng)各種問題。。最后都還是沒能完全解決。
首先是一個(gè),因?yàn)橐獛ebsocket所以我們需要在uwsgi啟動(dòng)的配置文件中寫上合適的配置項(xiàng),比如像下面這個(gè)一樣:
復(fù)制代碼
[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
復(fù)制代碼
project指出了項(xiàng)目目錄,%(project)是對已配置項(xiàng)project進(jìn)行一個(gè)取值,設(shè)置master是首先開啟一個(gè)uwsgi的管理進(jìn)程,然后由它開啟若干個(gè)worker子進(jìn)程,當(dāng)子進(jìn)程掛掉的時(shí)候還會(huì)自動(dòng)重啟。這些其實(shí)是對上面一般性配置描述的一個(gè)補(bǔ)充,并不是決定websocket特性的。
決定websocket特性的則是http-websockets,gevent,async這些配置項(xiàng),他們指出了通過這個(gè)配置文件啟動(dòng)的uwsgi進(jìn)程是支持websocket的(uwsgi版本在2.0之后才開始支持websocket)。另外還有一個(gè)很重要的改動(dòng):processes改成了1,并且注釋去掉了threads配置。如果不去掉threads,這會(huì)和gevent沖突,導(dǎo)致的現(xiàn)象就是通過nginx訪問uwsgi程序時(shí)總會(huì)返回502 bad gateway。如果processes設(shè)置大于1,那么導(dǎo)致的現(xiàn)象就是socket通信總是遲緩且沒有規(guī)律。這主要是因?yàn)閣ebsocket的通信是要基于一個(gè)sessionid的,而每個(gè)進(jìn)程接受請求時(shí)給出的sessionid都不同。uwsgi在做均衡的時(shí)候可能把發(fā)向某一個(gè)進(jìn)程的請求發(fā)給了另一個(gè)進(jìn)程,而那個(gè)進(jìn)程顯然沒有處理這個(gè)請求的上下文,導(dǎo)致返回400 bad request,所以在socket通信時(shí)總是會(huì)涌現(xiàn)出大量的400和502錯(cuò)誤。
把processes改成1顯然不是一個(gè)萬全之策,如此,性能上就出現(xiàn)了問題,這個(gè)要如何解決還有待研究。
然后貼出改造成兼容websocket之后的nginx配置,至少我是這么配置啟動(dòng)之后可以正常運(yùn)行:
復(fù)制代碼
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";
? ? }
}
復(fù)制代碼
■ 在一個(gè)nginx下部署多個(gè)應(yīng)用的location配置簡單說明
上述location配置可以保證我們直接訪問這個(gè)IP(端口默認(rèn)是80)就可以看到web應(yīng)用響應(yīng)的界面。但是有一個(gè)問題,如果這個(gè)機(jī)器上有好多應(yīng)用呢?此時(shí)應(yīng)該考慮在nginx的配置中體現(xiàn)出多應(yīng)用的方法。一個(gè)簡單的辦法就是多加幾條location配置來把指向不同URI的訪問路由到不同的應(yīng)用上去。
然而這個(gè)過程并沒有說說的這么簡單。比如沿用上面的例子,假如在這個(gè)nginx上我們還要部署一個(gè)到zabbix的路由,那么可以把配置文件改成這樣:(只寫location部分):
復(fù)制代碼
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;
}
復(fù)制代碼
把location /中間加上一個(gè)^~,是指出了URI從頭開始匹配“/”的將全部轉(zhuǎn)發(fā)到這個(gè)路由,當(dāng)然/zabbix/開頭的URI由于下面還配置了一條^~ /zabbix/,所以會(huì)轉(zhuǎn)發(fā)到zabbix下面去。這個(gè)匹配和轉(zhuǎn)發(fā)的詳細(xì)規(guī)則可以學(xué)習(xí)下nginx的配置明細(xì),就不再多說。
上面的location配置中,使用了include uwsgi_param,所以緊跟的配置項(xiàng)是uwsgi_pass,注意這個(gè)配置項(xiàng)無需也不能寫出http://和后面的URI,這也就意味著,原生請求的URI只能一一對應(yīng)到uwsgi_pass設(shè)置的值的這個(gè)根URL上去。考慮的這邊下面配置了^~ /zabbix/,所以綜合來看,除了http://xxxx:xx/zabbix/以及其他zabbix開頭的URI之外都會(huì)路由到5050端口的那個(gè)web應(yīng)用中去,并且請求URI不會(huì)被nginx做任何加工,比如原生請求指向http://xxxx:xx/a/b/c/ 那么最終路由到的地址就是127.0.0.1:5050/a/b/c/。這看起來似乎理所當(dāng)然,但是如果改成location ^~ /fullpack/ 呢,此時(shí)如果原生請求是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/,那么就可以發(fā)現(xiàn),原生的/zbx/a/b/請求將會(huì)路由到8881端口的/zabbix/a/b/請求。這證明了nginx對proxy_pass方式的配置收到的URI是有處理的。
■ 通過nginx訪問時(shí)自動(dòng)加末尾斜杠的問題
在上面的實(shí)驗(yàn)中,其實(shí)我遇到了一個(gè)小坑。就是配置完nginx之后訪問每次都是404,經(jīng)過原因排查,發(fā)現(xiàn)是這么回事:
在后端代碼中,我寫的是@app.route('/info',methods=['GET','POST'])這樣的。當(dāng)不使用uwsgi+nginx部署,而是用flask自帶的web服務(wù)器進(jìn)行測試時(shí),我訪問xxxx:xx/info,可以訪問到界面。但是通過nginx訪問時(shí),nginx會(huì)把所有末尾不帶斜杠的非文件類請求都加上斜杠,并且給出301回應(yīng),然后重定向到有斜杠的URL下。這可能是因?yàn)槠渌恍┍容^經(jīng)典的WEB開發(fā)語言中請求往往是一個(gè)文件如.php,.aspx,.html等,而python的框架實(shí)際上是把一個(gè)“目錄”節(jié)點(diǎn)作為一個(gè)html文件給出了。這就使得末尾要加上一個(gè)斜杠,才能讓nginx知道這是一個(gè)指向目錄的請求。
解決的辦法也很簡單,通過瀏覽器直接發(fā)起GET請求的頁面(也就是一定要經(jīng)過nginx訪問的),路由設(shè)置時(shí)記得加上末尾的斜杠就好了。因?yàn)椴煌^鍵盤打到瀏覽器地址欄這種方式的GET請求(比如頁面的一個(gè)超鏈接的href值,或者AJAX發(fā)起指向的URL)都是不會(huì)自動(dòng)補(bǔ)齊斜杠的,所以其他那些頁面也都不會(huì)受影響。另外加了斜杠的設(shè)置也可以估計(jì)沒加斜杠的請求,比如我改成@app.route('/info/')之后,瀏覽器地址欄里打/info會(huì)自動(dòng)補(bǔ)齊成/info/,而點(diǎn)擊頁面上href="/info"或者通過程序手段如requests.get('xxxx/info')也都可以訪問到那個(gè)頁面的。要是反過來,route('/info')而href="/info/"則不行。