fsync()、fdatasync()、sync() 是什么?
首先它们是系统调用。
- fsync(int fd) 系统调用把打开的文件描述符 fd 相关的所有缓冲元数据和数据都刷新到磁盘上(non-volatile storage,非易失性存储),等待写磁盘操作结束,然后返回。
- fdatasync(int fd) 类似 fsync,但不 flush 元数据,除非元数据影响后面读数据。比如文件修改时间元数据变了就不会刷,而文件大小变了影响了后面对该文件的读取,这个会一同刷下去。所以 fdatasync 的性能要比 fsync 好。
- sync(void) 系统调用会使包含更新文件的所有内核缓冲区(包含数据块、指针块、元数据等)都 flush 到磁盘上。
O_DIRECT O_SYNC、REQ_PREFLUSH REQ_FUA 是什么?
它们都是 flag,可能最终的效果相同,但它们在不同的层面上。O_DIRECT、O_SYNC 是系统调用 open 的 flag 参数,REQ_PREFLUSH、REQ_FUA 是 kernel bio 的 flag 参数。要理解这几个参数要需要知道两个页缓存:
- 一个是你的内存,free -h 可以看到的 buff/cache;
- 另外一个是硬盘自带的 page cache。
一个 io 写盘的简单流程如下:
O_DIRECT
O_DIRECT 表示io不经过系统缓存,这可能会降低你的io性能。它同步传输数据,但不保证数据安全。
备注:后面说的数据安全皆表示数据被写到磁盘的 non-volatile storage。
通过 dd 命令可以清楚看到 O_DIRECT 和非 O_DIRECT 区别,注意 buff/cache 的变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# 清理缓存 $ echo 3 > /proc/sys/vm/drop_caches $ free -h total used free shared buff/cache available Mem: 62G 1.1G 61G 9.2M 440M 60G Swap: 31G 0B 31G # dd without direct $ dd if=/dev/zero of=/dev/bcache0 bs=1M count=1024 1024+0 records in 1024+0 records out 1073741824 bytes (1.1 GB) copied, 27.8166 s, 38.6 MB/s $ free -h total used free shared buff/cache available Mem: 62G 1.0G 60G 105M 1.5G 60G Swap: 31G 0B 31G $ echo 3 > /proc/sys/vm/drop_caches $ free -h total used free shared buff/cache available Mem: 62G 626M 61G 137M 337M 61G Swap: 31G 0B 31G # dd with direct $ dd if=/dev/zero of=/dev/bcache0 bs=1M count=1024 oflag=direct 1024+0 records in 1024+0 records out 1073741824 bytes (1.1 GB) copied, 2.72088 s, 395 MB/s $ free -h total used free shared buff/cache available Mem: 62G 628M 61G 137M 341M 61G Swap: 31G 0B 31G |
O_SYNC
O_SYNC 同步 io 标记,保证数据安全写到 non-volatile storage。
REQ_PREFLUSH
REQ_PREFLUSH 是 bio 的 request flag,表示在本次 io 开始时先确保在它之前完成的 io 都已经写到非易失性存储里。我理解 REQ_PREFLUSH 之确保在它之前完成的 io 都写到非易失物理设备,但它自己可能是只写到了 disk page cache 里,并不确保安全。
可以在一个空的 bio 里设置 REQ_PREFLUSH,表示回刷 disk page cache 里数据。
REQ_FUA
REQ_FUA 是 bio 的 request flag,表示数据安全写到非易失性存储再返回。
innodb_flush_method
在某些版本的 GNU/Linux 和 Unix 中,使用 Unix 的 fsync() 系统调用(InnoDB 默认情况下使用)将数据刷新到磁盘是非常缓慢的。如果数据库写入性能存在问题,请将 innodb_flush_method 参数设置为 O_DIRECT 进行基准测试。
参数 innodb_flush_method 用于定义数据文件和日志文件刷新的方法(设置 InnoDB 的数据和 redo 日志文件 flush 行为),这可能会影响磁盘 I/O 吞吐量。
Property | Value |
---|---|
Command-Line Format | --innodb-flush-method=value |
System Variable | innodb_flush_method |
Scope | Global |
Dynamic | No |
Type | String |
Default Value | NULL |
Valid Values (Windows) |
|
Valid Values (Unix) |
|
如果在类 Unix 系统上将 innodb_flush_method 设置为 NULL,则默认使用 fsync 选项。如果在 Windows 系统上将 innodb_flush_method 设置为 NULL,则默认使用 async_unbuffered 选项。
对于类 Unix 系统的 innodb_flush_method 选项包括:
- fsync:使用 fsync() 系统调用来刷新数据和日志文件,fsync 也是默认设置。
- O_DSYNC:使用 O_SYNC 标志打开并刷新日志文件,并使用 fsync() 刷新数据文件。InnoDB 没有直接使用 O_DSYNC,因为在很多种 Unix 上都存在问题。O_DSYNC 方式表示以同步 I/O 的方式打开文件,任何写操作都将阻塞到数据写入物理磁盘后才返回。
- O_DIRECT:使用 O_DIRECT(或 Solaris 上的 directio())标志打开数据文件,并使用 fsync() 来刷新数据和日志文件。某些 GNU/Linux 版本,FreeBSD 和 Solaris 上提供了此选项。在类 Unix 操作系统中,文件的打开方式为 O_DIRECT 时会尝试最小化 I/O 对此文件的缓存效果,该文件的 I/O 是直接在用户空间的 Buffer 上操作的(就是绕过系统缓存,直接操作文件),并且 I/O 操作是同步的,因此不管是 read() 系统调用还是 write() 系统调用,数据都保证是从磁盘上读取的,这通常这会降低性能。但它在特殊情况下很有用,例如当应用程序有自己的缓存机制时。
- O_DIRECT_NO_FSYNC:从 MySQL 5.6.7 的版本加入,表示 InnoDB 刷新 I/O 时使用 O_DIRECT,但跳过每次写操作后的 fsync() 系统调用。
在 MySQL 8.0.14 版本之前,O_DIRECT_NO_FSYNC 设置不适用于某些类型的文件系统。例如 XFS 和 EXT4,它们需要一个 fsync() 系统调用来同步文件系统元数据的变化。如果你不知道你的文件系统是否需要 fsync() 系统调用来同步文件系统元数据的变化,使用 O_DIRECT 选项最为安全(哪些文件系统适合设置此参数官方并没有告知,很迷)。
在 MySQL 8.0.14 版本之后,如果使用 O_DIRECT_NO_FSYNC 选项,那么只有在创建一个新文件,增加文件大小后,以及关闭文件后调用 fsync() 系统调用,以确保文件系统元数据的变化同步。其余写操作并不会每次都调用 fsync() 系统调用,算是 8.0 一个大的性能提升点。
在 InnoDB 存储引擎的配置中参数 innodb_flush_method 通常设置为 O_DIRECT,这也是 MySQL 8.0.14 版本之前官方文档所推荐的设置值。即数据文件 IO 走 direct_io 模式,redo 日志文件走系统缓存(Linux Page Cache)模式,在 IO 完成后均使用 fsync() 进行持久化。其在 InnoDB 存储引擎中的表现为对于写入到数据表空间将绕过操作系统缓存,直接写。不过 redo 日志是否调用 fsync() 还依赖 innodb_flush_log_at_trx_commit 参数。
细心的读者可能会产生一些困惑,因为不管参数 innodb_flush_method 的设置为何值,在刷新脏页时都会调用 fsync()。那么,当用户已经打开文件操作的 O_DIRECT 标识,为什么还需要进行一次 fsync() 操作确保文件的写入呢?
这里简单说下,为什么采用 direct_io 模式绕过 Linux Page Cache 直接写磁盘文件,还需要调用 fsync() 刷盘,原因就是还存在文件系统元数据缓存,包括 vfs 中的 inode cache 和 dentry cache 等,以及具体文件系统元数据。在有些文件系统中,例如 ext4、xfs,文件(包括目录,在 Linux 中所有对象都是文件)都有一个 inode 与之对应,其保存有两部分的内容,元数据和文件的存储数据。根据wiki的介绍,元数据包含的内容有:文件的字节数、文件的权限、文件的时间戳、链接的数量,即有多少文件指向该 inode、指向数据块的链接、etc 等等。
可以发现元数据及其重要的,不仅仅是文件最后修改时间、权限等信息,它还包含有指向存储块的信息。若在数据增长时,元数据没有及时更新,那么同样可能会导致数据丢失的情况。虽然此时,数据可能在磁盘上,但文件不知道那些块也是其组成部分。
比如往一个新文件写入数据,除了将数据写入指定的文件系统数据 block 中,还需要确保文件系统的磁盘元数据上有对应的文件名和文件路径,而且还需要将对应的数据 block 标记为已使用状态,需要将保存文件 inode 的 inode block 也标记为已使用状态。
Note
最早发现这个问题的是 Facebook 的 MySQL 团队负责人 Mark Calleghan。其在 2009 年时在 MySQL 数据库的官方论坛中提交Bug #45892。
inode 中的元数据是保存在 inode cache 中,inode 的对应文件的数据是保存在操作系统缓存中(若未开启 O_DIRECT 标识)。
读者可以观察下图 Linux 文件系统的实现方式:
fsync 操作会同步上图中的 Inode cache,Buffer cache(也就是操作系统缓存),Directory cache。 因此这就是为什么 InnoDB 存储引擎即使在文件打开时加上 O_DIRECT 标识,刷新脏页依然需要 fsync 操作。这是因为 O_DIRECT 标识只是忽略了图中 Buffer cache。刷新文件的另一个函数 fdatasync(),其仅刷新 buffer cache 的内容到磁盘,因此比 fsync 有更好的性能,但是存在数据丢失的风险。
若用户想通过 O_DIRECT 写入文件,但避免可能存在的潜在风险时,可以再加上 O_SYNC 标识,此时写入实际变为了一个同步写(synchronous I/O)操作,因此不再需要额外的 fsync 操作。
上面已经解释了 InnoDB 存储引擎为什么即使在开启 O_DIRECT 选项后依然需要调用 fsync 操作。下面将说明 MySQL 5.6 中 InnoDB 存储引擎的变化以及 O_DIRECT 对重做日志文件的影响。
继续深入思考一个问题,为什么 InnoDB 的重做日志不使用 O_DIRECT 标识进行打开,而依然使用 buffered I/O 呢?我的理解是为了更为有效的 Group Commit。若深入源码内部来看事务提交时的操作,InnoDB 存储引擎的处理方式:
1 2 3 4 5 6 |
# < MySQL 8.0 lock log->mutex copy last redo log block to a new block write redo log buffer to disk unlock log->mutex fsync redo log files |
可以看到 fsync 操作是在释放 log->mutex 之后。这样做的目的是在 fsync 时,由于已经释放 log->mutex,那么其他事务可以继续将重做日志条目写入到 redo log buffer 中,同时这也是为什么在事务提交时,InnoDB 会拷贝最后一个 redo log block。若重做日志使用 O_DIRECT,写入重做日志文件的过程会变慢(因为不是仅写入到操作系统缓存),Group Commit 的效率就会变差。
MySQL 5.6 开始 InnoDB 可以将重做日志文件组设置为最大 512G,之前的限制为 4G。这对 SSD 和写入密集型应用会带来明显的帮助。但是重做日志 buffered I/O 的问题是会导致使用过多的操作系统缓存,这也是为什么Mark Callaghan会尝试使用 O_DIRECT 的方式来打开重做日志。
既然不能在 InnoDB 内部处理该问题(至少目前),我想可以通过操作系统提供的接口来刷新操作系统中缓存的数据(vm.drop_caches = 3),从而减少内存过度的使用问题。关于 O_DIRECT 的话题暂时结束了,希望用户能更好的理解其对 InnoDB 内部的影响。
从 MySQL 8.0.14 版本开始,这个问题得到了进一步解决。从上面的阐述可以看出只有在修改了文件系统元数据的操作时才需要 fsync,所以并不是每次 IO 操作都会导致文件系统元数据的更新,比如单纯修改一条记录的值,可能就不会。因此,某些 IO 操作需要采用 O_DIRECT 模式,另一些 IO 操作可以采用 O_DIRECT_NO_FSYNC 模式。如果能够区分这些不同的 IO 操作类型,那么就可以提升 IO 性能。
从 MySQL 8.0.14 版本开始,社区版本就已经为我们做了这样的事情,fsync() 只有在创建一个新文件,增加文件大小后,以及关闭文件后之后被调用,以确保文件系统元数据的变化同步;其余写操作则会跳过 fsync() 操作。因此,现在 O_DIRECT_NO_FSYNC 是可以取代 O_DIRECT 的。而 MySQL 也已经这么做了,虽然没有直接修改该参数默认值(fsync),但在专用的 MySQL 服务器上,推荐值已经变了。详见 innodb-dedicated-server。
现在已经是 MySQL 8.0.20 了,应该说,在该版本上,大家可以放心使用 O_DIRECT_NO_FSYNC了,能够有更好的性能(从上面的测试结果看,性能至少提升了 20%+),干嘛不用呢。
<参考>
InnoDB O_DIRECT选项漫谈