0104: fsync
保证写入硬盘
既然数据存储在硬盘里,就要保证数据确实写入到了硬盘里。如果只是简单的写入文件,就会遇到机器断电后,文件丢失、文件里填满了0x00等情况。这些问题都是数据库要解决的。
数据库保证写入的数据不丢失,这个特性一般叫做 durability。这个保证是以成功返回给调用方(客户端)为界的。如果数据库在返回结果之前挂了,在调用方看来,状态是不确定的;但如果调用方收到成功结果,就可以依赖这次写入不会消失。
缓存与 fsync
文件的每次 write 并不是对应到硬盘的 write。操作系统有一层内存缓存,写入时先到缓存里,之后再同步到硬盘上。这样可以合并重复的写入,增加吞吐。重复的读取也可以利用到缓存。
这个缓存叫做 page cache,这里的 page 是跟 CPU 内存管理的 page 对应的。一个 page 就是缓存操作的最小粒度(一般是 4K 或 16K)。
除了 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) {
// 1. obtain the directory fd
flags := os.O_RDONLY | syscall.O_DIRECTORY
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
if err != nil {
return nil, err
}
defer syscall.Close(dirfd)
// 2. open or create the file
flags = os.O_RDWR | os.O_CREATE
fd, err := syscall.Openat(dirfd, path.Base(file), flags, 0o644)
if err != nil {
return nil, err
}
// 3. fsync the directory
if err = syscall.Fsync(dirfd); err != nil {
_ = syscall.Close(fd)
return nil, err
}
// done
return os.NewFile(uintptr(fd), file), nil
}用这个函数替换 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。
我们获取目录 fd 的目的是对它使用 fsync。但目录 fd 还有一个其他用处:通过目录 fd 来 open 这个目录里的某个文件,也就是 openat 这个 syscall。 openat 用来保证第2步创建的文件是在第1步 open 的目录之下,即使这个目录路径在两步之间被替换了。
总结
通过 fsync 做到了确保数据写入(durability)。然而对于断电这种情况,还是不够的:往 log 写入一条记录,可能是只写入了一半就挂了(atomicity)。下一步处理这种情况。