Hadoop介绍
Hadoop的特点
Hadoop是Apache软件基金会旗下的一款开源软件框架,它主要
- 以分布式 (Distributed) 的方式存储海量数据
- 并行处理 (Parallelly Processing) 大数据
- 建立在大规模的商品硬件集群上 (Builds on large clusters of commodity hardware)
因此,Hadoop可以有效的解决大数据的基础3V问题,即:
a) Volume (Scale of data):Hadoop可轻易处理PB量级的数据
b) Velocity (Speed):并行式的数据处理效率更高,且实时性可以得到保障
c) Variety (Diversity):Hadoop可存储任意类型的数据
综上所述,我们不难总结出,Hadoop主要解决大数据中的两个关键问题:1) 分布式存储;2) 并行处理。 针对这两个关键问题,Hadoop提供以下两个核心服务:
- 分布式文件存储系统 (Hadoop Distributed File System, HDFS):这是一个冗余且具有容错性的数据存储系统。
- 分布式并行计算框架 (MapReduce)
从Hadoop v2.0开始,在以上2个核心服务之外,又加入了一个新的框架YARN。在这里我们需要注意的是,YARN并不是一个完完全全的新框架,实际上我们可以把它看作是对Hadoop v1.0的MapReduce进行任务分割的产物。
在Hadoop v1.0中,MapReduce除了数据处理 (Data Processing) 之外,还要进行集群资源管理 (Cluster resource management),即如何把底层的CPU,内存,带宽等资源分配给上层的计算任务。这样会使得MapReduce的工作效率很低,因此在Hadoop v2.0时,将资源调度这部分的任务单独划分出来,就是现在我们看到的YARN,如此一来,MapReduce就可专心进行数据处理的任务。
如今,Hadoop已经形成了一个相当完整且庞大的生态系统。在当下的Hadoop中,它的主要组成部分可以看做有3个:
1. HDFS:分布式文件存储系统
2. MapReduce:分布式并行处理框架
3. YARN (Yet Another Resource Negotiator):工作调度与资源分配框架
除此以外还有诸如Hive, Pig, Spark等众多的服务框架。
Hadoop的优点
Hadoop对于使用者屏蔽了所有大数据技术的底层实现细节,因此,用户无需担心:
Hadoop结构(Architecture)
Hadoop采用Master-Slave/Manager-Worker结构。这一结构的作用我们用一张图就可以很好理解:
即主节点 (Master/Manager Node) 将任务分发 (Distribute) 给多个工作节点 (Slave/Worker Node) 进行处理,这就是分布式处理思想的具象化,比起传统单一节点慢慢处理,效率有了巨大的提升。但若仅仅是这样,仍然有一个巨大隐患。
我们可以想象一下,如果此时有某个员工C(工作节点)罢工了:
那么,分发给该员工(工作节点)的任务将会面临无法验收的境地,老板对此必然气得跳脚。当然,老板(主节点)也可以把员工C的任务再分配给员工A或者其他“在职”的员工,但如果每次都如此处理,那么会很不方便,因此老板有一套新的策略:
把同一个任务分配给两个员工来做(鸡蛋不放在一个篮子里,反正工作的也不是老板),这样一来:
即使有某个员工“罢工”了,老板仍能收到所有任务的成果,不必担心因“罢工”带来的损失。
Hadoop的结构特征体现在它提供的众多框架之中,比如我们接下来要介绍的HDFS。在这里我们还要注意一点,在Hadoop中,工作节点 (Slave Node/Worker Node) 不必是具有高性能的服务器,它可以是任何一台普通的PC机,这也从另一个层面体现出Hadoop的易用性。
HDFS介绍
HDFS (Hadoop Distributed File System) 是一个:
- 遵循Master/Slave结构
- 允许我们在多个节点 (Node)/机器 (Machine) 上存储数据
- 允许多个用户访问数据 (Allows multiple users to access data)
- 与个人电脑 (PC) 中的文件系统类似的
文件系统 (File System),该系统支持:
- 分布式存储 (Distributed Storage)
- 分布式计算 (Distributed Computation):比如MapReduce
- 水平扩展性 (Horizontal Scalability)
这里特别介绍一下什么是水平扩展性 (Horizontal Scalability),既然有水平扩展 (Horizontal Scaling),那么就有垂直扩展 (Vertical Scaling)。我们用一张图来体会它们的区别:
垂直扩展 (Vertical Scaling) 指的是单纯通过升级硬件性能来增大存储空间。但是这里有两个问题,一是硬件性能是有上限的,二是升级硬件时,必须暂停运行。与之相对的,就是水平扩展 (Horizontal Scaling),水平扩展指的是对现有的工作集群添加更多的工作节点 (Node) 以增加集群整体的容量,提升工作性能,这样的操作打破了硬件的上限,我们可以尽可能多地给集群添加新的节点,从理论上来说,可以无限增长容量,而且在添加新的节点 (Node) 时,我们不需要暂停集群中的节点的工作。
HDFS结构
我们首先给出HDFS的结构图:
根据上图,我们很清晰的看到,HDFS主要由4部分构成:
- HDFS Client(与NameNode交流,当进行读/写时,与DataNode交流)
- NameNode
- DataNode(DataNode之间无交流)
- SecondaryNameNode(仅与NameNode交流)
其中Client很好理解,就是用户使用的机器,因此我们主要把目光放在NameNode, DataNode以及SecondaryNameNode上。
NameNode
NameNode是一个Master Node,它主要用来维持和管理DataNodes(Slave Nodes)中的存储的Blocks。 它的主要功能有:
- 记录所有文件的元数据(Meta Data)。其中有两个文件对于NameNode来说至关重要:
- FsImage:该文件包含了File System Namespace的完整状态,所谓的File System Namespace即整个文件系统的树状结构(Tree Structure)。所以FsImage相当于整个HDFS系统的在某一时刻的一个快照 (Snapshot)
- EditLogs:就是编辑日志,它记录了所有对HDFS进行的操作(比如添加/删除文件)。
- 周期性检查DataNodes的状态,主要是看DataNode“是死是活” (Dead or Alive) 。为了做到这一点,DataNode需要周期性向NameNode发送Heartbeat Message(默认是每隔3s发一次)。
- 对HDFS中的所有blocks保持记录。诸如block的位置以及该block属于哪个文件(File)之类的信息,都会被记录。
- 在DataNode失效(Failure)时,NameNode负责进行数据恢复。
DataNode
DataNode是Slave Node,主要是用于存储数据的商业硬件 (commodity hardware)。它有以下的功能:
- 存储实际数据 (Stores actual data)
- 执行读/写请求 (Perform read/write requests)
- 通过发送Heartbeat Message向NameNode报告自己的“健康状况”
下面用一张表格直观对比NameNode和DataNode:
接下来,我们思考一个问题。在之前介绍NameNode时,我们提到了,它可以在DataNode失效时进行数据恢复,那么如果NameNode失效了,该怎么办?
NameNode失效带来的后果远比DataNode严重得多,一旦NameNode失效,所有的文件都将丢失,因为没有NameNode中保存的元数据 (Metadata),无法进行数据的重建。为了保证NameNode能够从失效中能迅速恢复 (Resilient),就有了两种思路:
- 备份 (Backup) NameNode中的元数据,在HDFS中通常会选择把备份存放于NFS (Network File System)。
- 使用SecondaryNameNode。
Secondary NameNode
Secondary NameNode为NameNode中的文件系统元数据提供Checkpoints。这里,我们需要注意,Secondary NameNode它并不是NameNode的备份!
它的功能主要有:
- 存储一个FsImage和Editlogs的副本 (Copy)
- 周期性地对FsImage调用Editlogs并刷新Editlogs
- 若NameNode失效了,可从Secondary NameNode最后存储的FsImage恢复整个文件系统的元数据 (Metadata)
之所以周期性对FsImage调用Editlogs,除了跟踪操作以外,一个很重要的原因是为了避免Editlogs占用的空间越来越大。一般来说,Secondary NameNode会运作在一个单独的机器上,因为它需要充足的CPU资源来进行FsImage和Editlogs的合并 (Merge) 操作。
从以上Secondary NameNode的功能来看,它确实保存了FsImage和Editlogs的副本,因此可以用来帮助解决NameNode失效的问题,但是,我们要注意,SecondaryNameNode的状态实际上滞后于NameNode,所以他对于解决NameNode失效的问题实际上帮助有限。因此,实际情况中,通常会使用元数据备份至NFS的方式来解决问题(更加简单粗暴)。
那么SecondaryNameNode的主职工作以及它的NameNode的关系到底是什么样的呢,我们依然用一张图来看:
- Secondary NameNode会定期从NameNode获取Editlogs,并使用该Editlogs去更新它所存储的FsImage
- 之后,将更新后的FsIamge复制一份发送给NameNode
因此,NameNode实际上不会直接更新自己的FsImage,这些工作都是交给Secondary NameNode完成的(老板负责宏观调控就好了,具体任务不需要亲力亲为)。NameNode做的只是将FsImage和Editlogs关联起来,而不需要亲自使用Editlogs去更新FsImage(进行merge操作)。
在介绍了HDFS的结构以及各组成部分的具体职能之后,我们来看一看HDFS到底是如何存储文件(File)的,具体的存储策略有两种,接下来将一一介绍。
副本存放 (Replication)
Blocks
首先,我们要明确一点,HDFS在存储文件 (File) 时,并不是把一整个文件存储在某个DataNode,而是会把文件 (File) 分割为多个Blocks,分散存储。
- 数据以HDFS中的一组blocks的形式被存储
- 默认的block尺寸为128MB(Hadoop 2.x/Hadoop 3.x)
这里用一个例子就能很好地理解:
我们可以直观地得出一个简单的公式:
为何block size要设定的这么大呢?
因为HDFS存储的是量级巨大的数据集,如果block size很小(比如像Linux一样为4kb),那么block的数量将会变得很多,这也将造成:
- NameNode存储的元数据 (Metadata) 会非常多
- 过多的seek操作,会影响数据的读取速度(read speed = seek time + transfer time,seek time与block的数量呈正比)
- 进一步影响MapReduce的性能
这也是为什么我们不推荐使用HDFS存储小文件的原因,因此即使一个仅有4kb的小文件,它依然会占用一整个block,会极大造成资源的浪费。
Replication Management
好的,到目前为止我们已经知道,文件会被分割为一系列的blocks存储于DataNode之中,那么,我们再来考虑一遍那个经典的问题,如果DataNode失效了,该怎么?在Hadoop中,如果NameNode 10分钟未从DataNode接收到Heartbeat Message,就会认为知道该DataNode“死亡”。
为了保证数据的可靠性,Hadoop对数据 (data) 生成多个副本 (multiple replications),这就是我们要讨论的副本存放策略。
- 每个block都会有多个副本 (replications),每个副本内容相同,副本的数量由预设的replication factor决定,默认每个block有3个replications
- 所有的副本 (replication) 都会存储在不同的DataNode上
- 如果因为某个DataNode失效,造成其中的blocks丢失,那么这些blocks可以从其他的副本 (replications) 得到恢复
- 缺点是总的消耗空间为数据量的3倍
我们之前提到过,NameNode会在DataNode失效时,负责进行数据恢复,这里我们就具体说一说NameNode在此过程中到底做了什么。首先,正如前文所说,NameNode保存了所有DataNode的元数据,因此它知道关于所有DataNode的一切(除了DataNode保存的数据的内容)。当NameNode探测到某个DataNode失效时,它会知道该DataNode中原本保存着哪些blocks,它同样知道这些丢失的blocks的副本在其他哪些DataNodes中,因此NameNode就可以从其他的replications中复原出丢失的blocks。
这种存储策略同样有助于保证数据的完整性 (Integrity),即知悉保存于HDFS中数据是否正确。HDFS会持续使用Checksum来校验存储于DataNode中的数据是否损坏,一旦发现某些blocks损坏了,就会立刻报告给NameNode,NameNode会根据该block的其他副本 (replication) 创建一个新的replication,然后把损坏的block删除。
这里我们来看一个例子:
这里我们将一个文件 (file) 划分为5个blocks,且每个block有3个replications,此时若有一个DataNode失效:
我们可以看到,已丢失的block仍各自留有2个副本在其他的DataNode中。因此,当DataNode1重新上线,我们可以还原其中的blocks。或者寻找一个新的DataNode,比如DataNode5,把恢复的blocks存入其中,以保每个block证始终有3个副本存于HDFS中。
*关于为何默认replication factor为3,之后我会单独用一篇博客来进行解释,这里就先不赘述
我们已经考虑了DataNode一个接一个失效的情况,但仍不够全面,如果有多个DataNode同时失效 (Simultaneous Failure),那么会怎样呢?
- 如果有一个DataNode失效,那么会有B个blocks丢失自己的第1个副本
- 如果有两个DataNode失效,那么会有一些blocks丢失2个副本,更多的blocks丢失一个副本
- 如果有三个DataNode失效,那么可能会有一些blocks丢失全部3个副本,一些丢失2个副本,更多的blocks丢失自己的第一1个副本
我们用数学语言来看一下:
- 假设N个DataNode中有k个节点同时失效,因此会有:
- L1(k, N) 个blocks失去一个副本
- L2(k, N) 个blocks失去两个副本
- L3(k, N) 个blocks失去全部三个副本
- 用 B 表示一个节点 (Node) 失效会丢失的block的数量 - k = 0:
- L1(0, N) = L2(0, N) = L3(0, N) = 0 - k = 1:
- L1(1, N) = B
- L2(1, N) = L3(1, N) = 0 - k = 2:
- L1(2, N) = 2B - 2 * L2(2, N)
- L2(2, N) = 2 * L1(1, N) / (N-1)
- L3(2, N) = 0 - k = 3:
- L1(3, N) = 3B - 2 * L2(3,N) - 3 * L3(3,N)
- L2(3, N) = 2 * L1(2, N) / (N-2) + L2(2, N) - L2(2, N) / (N - 2)
- L3(3, N) = L2(2, N) / (N-2) - …
这里我们着重讲一下 L2(2, N) = 2 * L1(1, N) / (N-1):
这里的 L1(1, N) 表示已经失去第一个副本的blocks的数量,我们用集合A表示这些遗失的blocks。对于集合A中的这些blocks,他们仍然有2个副本保存于其他的DataNode中,我们假设这些副本均匀分布 (Uniformly distributed) 于剩下的N - 1个节点中。
因此,对于每一个DataNode,它拥有集合A中的blocks的对应副本的期望值 (expected number) 为 2 * L1(1, N) / (N-1)。
所以,当第二个DataNode(剩余 N - 1个节点中的1个) 失效时,我们预计会有 2 * L1(1, N) / (N-1) 个blocks失去它们的第二个副本。
我们可以得到一个式子:
L2(k, N) = 2 * L1(k-1, N) / (N - k + 1) + L2(k-1, N) - L2(k-1, N) / (N-k+1)
我们考虑一个实际的情况:N = 4000,B = 750,那么我们会得到以下结果:
可以明显地看出来,即使有200个节点同时失效,失去全部三个副本的blocks的数量仍在一个可以接受的范围之内,这样就从另一个层面证明了把replication factor设置为3能有效保证数据的可靠性 (Reliability)。
Rack Algorithm
回顾一下HDFS的结构图,现在,我们来了解一下其中的最后一个要素Rack。我们要记住其最重要的一个概念:同一个Rack中的Nodes,在物理意义上彼此十分靠近。
所以Rack Algorithm的存在意义就是为了在保证数据不会丢失的同时,提高读写以及数据恢复的速度。同一个Rack中的Nodes之间,速度很快,处于不同Rack的Nodes之间,速度较慢。
具体来看Rack Algorithm。当我们设定了Replication Factor为3后:
- 第一个副本会被存储在Local DataNode(这个Local DataNode可以是Client,如果不想把Client用作DataNode,那么会根据距离来随机选择一个Node)
- 第二个副本会存储在与第一副本不同的rack中
- 第三个副本会存储在与第二副本相同的rack中,但会在不同的node
用一张图来看一下:
以Block 1为例,第一副本存储于Rack 1的Node 1中,第二副本和第三部分都存储于Rack 2,但是它们所处的Node不同,一个在Node 5,一个在Node 6。
为什么我们要秉持这种Rack的思想呢?
这里我们先给出一幅“距离 (Distance)”度量图:
我们可以很清晰地看到:
- 同一个Node的blocks之间,距离d = 0(最小)
- 同一个Rack的Nodes之间,距离d = 2
- 不同Rack的Nodes之间,距离d = 4
- 不同的Data Center之间,距离d = 6 (最大)
而距离 (Distance),就直接影响了速度 (Speed)。
所以,我们在此秉持的这种Rack思想,可以:
- 降低延迟(Reduce Latency)
a) 写(write):每个block只需写入2个racks,而不是3个
b) 读(read):从多个racks并行读取blocks - 容错 (Fault Tolerance):很好地体现了不将鸡蛋放在一个篮子的思想。
读写操作
Write in HDFS
HDFS的write操作有三个阶段:
- Create File
- Write File
- Close File
这里的DistributedFileSystem和FSDataOutputStream是两个Hadoop的Java class的实例(instance),我们在此不做过多的解释。
以下为3个阶段的具体工作流程:
1. Create File
- Step 1: Client从DistributedFileSyetem调用create函数,即Client告诉后者,“我想建立一个文件”。
- Step 2: DistributedFileSystem会把这个消息告知NameNode,并希望NameNode能为自己分配一些blocks以便写入数据。
- Step 3: NameNode会进行一系列检查,去核实想要创建的这个文件在当前的文件系统中并不存在,且Client有写入文件的许可。当所有检查通过,NameNode会对新文件 (New File) 进行记录并回复DistributedFileSystem。
- Step 4: DistributedFileSystem会为Client创建一个FSDataOutputStream,Client会将文件写入其中。
2. Write File
- Step 5: DistributedFileSystem会将Client写入的文件分割为Packets并将这些Packets写入一个由三个DataNode组成的Data Cube
- Step 6: 这三个DataNode会组成一个Pipeline,第一个DataNode会存储传递来的每一个Packet并传递给下一个DataNode。当所有DataNode结束了写入操作,会返回Ack Packet至FSDataOutputStream
3. Close File
- Step 7: 当Client结束写入数据,会告知FSDataOutputStream操作结束, DistributedFileSystem会将此消息传递给NameNode
在整个过程中,我们有以下几点需要注意:
- 任何时候都只有一个writer被允许进行工作,若有多个client想要进行写操作,就必须等前一个操作结束
- blocks是同时被写入的。 具体一点说,比如一个文件有多个blocks,这些blocks是同时被写入系统的,但每一个block的replications是以顺序方式写入多个节点(就是上图中的pipeline)
- DataNode是基于Rack Algorithm和replication management policy
Read in HDFS
具体的流程为:
- Client从DistributedFileSyetem调用open函数以打开自己想要read的文件,后者会告知NameNode,“有位client想要read某个文件”
- NameNode会将文件的blocks的位置 (locations) 回复给DistributedFileSyetem
- Client此时通过FSDataIntputStream读取文件,当读取结束后,Client会通过调用close function告知后者操作结束
同样,read操作也有一些我们需要注意的地方:
- 多个reader可以同时进行工作(即使是对同一个File进行读取)
- Blocks被同时读取,每个Block只会被读取一个Replication,至于读取哪一个,会根据网络拓补结构来选择距离Client最近的DataNode
- 在读取过程中也会处理错误以及blocks损坏的问题:
- 在读取过程中,如果发现某个DataNode失效,那么FSDataIntputStream会选择第二近的DataNode进行读取,同时它也会记住该DataNode失效,所以之后的读取过程中,不会再次访问该节点。同时也会将这个节点报告给NameNode,让其进行恢复操作。
纠删码技术 (Erasure Coding)
尽管我们用3 Replications这种副本存放方式能够有效保证数据可靠性 (Data Reliability),但这种方式仍有弊端。首先,这种方式会比单纯存放数据多花费2倍的空间(在进行写入操作时,也会消耗更多的带宽资源)。其次,一些冷门的数据集,它们本身就不常被访问,但这些数据仍会和那些热门的数据占用同样的空间,这也是一种浪费。
因此,这就诞生了另一种保障数据可靠性,且能避免以上两种问题的新技术 - Erasure Coding。
Erasure Coding的思想并不复杂,比如我们想要存储一个数字39:
我们直接将39分割为x = 3和y = 9,并通过对x和y 进行编码 (encoding),生成了三个式子,我们存储这三个式子,这样,不管其中哪个式子丢失了,我们仍能用剩下的两个式子解码 (decoding) 出x和y。
可能用数字不是很直观,那么我们以File的形式重新看一遍,我们把39看做是一个File,x和y是该文件分割成的2个blocks。此时若们仍用Replications进行存储,那么我们分别对x和y建立3个副本,一共就需要6个副本来存储该文件。但我们若用Erasure Coding,那么只需要对用x和y进行编码 (encoding) 得出的3个副本进行存储,因此只需要用3个副本即可存储该文件。
接下里我们看一看具体的操作
(6,3)-Reed-Solomon
现在考虑:
- X 为我们想要进行编码的数据,它是一个有6个元素的向量
- G 是一个矩阵,它的前6行是一个单位矩阵,最后3行分别为g1, g2和g3 ,同时从该矩阵中任意选择6行组成的矩阵,都是满秩的。
然后用X和G做内积,得到新的矩阵P:
直观表示为:
以上就是编码 (encoding) 的过程,在解码 (decoding) 时,我们只需要G和P中的任意6行 (G’和P’),即可复原出X:
我们稍微总结一下:
- X 就是我们想要存储的数据
- G 是一个预设 (Predifine) 的矩阵,它可以对不同的X进行调用,它不占用任何空间
- 我们最终用存储 P 来代替存储 X
我们可以简单地看做把 xi 和 parities存储于不同的blocks,即使有3个blocks丢失了,最后我们仍能复原出存储的数据。这项技术已经从v3.0开始在Hadoop中使用。接下来我们来看具体的流程。
Striped Block Management
- 原始数据被划分为多个cells (Raw data is stripped into cells)
- 与划分blocks类似,cells的数量取决于数据大小与cell大小,默认的cell尺寸为64kb。可以看出cells的尺寸远小于blocks,平均一个blocks可分为128MB / 64KB = 200cells/block - Cells会按顺序被写入Blocks(这里我们需要6个blocks)。具体的写入顺序为:
这样的写入方式 (Online writing) 同时也支持streaming input(流输入),因为数据持续流入的时候,我们并不知道它到底有多大。
- 之后用6个cells来计算3个parities,如此一来6个cells和3个parities就构成一条 (stripe)
- 最终我们会得到一个Block Group
- 该Block Group由9个blokcs组成,其中6个raw data blocks,3个parity blocks
- 这些blocks会存储在不同的DataNodes中
- Block Group的相关信息会存储在NameNode中
此时,若其中有一个block失效,我们仍能够成功复原:
即使是block 6和block 7同时失效,仍能够借助block 1- 5 + block 8先复原block 6,再复原 block 7
读写操作
Parallel write
用户 (client) 会同时 (simutaneously) 写入一个由9个DataNode(因为9个blocks在不同的DataNode中)
如果在写入的过程中发生问题:
- Client会忽略失效的DataNode,继续进行写入。同时Client最多容许有3个DataNode失效
- 因为DataNode失效而丢失的blocks,之后会进行恢复
Parallel read
从有data blocks的6个DataNode并行读取。注意,我们不会直接读取parityies block!!
但如果在读取过程中有DataNode失效,那么,就会用剩下的parity block所在的DataNode进行补位,之后,重建失效的DataNode
Replication vs. Erasure Coding
- Data Durability 指的是处理错误的能力,也可以看做是容错 (Fault Tolerance) 的程度
- Data Locality 指的是将计算向数据靠拢,而不是让数据项计算靠拢,这个理念在MapReduce中有更直观的体现,我们后续会介绍。它会减少网络拥塞,但会增加系统的吞吐量。
- Write Performance: 对parities的计算会降低写入的吞吐量
- Read Performance: 都是平行读取,但是Erasure Coding会进行数据重建,降低了数据吞吐
- Recovery Cost: 数据重建的过程中,Erasure Coding需要从多个DataNode读取数据(需要额外的带宽),重建计算需要额外的CPU资源
上表中的第一列表示一个文件 (File) 有多少个blocks。我们可以看到,使用3-Replication存储时,我们在进行读取 (read) 操作的时候,需要读取的blocks数量等于文件的block数量,比如一个文件有3个blcoks,那么,我们在读取该文件时,就需要读取3个DataNode。若使用(6, 3) - RS,我们假设一个文件 (File) 有B个blocks,那么,使用(6, 3) - RS需要读取
综上所述,我们可以看出,Erasure Coding更适合大型的以及鲜少访问的文件。HDFS的用户以及管理员对单个文件/路径可以选择是开启/关闭Erasure Coding。