对于提供事务支持的数据库,在事务提交时,都要确保事务日志(包含该事务所有的修改操作以及一个提交记录)完全写到硬盘上,才认定事务提交成功并返回给应用层。
一个简单的问题:在*nix操作系统上,怎样保证对文件的更新内容成功持久化到硬盘?
1 #include <unistd.h>
2 int fsync(int fd);
1 #incude <sys/mman.h>
2 int msync(void *addr, size_t length, int flags)
msync需要指定同步的地址区间,如此细粒度的控制似乎比fsync更加高效(因为应用程序通常知道自己的脏页位置),但实际上(Linux)kernel中有着十分高效的数据结构,能够很快地找出文件的脏页,使得fsync只会同步文件的修改内容。
"Unfortunately fsync() will always initialize two write operations : one for the newly written data and another one in order to update the modification time stored in the inode. If the modification time is not a part of the transaction concept fdatasync() can be used to avoid unnecessary inode disk write operations."
多余的一次IO操作,有多么昂贵呢?根据Wikipedia的数据,当前硬盘驱动的平均寻道时间(Average seek time)大约是3~15ms,7200RPM硬盘的平均旋转延迟(Average rotational latency)大约为4ms,因此一次IO操作的耗时大约为10ms左右。这个数字意味着什么?下文还会提到。
Posix同样定义了fdatasync,放宽了同步的语义以提高性能:
1 #include <unistd.h>
2 int fdatasync(int fd);
"fdatasync does not flush modified metadata unless that metadata is needed in order to allow a subsequent data retrieval to be corretly handled."
在Berkeley DB下,如果开启了AUTO_COMMIT(所有独立的写操作自动具有事务语义)并使用默认的同步级别(日志完全同步到硬盘才返回),写一条记录的耗时大约为5~10ms级别,基本和一次IO操作(10ms)的耗时相同。
1.每个log文件固定为10MB大小,从1开始编号,名称格式为“log.%010d"2.每次log文件创建时,先写文件的最后1个page,将log文件扩展为10MB大小3.向log文件中追加记录时,由于文件的尺寸不发生变化,使用fdatasync可以大大优化写log的效率4.如果一个log文件写满了,则新建一个log文件,也只有一次同步metadata的开销
关于I/O,也是分为几类的,存在文件I/O,网络I/O,有的甚至将标准库I/O也作为一类来分。上篇文章中介绍了网络I/O的调用过程和阻塞分析,在文件I/O中,阻塞是很少发生的,所以很少讨论文件I/O,使用read()/write()函数造成的阻塞!