数据稳定落盘基础知识
why:
在计算的理想世界中,不存在机器宕机、断电、磁盘故障的情况。但是事实上,这些事情是实时发生的。特别是在和数据存储相关的系统中,更是需要清楚的知道数据落盘的机制。
what:
机器世界的I/O数据落盘架构一般,如下图:
分3层(从上到下):
第1层是:应用程序层。该层包含“应用程序自己的缓存数据”,和“lib库中的缓存数据”;这两种缓存数据都在应用程序的地址空间中。
第2层是:内核层。该层是回写缓存的方式,称为页面缓存。脏页可以在内存缓存区存放不确定的时间。具体的写入磁盘的时机,由系统的IO负载和I/O模式决定。
第3层是:存储设备层。该层也可以支持回写缓存,但是在一般掉电、宕机情况下,数据是会丢失数据的(即volatile cache)。回写缓存下面才是真正的“非易失”存储设备。
where(场景):
以“应用程序监听网络套接字以获取连接并从每个客户端接收到的数据写入文件”为栗子,准备了个demo进行说明:
其中buf是应用程序的数据缓存区。从sockfd中读取数据并存入buf中。现在,由于传输的数据量是已知的,并且考虑到网络通信的性质(它们可能是突发的或缓慢的),我们决定使用函数fwrite() 和 fflush()(表示为上图中的“Library Buffers”)以进一步缓冲数据。第10-21行负责从套接字读取数据并将其写入文件流。在第22行,所有数据都已写入文件流。 在第23行,文件流被刷新,导致数据移动到“内核缓冲区”。 然后,在第27行,fsync函数确保数据落到“稳定存储”层才返回。
when(使用时机):
将I/O分成三种类型,分别是:system I/O、stream I/O、mmap I/O(即:memory mapped)。
system I/O:定义将数据写入存储设备层的操作。这些存储设备层的数据访问,只能通过内核的系统接口调用来完成。下图是常有操作:
stream I/O:使用库启动的流式I/O接口。这些函数的写入操作可能不会导致系统调用(system I/O),即流式I/O接口写入后,数据仍位于应用程序地址空间的缓冲区中。下图是常有操作:
内存映射接口,类似于system I/O。文件仍然使用和system I/O相同的接口打开和关闭,但是文件数据的访问是通过地址映射,来将数据映射到进程的地址空间中,再使用时就可以像使用应用程序的内存数据一样了。常有的操作如下:
补充知识:
1、改变缓存机制的方案:
一般有指定两个标志:O_SYNC
(和相关的 O_DSYNC
)和 O_DIRECT
。
O_SYNC: 文件数据和所有文件元数据同步写入磁盘。
O_DSYNC::仅将访问文件数据所需的文件数据和元数据同步写入磁盘。
O_DIRECT:使用 O_DIRECT
打开的文件,执行的 I/O 操作会绕过内核的页面缓存,直接写入存储。回想一下,存储本身可能将数据存储在存储设备回写缓存中,因此使用 O_DIRECT
打开的文件仍然需要 fsync()
以将数据保存到稳定存储中。O_DIRECT
标志仅与system I/O
API 相关。
2、确保文件的原子更新:
在覆盖文件时遇到系统故障(例如断电、ENOSPC 或 I/O 错误),可能会导致现有数据丢失。 为了避免这个问题,通常的做法将更新的数据写入临时文件,确保它在稳定存储上是安全的,然后将临时文件重命名为原始文件名(从而替换内容)。具体的步骤如下:
- 创建一个新的临时文件
- 将数据写入临时文件中
- 对该临时文件进行
fsync()
操作 - 重命名该临时文件为合适的名字
- 对当前目录进行
fsync()
操作
3、确保写入真正成功:
库缓冲或内核缓冲时执行写 I/O 操作时,在 write()
或 fflush()
调用时可能不会报告错误,因为数据可能仅写入页面缓存。
写入错误通常在调用 fsync()
、msync()
或 close()
期间发生。 因此,检查这些调用的返回值非常重要。
4、确保存储设备的回写缓存:
存储设备上的回写缓存可以有多种不同的形式,存在易失性写回缓存。大多数存储设备都可以配置为在无缓存模式或直写缓存模式下运行。例如: 对于内核版本 2.6.35 起的 ext3、ext4、xfs 和 btrfs,挂载选项是“-o barrier”
以打开回写缓存刷新(默认),或“-o nobarrier”
以关闭屏障。