确保数据存入磁盘

标签: 数据 磁盘 | 发表时间:2011-10-07 17:10 | 作者:Linux@SOHU khsing
出处:http://sohulinux.blog.sohu.com/

翻译:靳彬
原文:Ensuring data reaches disk
September 7, 2011
This article was contributed by Jeff Moyer

在理想的情况下,系统崩溃、断电、磁盘访问失败这些情况是不会出现的,开发者编写程序时也不用为这些情况担忧。不幸的是,这些情况比我们想像的还经常出现。本文描述了数据是怎样一步步被写入磁盘上的,尤其是其中被缓冲的几个步骤。本文也提供了数据被正确写盘的最佳实践,以确保意外发生的时候,数据不会丢失。主要是面向 C 语言的,其中的系统调用也有其它语言的实现。

I/O缓存
考虑到开发系统时数据的完整性,有必要理解系统的整体架构。在数据最终存入稳定存储器前,可能会经过多个层,如下图所示:

处于顶层的是需要将数据写入永久存储的应用程序。数据最初存在于应用程序的内存或缓存中的一个或多个块中。这些缓存中的数据可能被提交给一个具有自己缓存的库。抛开应用程序缓存与库缓存,这些数据都存在于应用程序地址空间中。数据经过的下一层是内核,内核有一个叫做页缓存的回写缓存。脏页会存放在页缓存中一段时间,这段时间的长短取决于系统的负载与I/O模式。最后,当脏数据离开内核的页缓存时,会被写入存储设备(如磁盘)。存储设备可能会进一步将数据缓存在临时的回写缓存中。如果这时发生了断电的情况,数据可能会丢失。最后一层是稳定存储。当数据到达这层时,就可以认为数据安全存入稳定存储器了。

为进一步说明分层的缓存,来看一个在 socket 上监听连接的程序,它把从各个客户机收到的数据写入一个文件的应用程序。在关闭连接前,服务器要确保数据写入稳定存储设备中,并向客户机发送确认信息。

 0 int
 1 sock_read(int sockfd, FILE *outfp, size_t nrbytes)
 2 {
 3      int ret;
 4      size_t written = 0;
 5      char *buf = malloc(MY_BUF_SIZE);
 6 
 7      if (!buf)
 8              return -1;
 9
10      while (written < nrbytes) {
11              ret = read(sockfd, buf, MY_BUF_SIZE);
12              if (ret =< 0) {
13                      if (errno == EINTR)
14                              continue;
15                      return ret;
16              }
17              written += ret;
18              ret = fwrite((void *)buf, ret, 1, outfp);
19              if (ret != 1)
20                      return ferror(outfp);
21      }
22 
23      ret = fflush(outfp);
24      if (ret != 0)
25              return -1;
26 
27      ret = fsync(fileno(outfp));
28      if (ret < 0)
29              return -1;
30      return 0;
31 }

第5行是一个应用程序缓存的例子;从socket中读取的数据被存入这个缓存中。现在,要传输的数据量已经知道,由于网络传输的特性(可能会是突发的或缓慢的),我们决定使用libc的流函数(fwrite() and fflush(),由上图中的"Library Buffers"表示)进一步缓存数据。10到21行负责从socket中读取数据,并写入文件流。在22行时,所有的数据都已经被写入文件流了。在23行,文件流进行刷新,并把数据送入内核缓存。然后,在27行,数据被存入稳定存储设备。

I/O APIs

既然我们已经深入了解了API与层次模型的关系,现在让我们更详细的探寻接口的复杂性。对于本次讨论,我们将I/O分为3个部分:系统I/O,流I/O,内存映射I/O。

系统I/O可以被定义为任何通过内核系统调用将数据定入内核地址空间中的存储层的操作。下面的程序(不全面的,重点在写操作)是系统调用的一部分:

OperationFunction(s)
Openopen()creat()
Writewrite()aio_write(),
pwrite()pwritev()
Syncfsync()sync()
Closeclose()

流I/O是用C语言库的流接口进行初始化的I/O。使用这些函数进行写的操作并不一定产生系统调用,即在一次这样的函数调用后,数据仍然存在于应用程序地址空间中的缓存中。下面的库程序(不全面)是流接口的一部分:

OperationFunction(s)
Openfopen()fdopen(),
freopen()
Writefwrite()fputc(),
fputs()putc()putchar(),
puts()
Syncfflush(), followed by
fsync() or sync()
Closefclose()

内存映射文件与系统I/O类似。文件仍然使用相同的接口打开与关闭,但对文件数据的访问,是通过将数据映射入进程的地址空间进行的,然后像读写其它应用程序缓存一样进行读写操作:

OperationFunction(s)
Openopen()creat()
Mapmmap()
Writememcpy()memmove(),
read(), or any other routine that
writes to application memory
Syncmsync()
Unmapmunmap()
Closeclose()

打开一个文件时,有两个标志可以指定,用以改变缓存行为:O_SYNC( 或相关的O_DSYNC)与O_DIRECT。对以O_DIRECT方式打开的文件的I/O操作,会绕开内核的页缓存,直接写入存储器。回想下,存储系统仍然可能将数据存入一个写回缓存中,因此,对于以O_DIRECT打开的文件,需要调用fsync()确保将数据存入稳定存储器中。O_DIRECT标志仅与系统I/O API相关。

原始设备(/dev/raw/rawN)是O_DIRECT I/O的一种特殊情况。这些设备在打开时不需要显式指定O_DIRECT标志,但仍然提供直接I/O语义。因此,适用于原始设备的规则,同样也适用于以O_DIRECT方式打开的文件(或设备)。

同步I/O(O_DIRECT或非O_DIRECT方式的系统I/O,或流I/O)是任何对以O_SYNC 或O_DSYNC方式打开的文件描述符的I/O.以下是POSIX定义的同步模式:

  • O_SYNC: 文件数据与所有元数据同步写入磁盘。
  • O_DSYNC: 仅需要访问文件数据的文件数据与元数据同步写入磁盘。
  • O_RSYNC: 未实现。

用以对文件描述符进行写调用的数据与相关的元数据,在数据进入稳定存储时,生命周期结束。注意,那些不是用于检索文件数据的元数据,可能不会被立即写入。这些元数据包括文件的访问时间,创建时间,和修改时间。

值得指出的是,以O_SYNC 或 O_DSYNC方式打开文件描述符,并将其与一个libc文件流联系在一起的微妙之处。记住,对文件指针的fwrite()操作会被C语言库缓存。直到fflush()被调用,系统才会知道数据要写入磁盘。本质上来说,将文件流与一个同步的文件描述符关联在一起,意味着在fflush()操作后,不需要对文件描述符调用fsync()。

什么时候执行fsync操作?

可以根据一些简单的规则,决定是否调用fsync()。首先,也是最重要的,你必须明白:有没有必要将数据立即存入稳定存储中?如果是不重要的数据,那么不必立即调用fsync(). 如果是可再生的数据,也没有太大的必要立即调用fsync()。另一方面,如果你要存储一个事务的结果,或更新用户的配置文件,你很希望得到正确的结果。在这些情况下,应该立即调用fsync()。

更微妙之处在于新创建的文件,或重写已经存在的文件。新创建的文件不仅仅需要fsync(),其父目录也需要fsync()(因为这是文件系统定位你的文件之处)。这类同步行为依赖于文件系统(和挂载选项)的实现。你可以对专门为每一个文件系统与挂载选项进行特殊编码,或者显示调用fsync(),以确保代码的可移植性。

类似的,当你覆盖一个文件时,如果遭遇系统失败(例如断电,ENOSPC或I/O错误),很可能会造成已有数据的丢失。为避免这种情况,通常的做法(也是建议的做法)是将要更新的数据写入一个临时文件,确保它在稳定存储上的安全,然后将临时文件重命名为原始的文件名(以代替原始的内容)。这确保了对文件更新操作的原子性,以使其它读取用户得到数据一个副本。以下是这种更新类型的操作步骤:

  1. create a new temp file (on the same file system!)
  2. write data to the temp file
  3. fsync() the temp file
  4. rename the temp file to the appropriate name
  5. fsync() the containing directory

错误检查

进行由库或内核缓存的写I/O时,由于数据可能仅仅被写入页缓存,例如在执行write()或fflush()时,可能会产生不被报告的错误。相反,在调用fsync(),msync()或close()时,由写操作产生的错误会被报告。因此,检查这些调用的返回值是很重要的。

写回缓存

这部分介绍了一些关于磁盘缓存的一般知识,与操作系统对这些缓存进行控制的知识。这部分的讨论不影响程序是如何构建的,因此,这部分的讨论是以提供信息为目的的

存储设备上的写回缓存有多种形式。有我们在这篇文章中假设的临时写回缓存。这种缓存会由于断电而丢失数据。大多数存储设备可以通过配置,使其运行在 cache-less 模式,或 write-through 模式。对于写操作的请求,每一种模式,只有当数据写入稳定存储时,才会成功返回。外部存储阵列通常具有非临时的,或具有后备电源的写缓存。这种配置,即使发生了断电的情况,数据也不会丢失。程序开发者可能不会考虑到这些。最好能够考虑到临时缓存与程序防护。在数据被成功保存的前提下,操作系统会尽可能的进行优化,以获得最高性能。

一些文件系统提供挂载选项,以控制缓存刷新行为。从2.6.35的内核版本起,ext3,ext4,xfs和btrfs的挂载命令是"-o barrier",以打开写回缓存的刷新(这也是系统缺省的),"-o nobarrier"用以关闭写回缓存的刷新。之前的内核版本可能需要不同的命令("-o barrier=0,1"),这依赖于不同的文件系统。程序开发者不必考虑这些。当文件系统的刷新被禁用时,意味着fsync调用不会导致磁盘缓存的刷新。

相关 [数据 磁盘] 推荐:

确保数据存入磁盘

- khsing - Linux@SOHU
原文:Ensuring data reaches disk. 在理想的情况下,系统崩溃、断电、磁盘访问失败这些情况是不会出现的,开发者编写程序时也不用为这些情况担忧. 不幸的是,这些情况比我们想像的还经常出现. 本文描述了数据是怎样一步步被写入磁盘上的,尤其是其中被缓冲的几个步骤. 本文也提供了数据被正确写盘的最佳实践,以确保意外发生的时候,数据不会丢失.

数据的存储介质-磁盘的RAID

- - 淘宝中间件团队博客
上次介绍了磁盘,这篇来介绍一下 RAID. 要介绍 RAID技术的原因,其实是因为目前大部分分布式存储在做的事情其实 RAID在很多年前就已经做到了,所以如果你希望做存储相关的事情,那么 RAID是必须要理解,但不一定要用到的概念:). 计算机要存储和读取数据,主要依托这么两个部件:. 1.通信管道和通信协议,心灵感应还需要靠电波通信呢不是.

Docker 命令查看并清理数据卷及磁盘

- - 小决的专栏
Docker 在长时间使用的情况下,经常需要删除旧的容器并创建新的容器,长此以往,Docker 的数据卷 volumes 会产生了非常多的僵尸文件. 以下介绍如何查看并清理这些僵尸文件. 在 Docker 1.9 以上的版本中,官方提供用于查询僵尸文件的命令:. Docker 1.13 引入了类似于 Linux 上 df 的命令,用于查看 Docker 的磁盘使用情况.

Chrome OS 会在磁盘空间不足之时自动删除用户数据

- delphij - 谷奥——探寻谷歌的奥秘
在最新版的Chrome OS Dev分支 (14.0.835.87) 里,文件管理器最底部多出了一行提示,说:. 警告:这些临时文件可能会被自动删除以释放磁盘空间. 如果你的Chromebook磁盘空间不多的时候,你的浏览器数据会被自动删除. 在需要额外空间的时候,超过3个月没登录过的非拥有者帐户的信息都会被删除(包括下载的文件).

当内存512遇上Access数据库600M,IO磁盘受伤了

- - 博客园_首页
服务器内存就512M,Access数据库(文章库)600多M,结果竟然就是IO受伤了. 秋色园技术原理解析 系列,园里不少看过的帅歌,应该有点印象,从开始到现在,还是铁打的Access数据库. 虽然本人目前对Access恨入之骨,皆因囊中羞涩,暂时不得不与之同流合污. 忙碌 微博粉丝精灵几个月来, 秋色园一直运行正常,除了远程界面都变的很卡之外,基本上也没发现什么异常.

Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理

- - BlogJava-首页技术区
在EHCache中,如果设置了overflowToDisk属性,当Cache中的数据超过限制时,EHCache会根据配置的溢出算法(先进先出(FIFO)、最近最少使用算法(LRU)等),选择部分实例,将这些实例的数据写入到磁盘文件中临时存储,已减少内存的负担,当内存中的实例数减少(因为超时或手工移除)或某些实例被使用到时,又可以将这些写入磁盘的数据重新加载到内存中.

FAQ系列 | 是什么导致MySQL数据库服务器磁盘I/O高?

- - 运维派
有个MySQL服务器的磁盘I/O总有过高报警,怎么回事. 我的朋友小明,TA有个MySQL服务器最近总是报告磁盘I/O非常高,想着我这有免费的不用白不用的企业技术服务(TA自己这么想的),就找我帮忙给把把脉. 作为一个经验丰富(踩坑不断)的DBA,出现这种问题,一般来说,磁盘I/O很高无非是下面几个原因引起:.

Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理(2)

- - BlogJava-首页技术区
在上一篇《 Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理(1)》已经详细讲解了EHCache中在AATreeSet中对AA Tree算法的实现,并且指出EHCache是采用它作为空闲磁盘管理数据结构,本文主要关注于EHCache是如何采用AATreeSet类来管理空闲磁盘的(这里的磁盘管理是只EHCache data文件中的空闲磁盘).