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

上一篇文章: Python--Redis實戰:第五章:使用Redis構建支持程序:第3節:查找IP所屬城市以及國家

隨着咱們愈來愈多地使用Redis以及其餘服務,如何存儲各項服務的配置信息將變成一個棘手的問題:對於一個Redis服務器、一個數據庫服務器以及一個Web服務器來講,存儲它們的配置信息並不困難;但若是咱們使用了一個擁有好幾個從服務器的Redis主服務器,或者爲不一樣的應用程序設置了不一樣的Redis服務器,甚至爲數據庫也設置了主服務器和從服務器的話,那麼存儲這些服務器的配置信息將變成一件讓人頭痛的事情。web

用於鏈接其餘服務器以及服務器的配置信息通常都是以配置文件的形式存儲在硬盤裏面,每當機器下線、網絡鏈接斷開或者某些須要鏈接其餘服務器的狀況出現時,程序一般須要一次性地對不一樣服務器中的多個配置文件進行更新。而這一節要介紹的就是如何將大部分配置信息從文件轉移到Redis裏面,使得應用程序能夠本身完成絕大部分配置工做。redis

使用Redis存儲配置信息

爲了展現配置管理方面的難題是多麼的常見,來看一個很是簡單的配置例子:假設如今咱們要用一個標誌flag來表示web服務器是否正在進行維護,若是服務器正在進行維護,那麼它就不該該發送數據庫請求,而是應該向訪問們返回一條簡短的【抱歉,咱們正在進行維護,請稍後重試】的信息;相反,若是服務器並無進行維護,那麼它就應該按照既定的程序來運行。數據庫

在一般狀況下,即便只更新配置中的一個標誌,也會致使更新後的配置文件被強制推送至全部Web服務器,收到更新的服務器可能須要從新載入配置、甚至可能還要重啓應用程序服務器。json

與其嘗試爲不斷增多的服務寫入和維護配置文件,不如讓咱們直接配置寫入Redis裏面。只要將配置信息存儲在Redis裏面,並編寫應用程序來獲取這些信息,咱們就不用再編寫工具來向服務器推送配置信息了,服務器和程序也不用再經過載入配置文件的方式 來更新配置信息了。segmentfault

爲了實現這個簡單的功能,讓咱們假設本身已經構建了一箇中間層或者插件,這個中間層額做用在於:當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裏面呢?cookie

爲每一個應用程序組件分別配置一個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所屬城市以及國家
相關文章
相關標籤/搜索