在 Flask 應用中使用 gevent
普通的 flask 應用
通常在用 python 開發 Flask web 應用時,使用 Flask 自帶的調試模式能夠給開發帶來極大便利。Flask 自帶的調試模式可以讓我們在程序改動時自動重新加載我們的應用程序,而且 jinja2 的模板也會隨着改動自動刷新。一般用法是:
# app.py
from flask import Flask
app = Flask( __name__ )
@app.route( '/')
def hello():
return 'Hello World'
if __name__ == '__main__':
app.run( debug = True )
運行上面這個例子,就可以在本地的 5000 端口運行由 flask 提供的服務器程序。如果我們對這個文件進行修改,那么 flask 的底層框架 werkzuge 檢測到文件變動后就會自動重新加載我們的應用程序。
然而 Flask 是單線程運行,如果在某個頁面中執行了一些耗時的工作,那么程序就會在這里等待,無法響應其他的請求。也就是說,如果一個路由響應函數中有阻塞代碼,那么其他用戶無法訪問這個 web 服務器,而且自己也打不開其他頁面了。
在一個路由中添加阻塞代碼,如下所示:
# app.py
from time import sleep
@app.route('/testsleep')
def test_sleep():
sleep( 10 )
return 'Hi, You wait for about 10 seconds, right?'
當打開 /testsleep 頁面時,會發現瀏覽器一直在加載過程中,再去打開 / 頁面,發現這個頁面也是在加載中。只有等到 /testsleep 頁面加載完了,才會去加載 / 頁面。
在 flask 中使用 gevent
為了解決一個頁面耗時導致所有頁面都無法訪問的問題。考慮使用 gevent 非阻塞的運行服務器程序。在引入 gevent 前,可以在程序最開始執行的位置引入猴子補丁 gevent.monkey,這能修改 python 默認的 IO 行為,讓標准庫變成 協作式(cooperative)的 API。注意引入 gevent 后,不能再用原來的方式啟動我們的 web 應用了:
# app.py
from gevent import monkey
monkey.patch_all() # 打上猴子補丁
from flask import flask
...
if __name__ == '__main__':
from gevent import pywsgi
app.debug = True
server = pywsgi.WSGIServer( ('127.0.0.1', 5000 ), app )
server.serve_forever()
這個時候再去打開 /testsleep 頁面,還是要等待一些時間才會加載完頁面,但是這個時候已經訪問 / 頁面將會立即加載完畢。
啟用調試模式和自動刷新模板
如果在某個頁面中的代碼有問題,會出現運行時錯誤,那么訪問這個頁面只能看到 Internal Server Error 的提示,沒有了之前的調試窗口和錯誤信息。而且在上面的代碼中,我已經將 app 的 debug 標志設為了真,然而並沒有什么用。為了啟用調試模式,方便在開發時看到錯誤信息,我們需要用到 werkzuge 提供的 DebuggedApplication。
# app.py
if __name__ == '__main__':
from werkzeug.debug import DebuggedApplication
dapp = DebuggedApplication( app, evalex= True)
server = pywsgi.WSGIServer( ( '127.0.0.1', 5000 ), dapp )
server.serve_forever()
重新打開首頁,可以看到熟悉的錯誤信息。
如果你使用了模板,那么你可能已經注意到了,使用 gevent 后修改模板再次訪問可能也不會看到頁面上有相應的改動。那么你需要在修改 app 的配置,以便模板能夠自動刷新,以下兩種方式是等效的:
app.jinja_env.auto_reload = True
app.config['TEMPLATES_AUTO_RELOAD'] = True
嘗試自動重新加載
使用了 gevent 后,原有的一些功能都需要通過一定的配置之后才可以正常訪問。但是有一個功能我們仍然沒有解決,那就是修改代碼后 web 應用不會自動重新加載了。stackoverflow 和 gist 提到的一種解決方法是使用 werkzeug 提供的 run_with_reloader,可以寫出這樣的代碼:
# app.py
...
if __name__ == '__main__':
...
from werkzeug.serving import run_with_reloader
server = pywsgi.WSGIServer( ( '127.0.0.1', 5000 ), dapp )
run_with_reloader( server ).serve_forever()
然而如果你這樣做了就會發現一點用都沒有,甚至連 web 應用都不能正常啟動了。
按照這個思路來的還有這段代碼提供的 示例,但這個示例是將 run_with_reloader 作為裝飾器來使用,以下是該示例的代碼:
import gevent.wsgi
import werkzeug.serving
@werkzeug.serving.run_with_reloader
def runServer():
app.debug = True
ws = gevent.wsgi.WSGIServer(('', 5000), app)
ws.serve_forever()
然而這也沒有什么作用。看一下 flask 的源代碼可以發現,run_with_reloader 已經不是裝飾器了。而且開發者提醒我們不要使用下面的這個函數,這個 api 很明顯已經被廢棄了,flask 源代碼如下:
def run_with_reloader(*args, **kwargs):
# People keep using undocumented APIs. Do not use this function
# please, we do not guarantee that it continues working.
from werkzeug._reloader import run_with_reloader
return run_with_reloader(*args, **kwargs)
如果使用 gevent 作為 WSGI 的網關服務器,似乎就沒法使用自動加載應用的功能了。
實現自動重新加載
沒有其他可以借鑒的方法了,好在之前在查看廖雪峰的 Python 教程時,給出了一個自動重新加載應用的示例,主要原理是利用 watchdog 提供的文件監聽功能,在創建、修改文件時會觸發相應的處理器,這樣就可以實現自動重新加載功能。代碼可以去廖雪峰的教程中查看。
之后的應用啟動時就不能直接使用 python app.py 了。如果將自動加載的代碼保存在同級的 monitor.py 文件中,我們需要使用 python monitor.py app.py 啟動應用。最終就可以自動熱加載我們的 web 應用了。
關於文件改動事件,之前我也寫過一個類似的 JS 程序,原理類似,都是當文件改動時自動執行重新構建應用的命令。
相應的說明代碼在 github 上可以查看。
