1. 程式人生 > >在 Flask 應用中使用 gevent

在 Flask 應用中使用 gevent

在 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 應用不會自動重新載入了。stackoverflowgist 提到的一種解決方法是使用 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 上可以檢視。

References

  1. gevent monkey
  2. hot reload gevent wsgiserver
  3. gist
  4. code snippet
  5. 廖雪峰 Python 教程