程序需要更新文件。虽然大部分程序员知道在执行I/O的时候会发生不可预期的事情,但是我经常看到一些异常 幼稚的代码。在本文中,我想要分享一些如何在Python代码中改善I/O可靠性的见解。 考虑下述Python代码片段。对文件中的数据进行某些操作,然后将结果保存回文件中:
看起来很简单吧?可能看起来并不像乍一看这么简单。我在产品服务器中调试应用,经常会出现奇怪的行为。 这是我看过的失效模式的例子:
下面没有什么新的内容。本文的目的是为在系统编程方面缺少经验的Python开发者提供常见的方法和技术。我将 会提供代码例子,使得开发者可以很容易的将这些方法应用到自己的代码中。 “可靠性”意味着什么?广义的讲,可靠性意味着在所有规定的条件下操作都能执行它所需的函数。至于文件的操作,这个函数就是创建,替换或者 追加文件的内容的问题。这里可以从数据库理论上获得灵感。经典的事务模型的ACID性质作为指导来提高可靠性。 开始之前,让我们先看看我们的例子怎样和ACID4个性质扯上关系: 尽可能使用数据库系统如果我们能够获得ACID 四个性质,那么我们增加可靠性方面取得了长远发展。但是这需要很大的编码功劳。为什么 重复发明轮子?大多数数据库系统已经有ACID事务了。 可靠性数据存储已经是一个已解决的问题。如果你需要可靠性存储,请使用数据库。很可能,没有几十年的功夫, 你自己解决这方面的能力没有那些已经专注这方面好些年的人好。如果你不想安装一个大数据库服务器,那么你可 以使用sqlite,它具有ACID事务,很小,免费的,而且它包含在Python的标准库中。 原子性(Atomicity)要求这个事务要么完全成功,要么完全失败。在上面的实例中,磁盘满了可能导致部分 内容写入文件。另外,如果正当在写入内容时其它程序又在读取文件,它们可能获得是部分完成的版本,甚 至会导致写错误 一致性(Consistency) 表示操作必须从系统的一个状态到另一个状态。一致性可以分为两部分:内部和外 部一致性。内部一致性是指文件的数据结构是一致的。外部一致性是指文件的内容与它相关的数据是相符合的。 在这个例子中,因为我们不了解这个应用,所以很难推断是否符合一致性。但是因为一致性需要原子性,我们 至少可以说没有保证内部一致性。 隔离性(Isolation)如果在并发的执行事务中,多个相同的事务导致了不同的结果,就违反了隔离性。很明显 上面的代码对操作失败或者其它隔离性失败都没有保护。 持久性(Durability)意味着改变是持久不变的。在我们告诉用户成功之前,我们必须确保我们的数据存储是可 靠的并且不只是一个写缓存。上面的代码已经成功写入数据的前提是假设我们调用write()函数,磁盘I/O就立即 执行。但是POSIX标准是不保证这个假设的。 文章本该在这里就结束的,但是还有一些有根有据的原因,就是不使用数据。它们通常是文件格式或者文件位置约束。 这两个在数据库系统中都不好控制。理由如下:
...等等。你懂的。 如果我们自己动手实现可靠的文件更新,那么这里有一些编程技术供参考。下面我将展示四种常见的操作文件更新模式。 在那之后,我会讨论采取哪些步骤在每个文件更新模式下满足ACID性质。 文件更新模式文件可以以多种方式更新,但是我认为至少有四种常见的模式。这四种模式将做为本文剩余部分的基础。 截断-写这可能是最基本的模式。在下述例子中,假设的域模型代码读数据,执行一些计算,然后以写模式重新打开存在 的文件:
此模式的一个变种以读写模式打开文件(Python中的“加”模式),寻找到开始的位置,显式调用truncate(), 重写文件内容。
该变种的优势是只打开文件一次,始终保持文件打开。举例来说,这样可以简化加锁。 写-替换另外一种广泛使用的模式是将新内容写到临时文件,之后替换原始文件:
该方法与截断-写方法相比对错误更具有鲁棒性。请看下面对原子性和一致性的讨论。很多应用使用该方法。 这两个模式很常见,以至于linux内核中的ext4文件系统甚至可以自动检测到这些模式,自动修复一些可靠性 缺陷。但是不要依赖这一特性:你并不是总是使用ext4,而且管理员可能会关掉这一特性。 追加第三种模式就是追加新数据到已存在的文件:
这个模式用来写日志文件和其它累积处理数据的任务。从技术上讲,它的显著特点就是极其简单。一个有趣的扩 展应用就是常规操作中只通过追加操作更新,然后定期重新整理文件,使之更紧凑。 Spooldir这里我们将目录做为逻辑数据存储,为每条记录创建新的唯一命名的文件:
该模式与附加模式一样具有累积的特点。一个巨大的优势是我们可以在文件名中放入少量元数据。举例来说, 这可以用于传达处理状态的信息。spooldir模式的一个特别巧妙的实现是maildir格式。maildirs使用附加子目 录的命名方案,以可靠的、无锁的方式执行更新操作。md和gocept.filestore库为maildir操作提供了方便的封装。 如果你的文件名生成不能保证唯一的结果,甚至有可能要求文件必须实际上是新的。那么调用具有合适标志的 低等级os.open():
在以O_EXCL方式打开文件后,我们用os.fdopen将原始的文件描述符转化为普通的Python文件对象。 |