作者:用户1984346023

(1) 背景

早期的HDFS版本不支持HDFS append功能. 当一个文件被关闭时, 这个文件就不能再被修改了. 如果要修改的话, 就只能重读此文件并将数据写入一个新的文件. 虽然这种方式很简单, 但和map/reduce的需求却是非常match的. map/reduce jobs会向HDFS写入多个结果文件, 这种方式比修改已经存在的输入文件效率要高很多. 而且map/reduce模型通常是不会修改输入文件的.
在早期的HDFS(0.15之前), 如果一个文件没有被成功的关闭(通过FSDataOutputStream.close())之前,这个文件在NameSpace中是不可见的. 如果client在close文件之前失效或
者是调用close()方法时抛出了异常, 这个文件就永远是不可见的. 恢复文件的唯一方法就是从头重写. 实事上map/reduce是非常适合这种文件读写风格的. 例如: map/reduce job>的某个Task失败, 只需重新运行一次Task即可. 因为map/reduce jobs在启动之前将作业切成了很多细小的Task, 各个Task之间是没有任何关联, 而且一个task失败后重新运行的开销也非常的低.
(2) Append的第一次尝试
当然map/reduce只是架构在HDFS之上的一个应用, 其它应用并不是完全没有Append需求的. 直到发布了hadoop-0.15.0这个版本, 打开的文件在namespace中才变得可见了.也就>是说在文件没有close之前, 其它client是可以看到这个文件的. 这就是说在没有关闭文件之前client挂掉, 这个文件也还是会被保留在namespace中的. 当然这个版本没有去解决让正在写入的数据尽快变得可见的问题.参考: https://issues.apache.org/jira/browse/HADOOP-1708
与此同时, HADOOP-89这个ISSUE还解决了让正在写入文件也可以被其它clients读到,(但是只有最后一个被全部成功写入的块是可见的). 也就是说在文件写入过程中有部分是可见的. 这使得外部可以探知一个文件写入的进度. 当然也可以使用hadoop fs -tail 的方式依次读取己成功写入的block.
参考: https://issues.apache.org/jira/browse/HADOOP-89
(3) 更进一步的需求
对于某些应用来说, HDFS提供的API还不是足够强大. 例如, 对于某些数据库(HBase)希望将Transaction Log以非常可靠的方式写入到HDFS, 但是HDFS并没有提供. 对于HBase这样的应用, 需要有类似于sync()这样的API来保证所有的Transactoin Log记录被成功的持久化到HDFS中(类似于 POSIX's fsync), 如果应用挂掉了, 它仍然可以恢复已经flush的Transaction Log记录以便回放日志文件, 并且还能重新以 append 模式打开Transaction Log文件追加新的数据.
同样, HDFS没有办法满足以上持久化日志文件写入的这种需求. 因为类似于HBase这样的应用无法容忍因为client失效而导致最后一个block中的所有日志记录全部丢失.
终于在2007年8月HDFS Append功能的实现重新提上了日程(https://issues.apache.org/jira/browse/HADOOP-1700), 并且在2008年7月HDFS Append功能终于随hadoop-0.19.0这个版本发布了.
为了实现HDFS Append功能免不了要修改很多HDFS的核心代码. 例如, 在准备进行append之前, HDFS Blocks是不可以修改的. 然后当开始对一个文件进行Append时, 最后一个block就需要变得可以被修改, 在修改的过程中需要一些方法来更新block的GenerationStamp, 以保证在修改最后一个块的的过程中, 如果在某个Datanode挂掉, 这个Block就必须被认为是拥有一个过期的版本号(GenerationStamp),不然就会出现不一致的情况.细节可以关注:HADOOP-1700.
(4) Append的终极版本
在HADOOP-1700被committed后, 2008年10月有人提出了Append中的有一个功能并没有生效: Reader Client不能读到已经被writer flushed的数据(HDFS-200).并且还出现了一些其它相关的问题: 例如: 当用户从0.17这个版本升级到0.19这个版本时,Datanode应该删掉tmp目录中的文件; Namenode在块复制/删除坏块是限入了死循环; FSNameSystem#addStoredBlock方法没有正确处理块长度不一致的问题; 还有BlockReport时应该对比GenerationStamp.......因为这些问题的存在, 在发布HADOOP-0.19.1版本时append功能又被disabled掉了. 紧接着在0.20.0这个版本中 dfs.support.append 配置项默认都被设置为了false.(HADOOP-5332), 所以在线上生产环境尽量不能打开 dfs.support.append功能, 因为在这个版本中append功能是非常不稳定的. 除非是在测试环境中你才可以打开这个功能.
因为这一系列的问题, 社区的developers不得不重新看待Append这个功能了, Append功能实际上是非常复杂的. 与此同事社区重新开了一个ISSUE: HDFS-265 "Revisit append". 并且提交了新的设计文档和测试计划.
另外部分hadoop committer在Y!的办公室专门举办了一个会议来讨论appends相关的需求. 并且重新给sync定义了一个语义上更加精确的名字: hflush. hflush会保证数据被flush到所有的datanodes, 但是不保证数据会被flush到操作系统的buffers或持久化到硬盘. 注: hflush应该是代表hadoop flush.
# ############################################################################################################################################
(1) 实现HDFS Append挑战有:
a) 一致性: 在同一时间在进行写操作的文件的最后一个Block被写入了不同数量的字节, 那么HDFS怎么保证读取一致性? 甚至在出现异时一致性问题又怎么保证?
b) 错误恢复: 当一个错误发生的时候, 恢复工作不仅仅是简单的丢掉最后一个Block, 而是至少需要保证已经hflushed的字节还可以被正确的读到.
c) 实现append功能需要重新考虑BlockReport, blockWrite, block状态改变, Replication等方面, 需要改动很多HDFS核心代码;
d) 既然是要实现Append, 那就肯定要保证Append数据是可靠的, 即保证数据写入是成功的, 所以提供Append功能的文件系统必须要提供sync功能.
(2) HDFS Append的最实质的需求:
a) 支持一个wirter和多个readers访问同一个文件;
b) 在一个文件关闭之前,writer新写入的数据可以被其它reader读取;
c) writer通过调用flush可以保证数据安全的持久化到磁盘;
d) writer可以重复频繁的调用flush写入少量的数据而不会给HDFS带来一些不当的压力;
e) 保证flush后的数据能被readers读到, 当然readers要在writer flush之后打开文件才行;
f) 保证HDFS永远不会丢失数据(silently).
(3) HDFS Append设计不包含以下目标:
a) 在数据未持久化之前, 其它readers可以读取到writer写入的数据; 当然HDFS会尽最大的努力快速的进行数据持久化;
b) 一个flush调用不保证那些既存的readers可以读到刚刚flush的数据;
# ####################### Stale Replica Deletion(陈旧/过期副本删除) ###############
当Datanode失效时, 此Datanode上的Block副本可能会丢失一些更新, 从而导致副本过期. 因而检测陈旧的副本并阻止这些副本继续提供数据读服务变得非常紧迫.
Namenode通过维护一个全局的block version(GenerationStamp)来解决这个问题, 每次在创更新Block时, 会重新生成GenerationStamp. GenerationStamp是由8字节组成, 并且是全局递增的.如果write Client在flushing数据到Datanode(s)时碰到了某些错误,也必需要生成一个新的GenerationStamp.
# ####################### GenerationStamp ###############################
(1) GenerationStamp存在的两个原因
a) 检测过期副本;
b) 当Dead Datanode在过了很长一段时间后又重新加入集群时,可以通过GenerationStamp检测pre-historic副本;
(2) 在以下情形下需要生成GenerationStamp
a) 创建一个新文件;
b) 当client append 或 truncate 一个已经存在的文件;
c) 当client在写入数据到Datanode(s)时碰到错误或异常时需要申请一个新的GenerationStamp;
d) Namenode开始对一个文件进行lease recovery时;
# ####################### (Failure Modes and Recovery)故障情况及恢复 ###############################
(1) 本次设计将会处理以下故障情况:
# client在写block时挂掉;
# client写入数据时, pipeline中的Datanode挂掉;
# Client写入数据时, Namenode挂掉;
# The Namenode may encounter multiple attempts to restart before a successful restart occurs;
(2) append的数据写入流程:
a) The Writer Client 请求Namenode创建一个新的文件或者打开一个已经存在的文件进行appending. Namenode为新写入的Block生成一个新的blockId和一个新的GenerationStamp. 新的BlockGenerationStamp是根据一个Global GenerationStamp+1生成的, 同时还会将Global GenerationStamp存储到Transaction log中.同时在BlocksMap中还记录blockId, block locations和block generationstamp. 以上事情完成后,Namenode返回blockGenerationStamp和block locations给client.
b) Client发送blockId和BlockGenerationStamp给pipeline中的所有Datanodes. Datanodes接到请求后将会为给定的blockId创建一个block(if necessary), 同时将BlockGenerationStamp持久化到和block关联的meta-file中.要注意的是, block是直接在数据存储目录创建的, 而不是在临时目录下创建的.[如果在这个阶段出现error, 请看c_1].做完这些>时情后, Client将会通知Namenode持久化block list和blockGenerationStamp.
c) Client开始向pipeline写入数据流. 在pipeline中的每一个Datanode向前一个Datanode汇报是否写入成功, 并且还会将汇报的情况转发给pipeline中的下一个Datanode. 如
果pipeline中的任意一个Datanode出现error, Client将会收到通知并进行如下处理:
c_1) Client在pipeline中移除掉bad Datanode.
c_2) Client向Namenode请求一个新的BlockGenerationStamp. Client还会向Namenode汇报bad Datanode.
c_3) Namenode更新BlocksMap: 在valid block location中移除掉bad Datanode. 然后生成一个新的BlockGenerationStamp存储到in-memory OpenFile结构体中, 然后返
回给Client. (这个OpenFile应该是指的INodeUnderContruction)
c_4) Client收到BlockGenerationStamp后,发送给pipeline宫的所有good datanods.
c_5) 每一个good datanode收到新的BlockGenerationStamp后, 持久化存储到block对应的meta-file.做完这些时情后, Client将会通知Namenode持久化block list和blockGenerationStamp(OpenFile).
c_6) 现在一切又准备ok了, Client可以跳到[c]开始继续写入数据.
d) 当block全部接收成功时, Datanode会发送一个block received信息(包含blockId和BlockGenerationStamp)给Namenode.
e) Namenode将会从Datanode收到block receive的确认.
f) 如果Namenode从Datanode收到一个block receive的确认, 但是如果BlockGenerationStamp小于Namenode存储的BlockGenerationStamp, Namenode将会认为这是一个无效的block, 然后发送一条block delete的消息给datanode.
g) 当写入一个文件完成时The Writer会发送一个close命令给Namenode. Namenode收到命令后向Transaction log写入一个CloseFile的Transaction. CloseFile Transaction>记录包含了被关闭文件的全路径. 最后Namenode验证文件的每一个block的所有副本是否是含有相同的BlockGenerationStamp.
(3) append的数据读取流程:
Client在每次向Datanode发读请求时都会带上GenerationStamp, 如果Datanode上Block的GenerationStamp与Client发送的GenerationStamp不一致时, Datanode将会拒绝提供数据读
服务. 在这种情况下, Client将会转而去其它Datanodes上读取数据. 在重试前Client会重新向Namenode获取block locations和BlockGenerationStamp.
以下将列出Reader Client将会碰到的一些异常:
a) 不存在Writers: 在这种情况下Reader可以直接访问所有文件的内容, 在这种情况下不存在一致性问题题.
b) 有Writer正在写文件:
b_1) The Reader从Namenode获取所有blocks的list. list中的每一个block都包含有与它关联的block locations和Block GenerationStamp. 如果请求的文件正在进行写>入, Namenode只会返回离client最近的Datanode做为block location, 这主要是减少Client读到不一致数据的概率.(在读的过程中切换Datanodes会增加读取不致的概率)
c) 一个Reader已经打开一个文件并且cache住了block locations和BlockGenerationStamps.这时有一个Writer请求打开文件appending..
c_1) writer更新了文件最后一个Block的BlockGenerationStamp, 这时Reader如果尝试去读取最后一个Block将会因为过期的GenerationStamp而读取失败. Client不得不>重新向Namenode获取block locations和BlockGenerationStamp. 和Case_b一样, Namenode仅仅返回primary datanode做为唯一的block locatoin.
d) 一个Reader已经打开一个文件并且cache住了block locations和BlockGenerationStamps.这时有一个Writer请求打开文件truncate..
d_1) 这种情况和Case_c一样.
# ####################### Lease recovery ###############################
当一个文件被打开进行write(or append)时, Namenode会创建一个in-memory lease-record. lease record包含了正在进行写操作的文件名. 如果一个文件的lease过期(过期时间通常是1小时, lease过期一般是由于Client挂掉造成的), Namenode会对这个lease进行lease recovery. Namenode会选择Block size最小的一个块就行恢复(这样设计仅仅是为了减集群的压力), 然后把size更大的block截短;
# ####################### BlockId allocations and Client flushes ###############################
Writer为正在写的文件向Namenode申请一个新的Block. Namenode分配一个新的blockId和一个新的BlockGenerationStamp, 并且插入到BlocksMap中. 并且还会写入一个StoreGenerationStamp和OpenFile transaction到transaction log中. transation log并不需要fluse到磁盘上, 因为万一Namenode crash掉, 正在写入的block应该要被丢掉, 这是可以接受的.而且这个优化是必须的,因为如果每次block allocation都刷一次磁盘会给namenode带来性能瓶颈. 此外, 我们写入到内存的transaction log很快就可以被写入到磁盘, 因为其它>同步的transaction会引起buffer的log都被写到磁盘.
# ####################### Durable Edits in HBase ###############################
(1) HBase需要HDFS支持Durable
为了提供durability edits保证数据不会丢失, HBase要求HDFS支持sync调用. sync调用会使DFSClient中的pending data全部写入到HDFS pipeline中的所有Datanodes中, 并且还要接收到所有Datanodes的确认信息.(注: sync调用会保证数据已经成功写入到Datanodes中, 但不保证Datanodes中的数据已经写入到操作系统的buffers或者是说已经成功的写入到磁盘)
虽然hadoop-0.19.0 release版本就开始支持append功能了, 但是发布之后由于append功能使HDFS变得非常的不稳定, 出现了很多bug. 所以hadoop-0.19.1又将append功能disable掉了. 并且之后的版本虽然都支持append, 但默认append都是被关闭的, 只有在测试的时候才会打开. 目前只有hadoop-0.20-append/CDH3这两个版本支持append功能. 也就是说如>果不使用这两个版本中的一个HBase都是有数据丢失风险的(是否存在数据丢失风险和hbase版本无关, 不管是使用hbase-0.20或者hadoop-0.90都存在数据丢失风险).

欢迎访问肖海鹏老师的课程中心:

欢迎加入肖海鹏老师技术交流群:2641394058(QQ)