gunicorn + Flask架構中使用多程序全域性鎖
有之前的認識WSGI和WSGI的前世今世之後,現在就可以介紹如何在gunicorn + Flask架構模式下,在Flask處理執行緒中使用全域性鎖。
說到鎖在Python中也有很多鎖,最常見用的就是多程序鎖(multiprocessing.Lock)和多執行緒鎖(threading.Lock)。正常情況下,我們是可以直接使用這些鎖的。多程序鎖可以在多個子程序中實現鎖定臨界資源的功能,而多執行緒鎖則只在子執行緒中可以鎖定臨界資源。
而一旦你使用了gunicorn + Flask的架構。gunicorn就會啟動多個worker子程序,每個子程序可以看做是一個獨立的Flask程序。現在需要在所有worker程序中的Flask應用內申請鎖資源,並且該鎖資源需要在其它worker中是互斥的。
在不改變gunicorn原始碼的情況下,我們無法在主程序中建立一個鎖,之後在子程序中直接使用;並且在gunicorn呼叫flask介面的時候也未提供傳入額外引數的介面。所以在一開始的時候,並未找到讓各Falsk子程序共享鎖的方法。通過網上的相關查詢,覓得另外的解決方案:通過實現一個脫離Flask程序的外部鎖,比如:db記錄鎖、redis記錄鎖、檔案鎖等方式。最終我選擇了使用檔案鎖來解決Flask程序之間的鎖共享問題。檔案鎖程式碼如下:
該檔案鎖類可以提供一個基於外部檔案資源的鎖,在Windows環境下其實並不能完全鎖定資源,小概率的情況下會鎖定失敗。但在linux下則會正常工作,因為在linux下使用了檔案鎖模組,它可以確保加鎖過程是原子操作。這個鎖的使用方法也很簡單:#!/usr/bin/env python # coding=utf-8 # # File Lock For Multiple Process import os import time WINDOWS = 'windows' LINUX = 'linux' SYSTEM = None try: import fcntl SYSTEM = LINUX except: SYSTEM = WINDOWS class Lock(object): @staticmethod def get_file_lock(): return FileLock() class FileLock(object): def __init__(self): lock_file = 'FLASK_LOCK' if SYSTEM == WINDOWS: lock_dir = os.environ['tmp'] else: lock_dir = '/tmp' self.file = '%s%s%s' % (lock_dir, os.sep,lock_file) self._fn = None self.release() def acquire(self): if SYSTEM == WINDOWS: while os.path.exists(self.file): time.sleep(0.01) #wait 10ms continue with open(self.file, 'w') as f: f.write('1') else: self._fn = open(self.file, 'w') fcntl.flock(self._fn.fileno(), fcntl.LOCK_EX) self._fn.write('1') def release(self): if SYSTEM == WINDOWS: if os.path.exists(self.file): os.remove(self.file) else: if self._fn: try: self._fn.close() except: pass
只要在flaskapp中注入檔案鎖,此外在其它模組中通過current_app模組即可獲取到該鎖。from flask import Flask, current_app from lock import Lock app = Flask(__name__) app.lock = Lock.get_file_lock() ##給app注入一個外部鎖 @app.route("/") def hello(): current_app.lock.acquire() ##獲取鎖 current_app.lock.release() ##釋放鎖 return "Hello World!"
通常上述方法就可以解決Flask子程序之間的鎖共享問題了,但是如果你的環境必須是windows的,並且不能有任何的鎖定失敗情況,那麼你還是得查詢其它可用的方法。黃天不負有心人,gunicorn雖然預設沒有說支援注入鎖到flask程序的介面,但是它還是需要與flask通訊的。基於前面WSGI的文章,我們可以知道它們通訊的方式其實就是呼叫WSGI的介面。而該介面支援2個引數:
- 第一個引數:當前請求的相關資訊,比如:頭資訊、請求引數
- 第二個引數:一個返回狀態碼和響應頭的回撥函式
仔細想想第一個引數可能還是可以利用的,所以如果我們在這之前把鎖物件也能塞到這個引數中,那我們其實就可以獲取到外部的鎖了。因為該引數收集的基本上是請求頭資訊,所以如果我們可以把鎖把塞到請求頭,會如何呢?
正好gunicorn有server hook回撥函式,可以支援我們在server和worker工作的期間進行相關的物件操作。其中一個就是操作請求物件(request),所以我們現在就可以往請求頭中新增Lock物件了。新增之後能不能傳遞到flask子程序呢?測試一下即可。
首先,建立一個WSGI的app應用檔案app.py,內容如下:
#app.py
def app(environ, start_response):
print environ
data = b"Hello, World!\n"
start_response("200 OK", [
("Content-Type", "text/plain"),
("Content-Length", str(len(data)))
])
return iter([data])
接著,建立一個gunicorn的配置檔案cnf.py,內容如下:
#cnf.py
import multiprocessing
lock = multiprocessing.Lock()
def pre_request(worker, req):
req.headers['FLASK_LOCK'] = lock
pre_request = pre_request
bind = '0.0.0.0:8000'
workers = multiprocessing.cpu_count()
worker_class = 'gevent'
之後,我們就可以啟動gunicorn來測試下lock物件是否被傳遞給了WSGI介面引數中。執行如下命令:
gunicorn -c cnf.py fapp:app
最後,還需要在本地瀏覽器中訪問http://localhost:8000/來檢視執行結果。很開心的是這個lock物件【HTTP_FLASK_LOCK】被當作正常的請求頭資訊傳遞給了app介面。列印的效果如下:
{'HTTP_COOKIE': 'token=b3bdd0b4a64d7bd14851067a2775a71955d3ba8feyJpZCI6IDJ9; session=eyJ0b2tlbiI6eyJpZCI6Mn19.DO1Geg.LrqGWGQXeBEwKy-zw49rbi5Q4XY', 'SERVER_SOFTWARE': 'gunicorn/19.7.1', 'SCRIPT_NAME': '', 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/', 'SERVER_PROTOCOL': 'HTTP/1.1', 'QUERY_STRING': '', 'HTTP_FLASK_LOCK': <Lock(owner=None)>, 'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36', 'HTTP_CONNECTION': 'keep-alive', 'REMOTE_PORT': '10648', 'SERVER_NAME': '0.0.0.0', 'REMOTE_ADDR': '172.16.1.167', 'wsgi.url_scheme': 'http', 'SERVER_PORT': '88', 'wsgi.input': <gunicorn.http.body.Body object at 0x131c650>, 'HTTP_HOST': '172.16.1.156:88', 'wsgi.multithread': True, 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_CACHE_CONTROL': 'max-age=0', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'wsgi.version': (1, 0), 'RAW_URI': '/', 'wsgi.run_once': False, 'wsgi.errors': <gunicorn.http.wsgi.WSGIErrorsWrapper object at 0x131c7d0>, 'wsgi.multiprocess': True, 'HTTP_ACCEPT_LANGUAGE': 'zh-CN,zh;q=0.9', 'gunicorn.socket': <socket at 0x12be650 fileno=15 sock=172.16.1.156:88 peer=172.16.1.167:10648>, 'wsgi.file_wrapper': <class 'gunicorn.http.wsgi.FileWrapper'>, 'HTTP_ACCEPT_ENCODING': 'gzip, deflate'}
所以,結論是如果我們在gunicorn的server hook回撥函式中對request物件進行追加內容,那麼gunicorn會原封不動的給傳遞給WSGI介面函式。那麼,還有一個問題是在Flask中,要如何獲取這個lock物件呢?因為Flask已經對WSGI介面進行了封裝,我們正常是無法訪問其WSGI介面函式的引數。而由於gunicorn傳遞的引數都是請求頭資訊,所以第一時間可想到的可能物件,應該就是Flask的request物件。因為request物件中有headers物件,它就是存放當前請求的頭資訊,與之前新增lock時追加到headers物件相呼應。所以可以來測試一把。
新建一個Flask應用檔案fapp.py,內容如下:
##fapp.py
from flask import Flask, request
app = Flask(__name__)
@app.route("/")
def hello():
print request.headers
return "Hello World!"
啟動gunicorn命令:gunicorn -c cnf.py fapp:app
本地瀏覽器訪問http://localhost:8000檢視執行結果:又一次很開心的開到了lock【Flask-Lock】的存在。列印結果如下:
Flask-Lock: <Lock(owner=None)>
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36
Connection: keep-alive
Host: 172.16.1.156:88
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate
但是,在進一步讀取Flask-Lock頭資訊時,發現其值卻是一個str型別。所以,我們並沒有真正的獲取到lock物件。黃天不負有心人,在我再一次檢視request物件的成員時,很雞賊的發現有一個environ成員。於是毫不猶豫的打印出來,發現這個environ其實就是gunicorn呼叫WSGI介面函式時傳遞的那個environ引數。所以在Flask中正確獲取全域性鎖的姿勢是:
lock = request.environ['HTTP_FLASK_LOCK']
完整的程式碼內容如下:##fapp.py
from flask import Flask, request
app = Flask(__name__)
@app.route("/")
def hello():
lock = request.environ['HTTP_FLASK_LOCK']
lock.acquire()
##do something
lock.release()
return "Hello World!"
再次重啟gunicorn命令, 你的業務程式碼就可以正常獲取全域性的程序鎖了。