1. 程式人生 > >gunicorn + Flask架構中使用多程序全域性鎖

gunicorn + Flask架構中使用多程序全域性鎖

有之前的認識WSGIWSGI的前世今世之後,現在就可以介紹如何在gunicorn + Flask架構模式下,在Flask處理執行緒中使用全域性鎖。

說到鎖在Python中也有很多鎖,最常見用的就是多程序鎖(multiprocessing.Lock)和多執行緒鎖(threading.Lock)。正常情況下,我們是可以直接使用這些鎖的。多程序鎖可以在多個子程序中實現鎖定臨界資源的功能,而多執行緒鎖則只在子執行緒中可以鎖定臨界資源。

而一旦你使用了gunicorn + Flask的架構。gunicorn就會啟動多個worker子程序,每個子程序可以看做是一個獨立的Flask程序。現在需要在所有worker程序中的Flask應用內申請鎖資源,並且該鎖資源需要在其它worker中是互斥的。

在不改變gunicorn原始碼的情況下,我們無法在主程序中建立一個鎖,之後在子程序中直接使用;並且在gunicorn呼叫flask介面的時候也未提供傳入額外引數的介面。所以在一開始的時候,並未找到讓各Falsk子程序共享鎖的方法。通過網上的相關查詢,覓得另外的解決方案:通過實現一個脫離Flask程序的外部鎖,比如:db記錄鎖、redis記錄鎖、檔案鎖等方式。最終我選擇了使用檔案鎖來解決Flask程序之間的鎖共享問題。檔案鎖程式碼如下:

#!/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
該檔案鎖類可以提供一個基於外部檔案資源的鎖,在Windows環境下其實並不能完全鎖定資源,小概率的情況下會鎖定失敗。但在linux下則會正常工作,因為在linux下使用了檔案鎖模組,它可以確保加鎖過程是原子操作。這個鎖的使用方法也很簡單:
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!"
只要在flaskapp中注入檔案鎖,此外在其它模組中通過current_app模組即可獲取到該鎖。

通常上述方法就可以解決Flask子程序之間的鎖共享問題了,但是如果你的環境必須是windows的,並且不能有任何的鎖定失敗情況,那麼你還是得查詢其它可用的方法。黃天不負有心人,gunicorn雖然預設沒有說支援注入鎖到flask程序的介面,但是它還是需要與flask通訊的。基於前面WSGI的文章,我們可以知道它們通訊的方式其實就是呼叫WSGI的介面。而該介面支援2個引數:

  1. 第一個引數:當前請求的相關資訊,比如:頭資訊、請求引數
  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命令, 你的業務程式碼就可以正常獲取全域性的程序鎖了。