作为大数据领域的始祖,开源项目Hadoop已经诞生了近15年了,虽然今天大数据技术已经层出不穷,市场上涌现出了很多优秀的大数据架构和产品,但是Hadoop中的很多技术实现仍然有借鉴意义,本篇我们就来看下HDFS的架构与实现原理。
如果让我们来设计一个分布式文件系统,我们会考虑到哪些点呢?
1、首先,我们的应用场景是什么?是大数据集,处理超大文件。这里的超大文件通常是指百MB、设置数百TB大小的文件。目前在实际应用中,HDFS已经能用来存储管理PB级的数据了。如果是小量的数据,单机磁盘就能很好的处理了,但是当数据量巨大时,如何支持大数据集呢?如果能把大数据集分解成小数据,数据存储在很多个磁盘上,对多个磁盘上的文件进行管理就能解决这个问题。这就要求一个集群具备扩展到成百上千个节点的能力,才能支持容纳千万级数量的文件。
2、其次,要考虑的就是硬件成本,上一篇分析了Oracle RAC的技术实现,我们知道RAC是基于共享存储的,需要专用磁盘阵列,但其成本非常昂贵,因此,能否使用普通廉价的服务器,比如标准的X86服务器,甚至廉价PC来构建分布式存储呢?这样成本就降低了。
3、我们来看下什么是文件系统?文件系统是一种存储和组织计算机数据的方法,它使得对其访问和查找变得容易。我们常见的文件系统Ext4,包括本人之前做过的集群文件系统OCFS2,都是文件系统。既然是管理、存储磁盘上的数据的,数据在磁盘上是如何组织的便是首要考虑的问题,像前面所说的文件系统,首先要做的就是格式化磁盘,磁盘的数据布局就决定了这是什么样的文件系统。哪怕像LevelDB的SSTable文件也是有磁盘数据格式的,这些通常也都是元数据信息。无一例外,这些都是单个磁盘上的文件系统或数据组织方式,那么对于分布式文件系统,由于一个文件在物理上分别存储在不同的服务器上,该如何组织数据格式、如何维护就是首要考虑的点。一般文件系统都是按块Block来操作的,Linux内核默认的块大小4KB,分布式文件系统又支持多大的块合适。
4、超大数据集,因此,高吞吐量也是设计重点要考虑的,这就可能引入高延迟性。如何提高吞吐量?自福特发明流水线制造汽车以来,这种高效的生产模式就备受推崇。在这里也采用流式数据访问,什么流式数据访问?一次写入,多次读取的高效的访问模式。流式访问最小化了磁盘的寻址开销,只需要寻址一次,然后就一直读下去,适合一次写,多次读的数据访问。与流数据访问对应的是随机数据访问,它要求定位、查询或修改数据的延迟较小,传统关系型数据库很符合这一点。
5、既然是廉价的服务器,就不可避免的出现机器故障,硬件故障后,软件是否具备硬件故障检测能力,可自动快速恢复?因此高可靠性、高可用性、数据安全性就是必须要考虑的,需要引入数据冗余机制,常见的数据冗余技术:副本技术、条带技术。
6、在当前主流硬盘仍然是机械盘HDD的情况下,顺序写比随机写性能高效很多,像LevelDB中就是采用的追加写模式Append,大大地提高了写性能。现在分布式文件系统是大规模使用廉价服务器,如果想提高写性能,最好也是采用Append模式。又因为服务器是物理上分散的,数据冗余存储在不同的服务器上,数据的一致性问题就是必须要考虑的,是采用经典的数据一致性算法(如:Raft算法),还是采用什么策略,部分节点上写失败之后该如何处理也是要考虑的。
7、由于服务器是物理上分散的,这就需要用网络将这些服务器连接起来,从成本的角度考虑,自然希望是标准的以太网络,既然是以太网,网络问题就不可避免,网络拥塞该如何处理,怎么避免或者减少网络拥塞,处理策略又是什么。
一、简述HDFS
HDFS是一种分布式文件系统,对部署在多台独立物理机器上的文件进行管理。全称Hadoop Distributed File System,是Google File System(GFS)论文的实现,是Hadoop的核心子项目,承载Hadoop的存储业务,是分布式计算的数据存储的基础。HDFS是为基于流数据模式访问和处理超大文件的需求而开发的,是为运行在廉价的服务器上而设计的大规模数据分布式存储方案,它具有高容错性、高可靠性、高可扩展性、高吞吐量,为海量数据存储提供了便利。
HDFS适用场景:
1)构建在廉价的机器上;2)适合批量数据处理;3)适合大数据处理;4)适合流式数据访问。
HDFS的限制:
1)不适合低延迟访问;2)不适合存储小文件;3)不支持并发写入;4)不支持随机修改。
什么流式数据访问?
这样让人想到福特发明流水线制造汽车,这种高效的生产模式一直备受推崇。在这里是指:一次写入,多次读取的高效的访问模式。流式访问最小化了磁盘的寻址开销,只需要寻址一次,然后就一直读下去,适合一次写,多次读的数据访问。与流数据访问对应的是随机数据访问,它要求定位、查询或修改数据的延迟较小,传统关系型数据库很符合这一点。
HDFS的应用场景更适合批处理,而不是用户交互使用,其重点是数据访问的高吞吐量,而不是数据访问的低延迟。因此,HDFS的设计就是建立在“一次写入、 多次读写”任务的基础之上的,数据集一旦由数据源生成, 就会被复制分发到不同的存储节点中, 然后响应各种各样的数据分析任务请求。 在多数情况下,分析任务都会涉及数据集中的大部分数据, 也就是说, 对HDFS来说,请求读取整个数据集要比读取一条记录更加高效,即:只需要寻址一次,然后就一直读下去,因此,流式访问最小化了磁盘的寻址开销。
为什么不适合低延时访问呢?
低时延是几十毫秒范围内,而HDFS是为了处理大型数据集分析任务的,主要是为了能达到高的数据吞吐量而设计的,这样就会造成以高延迟为代价。
为什么不适合存储小文件呢?
因为Namenode把文件系统的元数据放置在内存中, 所以文件系统所能容纳的文件数目是由Namenode的内存大小来决定。内存总是有限的,因此所能容纳的文件数目也是有限的,一般来说, 每一个文件、 文件夹和Block需要占据150字节左右的空间, 所以, 如果你有100万个文件, 每一个占据一个Block, 你就至少需要300MB内存。 当前来说, 数百万的文件还是可行的, 当扩展到数十亿时,对于当前的硬件水平来说就没法实现了。
【解决办法】:
两个角度入手:一是从根源解决小文件的产生,二是解决不了就选择合并。
从数据来源入手,如每小时抽取一次改为每天抽取一次等方法来积累数据量。
如果小文件无可避免,一般就采用合并的方式解决,可以写一个MR任务读取某个目录下的所有小文件,并重写为一个大文件。
为什么块的大小不能设置太小,也不能设置太大? 像Linux的文件系统默认块大小4KB,为什么HDFS中块大小是64MB或128MB?
这个是由应用场景决定的,一般来说,块Block是文件系统的最小单元,HDFS是Hadoop的存储模块,如果以4KB块大小来存储大数据集中的数据,那么Hadoop中将拥有大量的块,海量块的存在将大大增加了寻址时间,降低了读写速率。而且HDFS是为分布式计算MapReduce是以块为单位处理的,如果块很小,MapReduce的任务数就会很多,任务之间的切换开销就会很大,效率就会降低。
(1)HDFS的块设置太小,会增加寻址时间,程序一直在找块的开始位置;
(2)如果块设置的太大,从磁盘传输数据的时间会明显大于定位这个块开始位置所需的时间,导致程序在处理这块数据时,会非常慢。
总结:HDFS块的大小设置主要取决于磁盘传输速率。
为什么不支持并发写入,不支持随机修改?
HDFS的一个文件只有一个写入者, 且写操作只在文件末尾追加。
目前HDFS还不支持多个用户对同一文件的写操作,因为Client通过Name node获取在数据块上写入的许可后,那个块会被锁定直到写操作完成,所以不能在同一个块上并发写入。
HDFS也不支持在文件任意位置进行修改。这样的好处就是提高了磁盘IO的效率,顺序写比随机写的效率高,因为目前标准的X86服务器一般自带的机械硬盘,机械臂寻道时间相对CPU运行速度来说太长,影响性能。
二、HDFS架构
从Apache Hadoop 3.3.1 – HDFS Architecture可以了解到HDFS的整体架构设计图:
从这张架构图上,我们可以看出HDFS由几个组件:Client、Name node、Data node(s)。
Client是负责响应读、写请求的。
Name node存储的是元数据信息,包括文件名(路径)、副本数等信息。
Data node存储的是文件数据,按块Block来存储,Data node(s)是按照物理机架Rack n来分布的,数据的冗余策略是副本Replication技术。
流程分为控制流Metadata ops和数据流Block ops。
从第二张架构图,我们还可以看到更多的细节信息,包括Secondary Name node,这是一个命名空间Namespace的备份,Name node是单节点的,势必存在单点故障问题,因此Secondary Name node的存在就起到备份的作用。Name node与Data node之间的交互关系:
1)心跳Heartbeats:维持Name node与Data node之间存活关系,一般来说,所有的心跳都是保活功能,无论是网络心跳,还是磁盘心跳,像OCFS2里面就用到了这两种心跳。有些心跳还会上报一些节点资源信息,比如:Kubenetes中的kubelet与API Server的交互。HDFS的心跳也会携带Data node的副本Replication信息给Name node。
2)平衡Balancing:负载均衡,如果某个Data node上副本数过多,Name node会自动调节副本在整个集群Data node中的分布。
3)副本Replication:副本技术是数据冗余策略的常用技术,默认是3副本,即:一份数据Block会保存3个副本,分别位于不同的机架和机架上不同的服务器上。所以,1GB的数据需要3GB的存储空间。
综上所述 :
HDFS由四部分组成,Client、Name node、Data node(s)、Secondary Name node。HDFS是一个主从(Master/Slave)架构体系,Name node是主Master,负责管理文件系统的元数据;Data node(s)是从Slave,负责存储实际的数据。
1)Client客户端:负责接收命令请求,管理、访问HDFS;与Data node交互,读取或者写入数据,读取时,与Name node交互,获取文件的位置信息;写入时,与Name node交互,获取文件的副本位置,Client将文件划分为一个一个的Block块,数据是按Block块写入文件的。
2)Name node:即Master,是HDFS的大脑,负责维护名字空间Namespace,数据块Block到文件的映射,配置副本策略,处理客户端的读写请求。
3)Data node:即Slave,执行Name node下达的命令,执行数据库的读写操作,存储实际的数据块。
4)Secondary Name node:Name node的备份,是冷备,而不是热备,即:当Name node挂掉后,并不是立即替换Name node提供服务,而是辅助Name node,分担其工作量。定期合并fsimage和edits,并推送给Name node,异常情况下,辅助恢复Name node。
HDFS基本信息:
1)HDFS将文件划分为一个个的Block存储,老版本默认一个Block为64MB,Hadoop2.x版本中是128M。除了最后一个Block外,其他所有的Block都是一样大小。
2)fsimage:元数据镜像文件(文件系统的目录树)。fsimage保存了最新的元数据checkpoint,在HDFS启动时,会加载fsimage文件,该文件包含了整个HDFS文件系统所有的目录和文件信息。对于文件来说,包括数据块描述信息、修改时间、访问时间等;对于目录来说,包括修改时间、访问权限控制信息(目录所属用户、所在组)等。
3)edits:元数据的操作日志(针对文件系统做的修改操作记录)。Client执行的所有写操作都会记录到editlog中。因为fsimage文件一般都很大(GB以上),如果所有的更新操作都往fsimage里面添加,会导致系统运行缓慢,因此所有的更新操作都记录在editlog中,为了避免editlog不断增大,Secondary Name node会周期性地合并fsimage和editlog成新的fsimage,新的操作记录写入新的editlog中,整个过程就是checkpoint。
4)metadata:元数据信息,保存文件的属性信息,如:文件名、文件大小、文件所属的名字空间、文件存储的位置等等。
三、HDFS的写入流程
假如写入一个文件FileA,大小100MB,Client将FileA写入到HDFS中,HDFS分布在三个机架Rack1、Rack2、Rack3。
1、Client按HDFS的块Block大小64MB将文件FileA分为两个块,Block1和Block2。
2、然后Client向Name node发起写请求,上图中的黑色实线1,Name node记录Block的信息,并返回可用的Data nodes(假如:Block1:host2, host1, host3;Block2:host7, host8, host4)上图中的黑色实线1'。
分配可用Data nodes的原理:
Name node具有机架感知功能RackAware,
1)如果Client为Data node节点,存储Block的规则:副本1存到同Client的节点上,副本2存到不同于副本1的机架上的某个节点上,副本3存到与副本2同一机架的不同节点上,若还有其他副本则随机挑选节点存储。
2)如果Client不是Data node节点,存储Block的规则:副本1随机挑选一个节点存储,其他副本同上,即:副本2存到不同于副本1的机架上的某个节点上,副本3存到与副本2同一机架的不同节点上,若还有其他副本则随机挑选节点存储。
之所以有这样的存储规则,实际上是充分利用了网络带宽,同一个机架Rack上的服务器节点一般通过二层交换机相连,不同机架Rack上的服务器节点可能汇聚到三层交换机相连。
3、Client向Data node发送Block1,发送过程是流式写入的过程:host2 -> host1 -> host3
3.1)将64MB的Block1按64KB的package划分,然后Client将第一个package1发送给host2,host2接收完package1后,将package1发送给host1,同时Client将package2发送给host2;host1接收完package1后,将package1发送给host3,同时接收host2发送的package2;以此类推,直到将Block1发送完毕。上图中的红色block1实线2所示。
3.2)host2, host1, host3分别回复ACK消息给Name node,表示Block1数据发送完成;host2向Client回复ACK消息,表示Block1数据发送完成。上图中的粉色实线2'所示。
3.3)Client收到host2回复的ACK消息后,向Name node发送消息说Block1数据写完成,这样Client端Block1的写操作才真的完成。上图中的黄色block1实线3所示。
4、发送完Block1后,Client再向Data node发送Block2,发送过程是流式写入的过程:host7 -> host8 -> host4,
4.1)流程跟步骤3是一样的。上图中的蓝色block2实线5所示。
4.2)同理,host7, host8, host4分别回复ACK消息给Name node,表示Block2数据发送完成;host7向Client回复ACK消息,表示Block2数据发送完成。上图中的浅绿色实线5'所示。
4.3)同理,Client收到host7回复的ACK消息后,向Name node发送消息说Block2数据写完成,这样Client端Block2的写操作才真的完成。上图中的黄色block2实线6所示。
这样一个写操作的主要流程就完成了,这样100MB的文件需要300MB的存储,300MB的网络带宽。
四、HDFS的读取流程
还是上面的100MB的文件FileA,由Block1和Block2组成。
1、Client向Name node发送读请求,Name node查该文件的Metadata信息,返回文件FileA的Block位置:
Block1:host2, host1, host3;Block2:host7, host8, host4
2、先读节点host2上的Block1,再读节点host7上的Block2。如果Client位于某个机架Rack内,如host6,则优先读取本机架上的数据。
读操作相对比较简单,由于副本技术的存在,哪怕某个节点挂掉,也可以读其他节点上的数据,甚至机架挂掉,仍然可以读其他机架上的数据。
在执行读或写的过程中,Name node和Data node通过心跳HeartBeat进行保活通信,确定某个Data node还活着,如果发现Data node挂掉了,就将其上的数据,迁移到其他节点上去。
五、流水线复制
默认HDFS的副本数是3副本,Client从Name node获取一个列表用于记录副本位置(Data node),Client向第一个Data node1传输数据,Data node1将一小部分一小部分的接收数据,并将其缓存在本地,同时将其发送给第二个Data node2;Data node2也是如此,一小部分一小部分的接收数据,并将其缓存在本地,同时将其发送给下一个Data node3,以此类推。数据是以流水线式的方式从一个Data node传递给下一个Data node,直到数据传输完成。
六、HDFS放置副本的策略
副本的放置对HDFS的可靠性和性能至关重要,机架感知副本放置策略的目的是提高数据可靠性、可用性和网络带宽利用率。大型HDFS实例通常运行分布在许多机架的计算机集群上,而不同机架中的两个节点之间的通信必须通过交换机,在大多数情况下,同一机架中机器之间的网络带宽大于不同机架间的机器之间的网络带宽。
通过在Hadoop Rack Awareness描述的进程,Name node决定每个Data node所属的rack id。常见的情况,当副本是3时,HDFS的副本放置策略是:如果Client是一个Data node,则将第一个副本放在本地机器上,否则,放置在与Client同机架上随机的Data node,另一个副本放置在远程不同机架的一个节点上,最后一个副本放置在远程不同机架的不同的节点上。该策略可以减少机架间写流量,提高写性能。机架故障的概率远小于节点故障的概率,该策略不影响数据可靠性和可用性。同时,它不会减少读取数据时使用的聚合网络带宽,因为一个块只放置在两个而不是三个机架上。使用这个策略,一个块的副本不会均匀地分布在机架上。两个副本位于一个机架的不同节点上,其余的副本位于另一个机架的节点上。在不影响数据可靠性和读性能的前提下,提高写性能。如果副本数大于3,第4个和后面的副本的放置是随机确定的,同时保持每个机架的副本数量低于上限(replicas - 1) / racks + 2,由于NameNode不允许datanode在同一块上有多个副本,所以最大的副本创建数量是同一时间datanode的总数。NameNode首先根据机架感知来选择节点,然后检查与文件关联的策略是否需要候选节点的存储空间。
机架感知Rack-Awareness
HDFS块放置策略使用的机架感知来实现的容错,将一个Block块副本放在不同的机架上,在出现网络交换机故障或集群内网络分区时,提供了数据可用性。
Hadoop的Daemon守护进程通过调用外部脚本或由配置文件指定的Java类来获取集群工作节点的机架号Rack Id,使用Java类或外部拓扑脚本,输出必须遵循java org.apache.hadoop.net.DNSToSwitchMapping接口,该接口需要维护一对一的通信,拓扑信息的格式为'/myrack/myhost',其中'/'是拓扑分隔符,'myrack'是机架标识符,'myhost'是单个主机。假设每个机架有一个/24子网,可以使用'/192.168.100.0/192.168.100.5'的格式作为唯一的机架-主机拓扑映射。要使用java类进行拓扑映射,类名由配置文件中的net.topology.node.switch.mapping.impl参数指定。使用Java类而不是外部脚本具有性能优势,因为Hadoop在新工作节点注册自己时不需要fork外部进程。如果实现一个外部脚本,它将在配置文件中用net.topology.script.file.name参数指定。与java类不同,外部拓扑脚本不包含在Hadoop发行版中,由管理员提供。在fork拓扑脚本时,Hadoop会向ARGV发送多个IP地址。发送到拓扑脚本的IP地址数量由net.topology.script.number.args控制,默认值为100。如果没有设置net.topology.script.file.name或net.topology.node.switch.mapping.impl,则对任何通过的IP地址返回机架号'/default-rack'。这样只有一个机架名为'/default-rack',它可能会导致HDFS块复制的问题。
读操作时副本的选择
为了最大限度地减少全局带宽消耗和读时延,HDFS的读请求会尽量满足离Client(reader)最近的副本。如果Client(reader)节点所在机架上存在副本,则优先使用该副本来响应读请求。如果HDFS集群跨多个数据中心,则优先选择本地数据中心的副本,而不是远程副本。
块放置策略
如上所述,当副本数是3,HDFS的放置政策是:如果Client是一个Data node,则将第一个副本放在本地机器上,否则,放置在与Client同机架上随机的Data node,另一个副本放置在远程不同机架的一个节点上,最后一个副本放置在远程不同机架的不同的节点上。如果副本数大于3,第4个和后面的副本的放置是随机确定的,同时保持每个机架的副本数量低于上限(replicas - 1) / racks + 2。除此之外,HDFS还支持4种不同的可插入块放置策略。用户可以根据他们的基础设施和用例选择不同的策略,HDFS默认支持BlockPlacementPolicyDefault。
Safemode
在启动时,NameNode进入一个称为Safemode的特殊状态。当NameNode处于Safemode状态时,不会发生数据块的复制。NameNode接收来自DataNode的Heartbeat和Blockreport消息,Blockreport包含DataNode所托管的数据块列表。每个块都有指定的最小副本数,当该数据块的最小副本数记录到NameNode时,该块就是安全副本。在NameNode记录了一定百分比的安全副本数据块后(再加上30秒),NameNode将退出Safemode状态。然后它确定数据块列表(如果有的话),这些数据块的副本数量少于指定的数量,NameNode会将这些块复制到其他DataNode。
七、简单一致性模型
HDFS的文件特点是一次写入多次读取(write-once-read-many),文件一旦创建、写入和关闭,就不需要更改,除非追加Append和截断Truncate。 支持将内容追加Append到文件末尾,但不能在任意点进行更新。 这种假设简化了数据一致性问题,并支持高吞吐量的数据访问。
文章开头已述,HDFS是GFS论文的实现,接下来我们就来分析一下GFS论文。