0104: fsync
保证写入硬盘
既然数据存储在硬盘里,就要保证数据确实写入到了硬盘里。如果只是简单的写入文件,就会遇到机器断电后,文件丢失、文件里填满了0x00等情况。这些问题都是数据库要解决的。
数据库保证写入的数据不丢失,这个特性一般叫做 durability。这个保证是以成功返回给调用方(客户端)为界的。如果数据库在返回结果之前挂了,在调用方看来,状态是不确定的;但如果调用方收到成功结果,就可以依赖这次写入不会消失。
缓存与 fsync
文件的每次 write 并不是对应到硬盘的 write。操作系统有一层内存缓存,写入时先到缓存里,之后再同步到硬盘上。这样可以合并重复的写入,增加吞吐。重复的读取也能利用到。
这个缓存叫做 page cache,这里的 page 是跟 CPU 虚拟内存管理的 page 对应的。一个 page 就是缓存操作的最小粒度,就像一本书里的页面一样大小是固定的(一般是 4K 或 16K)。你可能问:硬盘IO缓存跟虚拟内存有什么关系?这个可以了解下 mmap。另外数据库资料经常把B树的节点叫做 page,不要混淆。
除了 page cache,硬盘本身也可能自带内存缓存。要确保数据写入硬盘,就要有一个操作立即将各层缓存同步到硬盘上,并等待写入完成。这个在 Linux 上是 fsync 这个系统调用(syscall)。用 *os.File 的 Sync() 方法调用 fsync。给 Log.Write() 增加 fsync:
func (log *Log) Write(ent *Entry) error {
if _, err := log.fp.Write(ent.Encode()); err != nil {
return err
}
return log.fp.Sync() // fsync
}Windows 上有对应 fsync 的操作。所以 Sync() 方法也适用。
另外,应用层的文件操作可能也带有缓存,需要同步,比如C标准库里的 fflush()。 Go 标准库的文件操作是直接对接操作系统 API 的,没有额外的缓存。
fsync 父目录
Linux 下 fsync 可以保证文件数据写入,但不保证文件本身是存在的。这是因为:文件的父目录记录了子文件,如果往目录里增加了一条记录(创建文件),而这条记录没有写入硬盘就断电了,那这个文件就找不到了,即使文件数据写入到硬盘上也无济于事。要确保目录写入硬盘,就要对目录使用 fsync。
也就是说:创建文件、文件改名、删除文件,这些操作要对父目录使用 fsync。
这个操作是类 Unix 特有的,Windows 上没有,也不需要这个操作。 Go 标准库里没有 fsync 目录的方法,需要直接调用 syscall:
func createFileSync(file string) (*os.File, error) {
fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
if err != nil {
return nil, err
}
if err = syncDir(path.Base(file)); err != nil {
_ = fp.Close()
return nil, err
}
return fp, err
}
func syncDir(file string) error {
flags := os.O_RDONLY | syscall.O_DIRECTORY
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
if err != nil {
return err
}
defer syscall.Close(dirfd)
return syscall.Fsync(dirfd)
}用这个函数替换 os.OpenFile():
func (log *Log) Open() (err error) {
log.fp, err = createFileSync(log.FileName)
return err
}Unix 常识
open 打开或创建文件,如果成功,返回一个数字,用来标识这个文件。后续的操作都要通过这个数字,比如后面的 fsync 参数。这个数字叫做 file descriptor(fd)。
File descriptor 不一定跟“文件”有关,它可以指向操作系统层面上其他类型的资源,比如网络 socket,或者是一个文件系统里的目录。 Unix 下的 open 的对象除了文件,还可以是目录,当然返回的 fd 不能像文件一样 read、write。所有的 fd 最后都要 close。
读者可以看看标准库里 os.File 的代码,其实就是对各种 syscall 包装了一下。
总结
通过 fsync 做到了确保数据写入(durability)。然而对于断电这种情况,还是不够的:往 log 写入一条记录,可能是只写入了一半就挂了(atomicity)。下一步处理这种情况。
您正在阅读免费版教程,从第4章起只有简单的指引,适合爱好挑战和自学的读者。
可以购买有详细指导+背景知识的完整版。