- 这两天在研究ozone,把hdfs的论文重读了一下,简要梳理了一下hdfs的设计要点,如有问题麻烦指出,记录包括以下几个点。
- 架构
- 数据分布
- 数据一致性
- 元数据服务器
- 本地存储引擎
- 设计取舍
1 架构
- 组件架构
hdfs作为中心化的分布式存储,主要包含三个组件:前端client、元数据服务器、本地存储引擎。
client:提供接口、IO拆分及分发、元数据获取等。
元数据服务器(NameNode):存储路由信息、监控集群负载等。
本地存储引擎(DataNode):存储用户数据、一致性复制等。
- 物理拓扑
至少分为三层:顶层交换机、机架、服务器。
hdfs具备机架感知,可以感知集群的物理拓扑,因此在数据副本放置的时候可以根据物理拓扑进行分配,同时能够为用户就近选择读写的datanode节点。
- hdfs的datanode是一个server还是一个磁盘?
datanode对应是一个server。在ceph和tikv存储架构中,其一个存储节点对应一个磁盘。
2 数据分布
- 副本分布
hdfs拓扑模块具备机架感知功能,这个功能好处在于能够让client从最近的存储节点读数据,也能够在数据副本复制的时候按照从近到远的方式复制,提升总体带宽,同时也能够使副本放置尽可能合理,从而提高数据可靠性。 数据副本放置策略对于数据的可靠性、读写性能、可用性影响较大。 hdfs数据副本在client请求新的Block时由NameNode确定其存放在哪些DataNode节点,hdfs默认的副本分配方式是将第一个副本放置在离client最近的DataNode节点,其他两个副本放在不同的机架上。在充分保证读写性能的同时尽可能的保证最大的可靠性和可用性。 hdfs的副本放置可以总结为两点[1]:
- 一个DataNode上不会出现一个Block的两个副本
- 一个机架上不会存储一个Block的三个及以上的副本。(前提:机架数量充足)
- 副本管理
hdfs的副本管理粒度是以Block为单位的,Block大小为128MB,如果副本数量为3,那么一个Block就至少需要BlockID + 3*DataNodeID这样大小的元数据,这与当前很多流行的分布式系统设计是不一样的,比如tikv,其副本管理单位是一个raft group,这个raft group管理多个block,通常一个raft group管理的数据量大小在数十G规模。这也是目前很多分布式系统常用的副本管理方式。 hdfs的Block是动态创建的,client向NameNode申请新的block,NameNode会分配一个新的BlockID并为这个Block分配三个DataNode节点,用作副本存放。
3 数据一致性
hdfs只支持一写多读模式,这种模式简化了数据一致性的设计,因为不需要在client之间同步写入状态了,cephfs支持多写多读,其多个client之间的状态同步比较复杂。 另外hdfs的文件只支持追加写入,这同样有利于数据一致性的设计实现,当然这种只支持追加写的模式也是与其应用场景相结合的。同时仅支持追加写对于带宽也是友好的。
- 数据复制
hdfs的数据复制以pipeline方式进行,数据从client发到与其最近的DataNode节点,然后由第一个DataNode节点复制给第二个DataNode节点,这样以此类推,每个package的ack按照复制方向的反方向流动,最终返回给client。
- 数据一致性
- 写数据一致性
hdfs保证同一时间只有一个client可以写文件,同时可见性只是在文件close和用户显示调用hflush的时候。如果只是正常的写入返回并不保证写入的数据对用户可见,这个与文件创建时其配置有一定关系,具体可参考what's the HDFS writing consistency。
读数据一致性[3]
对于同一个client,hdfs保证“read your write”一致性语义,其实现方式主要是通过记录client的写状态ID,在执行读请求时会携带这个ID,这个ID会发给NamaNode,由NameNode保证在允许其读请求执行之前其写请求已经被执行。 对于多个client,hdfs提供msync调用,在读取非自身写的时候,先执行msync,msync会刷新NameNode上其自身的状态ID,使其ID保持最新状态,能够读到其他client写入的最新数据。
4 元数据服务器
hdfs的元数据服务器设计应该是关于hdfs的讨论中关注度最高的一点了,主要讨论点集中在两个方面:1.扩展性 2.可用性。 这两点也是Ozone解决方案的重点之一。
- 扩展性
NameNode用于存储元数据信息,且所有的元数据存储内存中。 一个hdfs文件需要存储的元数据信息如下:
文件相关元信息:
file name->fileinfo
filename->blockid list
Block ID->DataNodeid list
文件系统目录树
系统相关元信息:
DataNodeID->address
Topology
系统的元数据全部放在内存里好处是对于元数据操作能够做到非常高效,相比较操作数据库或者读写本地文件来说操作内存的效率肯定是最高的,这种高效率有利于大量client并发请求的处理效率。但是一个节点上的内存毕竟是受限的,这使得hdfs在应对更大规模集群更大规模数据量的时候显得力不从心。 Block size的大小也直接影响到元数据量,当前hdfs的block大小通常设置在64MB或者128MB,如果将Block size设置为4MB或者16MB来提高读写并发度,其Block数量增加数倍,从而也会导致元数据量增加数倍。 另外一方面,hdfs在每次写入分配Block和文件相关操作都会走NameNode,当client数量过大时,NameNode负载会变得很大,这也是NameNode存在的一个扩展性问题。
- 可用性
NameNode的从一开始的单点逐渐改进成了目前大多数厂商采用的share storage架构的高可用方式,[5]中阐述了两种share storage方式的高可用架构By QJM和By NFS两种方式,第三方存储主要用于存储操作日志即editlog。NameNode依然是分为主从两个节点,然后通过引入ZK进行主备切换。引入ZK好处在于使用第三方已有工具节省开发时间并有工业使用验证的保障,但是使用外部服务来判断内部节点是否需要切换并不是最好的选择,如果能够由系统节点自己内部协议去进行选主切换会更直接和可靠。
5 本地存储引擎
DataNode存储用户数据,他的存储及管理以Block为单位,每个Block大小在128MB,Block在DataNode中以文件形式存储,在DataNode中每个Block有两个文件标识,一个文件存储用户数据,另一个文件存储当前block的元数据信息,比如:校验和和时间戳信息。数据文件大小根据其存储的数据大小一致,如果存储的数据为1MB,那么这个数据文件也就是1MB大小,不会占用128MB。
DataNode存储了哪些Block以及DataNode自身的ID信息都有NameNode分配,并持久化在其本地。DataNode在启动时通过心跳向NameNode发送自己存储的block信息。所以NameNode并不持久化Block映射信息。
6 设计取舍
- 文件写入为什么只支持one writer many reader?
- 应用场景
hdfs设计目标是为了大数据场景,用于MapReduce等需要大批量计算处理的业务,业务对于数据的需求就是数据一旦被写入就不会被修改。 - 实现复杂度
如果支持多个写入,那么在数据一致性保证上就需要增加writer之间的同步逻辑,这本身大大增加了系统设计的复杂度。
- 为什么只支持append写?
- 在一致性模型保障上,追加写要比随机读写IO模型要简单效率高,对性能友好。
- 元数据管理上,只支持追加写使元数据设计上更简单,对于只支持追加写文件记录其最后一个块状态即可,不需要考虑文件“空洞”等情况。
- 其应用场景中,数据存在覆盖写的概率较小需求较少,其文件大都是一写多读的场景。
- 为什么元数据全部放在内存?
这一点主要是为了支持高并发client对mds的访问需求,操作内存效率更高。