2010-11-18 37 views
0

我正在一個我認爲可能會失敗的不可靠系統上進行項目。我想要保證的是,如果我write_state並且機器在操作中失敗,那麼read_state將會讀取有效狀態或根本沒有狀態。我已經實現了一些我認爲會起作用的東西 - 如果有人知道其中的一種,我有興趣批評該解決方案或其他解決方案。Python中的原子狀態存儲?

我的想法:

import hashlib, cPickle, os 

def write_state(logname, state): 
    state_string = cPickle.dumps(state, cPickle.HIGHEST_PROTOCOL) 
    state_string += hashlib.sha224(state_string).hexdigest() 

    handle = open('%s.1' % logname, 'wb') 
    handle.write(state_string) 
    handle.close() 

    handle = open('%s.2' % logname, 'wb') 
    handle.write(state_string) 
    handle.close() 

def get_state(logname): 
    def read_file(name): 
     try: 
      f = open(name,'rb') 
      data = f.read() 
      f.close() 
      return data 
     except IOError: 
      return '' 
    def parse(data): 
     if len(data) < 56: 
      return (None, '', False) 
     hash = data[-56:] 
     data = data[:-56] 
     valid = hashlib.sha224(data).hexdigest() == hash 
     try: 
      parsed = cPickle.loads(data) 
     except cPickle.UnpicklingError: 
      parsed = None 
     return (parsed, valid) 

    data1,valid1 = parse(read_file('%s.1'%logname)) 
    data2,valid2 = parse(read_file('%s.2'%logname)) 

    if valid1 and valid2: 
     return data1 
    elif valid1 and not valid2: 
     return data1 
    elif valid2 and not valid1: 
     return data2 
    elif not valid1 and not valid2: 
     raise Exception('Theoretically, this never happens...') 

例如爲:

write_state('test_log', {'x': 5}) 
print get_state('test_log') 

回答

3

你的兩個副本不起作用。文件系統可以對事物進行重新排序,以便在將任何文件寫入磁盤之前將這兩個文件截斷。

有幾個文件系統操作保證是原子操作:將文件重命名爲另一個文件系統,只要該文件位於一個位置或另一個位置即可。但是,就POSIX而言,它並不保證在文件內容到達磁盤之前完成移動,這意味着它只能讓你鎖定。

Linux文件系統在原子移動執行前(但不是同步)強制文件內容碰到磁盤,所以這就是你想要的。 ext4在短時間內破壞了這個假設,使得這些文件更可能最終變空。這是widely regarded as a dick move,並且自那以來已經被糾正。

無論如何,正確的方法是:在同一目錄下創建臨時文件(因此它在同一個文件系統上);寫新的數據; fsync臨時文件;將其重命名爲以前的版本。這與操作系統可以保證的原子一樣。它還以旋轉磁盤爲代價提供了持久性,這就是爲什麼應用程序開發人員更喜歡不使用fsync並將違規的ext4版本列入黑名單的原因。

+0

不應該調用file.close() FSYNC?或者它不同步? – sbirch 2010-11-19 01:49:43

+0

不是。 fsync意味着阻塞,直到它碰到磁盤。這是很昂貴的,因爲它激活了磁盤。如果你有大量的數據傳入,它也會很慢,並且它必須在你的文件可以寫入之前寫入(ext3的data = ordered保證)。關閉便宜且異步。 – Tobu 2010-11-19 02:01:02

+0

爲什麼不只是fsync文件本身? – sbirch 2010-11-19 03:02:42

0

我認爲你可以簡化一些東西

def read_file(name): 
    try: 
     with open(name,'rb') as f 
      return f.read() 
    except IOError: 
     return '' 

if valid1: 
    return data1 
elif valid2: 
    return data2 
else: 
    raise Exception('Theoretically, this never happens...') 

您可能不需要一直寫兩個文件,只需寫入file2並將其重命名爲file1即可。

我覺得還是有機會,一個硬復位(如停電)可能會由於延遲寫不寫入到磁盤正確這兩個文件

+0

這兩個文件都沒有被寫入 - 我希望它是原子的,所以完全失敗是一個選項。因爲這個原因,我認爲需要兩個完整的副本 - 我不想寫失敗的寫入來破壞已經存在的副本。 – sbirch 2010-11-19 00:37:13

+0

考慮到這一點,我也希望系統持久(如ACID的D) - 這就是爲什麼我需要兩個副本。 – sbirch 2010-11-19 00:39:03

+0

'data ='';打開(...)爲f:data = f.read();返回數據「更好(但用換行符代替';')。 – detly 2010-11-19 01:09:38

1

我從這樣的數據庫工作模糊的記憶是這樣的。它涉及三個文件。控制文件,目標數據庫文件和待處理的事務日誌。

控制文件具有全局事務計數器和散列或其他校驗和。這是一個小文件,它的大小是一個物理塊。一個OS級寫入。

在你的目標文件中有一個全局事務計數器,帶有真實數據,加上散列或其他校驗和。

有一個正在增長的掛起事務日誌,或者是一個有限大小的循環隊列,或者可能翻轉。這並不重要。

  1. 將所有未決事務記錄到簡單日誌中。有一個序列號和變化的內容。

  2. 更新事務計數器,更新控制文件中的散列。一個寫,刷新。如果失敗了,那麼沒有任何改變。如果成功,則控制文件和目標文件不匹配,表示事務已啓動但未完成。

  3. 對目標文件執行預期更新。尋找開始並更新計數器和校驗和。如果失敗,控制文件的計數器比目標文件多一個。目標文件已損壞。當這項工作時,最後一次記錄的交易,控制文件和目標文件都同意序列號。

您可以通過重播日誌來恢復,因爲您知道最後一個好序列號。

1

在UNIX系統中,通常的答案是做鏈接跳舞。用唯一名稱創建文件(使用tmpfile模塊),然後使用os.link()函數在將內容同步到所需(發佈)狀態後創建到目標名稱的硬鏈接。

根據此計劃,您的讀者在狀態正常後纔會看到該文件。鏈接操作是原子的。在成功鏈接到「就緒」名稱後,您可以取消臨時名稱的鏈接。如果您需要保證舊版本NFS的語義而不依賴於鎖定守護進程,則需要處理一些額外的皺紋。

+0

我似乎記得NFS上的額外皺摺包括打開目標鏈接並在打開的文件描述符上執行fstat(),以將其dev,inode元組與原始臨時文件的元組進行比較。不匹配意味着你的過程失去了競爭對手。 – 2010-12-09 19:45:54

1

我會添加一個異端的迴應:怎麼樣使用sqlite?或者,可能的話,bsddb,但似乎已被棄用,你將不得不使用第三方模塊。

+0

是的,我想到了這一點,但它似乎有點沉重 - 再加上答案很有趣。 – sbirch 2010-11-19 21:54:01