Python功能點(diǎn)實(shí)現(xiàn):數(shù)據(jù)熱更新

關(guān)鍵詞:熱更新 | 熱重載 | 定時更新 | 即時更新 | 緩存 | functools | cachetools | LRU | TTL

假設(shè)應(yīng)用需要加載一個配置文件config.txt细卧,一般的做法類似于:

with open('config.txt') as f:
    parameters = f.read()

接下來parameters中存儲的數(shù)據(jù)就可以被其他代碼使用够挂,但是這樣寫的話程序每次啟動后,數(shù)據(jù)是固定死的,無法動態(tài)地自我更新棺聊,每次要修改配置/模型只能重啟整個應(yīng)用。

本文中熱更新的意思是在應(yīng)用運(yùn)行時內(nèi),從外部(如文件剩檀、數(shù)據(jù)庫、REST API)中獲得數(shù)據(jù)并更新應(yīng)用內(nèi)的Python對象旺芽。應(yīng)用場景一般是應(yīng)用作為服務(wù)(有對外的API)沪猴,需要在不重啟的前提下更新自己的配置參數(shù)或者算法模型。

熱更新可以分為兩種:定時更新(periodic update)即時更新(on-call update)采章,前者周期性地运嗜、主動地執(zhí)行更新,后者則是被動地等待悯舟,直到接收到某種來自應(yīng)用外部的信號才會執(zhí)行更新担租。本文將通過內(nèi)存緩存(Memory Cache)和裝飾器(Decorator)技術(shù)來實(shí)現(xiàn)兩種熱更新。

內(nèi)存緩存

先來說明一下內(nèi)存緩存抵怎。緩存中的數(shù)據(jù)一般以鍵值對(key-value pair)的形式存在奋救,value中放數(shù)據(jù)本身,key中放數(shù)據(jù)的某種描述名便贵。緩存的容量決定了其最大可容納的數(shù)據(jù)條數(shù)菠镇,當(dāng)容量已滿時再向緩存中存入新的數(shù)據(jù),緩存就會采取開始清理行為:清除掉已存的部分?jǐn)?shù)據(jù)承璃,從而為新數(shù)據(jù)騰地方利耍。清理的策略(判定何時需要清理、具體如何清理數(shù)據(jù))有很多種盔粹,決定了緩存的不同類型隘梨。

Python中緩存常被寫成裝飾器的形式,緩存數(shù)據(jù)的key里放的是被裝飾原函數(shù)的參數(shù)值組合(key的生成方法可以不同舷嗡,后面還會提到)轴猎,value里放的則是原函數(shù)的返回值。這樣當(dāng)函數(shù)被調(diào)用時进萄,程序會先去緩存數(shù)據(jù)里找是不是已經(jīng)有相同的參數(shù)值捻脖,如果有就直接返回已緩存的返回值,不重復(fù)進(jìn)行原函數(shù)內(nèi)的計(jì)算中鼠。

注意緩存的使用有一個隱含前提:函數(shù)本身是無狀態(tài)的可婶。假如函數(shù)內(nèi)引用了全局變量,或者存在閉包援雇,那同樣的參數(shù)值不一定必然計(jì)算出相同的返回值矛渴。這樣緩存的返回值和實(shí)際期望的返回值就不一定一致了。

定時熱更新

定時更新的實(shí)現(xiàn)使用了來源于第三方庫cachetoolsTTLCache惫搏,TTL(Time-to-Live)指存在時長策略具温。這種緩存為每一條存入的數(shù)據(jù)記錄其存在的時長蚕涤。每次調(diào)用都會檢查是否存在超過某個設(shè)定時長閾值的數(shù)據(jù),如果有就會開始清理行為:所有超時數(shù)據(jù)都會被清除掉铣猩;如果沒有超時數(shù)據(jù)揖铜,緩存將會換用LRU策略,使緩存不超出容量大屑料啊(下一部分會提到LRU的具體策略)蛮位。

當(dāng)我們將TTL緩存的容量設(shè)為1時较沪、且用于加載數(shù)據(jù)的原函數(shù)參數(shù)不變的情況下鳞绕,邏輯就變成了定時更新:

  • 未超過時長:緩存保留,每次調(diào)用都使用緩存數(shù)據(jù)
  • 超過時長尸曼,緩存清空(只有一條數(shù)據(jù))们何,程序重新計(jì)算(在這里即重新加載數(shù)據(jù))

示例代碼如下(運(yùn)行需要安裝cachetools,并在文首鏈接里下載完整的項(xiàng)目):

import time
import cachetools

from utils import change_conf_file

ROTATE = 5

@cachetools.cached(cachetools.TTLCache(1, ROTATE))
def reload():
    print('Cache cleared, reloading config...')
    with open('config.txt') as f:
        parameters = f.read()
    return parameters

class Model():
    def log(self):
        self.model = reload()
        print(self.model)

if __name__ == '__main__':
    # Reload automatically every [ROTATE] seconds
    model = Model()
    while True:
        time.sleep(2)
        change_conf_file() # change data
        model.log()

即時熱更新

即時更新的實(shí)現(xiàn)使用了來源于Python內(nèi)置庫functoolslru_cache控轿,LRU(Least Recently Used)指最少使用策略冤竹。這種緩存為每一條存入的數(shù)據(jù)記錄其被使用的次數(shù),每次調(diào)用都會檢查緩存大小是否超出容量茬射,如超出就會開始清理行為:會從使用次數(shù)最少的數(shù)據(jù)開始清理鹦蠕,直到緩存大小處于容量以內(nèi)。

當(dāng)我們將LRU緩存的容量設(shè)為1在抛、且用于加載數(shù)據(jù)的原函數(shù)參數(shù)不變的情況下钟病,原函數(shù)只有在第一次被調(diào)用時才會發(fā)生計(jì)算,之后調(diào)用都會直接返回緩存中的數(shù)據(jù)刚梭,到這里與一般的讀取效果上并無區(qū)別肠阱。當(dāng)我們需要熱更新數(shù)據(jù)的時候,只需要主動清空緩存朴读。如下例中Getter.getModel.cache_clear()屹徘。其中Getter.getModel是裝飾后的函數(shù),其中帶有用于清理緩存的函數(shù)cache_clear()衅金。有了這個扳機(jī)噪伊,我們只需要額外開發(fā)一個API(比如REST下的GET)來觸發(fā)它,這樣通過外部即時call API就可以進(jìn)行熱更新了氮唯。

示例代碼如下(運(yùn)行需要在文首鏈接里下載完整的項(xiàng)目):

import time
from functools import lru_cache

from utils import change_conf_file

class Getter:
    @staticmethod
    @lru_cache(1)
    def getModel():
        with open('config.txt') as f:
            model = f.read()
        return model

class Model():
    def log(self):
        self.model = Getter.getModel()
        print(self.model)

if __name__ == '__main__':
    # Reload only when cache_clear() is called
    model = Model()
    while True:
        model.log()
        time.sleep(2)
        change_conf_file() # change data
        Getter.getModel.cache_clear()
        print('Cache cleared, reloading config...')
        model.log()

這里補(bǔ)充一個細(xì)節(jié)鉴吹,上面的示例中被緩存裝飾器裝飾的原函數(shù)getModel是一個無參數(shù)的函數(shù),這種情況下lru_cache是如何運(yùn)作的呢您觉?lru_cache的實(shí)現(xiàn)中使用函數(shù)functools._make_key來生成緩存的key拙寡。在Python中,當(dāng)原函數(shù)無參數(shù)時琳水,默認(rèn)參數(shù)args可認(rèn)為是空元組()肆糕,可選參數(shù)可認(rèn)為是空字典{}般堆,在這種情況下生成的key將會是空列表[](注意列表是不可hash的,不可直接作為字典的key诚啃,functools里的實(shí)際數(shù)據(jù)結(jié)構(gòu)較為復(fù)雜淮摔,這里沒有深入)。上一部分提到的第三方庫cachetools實(shí)現(xiàn)了類似的方法keys.typedkey始赎,兩者生成的key存在區(qū)別和橙,但是結(jié)合其他方法,行為在大部分情況是一樣的造垛,包括本文中的無參數(shù)函數(shù)情況魔招。

import time
from functools import _make_key
from cachetools.keys import typedkey

if __name__ == '__main__':
    print(_make_key((), {}, False)) # []
    print(typedkey((), {}, False)) # ((), {}, <class 'tuple'>, <class 'dict'>)

擴(kuò)展問題

  • 多線程情況:本文的熱更新方法均基于緩存,而由于緩存涉及到讀寫操作五辽,在多線程環(huán)境下我們需要考慮其正確性办斑。functools.lru_cachecachetools.TTLCache里均有使用到鎖的機(jī)制,再考慮到Python的GIL鎖杆逗,本文所述的熱更新在線程安全上應(yīng)該算是有保障的乡翅,但目前未經(jīng)試驗(yàn)無法完全下斷言。更正:functools.lru_cache的文檔中提到多線程環(huán)境下hit和miss的計(jì)數(shù)只是近似值罪郊,而Cachetool的文檔中則明確提到cachetools.TTLCache這樣的類是非線程安全的蠕蚜,需要額外提供鎖以實(shí)現(xiàn)同步。最開始寫這篇文章的時候我對GIL的理解有誤悔橄,按我現(xiàn)在的理解靶累,GIL寬泛地講只會阻止多線程調(diào)用多個CPU,但同一個CPU下的多線程仍然是有效的(不然還要多線程干嘛)橄维,所以同步的問題還是要考慮清除尺铣。
  • import:假如我們在一個模塊(module)里更新model,而另一個模塊import這個model争舞,那么當(dāng)原模塊的model熱更新后凛忿,import得到的model并不會更新,這種行為可能與Python自身的module cache有關(guān)竞川。要實(shí)現(xiàn)所有module的熱更新店溢,能考慮的一個辦法是讓數(shù)據(jù)自己成為一個模塊,使其變得可以在模塊之間共享委乌。

吐槽:這篇寫得我渾身無力啊床牧,本來覺得很簡單,就平時用的小東西拿來拎拎清遭贸,沒想到越拎越深...很多時候我們很happy是因?yàn)槲覀冋驹诒降淖钌厦娓昕龋挥妹鎸λ碌哪Ч砑?xì)節(jié)...作為搞技術(shù)的,我們還是不能光看臉,也要多盯襠(?_?)?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末著蛙,一起剝皮案震驚了整個濱河市删铃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌踏堡,老刑警劉巖猎唁,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異顷蟆,居然都是意外死亡诫隅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門帐偎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逐纬,“玉大人,你說我怎么就攤上這事肮街》缣猓” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵嫉父,是天一觀的道長。 經(jīng)常有香客問我眼刃,道長绕辖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任擂红,我火速辦了婚禮仪际,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘昵骤。我一直安慰自己树碱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布变秦。 她就那樣靜靜地躺著成榜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蹦玫。 梳的紋絲不亂的頭發(fā)上赎婚,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機(jī)與錄音樱溉,去河邊找鬼挣输。 笑死,一個胖子當(dāng)著我的面吹牛福贞,可吹牛的內(nèi)容都是我干的撩嚼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼完丽!你這毒婦竟也來了向瓷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤舰涌,失蹤者是張志新(化名)和其女友劉穎猖任,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓷耙,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡朱躺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了搁痛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片长搀。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鸡典,靈堂內(nèi)的尸體忽然破棺而出源请,到底是詐尸還是另有隱情,我是刑警寧澤彻况,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布谁尸,位于F島的核電站,受9級特大地震影響纽甘,放射性物質(zhì)發(fā)生泄漏良蛮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一悍赢、第九天 我趴在偏房一處隱蔽的房頂上張望决瞳。 院中可真熱鬧,春花似錦左权、人聲如沸皮胡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屡贺。三九已至,卻和暖如春瀑梗,著一層夾襖步出監(jiān)牢的瞬間烹笔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工抛丽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谤职,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓亿鲜,卻偏偏與公主長得像允蜈,于是被迫代替她去往敵國和親冤吨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345