设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

使用 Python 进行稳定可靠的文件操作

2013-7-30 10:20| 发布者: 红黑魂| 查看: 2695| 评论: 0|来自: 开源中国编译

摘要: 程序需要更新文件。虽然大部分程序员知道在执行I/O的时候会发生不可预期的事情,但是我经常看到一些异常幼稚的代码。在本文中,我想要分享一些如何在Python代码中改善I/O可靠性的见解。考虑下述Python代码片段。对文 ...

隔离性

隔离性意味着对同一文件的并发更新是可串行化的——存在一个串行调度使得实际执行的并行调度返回相同

的结果。“真实的”数据库系统使用像MVCC这种高级技术维护可串行性,同时允许高等级的可并行性。回到

我们的场景,我们最后使用加锁来串行文件更新。

截断-写更新进行加锁是容易的。仅仅在所有文件操作前获取一个独占锁就可以。下面的例子代码从文件

中读取一个整数,然后递增,最后更新文件:

1def update():
2   with open(filename, 'r+') as f:
3      fcntl.flock(f, fcntl.LOCK_EX)
4      = int(f.read())
5      += 1
6      f.seek(0)
7      f.truncate()
8      f.write('{}\n'.format(n))


使用 写-替换模式加锁更新就有点儿麻烦啦。像 截断-写那样使用锁可能导致更新冲突。某个幼稚的实

现可能看起来像这样:

01def update():
02   with open(filename) as f:
03      fcntl.flock(f, fcntl.LOCK_EX)
04      = int(f.read())
05      += 1
06      with tempfile.NamedTemporaryFile(
07            'w'dir=os.path.dirname(filename), delete=False) as tf:
08         tf.write('{}\n'.format(n))
09         tempname = tf.name
10      os.rename(tempname, filename)

这段代码有什么问题呢?设想两个进程竞争更新某个文件。第一个进程运行在前面,但是第二个进程

阻塞在fcntl.flock()调用。当第一个进程替换了文件,释放了锁,现在在第二个进程中打开的文件

描述符指向了一个包含旧内容的“幽灵”文件(任意路径名都不可达)。想要避免这个冲突,我们必

须检查打开的文件是否与fcntl.flock()返回的相同。所以我写了一个新的LockedOpen上下文管理器

来替换内建的open上下文。来确保我们实际打开了正确的文件:

01class LockedOpen(object):
02 
03    def __init__(self, filename, *args, **kwargs):
04        self.filename = filename
05        self.open_args = args
06        self.open_kwargs = kwargs
07        self.fileobj = None
08 
09    def __enter__(self):
10        = open(self.filename, *self.open_args, **self.open_kwargs)
11        while True:
12            fcntl.flock(f, fcntl.LOCK_EX)
13            fnew = open(self.filename, *self.open_args, **self.open_kwargs)
14            if os.path.sameopenfile(f.fileno(), fnew.fileno()):
15                fnew.close()
16                break
17            else:
18                f.close()
19                = fnew
20        self.fileobj = f
21        return f
22 
23    def __exit__(self, _exc_type, _exc_value, _traceback):
24        self.fileobj.close()
1def update(self):
2    with LockedOpen(filename, 'r+') as f:
3        = int(f.read())
4        += 1
5        with tempfile.NamedTemporaryFile(
6                'w'dir=os.path.dirname(filename), delete=False) as tf:
7            tf.write('{}\n'.format(n))
8            tempname = tf.name
9        os.rename(tempname, filename)


追加更新上锁如同给截断-写更新上锁一样简单:需要一个排他锁,然后追加就完成了。需要长期运行的

会将文件长久的打开的进程,可以在更新时释放锁,让其它进入。

spooldir模式有个很优美的性质就是它不需要任何锁。此外,你建立在使用灵活的命名模式和一个

健壮的文件名分代。邮件目录规范就是一个spooldir模式的好例子。它可以很容易的适应其它情况,

不仅仅是处理邮件。


持久性

持久性有点特殊,因为它不仅依赖于应用,也与OS和硬件配置有关。理论上来说,我们可以假定,

如果数据没有到达持久存储,os.fsync()或os.fdatasync()调用就没有返回结果。在实际情况中,

我们有可能会遇到几个问题:我们可能会面对不完整的fsync实现,或者糟糕的磁盘控制器配置,

它们都无法提供任何持久化的保证。有一个来自 MySQL 开发者 的讨论对哪里会发生错误进行了

详尽的讨论。有些像PostgreSQL 之类的数据库系统,甚至提供了持久化机制的选择 ,以便管理员

在运行时刻选择最佳的一个。然而不走运的人只能使用os.fsync(),并期待它可以被正确的实现。


通过截断-写模式,在结束写操作以后关闭文件以前,我们需要发送一个同步信号。注意通常这还

牵涉到另一个层次的写缓存。glibc 缓存 甚至会在写操作传递到内核以前,在进程内部拦住它。

同样为了得到空的glibc缓存,我们需要在同步以前对它flush():

1with open(filename, 'w') as f:
2   model.write(f)
3   f.flush()
4   os.fdatasync(f)

要不,你也可以带参数-u调用Python,以此为所有的文件I/O获得未缓冲的写。

大多数时候相较os.fsync()我更喜欢os.fdatasync(),以此避免同步元数据的更新

(所有权、大小、mtime…)。元数据的更新可最终导致磁盘I/O搜索操作,这会使整个过程慢不少。


写-替换风格更新使用同样的技巧只是成功了一半。我们得确保在代替旧文件之前,新写入文件的内容已经

写入了非易失性存储器上了,但是替换操作怎么办?我们不能保证那个目录更新是否执行的刚刚好。在网络

上有很多关于怎么让同步目录更新的长篇大论。但是在我们这种情况,旧文件和新文件都在同一个目录下,

我们可以使用简单的解决方案来逃避这个这题。

1os.rename(tempname, filename)
2dirfd = os.open(os.path.dirname(filename), os.O_DIRECTORY)
3os.fsync(dirfd)
4os.close(dirfd)

我们调用底层的os.open()来打开目录(Python自带的open()方法不支持打开目录),然后在目录文件描述

符上执行os.fsync()。

对待追加更新和我以及说过的截断-写是相似的。

spooldir模式与写-替换模式同样的目录同步问题。幸运地是,可以使用同样的解决方案:第一步同步文件,

然后同步目录。


总结

这使可靠的更新文件成为可能。我已经演示了满足ACID的四大性质。这些展示的实例代码充当一个工具箱。

掌握这编程技术最大的满足你的需求。有时,你并不需要满足所有的ACID性质,可能仅仅需要一到两个。

我希望这篇文章可以帮助你去做已充分了解的决定,什么该去实现以及什么该舍弃。


英文原文:Reliable file updates with Python

参与翻译(3人)




酷毙
2

雷人

鲜花

鸡蛋

漂亮

刚表态过的朋友 (2 人)

  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部