设为首页收藏本站

LUPA开源社区

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

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

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

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

应用ACID属性到文件更新

下面,我将尝试加强文件更新模式。反过来让我们看看可以做些什么来满足ACID属性。我将会尽可能保持简单,

因为我们并不是要写一个完整的数据库系统。请注意本节的材料并不彻底,但是可以为你自己的实验提供一个

好的起点。

原子性

写-替换模式提供了原子性,因为底层的os.rename()是原子性的。这意味着在任意给定时间点,进程或者看到

旧的文件,或者看到新的文件。该模式对写错误具有天然的鲁棒性:如果写操作触发异常,重命名操作就不会

被执行,所有就没有用损坏的新文件覆盖正确的旧文件的风险。

附加模式并不是原子性的,因为有附加不完整记录的风险。但是有个技巧可以使更新具有原子性:为每个写操

作标注校验和。之后读日志的时候,忽略所有没有有效校验和的记录。以这种方式,只有完整的记录才会被处

理。在下面的例子中,应用做周期性的测量,每次在日志中附加一行JSON记录。我们计算记录的字节表示形式

的CRC32校验和,然后附加到同一行:

1with open(logfile, 'ab') as f:
2    for in range(3):
3        measure = {'timestamp': time.time(), 'value': random.random()}
4        record = json.dumps(measure).encode()
5        checksum = '{:8x}'.format(zlib.crc32(record)).encode()
6        f.write(record + b' ' + checksum + b'\n')

该例子代码通过每次创建随机值模拟测量。

1$ cat log
2{"timestamp"1373396987.258189"value"0.93601231512178289495b87a
3{"timestamp"1373396987.25825"value"0.40429005476999424149afc22
4{"timestamp"1373396987.258291"value"0.232021160265939} d229d937

想要处理这个日志文件,我们每次读一行记录,分离校验和,与读到的记录比较。

1with open(logfile, 'rb') as f:
2    for line in f:
3        record, checksum = line.strip().rsplit(b' '1)
4        if checksum.decode() == '{:8x}'.format(zlib.crc32(record)):
5            print('read measure: {}'.format(json.loads(record.decode())))
6        else:
7            print('checksum error for record {}'.format(record))

现在我们通过截断最后一行模拟被截断的写操作:

1$ cat log
2{"timestamp"1373396987.258189"value"0.93601231512178289495b87a
3{"timestamp"1373396987.25825"value"0.40429005476999424149afc22
4{"timestamp"1373396987.258291"value"0.23202

当读日志的时候,最后不完整的一行被拒绝:

1$ read_checksummed_log.py log
2read measure: {'timestamp'1373396987.258189'value'0.9360123151217828}
3read measure: {'timestamp'1373396987.25825'value'0.40429005476999424}
4checksum error for record b'{"timestamp": 1373396987.258291, "value":'

添加校验和到日志记录的方法被用于大量应用,包括很多数据库系统。


spooldir中的单个文件也可以在每个文件中添加校验和。另外一个可能更简单的方法是借用写-替换模式:

首先将文件写到一边,然后移到最终的位置。设计一个保护正在被消费者处理的文件的命名方案。在下面

的例子中,所有以.tmp结尾的文件都会被读取程序忽略,因此在写操作的时候可以安全的使用。

1newfile = generate_id()
2with open(newfile + '.tmp''w') as f:
3   f.write(model.output())
4os.rename(newfile + '.tmp', newfile)

最后,截断-写是非原子性的。很遗憾我不能提供满足原子性的变种。在执行完截取操作后,文件是空的,

还没有新内容写入。如果并发的程序现在读文件或者有异常发生,程序中止,我们既看不久的版本也看不

到新的版本。

一致性

我谈论的关于原子性的大部分内容也可以应用到一致性。实际上,原子性更新是内部一致性的前提条件。

外部一致性意味着同步更新几个文件。这不容易做到,锁文件可以用来确保读写访问互不干涉。考虑某

目录下的文件需要互相保持一致。常用的模式是指定锁文件,用来控制对整个目录的访问。

写程序的例子:

1with open(os.path.join(dirname, '.lock'), 'a+') as lockfile:
2   fcntl.flock(lockfile, fcntl.LOCK_EX)
3   model.update(dirname)

读程序的例子:

1with open(os.path.join(dirname, '.lock'), 'a+') as lockfile:
2   fcntl.flock(lockfile, fcntl.LOCK_SH)
3   model.readall(dirname)

该方法只有控制所有读程序才生效。因为每次只有一个写程序活动(独占锁阻塞所有共享锁),所有该

方法的可扩展性有限。


更进一步,我们可以对整个目录应用写-替换模式。这涉及为每次更新创建新的目录,更新完成后改变符

合链接。举例来说,镜像应用维护一个包含压缩包和列出了文件名、文件大小和校验和的索引文件的目录。

当上流的镜像更新,仅仅隔离地对压缩包和索引文件进项原子性更新是不够的。相反,我们需要同时提供

压缩包和索引文件以免校验和不匹配。为了解决这个问题,我们为每次生成维护一个子目录,然后改变符

号链接激活该次生成。

01mirror
02|-- 483
03|   |-- a.tgz
04|   |-- b.tgz
05|   `-- index.json
06|-- 484
07|   |-- a.tgz
08|   |-- b.tgz
09|   |-- c.tgz
10|   `-- index.json
11`-- current -483

新的生成484正在被更新的过程中。当所有压缩包准备好,索引文件更新后,我们可以用一次原子调用

os.symlink()来切换current符号链接。其它应用总是或者看到完全旧的或者完全新的生成。读程序需

要使用os.chdir()进入current目录,很重要的是不要用完整路径名指定文件。否在当读程序打开

current/index.json,然后打开current/a.tgz,但是同时符号链接已经改变时就会出现竞争条件。



酷毙
2

雷人

鲜花

鸡蛋

漂亮

刚表态过的朋友 (2 人)

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

最新评论

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

返回顶部