原子性
写-替换模式提供了原子性,因为底层的os.rename()是原子性的。这意味着在任意给定时间点,进程或者看到
旧的文件,或者看到新的文件。该模式对写错误具有天然的鲁棒性:如果写操作触发异常,重命名操作就不会
被执行,所有就没有用损坏的新文件覆盖正确的旧文件的风险。
附加模式并不是原子性的,因为有附加不完整记录的风险。但是有个技巧可以使更新具有原子性:为每个写操
作标注校验和。之后读日志的时候,忽略所有没有有效校验和的记录。以这种方式,只有完整的记录才会被处
理。在下面的例子中,应用做周期性的测量,每次在日志中附加一行JSON记录。我们计算记录的字节表示形式
的CRC32校验和,然后附加到同一行:
1 | with open (logfile, 'ab' ) as f: |
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' ) |
该例子代码通过每次创建随机值模拟测量。
2 | { "timestamp" : 1373396987.258189 , "value" : 0.9360123151217828 } 9495b87a |
3 | { "timestamp" : 1373396987.25825 , "value" : 0.40429005476999424 } 149afc22 |
4 | { "timestamp" : 1373396987.258291 , "value" : 0.232021160265939 } d229d937 |
想要处理这个日志文件,我们每次读一行记录,分离校验和,与读到的记录比较。
1 | with open (logfile, 'rb' ) as 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()))) |
7 | print ( 'checksum error for record {}' . format (record)) |
现在我们通过截断最后一行模拟被截断的写操作:
2 | { "timestamp" : 1373396987.258189 , "value" : 0.9360123151217828 } 9495b87a |
3 | { "timestamp" : 1373396987.25825 , "value" : 0.40429005476999424 } 149afc22 |
4 | { "timestamp" : 1373396987.258291 , "value" : 0.23202 |
当读日志的时候,最后不完整的一行被拒绝:
1 | $ read_checksummed_log.py log |
2 | read measure: { 'timestamp' : 1373396987.258189 , 'value' : 0.9360123151217828 } |
3 | read measure: { 'timestamp' : 1373396987.25825 , 'value' : 0.40429005476999424 } |
4 | checksum error for record b '{"timestamp": 1373396987.258291, "value":' |
添加校验和到日志记录的方法被用于大量应用,包括很多数据库系统。
spooldir中的单个文件也可以在每个文件中添加校验和。另外一个可能更简单的方法是借用写-替换模式:
首先将文件写到一边,然后移到最终的位置。设计一个保护正在被消费者处理的文件的命名方案。在下面
的例子中,所有以.tmp结尾的文件都会被读取程序忽略,因此在写操作的时候可以安全的使用。
2 | with open (newfile + '.tmp' , 'w' ) as f: |
3 | f.write(model.output()) |
4 | os.rename(newfile + '.tmp' , newfile) |
最后,截断-写是非原子性的。很遗憾我不能提供满足原子性的变种。在执行完截取操作后,文件是空的,
还没有新内容写入。如果并发的程序现在读文件或者有异常发生,程序中止,我们既看不久的版本也看不
到新的版本。
一致性
我谈论的关于原子性的大部分内容也可以应用到一致性。实际上,原子性更新是内部一致性的前提条件。
外部一致性意味着同步更新几个文件。这不容易做到,锁文件可以用来确保读写访问互不干涉。考虑某
目录下的文件需要互相保持一致。常用的模式是指定锁文件,用来控制对整个目录的访问。
写程序的例子:
1 | with open (os.path.join(dirname, '.lock' ), 'a+' ) as lockfile: |
2 | fcntl.flock(lockfile, fcntl.LOCK_EX) |
读程序的例子:
1 | with open (os.path.join(dirname, '.lock' ), 'a+' ) as lockfile: |
2 | fcntl.flock(lockfile, fcntl.LOCK_SH) |
该方法只有控制所有读程序才生效。因为每次只有一个写程序活动(独占锁阻塞所有共享锁),所有该
方法的可扩展性有限。
更进一步,我们可以对整个目录应用写-替换模式。这涉及为每次更新创建新的目录,更新完成后改变符
合链接。举例来说,镜像应用维护一个包含压缩包和列出了文件名、文件大小和校验和的索引文件的目录。
当上流的镜像更新,仅仅隔离地对压缩包和索引文件进项原子性更新是不够的。相反,我们需要同时提供
压缩包和索引文件以免校验和不匹配。为了解决这个问题,我们为每次生成维护一个子目录,然后改变符
号链接激活该次生成。
新的生成484正在被更新的过程中。当所有压缩包准备好,索引文件更新后,我们可以用一次原子调用
os.symlink()来切换current符号链接。其它应用总是或者看到完全旧的或者完全新的生成。读程序需
要使用os.chdir()进入current目录,很重要的是不要用完整路径名指定文件。否在当读程序打开
current/index.json,然后打开current/a.tgz,但是同时符号链接已经改变时就会出现竞争条件。