Python–Redis實戰:第五章:使用Redis構建支援程式:第4節:服務的發現與配置

Mark發表於2019-02-16

上一篇文章:Python–Redis實戰:第五章:使用Redis構建支援程式:第3節:查詢IP所屬城市以及國家

隨著我們越來越多地使用Redis以及其他服務,如何儲存各項服務的配置資訊將變成一個棘手的問題:對於一個Redis伺服器、一個資料庫伺服器以及一個Web伺服器來說,儲存它們的配置資訊並不困難;但如果我們使用了一個擁有好幾個從伺服器的Redis主伺服器,或者為不同的應用程式設定了不同的Redis伺服器,甚至為資料庫也設定了主伺服器和從伺服器的話,那麼儲存這些伺服器的配置資訊將變成一件讓人頭痛的事情。

用於連線其他伺服器以及伺服器的配置資訊一般都是以配置檔案的形式儲存在硬碟裡面,每當機器下線、網路連線斷開或者某些需要連線其他伺服器的情況出現時,程式通常需要一次性地對不同伺服器中的多個配置檔案進行更新。而這一節要介紹的就是如何將大部分配置資訊從檔案轉移到Redis裡面,使得應用程式可以自己完成絕大部分配置工作。

使用Redis儲存配置資訊

為了展示配置管理方面的難題是多麼的常見,來看一個非常簡單的配置例子:假設現在我們要用一個標誌flag來表示web伺服器是否正在進行維護,如果伺服器正在進行維護,那麼它就不應該傳送資料庫請求,而是應該向訪問們返回一條簡短的【抱歉,我們正在進行維護,請稍後重試】的資訊;相反,如果伺服器並沒有進行維護,那麼它就應該按照既定的程式來執行。

在通常情況下,即使只更新配置中的一個標誌,也會導致更新後的配置檔案被強制推送至所有Web伺服器,收到更新的伺服器可能需要重新載入配置、甚至可能還要重啟應用程式伺服器。

與其嘗試為不斷增多的服務寫入和維護配置檔案,不如讓我們直接配置寫入Redis裡面。只要將配置資訊儲存在Redis裡面,並編寫應用程式來獲取這些資訊,我們就不用再編寫工具來向伺服器推送配置資訊了,伺服器和程式也不用再通過載入配置檔案的方式 來更新配置資訊了。

為了實現這個簡單的功能,讓我們假設自己已經構建了一箇中間層或者外掛,這個中間層額作用在於:當is_under_maintenance()函式返回True時,它將向使用者顯示維護頁面;與此相反,如何is_under_maintenance()函式返回False,它將如常地處理使用者的訪問請求。其中is_under_maintenance()函式通過檢查一個名為is-under-maintenance的鍵來判斷伺服器是否正在進行維護:如果is-under-maintenance鍵非空,那麼函式返回True;否則返回False,另外,因為訪客在看見維護頁面的時候通常都會不耐煩的頻繁重新整理頁面,所以為了儘量降低Redis在處理高訪問量Web伺服器時的負載,is_under_maintenance()函式最多隻會每秒更新一次伺服器維護資訊。

下面程式碼展示了is_under_maintenance()函式的具體定義:

import time

LAST_CHECKED=None
IS_UNDER_MAINTENANCE=False

def is_under_maintenance(conn):
    #將連個變數設定為全域性變數以便在之後對它們進行寫入
    global LAST_CHECKED,IS_UNDER_MAINTENANCE
    #距離上次檢查是否以及超過1秒?
    if LAST_CHECKED<time.time()-1:
        #更新最後檢查時間
        LAST_CHECKED=time.time()
        #檢查系統是否正在進行維護
        IS_UNDER_MAINTENANCE=bool(conn.get(`is-under-maintenance`))
    #返回一個布林值,用於表示系統是否正在進行維護。
    return IS_UNDER_MAINTENANCE

通過將is_under_maintenance()函式插入應用程式的正確位置上,我們可以在1秒內改變數以千計Web伺服器的行為。為了降低Redis在處理高訪問量web伺服器時的負載,is_under_maintenance()函式將伺服器維護狀態資訊的更新頻率限制為最多每秒1次,但如果有需要的話,我們也可以加快資訊的更新頻率,甚至直接移除函式裡面限制更新速度的那些程式碼。雖然is_under_maintenance()函式看上去似乎並不實用,但它的確展示了將配置資訊儲存在一個普通可訪問位置的威力。

接下來我們要考慮的是,怎樣才能將更復雜的配置選項儲存到Redis裡面呢?

為每個應用程式元件分別配置一個Redis伺服器

在我們越來越多地使用Redis的過程中,無數的開發者已經發現,最終在某個時間點上,只使用一臺Redis伺服器將不能滿足我們的需要。因為我們可能需要記錄更多資訊,可能需要更多用於快取的空間,還可能會使用本書之後的章節會介紹的、使用Redis構建的高階服務。但不管何種原因,我們都需要用到更多Redis伺服器。

為了平滑地從單臺伺服器過渡到多臺伺服器,使用者最好還是為應用程式中的每個獨立部分都分別執行一個Redis伺服器,比如說,一個專門負責記錄日誌、一個專門負責記錄統計資料、一個專門負責進行快取、一個專門負責儲存cookies等。別忘了,一臺機器是可以執行多個Redis伺服器的,只要這些伺服器使用的埠號各不同就可以了。除此之外,在一個Redis伺服器裡面使用多個【資料庫】,也可以減少系統管理的工作量。以上提到的兩種方法,都是通過將不同資料劃分至不同鍵空間的方式,來或多或少的簡化遷移至更大或更多伺服器時所需的工作。但遺憾的是,隨著Redis伺服器的數量或者Redis資料庫的數量不斷增多,為所有Redis伺服器管理和分發配置資訊的工作將變得越來越煩瑣和無趣。

在上一節中,我們用了Redis來儲存表示伺服器是否正在進行維護的標誌,並通過這個標誌來決定是否需要向訪客顯示維護頁面。而這一次,我們同樣可以使用Redis來儲存與其他Redis伺服器有關的資訊。說的更詳細一點,我們可以把一個已知的Redis伺服器用作配置資訊字典,然後通過這個字典儲存的配置資訊來連線為不同應用或服務元件提供資料的其他Redis伺服器。此外,這個字典還會在配置出現變更時,幫助客戶端連線至正確的伺服器。字典的具體實現比這個例子所要求的更為通用一些,因為我敢肯定,當你開始使用這個字典來獲取配置資訊的時候,你很快就會把它應用到其他伺服器以及其他服務上面,而不僅僅用於獲取Redis伺服器的配置資訊。

我們將構建一個函式,該函式可以從一個鍵裡面取出一個JSON編碼的配置值,其中,儲存配置值的鍵由服務的型別以及使用該服務的應用程式命名。舉個例子,如何我們想要獲取連線儲存統計資料的Redis伺服器所需的資訊,那麼就需要獲取config:redis:statistics鍵的值。下面函式展示了設定配置值的具體方法:

def set_config(conn,type,component,config):
    conn.set(`config:%s:%s`%(type,component))
    json.dumps(config)

通過這個函式,我們可以隨心所欲的設定任何JSON編碼的配置資訊。因為get_config()函式和前面介紹過的is_under__maintenance()函式具有相似的結構,所以我們只要在語義上稍作修改,就可以使用get_config()函式來替代is__under_maintenance()函式。下面程式碼列出了與set_config()相對應的get_config()函式,這個函式可以按照使用者的需要,對配置資訊進行0秒、1秒或者10秒的區域性快取。

import json
import time

CONFIGS={}
CHECKED={}

def get_config(conn,type,component,wait=1):
    key=`config:%s:%s`%(type,component)
    #檢查是否需要對這個元件的資訊進行更新
    if CHECKED.get(key)<time.time()-wait:
        #有需要對配置進行更新,記錄最後一次檢查這個連線的時間
        CHECKED[key]=time.time()
        #取得Redis儲存的元件配置
        config=json.loads(conn.get(key) or `{}`)
        #將潛在的Unicode關鍵字引數轉換為字串的關鍵字引數
        config=dict((str(k),config[k]) for k in config)
        #取得元件正在使用的配置
        old_cofig=CONFIGS.get(key)
        #如果兩個配置並不相同
        if config!=old_cofig:
            #那麼對元件的配置進行更新
            CONFIGS[key]=config

    return CONFIGS.get(key)

在擁有了配置資訊和獲取配置資訊的兩個函式之後,我們還可以在此之上更近一步。我們在前面一直考慮的都是怎樣儲存和獲取配置資訊以便連線各個不同的Redis伺服器,但直到目前為止,我們編寫的絕大多數函式和第一個引數都是一個連線引數。因此,為了不再需要手動獲取我們正在使用的各項服務的連線,下面讓我們來構建一個能夠幫助我們自動連線這些服務的方法。

自動Redis連線管理

手動建立和傳遞Redis連線並不是一件容易地事情,這不僅是因為我們需要重複查閱配置資訊,還有一個原因就是,即使使用了 上一節介紹的配置管理函式,我們還是需要獲取配置、連線Redis,並在使用完連線之後關閉連線。為了簡化連線的管理操作,我們將編寫一個裝飾器,讓它負責連線除配置伺服器之外的所有其他Redis伺服器。

裝飾器

Python提供了一種語法,用於將函式X傳入另一個函式Y的內部,其中函式Y就被成為裝飾器。裝飾器給使用者提供了一個修改函式X行為的機會。有些裝飾器可以用於校驗引數,而有些裝飾器則可以用於註冊回撥函式,甚至還有一些裝飾器可以用於管理連線:就像我們接下來要做的那樣。

下面程式碼展示了我們定義的裝飾器,它接受一個指定的配置作為引數並生成一個包裝器,這個包裝器可以包裹一個函式,使得之後對被包裹函式的呼叫可以自動連線至正確的Redis伺服器,並且連線Redis伺服器所使用的那個連線會和使用者之後提供的其他引數一同傳遞至包裹的函式:

REDIS_CONNECTIONS={}

#將應用元件的名字傳遞給裝飾器
def redis_connection(component,wait=1):
    #因為函式每次被呼叫都需要獲取這個配置鍵,所以我們乾脆把它快取起來
    key=`config:redis:`+component
    #包裝器接受一個函式作為引數,並使用另一個函式來包裹這個函式。
    def wrapper(function):
        #將被包裹函式的一些有用的後設資料複製給配置處理器。
        @function.wraps(function)
        def call(*args,**kwargs):#建立負責管理連線資訊的函式
            #如果有就配置存在,那麼獲取它
            old_config=CONFIGS.get(key,object())
            #如果有新配置存在,那麼獲取它
            _config=get_config(config_connection,`redis`,component,wait)

            config={}
            #對配置進行處理並將其用於建立Redis連線
            for k,v in _config.iteritems():
                config[k.encode(`utf-8`)]=v

            #如果新舊配置並不相同,那麼建立新的連線
            if config!=old_config:
                REDIS_CONNECTIONS[key]=redis.Redis(**config)

            #將Redis連線以及其他匹配的引數傳遞給包裹函式,然後呼叫該函式並返回它的執行結果。
            return function(REDIS_CONNECTIONS.get(key),*args,**kwargs)
        #返回被包裹的函式
        return call
    #返回用於包裹Redis函式的包裝器
    return wrapper

同時使用*args和**kwargs

在Python中,函式定義的args變數用於獲取所有位置引數,而kwargs變數則用於獲取所有命令出納和素,這兩種引數傳遞方式都可以將給定的引數傳入被呼叫的函式裡面。

上面戰術的一系列巢狀函式初看上去可能會讓人感動頭昏目眩,但它們實際上並沒有想象中的那麼複雜。redis_connection()裝飾器接受一個應用元件的名字作為引數並返回一個包裝器。這個包裝器接受一個我們想要將連線傳遞給它的函式為引數,然後對函式進行包裹並返回被包裹函式的調研器。這個呼叫器負責處理所有獲取配置資訊的工作,除此之外,它還負責連線Redis伺服器並呼叫被包裹的函式。儘管redis_connecition()函式描述起來相當複雜,但實際使用起來卻是非常方便的,下面程式碼就展示了怎樣將redis_connection()函式應用到之間介紹的log_recent()函式上面。


@redis_connection(`logs`)
def log_recent(conn,app,message,severity=logging.INFO,pipe=None):
    # 嘗試將日誌的安全級別準還為簡單的字串
    severity = str(SEVERITY.get(severity, severity)).lower()
    # 建立負責儲存訊息的鍵
    destination = `recent:%s:%s` % (name, severity)
    # 將當前時間新增到訊息裡面,用於記錄訊息的傳送時間
    message = time.asctime() + `  ` + message
    # 使用流水線來將通訊往返次數降低為一次
    pipe = pipe or conn.pipeline()
    # 將訊息新增到日誌列表的最前面
    pipe.lpush(destination, message)
    # 對日誌列表進行修建,讓它只包含最新的100條訊息
    pipe.ltrim(destination, 0, 99)
    # 執行兩個命令
    pipe.execute()

log_recent(`main`,`User 235 logged in`)

現在你已經看到怎樣使用redis_connection()來裝飾log_recent()函式,這個裝飾器還是蠻有用的,不是嗎?通過使用這個改良後的方法來處理連結和配置,我們幾乎可以把我們要呼叫的所有函式的程式碼都刪去好幾行。

作為練習,請嘗試使用redis_connection()去裝飾之前介紹的access_time()上下文管理器,使得這個上下文管理器可以在不必手動傳遞Redis伺服器連線的情況下執行。

本章小結

本章介紹的所有主題都直接或間接地用於對應用程式進行幫助和支援,這裡展示的函式和裝飾器都旨在幫助讀者學會如何使用Redis來支撐應用程式的不用部分:日誌、計數器以及統計資料可以幫助使用者直觀地瞭解應用程式的效能,而IP所屬地查詢程式則可以告訴你客戶所在的地點。除此之外,儲存服務的發現和配置資訊可以幫助我們減少大量需要手動處理連線的工作。

現在我們已經知道了怎樣使用Redis來對應用程式進行支援了,在接下來的第6章,我們將學習如何使用Redis來構建應用程式元件。

上一篇文章:Python–Redis實戰:第五章:使用Redis構建支援程式:第3節:查詢IP所屬城市以及國家

相關文章