2010-11-18 87 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