你好,游客 登录
背景:
阅读新闻

大数据处理之如何确保断电不丢数据

[日期:2014-04-08] 来源:INfor  作者: [字体: ]

  去年7、8月份杭州实行拉闸限电时,导致阿里余杭机房的机器意外断电,造成HDFS集群上的部分数据丢失。在hadoop 2.0.2-alpha之前,HDFS在机器断电或意外崩溃的情况下,有可能出现正在写的数据丢失的问题。而最近刚发布的CDH4中HDFS在Client端提供了hsync()的方法调用(HDFS-744),从而保证在机器崩溃或意外断电的情况下,数据不会丢失。这篇文件将围绕这个新的接口对其实现细节进行简单的分析,从而希望找出一种合理使用hsync()的策略,避免重要数据丢失。HDFS中sync(),hflush()和hsync()的差别在hsync()之前,HDFS就已经提供了sync()和hflush()的调用,单从方法的名称上看,很难分辨这三个方法之间的区别。咱们先从这几个方法之间的差别介绍起。相关厂商内容高德地图新增3D矢量地图引擎,API向LBS开发者和企业免费开放互联网化的企业应用架构实践《搜狗商业广告平台Java生态演化之路》——搜狗商用平台架构师刘建下厨房创始人王旭升《下厨房团队成长记录》唯品会黎毅敏《唯品会运维架构和流程改造之路》相关赞助商

大数据处理,数据,大数据

 

  QCon全球软件开发大会(北京站)2014,4月25-27日,诚邀莅临。在HDFS中,调用hflush()会将Client端buffer中的存放数据更新到Datanode端,直到收到所有Datanode的ack响应时结束调用。这样可保证在hflush()调用结束时,所有的Client端都可以读到一致的数据。HDFS中的sync()本质也是调用hflush()。hsync()则是除了确保会将Client端buffer中的存放数据更新到Datanode端外,还会确保Datanode端的数据更新到物理磁盘上,这样在hsync()调用结束后,即使Datanode所在的机器意外断电,数据并不会因此丢失。而hflush()在机器意外断电的情况下却有可能丢失数据,因为Client端传给Datanode的数据可能存在于Datanode的cache中,并未持久化到磁盘上。下图描述了从Client发起一次写请求后,在HDFS中的数据包传递的流程。

大数据处理,数据,大数据

hsync()的实现本质hsync()执行时,实际上会在对应Datanode的机器上产生一个fsync的系统调用,从而将内存中的相关文件的数据更新到磁盘。Client端执行hsync时,Datanode端会识别到Client发送过来的数据包中的syncBlock_字段为true,从而判定需要将内存中的数据更新到磁盘。此时会在BlockReceiver.java的flushOrSync()中执行如下语句:((FileOutputStream)cout).getChannel().force(true);

 

  而FileChannel的force(boolean metadata)方法在JDK中,底层为于FileDispatcherImpl.c中调用fsync或fdatasync。metadata为true时执行fsync,为false时执行fdatasync。Java_sun_nio_ch_FileDispatcherImpl_force0(JNIEnv *env, jobject this,

  jobject fdo, jboolean md)

  {

  jint fd = fdval(env, fdo);

  int result = 0;

  if (md == JNI_FALSE) {

  result = fdatasync(fd);

  } else {

  result = fsync(fd);

  }

  return handle(env, result, "Force failed");

  }

  当Datanode将数据持久化到磁盘上后,会发ack响应给Client端。当收到所有Datanode的ack响应时,hsync()的调用结束。值得注意的是,fsync或fdatasync本身是一个非常耗时的调用,因为磁盘的读写速度远低于内存的读写速度。在不调用fsync或fdatasync的情况下,数据可能保存在各级cache中。

大数据处理,数据,大数据

最开始笔者在测hsync()的读写性能时,发现不同机器上测试结果hsync()耗时差别巨大,有的集群平均调用耗时为4ms,而有的集群平均调用耗时则需25ms。后来在公司各位大神的点拨下才意识到是跟Linux文件系统的机制有关。在这种情况下,只有一探Linux相关部分的源码才能解开心中的疑惑,下面这节就将从更底层的角度来解析与hsync()密切相关的系统调用fsync及fdatasync方法。fsync和fdatasync的大致实现过程对ext4格式的文件系统来说,fsync和fdatasync方法的实现代码位于fs/ext4/fsync.c这个文件中。在追加写文件的情况下,fsync和fdatasync的流程几乎一致,因为对HDFS的写操作基本都是追加写,下面我们只讨论追加写文件下的情景。ext4格式的文件系统中布局大致如下:Group 0 PaddingSuper BlockGroup DescriptorsReserved GDT Blocks DataData Block Bitmapinode Bitmapinode TableData Blocks

 

  1024 bytes1 blockmany blocksmany blocks1 block1 blockmany blockmany more blocks

  在我们追加写文件时,涉及到修改的有DataBlock BitMap、inode BitMap、inode Table、Data Blocks。但从代码中来看,实际上对文件的追加会被合并成两次写(这里是指逻辑意义上的两次写,实际在从系统Cache刷新到磁盘时,读写操作会被再次合并),第一次为写DataBlock和DataBlock Bitmap,第二次为写inode BitMap和更新inode BitMap中的inode。ext4为了支持更大的容量,使用了extend tree来实现块映射。在追加文件的情况下,fsync和fdatasync除了更新inode中的extend tree外,还会更新inode中文件大小,块计数这些metadata。对fsync来说,还会修改inode中的文件修改时间、文件访问时间(在mount选项不含noatime的情况下)和inode修改时间。写障碍和Disk Cache的影响在了解了fsync()和fdatasync()方法会对文件系统进行的改动后,离找出之前为什么在不同集群上hsync()的调用平均耗时的原因仍还有一段距离。这时我发现了不同的磁盘挂载选项会影响到fsync()和fdatasync()的执行时间,进而确定是写障碍和Disk Cache在搞怪。下面这节就将分析写障碍和Disk Cache对hsync()方法调用耗时的影响。由于市面上大部分的磁盘都是带Disk Cache的,这导致在不开启写障碍的情况下,机器意外断电可能会对其造成metadata的不一致。对ext4这种journal文件系统来说,journal写入一个事务后,会对metadata进行更新,更新完成后会将该事务标记从未执行修改为完成。举个例子,加入我们要创建并写一个文件,那么在journal中可能会产生三个事务。那么创建并写一个文件的执行流程如下:

大数据处理,数据,大数据

在磁盘没有Disk Cache的情况下,即时机器意外断电,那么重启自检时,可通过journal中最后事务的状态来对metadata进行重新执行修复或者废弃该事务。从而保证了metadata的一致性。但在磁盘有Disk Cache的情况下,IO事件会当数据写到Disk Cache中就响应完成。虽然journal按上图的流程进行执行,但是执行完成后这些数据仍可能有部分并未持久化到磁盘上。假如在执行第6个步骤的时候机器意外断电,同时第4个步骤中的数据暂未更新到磁盘,而第1,2,3,5个步骤的数据已经同步到磁盘的话。这时机器重启自检时,由于第5个步骤中journal的执行状态为未完成,会重新执行第6个步骤一次。但第6个步骤对metadata的修改是建立在第4个步骤已经完成的基础之上的,由于第4个步骤并未持久化到磁盘,所以重新执行第6个步骤时会发生异常,造成metadata的错误。

大数据处理,数据,大数据

Linux中为了避免这一情况,可以在ext4的mount选项中加barrier=1,data=ordered开启写障碍,来确保数据持久化到磁盘的顺序。在写障碍前的数据会先于写障碍后的数据刷新到磁盘,Linux会在journal的事务写到Disk Cache中后放置一个写障碍。这样journal的事务位于写障碍之前,而对应的metadata的修改数据位于写障碍之后。避免了Disk Cache中合并IO时,对读写操作进行重排序后,由于读写操作执行顺序的改变而造成意外断电后metadata无法修复的情况。关闭写障碍,即ext4的mount选项为barrier=0时,除了有可能造成在机器断电或异常崩溃重启后metadata错误外,fsync和fdatasync的调用还会在数据更新到Disk Cache时就返回,而非等到数据刷新到磁盘上后才结束调用。因为在不开写障碍的情况下,Linux会将此时的磁盘当做没有Disk Cache的磁盘来处理,当数据只是更新到Disk Cache,就会认为该IO操作已完成,这也正是前文中提到的不同集群上hsync()的平均调用时长差别巨大的原因。所以关闭写障碍的情况下,调用fsync或fdatasync并不能确保数据在机器断电或异常崩溃时不丢失。Disk Cache的存在可以提高磁盘每秒的吞吐量,通过重排序IO,尽量将IO读写变成顺序读写提高速率,同时减少文件系统碎片。而通过开启写障碍,可避免意外断电情形下metadata异常,同时确保调用fsync或fdatasync时Disk Cache中的数据持久到磁盘。开启journal的影响除了写障碍和Disk Cache会影响到hsync()的调用时长外,Datanode上文件系统有没有打开journal也是影响因素之一。关闭journal的情况下可以减少hsync()的调用时长。在不开启journal的情况下,调用fsync或fdatasync主要是由generic_file_fsync这个方法来实现将数据刷新到磁盘。在追加写文件的情况下,不论是fsync还是fdatasync,在generic_file_fsync这个方法中都会先更新Data Block数据,再更新inode数据。如果执行fsync或fdatasync的文件为新创建的文件,在不开启journal的情况下,还会在更新完文件的inode后,更新该文件的父结点的Data Block和inode。而开启journal的情况下,调用fsync或fdatasync会先写Data Block,然后提交journal的事务。虽然调用fsync或fdatasync是指定对某个文件进行操作,但在ext4中,整个文件系统只有一个journal文件,提交journal的修改事务时会将整个文件系统的metadata的修改事务一并提交。在文件系统写入操作频繁时,这一步操作会比较耗时。fsync及fdatasync耗时测试测试使用的代码如下:

 

  代码中以追加的方式向一个已存在的文件写入4k数据,4k刚好为内存页和磁盘块的大小。下面分别以几种模式来测试fsync和fdatasync的耗时。#define BLOCK_LEN 1024

  static long long microseconds(void) {

  struct timeval tv;

  long long mst;

  gettimeofday(&tv, NULL);

  mst = ((long long)tv.tv_sec) * 1000000;

  mst += tv.tv_usec;

  return mst;

  }

  int main(void) {

  int block = open("./block", O_WRONLY|O_APPEND, 0644);

  long long block_start, block_end, fdatasync_time, fsync_time;

  char block_buf[BLOCK_LEN];

  int i = 0;

  for(i = 0; i < BLOCK_LEN; i++){

  block_buf[i] = i % 50;

  }

  if (write(block, block_buf, BLOCK_LEN) == -1) {

  perror("write");

  exit(1);

  }

  block_start = microseconds();

  fdatasync(block);

  block_end = microseconds();

  fdatasync_time = block_end - block_start;

  if (write(block, block_buf, BLOCK_LEN) == -1) {

  perror("write");

  exit(1);

  }

  block_start = microseconds();

  fsync(block);

  block_end = microseconds();

  fsync_time = block_end - block_start;

  printf("fdatasync spent: %lld, fsync spent: %lld\n",

  fdatasync_time,

  fsync_time);

  close(block);

  exit(0);

  }

  测试准备文件系统:ext4

  操作系统内核:Linux 2.6.18-164.el5

  硬盘型号:WDC WD1003FBYX-1 1V02,SCSI接口

  通过sdparm--set=WCE /dev/sdx开启Disk Write Cache,sdparm--clear=WCE /dev/sdx关闭Disk Write Cache

  通过barrier=1,data=ordered开启写障碍,barrier=0关闭写障碍

  通过tune4fs-O has_journal /dev/sdxx开启Journal,tune4fs-O ^has_journal /dev/sdxx关闭Journal

  关闭Disk Cache,关闭Journal类型耗时(微秒)

  fdatasync8368

  fsync8320

  Devicewrqm/sw/swkB/savgrq-szavgqu-szawaitsvctm%util

  sdi0.00120.00480.008.001.008.338.33100.00

  可以看到,iostat为8ms,对inode、Data Block、inode Bitmap、DataBlock Bitmap的数据更新合并为了一次写操作。关闭Disk Cache,开启Journal类型耗时(微秒)

  fdatasync33534

  fsync33408

  Devicewrqm/sw/swkB/savgrq-szavgqu-szawaitsvctm%util

  sdi37.0074.00444.0011.951.2216.1513.3299.90

  通过使用blktrace跟踪对磁盘块的读写,发现此处写journal会比较耗时,下面的记录为fsync过程中对磁盘发送的写操作,已预处理掉了大部分不重要的信息,可以看到,后面三条记录都是journal的写操作(通过此处kjournald的进程id为3001来识别)。0,01310.0000000008835AW2855185 + 8 <- (8,129) 2855184

  0,0450.0003130013001AW973352281 + 8 <- (8,129) 973352280

  0,0410.0003053253001AW973352273 + 8 <- (8,129) 973352272

  0,04120.0147803573001AWS973352289 + 8 <- (8,129) 973352288

  开启Disk Cache,开启写障碍,开启Journal类型耗时(微秒)

  fdatasync23759

  fsync25006

  从结果可以看到,Disk Cache的开启可以合并更多IO,从而减少耗时。值得注意的是,在开启Disk Cache时,iostat的await是按照从内存写完到Disk Cache中来统计耗时,并非是按照写到磁盘上来计时,所以此种情况下iostat的await参数会比较小,并无参考意义。小结从这次测试结果可以看到,虽然CDH4提供了hsync()方法,但是若我们对每次写操作都执行hsync(),会严重加剧磁盘的写延迟。通过一些策略,比方说定期执行hsync()或当存在于Cache中的数据达到一定数目时,执行hsync()会是更可行的方案,从而尽量减少机器意外断电所带来的影响。附:术语解释Hadoop: Apache基金会的开源项目,用于海量数据存储与计算。

  CDH4: Cloudera公司在Apache社区发行版基础之上进行改进后的发行版,更稳定更适用于生产环境。

  Namenode: Hadoop的HDFS模块中管理所有文件元数据的组件。

  Datanode: Hadoop的HDFS模块中存储文件实际数据的组件。

  HDFS Client: 这里指连接HDFS对其中文件进行读写操作的客户端。





收藏 推荐 打印 | 录入: | 阅读:
本文评论   查看全部评论 (0)
表情: 表情 姓名: 字数
点评:
       
评论声明
  • 尊重网上道德,遵守中华人民共和国的各项有关法律法规
  • 承担一切因您的行为而直接或间接导致的民事或刑事法律责任
  • 本站管理人员有权保留或删除其管辖留言中的任意内容
  • 本站有权在网站内转载或引用您的评论
  • 参与本评论即表明您已经阅读并接受上述条款