第四次作业
一、设计思想
1.分块存储:
举个例子,如有有100T文件,存在3个节点,怎么存?100T存在一个节点上吗?直接存储在一台机器上 合理吗?不合理!负载不均衡。我们可以将100T分成多个部分进行存储,分块存储。
每个部分(块)应该分很多合适?1T,如果文件只有2T,分成2个块,还是负载不均衡!
如果块太大不合适,负载会不均衡。
块大小1kb合适吗?也不合适。元数据存储的时候,一个块会存储一条元数据,这样会导致需要存很多元数据。namenode的压力会很大。
hadoop中已经对块做了设计:hadoop1:块大小默认64M ;hadoop2:块大小默认是128M;
2. 备份存储—-副本机制
副本是什么?
副本相当于是备份,完全复制出来的,文件完全一样。
在hdfs中数据块的存储是采用多副本存储的
各个副本之间没有主次之分,多个副本之间是相互互为副本,地位一样的。这里副本的个数也就是文件的总份数。例如副本数量为3,也就是这个文件的在集群中的总数量是3。
hadoop集群中默认存储的副本个数:3.
一个块这里配置了几个副本 就会复制几份出来
1)如果配置了副本,则会覆盖默认的副本数量
如果只有3个节点,副本有2个,则每一个块会被存2份。假设有一个块损坏了,剩下1个,这个时候hdfs会进行复制,达到块的个数2。假设复制完了,刚才的损坏的块又恢复了,块的个数变为3 ,这个时候块多1个,namenode在一定的时间内如果发现还是3个块,则会删除一个块,最终达到2个块。
2)同一个块多副本如何存储:
多副本存在一个节点上有没有意义?没有意义,因为如果机器宕机所有副本都没有了。所以每个副本存储在不同的节点上,任何两个副本都不可能存储在相同的节点上。也就是说每一个节点上只能存储同一个块的一个副本,一个节点上可以存多个块,但是这些块都是不同的块。
3)如果节点2个,副本4个怎么存储?
实际存储的时候会存储2个,集群会进行记账,欠2个副本,当集群中的节点个数增加的时候会进行复制,最终复制到4个。
4)副本个数越多越好吗?
副本个数越多数据安全性越高,但是数据维护起来越困难,成本加大。hdfs底层用空间换取数据安全的,因为目前硬件成本相对于数据价值来说是比较低的。
二、hdfs主要架构

namenode的作用一:存储元数据
元数据记录了—-抽象目录树:hdfs是一个文件系统 类似于linux根目录/(所有集群共同存储的根目录,不指某一台机器的根目录)
这里的抽象目录树指的是hdfs上的文件目录结构对于用户是看不到底层的一个个节点的,他们看到的是一个抽象出来的目录树。
元数据记录了—-数据和块的映射关系
namenode的作用二:接收客户端的读写请求
datanode的作用一:负责真正的数据存储
datanode的作用二:负责处理客户端的真正读(下载)写(上传)请求
secondarynamenode:是namenode的备份节点 (冷备份)
帮助namenode进行元数据备份;帮namenode做一些工作,减轻nanenode压力
Hadoop 原理总结
一、Hadoop技术原理
Hdfs主要模块:NameNode、DataNode
Yarn主要模块:ResourceManager、NodeManager
常用命令:
1)用hadoop fs 操作hdfs网盘,使用Uri的格式访问(URI格式:secheme://authority/path ,默认是hdfs://namenode:namenode port /parent path / child , 简写为/parent path / child)
2)使用start-dfs.sh启动hdfs
1 MR执行流程:
1)客户端提交Mr 的jar包程序给JobClient
2)JobClient通过RPC和JobTracker 进行通信返回新的JOB ID 和路径
3)Client将jar包写入到HDFS当中(提交10份)
4)开始提交任务(任务的描述信息,不是Jar),有任务的详细信息
5)JobTracker进行初始化任务,把任务放到调度器中,在一台机器上
6)读取HDFS上的要处理的文件,开始计算输入分片
7)TaskTracker通过心跳机制领取任务
8)下载所需要的jar,配置文件等
9)TaskTracker启动一个Java child子进程
10)将结果写入HDFS 当中
二、HDFS主要模块及运行原理
Hdfs主要模块:
Hdfs的块的实际保存位置:tmpHdfsPath + /dfs/data/current/BP-126879239-192.168.1.213-1648462/cureent/finalized中,保存俩文件一个是块,一个是块的描述信息,即:blk_1073434 、blk_1073434_1015.meta
1)NameNode:
功能:是整个文件系统的管理节点。维护整个文件系统的文件目录树,文件/目录的元数据和每个文件对应的数据块列表。接收用户的请求。
存储:存储DataNode中各个文件的基本元数据信息,其中元数据存储是瓶颈,因为元数据需要保存2份,一份存在内存中(内存中有3个文件,fsimage,edits,内存中的metaData),一份序列化到硬盘上,但是内存空间有限,如果不停的保存几K的元数据,容易导致内存的不足,同时由于不停的从内存序列化到硬盘,也占CPU。
结构:
fsimage元数据镜像文件:存储某一段时间的NameNode的内存元数据信息(fsimage.ckpt文件)
edits:操作日志文件。(上传文件的过程中,不停的向edits写日志,不断的追加,直到成功后,内存的元数据才会更新元数据。edits都是从0开始的)
fstime:保存最近一次checkpoint的时间(checkpoint跟文件的一键还原点意义相同)
以上文件都保存在Linux系统中,edits日志是实时保存在磁盘,但edits与fsimage是v2.0版本,才是实时保存,2.0没有SecondaryNameNode。
2)DataNode:
以下针对Hadoop V 1.0 、V 0 的版本
SecondaryNameNode
功能:是HA(高可用性)的一个解决方案,是备用镜像,但不支持热备
执行过程:
1)Secondary通知NameNode切换edits文件
2)Secondary从NameNode中获取fsimage和edits(通过http),Secondary获取文件后,NameNode会生成新的edits.new文件,该文件从0开始。
3)Secondary将fsimage载入内存,然后开始合并
4)Secondary将新生成的fsimage,在本地保存,并将其推送到NameNode
5)NameNode替换旧的镜像。
说明:SecondNameNode默认是安装在NameNode节点上,但是这样不安全。
Yarn主要模块:ResourceManager、NodeManager
常用命令:
1、用hadoop fs 操作hdfs网盘,使用Uri的格式访问(URI格式:secheme://authority/path ,默认是hdfs://namenode:namenode port /parent path / child , 简写为/parent path / child)
2、使用start-dfs.sh启动hdfs
hbase行键设计原理~如何进行复杂表的查询~redis原理~hdfs原理~job提交过程~hbase,hive,mapreducejvm的优化方式~数据如何采集~集群的动态添加去除节点方法!
三、MapReduce运行原理
1、Map过程简述:
1)读取数据文件内容,对每一行内容解析成<k1,v1>键值对,每个键值对调用一次map函数
2)编写映射函数处理逻辑,将输入的<k1,v1>转换成新的<k2,v2>
3)对输出的<k2,v2>按reducer个数和分区规则进行分区
4)不同的分区,按k2进行排序、分组,将相同的k2的value放到同一个集合中
5)(可选)将分组后的数据重新reduce归约
2、reduce处理过程:
1)对多个Map的输出,按不同分区通过网络将copy到不同的reduce节点
2)对多个map的输出进行排序,合并,编写reduce函数处理逻辑,将接收到的数据转化成<k3,v3>
3)将reduce节点输出的数据保存到HDFS上
说明:
1)Mapper Task 是逻辑切分。因为Maper记录的都是block的偏移量,是逻辑切分,但相对于内存中他确实是物理切分,因为每个Mapper都是记录的分片段之后的数据。
2)shuffle是物理切分。MapReduce的过程是俩过程需要用到Shuffle的,1个mapper的Shufflle,1个多个reduce的Shuffle,一般每个计算模型都要多次的reduce,所以要用到多次的Shuffle。.
MapReduce原理图
正常HDFS存储3份文件,Jar包默认写10份,NameNode通过心跳机制领取HDFS任务,运行完毕后JAR包会被删除。
Map端处理流程分析:
1) 每个输入分片会交给一个Map任务(是TaskTracker节点上运行的一个Java进程),默认情况下,系统会以HDFS的一个块大小作为一个分片(hadoop2默认128M,配置dfs.blocksize)。Map任务通过InputFormat将输入分片处理成可供Map处理的<k1,v1>键值对。
2) 通过自己的Map处理方法将<k1,v1>处理成<k2,v2>,输出结果会暂时放在一个环形内存缓冲(缓冲区默认大小100M,由mapreduce.task.io.sort.mb属性控制)中,当缓冲区快要溢出时(默认为缓冲区大小的80%,由mapreduce.map.sort.spill.percent属性控制),会在本地操作系统文件系统中创建一个溢出文件(由mapreduce.cluster.local.dir属性控制,默认${hadoop.tmp.dir}/mapred/local),保存缓冲区的数据。溢写默认控制为内存缓冲区的80%,是为了保证在溢写线程把缓冲区那80%的数据写到磁盘中的同时,Map任务还可以继续将结果输出到缓冲区剩余的20%内存中,从而提高任务执行效率。
3) 每次spill将内存数据溢写到磁盘时,线程会根据Reduce任务的数目以及一定的分区规则将数据进行分区,然后分区内再进行排序、分组,如果设置了Combiner,会执行规约操作。
4) 当map任务结束后,可能会存在多个溢写文件,这时候需要将他们合并,合并操作在每个分区内进行,先排序再分组,如果设置了Combiner并且spill文件大于mapreduce.map.combine.minspills值(默认值3)时,会触发Combine操作。每次分组会形成新的键值对<k2,{v2...}>。
5) 合并操作完成后,会形成map端的输出文件,等待reduce来拷贝。如果设置了压缩,则会将输出文件进行压缩,减少网络流量。是否进行压缩,mapreduce.output.fileoutputformat.compress,默认为false。设置压缩库,mapreduce.output.fileoutputformat.compress.codec,默认值org.apache.hadoop.io.compress.DefaultCodec。
Reduce端处理流程分析:
1) Reduce端会从AM那里获取已经执行完的map任务,然后以http的方法将map输出的对应数据拷贝至本地(拷贝最大线程数mapreduce.reduce.shuffle.parallelcopies,默认值5)。每次拷贝过来的数据都存于内存缓冲区中,当数据量大于缓冲区大小(由mapreduce.reduce.shuffle.input.buffer.percent控制,默认0.7)的一定比例(由mapreduce.reduce.shuffle.merge.percent控制,默认0.66)时,则将缓冲区的数据溢写到一个本地磁盘中。由于数据来自多个map的同一个分区,溢写时不需要再分区,但要进行排序和分组,如果设置了Combiner,还会执行Combine操作。溢写过程与map端溢写类似,输出写入可同时进行。
2) 当所有的map端输出该分区数据都已经拷贝完毕时,本地磁盘可能存在多个spill文件,需要将他们再次排序、分组合并,最后形成一个最终文件,作为Reduce任务的输入。此时标志Shuffle阶段结束,然后Reduce任务启动,将最终文件中的数据处理形成新的键值对<k3,v3>。
3) 将生成的数据<k3,v3>输出到HDFS文件中。
Map与Reduce执行过程图
三 Hadoop序列化--Writable
序列化就是将内存当中的数据序列化到字节流中,
他实现了WritableComparable 接口,并继承了Writable(Write和ReadFile需要被实现)和Compare接口
1 特点:
1 )紧凑:高校使用存储空间
2 )快速:读写数据的额外开销小
3 )可扩展:可透明的读取老格式的数据
4 )互操作:支持多语言的交互
说明:JAVA 的序列化对继承等的结构都保存了,而对hadoop用不着,只需要存储字符就可以,所以有自己的机制。
HDFS读写流程
hdfs的读写主要设计Client、NameNode、DataNode等节点

1.打开HDFS文件,构造DFSInputStream输入流
HDFS客户端调用DistributesFileSystem.open()方法打开HDFS文件,其底层实际上是调用ClientPropocol.open()方法,返回一个HdfsDataInputStream(DFSInputStream的装饰类,真正进行读取操作是DFSInputStream)。
2.从NameNode端获取数据存储的DataNode地址
DFSInputStream的构造方法中,会调用ClientProtocol.getBlockLocations()方法获取该JDFS文件起始位置数据块的位置信息。NameNode会按照储存位置与客户端的距离排序选择最优的一个DataNode节点。
3.建立数据连接(流式接口)
DFSInputStream.read()方法从数据节点读取数据,数据会以数据包(packet)为单位从数据节点传送到客户端。当一个数据块读取结束,则DFSInputStream会重新调用ClientProtocol.getBlockLocations()获取文件的下一个数据块位置,并与其最优节点建立连接,重复以上动作。
4.关闭输入流
当客户端完成文件读取后,通过HdfsDataInputStream.close()方法关闭输入流。
另外数据块的应答包中除了包含数据还包含校验码,HDFS收到数据后会进行校验,如果发生错误,则会通过ClientProtocol.reportBadBlocks()向名字节点汇报损坏的数据块的信息,并且DFSInputStream会切换到另外一个保存该数据块的节点读取文件。
1.创建文件
client发起文件上传请求,调用DistributedFileSystem对象的create方法,在HDFS系统中创建一个新的空文件,该方法在底层调用ClientProtocol.creat()方法通过RPC与NameNode建立连接,NameNode检查目标文件是否已经存在,父目录是否存在,并检查用户是否有相应的权限,若检查通过,Namenode会在文件系统目录树下的指定目录下创建一个新文件文件,但未申请任何Block,并将该操作记录在editlog中,否则的话文件创建失败,客户端得到异常信息
2.获取HdfsDataOutputStream对象
ClientProtocol.creat()调用结束后,DistributedFileSystem.create()方法会返回一个HdfsDataOutputStream(DFSOutputStream的包装类,真正起作用的是DFSOutputStream)
3.获取Block信息及信息流管道
这时,名字节点只是在目录上创建一个空文件,DFSOutputStream会调用ClientProtocol.addBlock()向NameNode获取资源,NameNode根据配置文件中指定的备份(replica)数量及机架感知原理进行文件分配,返回LocatedBlock对象,其记录了保存该数据块的节点的信息,就DFSOutputStream会建立数据管道写数据块了。以三台DataNode为例:A B C。注: Hadoop在设计时考虑到数据的安全与高效,数据文件默认在HDFS上存放三份,存储策略为:第一个备份放在客户端相同的datanode上(若客户端在集群外运行,就随机选取一个datanode来存放第一个replica),第二个replica放在与第一个replica不同机架的一个随机datanode上,第三个replica放在与第二个replica相同机架的随机datanode上,如果replica数大于三,则随后的replica在集群中随机存放,Hadoop会尽量避免过多的replica存放在同一个机架上.选取replica存放在同一个机架上.(Hadoop 1.x以后允许replica是可插拔的,意思是说可以定制自己需要的replica分配策略)
4.成功建立数据管道后,HDFS客户端可以开始写数据,写入DFSOutputStream的数据会被缓存在数据流中,以数据包(packet)的形式存在,写满一个packet对象则将其放在输出队列中(dataQueue)中,等待DataStream线程处理。
5.DataStream线程处理dataQueue中的数据
首先会进行错误处理(处理方式主要为三步:关闭当前IO流、将ackQueue中等待确认的数据包全部移到dataQueue中、重新初始化数据流管道进行写入),之后等待dataQueue写满,然后在第一个datanode上建立数据流管道,发送数据包。当写完当前Block后会重新申请Block.例如:client向请求的3台的DataNode中的A上传数据,(本质是一个RPC调用,建立pipeline),A收到请求会继续调用B,然后B调用C,将整个pipeline建立完成后,逐级返回client。client开始往A上传第一个block(先从磁盘读取数据放到一个本地内存缓存),以packet为单位(默认 64K),A收到一个packet就会传给B,B传递给C;A每传一个packet会放入一个应答队列等待应答,如果数据包都得到确认则删除应答队列中的数据包。
6.关闭输入流并提交文件
当所有数据上传完成后,可以调用close()方法关闭输入流,并调用Clientprotocol.complete()告诉NameNode文件写入完成。
有可能管道线中的多个datanode宕掉(一般这种情况很少),但只要dfs.relication.min(默认值为1)个replica被创建,我么就认为该创建成功了,剩余的relica会在以后异步创建以达到指定的replica数。当一个block传输完成后,client再次发送请求NameNode上传第二个block到服务器

4.梳理HBase的结构与运行流程
一、 简介
hbase是bigtable的开源山寨版本。是建立的hdfs之上,提供高可靠性、高性能、列存储、可伸缩、实时读写的数据库系统。它介于nosql和RDBMS之间,仅能通过主键(row key)和主键的range来检索数据,仅支持单行事务(可通过hive支持来实现多表join等复杂操作)。主要用来存储非结构化和半结构化的松散数据。与hadoop一样,Hbase目标主要依靠横向扩展,通过不断增加廉价的商用服务器,来增加计算和存储能力
HBase中的表一般有这样的特点:
1 大:一个表可以有上亿行,上百万列
2 面向列:面向列(族)的存储和权限控制,列(族)独立检索。
3 稀疏:对于为空(null)的列,并不占用存储空间,因此,表可以设计的非常稀疏
下面一幅图是Hbase在Hadoop Ecosystem中的位置。

二、 逻辑视图
HBase以表的形式存储数据。表有行和列组成。列划分为若干个列族(row family)

Row Key
与nosql数据库们一样,row key是用来检索记录的主键。访问hbase table中的行,只有三种方式:
1 通过单个row key访问
2 通过row key的range
3 全表扫描
Row key行键 (Row key)可以是任意字符串(最大长度是 64KB,实际应用中长度一般为 10-100bytes),在hbase内部,row key保存为字节数组。
存储时,数据按照Row key的字典序(byte order)排序存储。设计key时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。(位置相关性)
注意:
字典序对int排序的结果是1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。要保持整形的自然序,行键必须用0作左填充。
行的一次读写是原子操作 (不论一次读写多少列)。这个设计决策能够使用户很容易的理解程序在对同一个行进行并发更新操作时的行为。
列族
hbase表中的每个列,都归属与某个列族。列族是表的chema的一部分(而列不是),必须在使用表之前定义。列名都以列族作为前缀。例如 courses:history , courses:math 都属于 courses 这个列族。
访问控制、磁盘和内存的使用统计都是在列族层面进行的。实际应用中,列族上的控制权限能 帮助我们管理不同类型的应用:我们允许一些应用可以添加新的基本数据、一些应用可以读取基本数据并创建继承的列族、一些应用则只允许浏览数据(甚至可能因 为隐私的原因不能浏览所有数据)。
时间戳
HBase中通过row和columns确定的为一个存贮单元称为cell。每个 cell都保存着同一份数据的多个版本。版本通过时间戳来索引。时间戳的类型是 64位整型。时间戳可以由hbase(在数据写入时自动 )赋值,此时时间戳是精确到毫秒的当前系统时间。时间戳也可以由客户显式赋值。如果应用程序要避免数据版本冲突,就必须自己生成具有唯一性的时间戳。每个 cell中,不同版本的数据按照时间倒序排序,即最新的数据排在最前面。
为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担,hbase提供了两种数据版本回收方式。一是保存数据的最后n个版本,二是保存最近一段时间内的版本(比如最近七天)。用户可以针对每个列族进行设置。
Cell
由{row key, column( =<family> + <label>), version} 唯一确定的单元。cell中的数据是没有类型的,全部是字节码形式存贮。
三、 物理存储
1 已经提到过,Table中的所有行都按照row key的字典序排列。
2 Table 在行的方向上分割为多个Hregion。

3 region按大小分割的,每个表一开始只有一个region,随着数据不断插入表,region不断增大,当增大到一个阀值的时候,Hregion就会等分会两个新的Hregion。当table中的行不断增多,就会有越来越多的Hregion。

4 Hregion是Hbase中分布式存储和负载均衡的最小单元。最小单元就表示不同的Hregion可以分布在不同的HRegion server上。但一个Hregion是不会拆分到多个server上的。

5 HRegion虽然是分布式存储的最小单元,但并不是存储的最小单元。事实上,HRegion由一个或者多个Store组成,每个store保存一个columns family。
每个Strore又由一个memStore和0至多个StoreFile组成。如图:StoreFile以HFile格式保存在HDFS上。

HFile的格式为:

Trailer部分的格式:

HFile分为六个部分:
Data Block 段–保存表中的数据,这部分可以被压缩
Meta Block 段 (可选的)–保存用户自定义的kv对,可以被压缩。
File Info 段–Hfile的元信息,不被压缩,用户也可以在这一部分添加自己的元信息。
Data Block Index 段–Data Block的索引。每条索引的key是被索引的block的第一条记录的key。
Meta Block Index段 (可选的)–Meta Block的索引。
Trailer–这一段是定长的。保存了每一段的偏移量,读取一个HFile时,会首先 读取Trailer,Trailer保存了每个段的起始位置(段的Magic Number用来做安全check),然后, DataBlock Index会被读取到内存中,这样,当检索某个key时,不需要扫描整个HFile,而只需从内存中找到key所在的block,通过一次磁盘io将整个 block读取到内存中,再找到需要的key。DataBlock Index采用LRU机制淘汰。
HFile的Data Block,Meta Block通常采用压缩方式存储,压缩之后可以大大减少网络IO和磁盘IO,随之而来的开销当然是需要花费cpu进行压缩和解压缩。
目标Hfile的压缩支持两种方式:Gzip,Lzo,null,snappy,推荐使用snappy。
HLog(WAL log)
WAL 意为Write ahead log(http://en.wikipedia.org/wiki/Write-ahead_logging),类似mysql中的binlog,用来 做灾难恢复只用,Hlog记录数据的所有变更,一旦数据修改,就可以从log中进行恢复。
每个Region Server维护一个Hlog,而不是每个Region一个。这样不同region(来自不同table)的日志会混在一起,这样做的目的是不断追加单个 文件相对于同时写多个文件而言,可以减少磁盘寻址次数,因此可以提高对table的写性能。带来的麻烦是,如果一台region server下线,为了恢复其上的region,需要将region server上的log进行拆分,然后分发到其它region server上进行恢复。
HLog文件就是一个普通的Hadoop Sequence File,Sequence File 的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是”写入时间”,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。HLog Sequece File的Value是HBase的KeyValue对象,即对应HFile中的KeyValue,可参见上文描述。
四、 系统架构


Client
1 包含访问hbase的接口,client维护着一些cache来加快对hbase的访问,比如regione的位置信息
Zookeeper
1 保证任何时候,集群中只有一个master
2 存贮所有Region的寻址入口。
3 实时监控Region Server的状态,将Region server的上线和下线信息实时通知给Master
4 存储Hbase的schema,包括有哪些table,每个table有哪些column family
Master
1 为Region server分配region
2 负责region server的负载均衡
3 发现失效的region server并重新分配其上的region
4 GFS上的垃圾文件回收
5 处理schema更新请求
Region Server
1 Region server维护Master分配给它的region,处理对这些region的IO请求
2 Region server负责切分在运行过程中变得过大的region可以看到,client访问hbase上数据的过程并不需要master参与(寻址访问zookeeper和region server,数据读写访问regione server),master仅仅维护者table和region的元数据信息,负载很低。
五、关键算法 / 流程
Region定位
系统如何找到某个row key (或者某个 row key range)所在的region
bigtable 使用三层类似B+树的结构来保存region位置。
第一层是保存zookeeper里面的文件,它持有root region的位置。
第二层root region是.META.表的第一个region其中保存了.META.z表其它region的位置。通过root region,我们就可以访问.META.表的数据。
.META.是第三层,它是一个特殊的表,保存了hbase中所有数据表的region 位置信息。

说明:
1 root region永远不会被split,保证了最需要三次跳转,就能定位到任意region 。
2.META.表每行保存一个region的位置信息,row key 采用表名+表的最后一样编码而成。
3 为了加快访问,.META.表的全部region都保存在内存中。
假设,.META.表的一行在内存中大约占用1KB。并且每个region限制为128MB。
那么上面的三层结构可以保存的region数目为:(128MB/1KB) * (128MB/1KB) = = 2(34)个region
4 client会将查询过的位置信息保存缓存起来,缓存不会主动失效,因此如果client上的缓存全部失效,则需要进行6次网络来回,才能定位到正确的region(其中三次用来发现缓存失效,另外三次用来获取位置信息)。
读写过程
上文提到,hbase使用MemStore和StoreFile存储对表的更新。
数据在更新时首先写入Log(WAL log)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,并 且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时,系统会在zookeeper中 记录一个redo point,表示这个时刻之前的变更已经持久化了。(minor compact)
当系统出现意外时,可能导致内存(MemStore)中的数据丢失,此时使用Log(WAL log)来恢复checkpoint之后的数据。
前面提到过StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更 新其实是不断追加的操作。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(major compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对 StoreFile进行split,等分为两个StoreFile。
由于对表的更新是不断追加的,处理读请求时,需要访问Store中全部的 StoreFile和MemStore,将他们的按照row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,合并的过程还是比较快。
写请求处理过程

1 client向region server提交写请求
2 region server找到目标region
3 region检查数据是否与schema一致
4 如果客户端没有指定版本,则获取当前系统时间作为数据版本
5 将更新写入WAL log
6 将更新写入Memstore
7 判断Memstore的是否需要flush为Store文件。
region分配
任何时刻,一个region只能分配给一个region server。master记录了当前有哪些可用的region server。以及当前哪些region分配给了哪些region server,哪些region还没有分配。当存在未分配的region,并且有一个region server上有可用空间时,master就给这个region server发送一个装载请求,把region分配给这个region server。region server得到请求后,就开始对此region提供服务。
region server上线
master使用zookeeper来跟踪region server状态。当某个region server启动时,会首先在zookeeper上的server目录下建立代表自己的文件,并获得该文件的独占锁。由于master订阅了server 目录上的变更消息,当server目录下的文件出现新增或删除操作时,master可以得到来自zookeeper的实时通知。因此一旦region server上线,master能马上得到消息。
region server下线
当region server下线时,它和zookeeper的会话断开,zookeeper而自动释放代表这台server的文件上的独占锁。而master不断轮询 server目录下文件的锁状态。如果master发现某个region server丢失了它自己的独占锁,(或者master连续几次和region server通信都无法成功),master就是尝试去获取代表这个region server的读写锁,一旦获取成功,就可以确定:
1 region server和zookeeper之间的网络断开了。
2 region server挂了。
其中一种情况发生了,无论哪种情况,region server都无法继续为它的region提供服务了,此时master会删除server目录下代表这台region server的文件,并将这台region server的region分配给其它还活着的同志。
如果网络短暂出现问题导致region server丢失了它的锁,那么region server重新连接到zookeeper之后,只要代表它的文件还在,它就会不断尝试获取这个文件上的锁,一旦获取到了,就可以继续提供服务。
master上线
master启动进行以下步骤:
1 从zookeeper上获取唯一一个代码master的锁,用来阻止其它master成为master。
2 扫描zookeeper上的server目录,获得当前可用的region server列表。
3 和2中的每个region server通信,获得当前已分配的region和region server的对应关系。
4 扫描.META.region的集合,计算得到当前还未分配的region,将他们放入待分配region列表。
master下线
由于master只维护表和region的元数据,而不参与表数据IO的过 程,master下线仅导致所有元数据的修改被冻结(无法创建删除表,无法修改表的schema,无法进行region的负载均衡,无法处理region 上下线,无法进行region的合并,唯一例外的是region的split可以正常进行,因为只有region server参与),表的数据读写还可以正常进行。因此master下线短时间内对整个hbase集群没有影响。从上线过程可以看到,master保存的 信息全是可以冗余信息(都可以从系统其它地方收集到或者计算出来),因此,一般hbase集群中总是有一个master在提供服务,还有一个以上 的’master’在等待时机抢占它的位置。
六、HBASE API
Hbase是用Java语言编写的,支持Java编程 是自然而然的事情。其支持CRUD操作包括Create, Read, Update和 Delete 。 Java API包含Hbase shell支持的所有功能, 甚至更多。其是访问Hbase最快的方式。
Java API程序设计步骤 :
步骤1:创建一个Configuration对象 。包含各种配置信息 ;
Configuration conf = HbaseConfiguration.create();
步骤2:构建一个HTable句柄 。 提供Configuration对象;提供待访问Table的名称 ;
HTable table = new HTable(conf, tableName);
步骤3:执行相应的操作 。执行put、get、delete、scan等操作 ;
table.getTableName();
步骤4:关闭HTable句柄 。 将内存数据刷新到磁盘上; 释放各种资源 。
table.close();
Configuration对象
Configuration对象包装了客户端程序连接Hbase服务 所需的全部信息; 包括Zookeeper位置,Zookeeper连接超时时间 。HbaseConfiguration.create()内部逻辑是从CLASSPATH中加载hbase-default.xml和hbase-site.xml 两个文件 。hbase-default.xml已经被打包到Hbase jar包中。hbase-site.xml需添加到CLASSPATH中 。hbase-site.xml将覆盖hbase-default.xml中的同名属性。
Hbase首先,通过修改hadoop脚本,将Hbase CLASSPATH加入 。然后,在<hadoop_install>/conf/hadoop-env.sh中设置 export HADOOP_CLASSPATH=$HBASE_HOME/*:$HBASE_HOME/conf:$HA DOOP_CLASSPATH来从CLASSPATH中获取hbase-site.xml信息。
如果已经有一个Configuration文件,可进行如下操作。Configuration newConf = Configuration.create(existingConf)。 用户自定义的配置文件将在已有配置文件之后加载。其将覆盖hbase-default.xml和hbase-site.xml中的配置 。
HBase创建名字空间
步骤1:初始化hbase 的conf; Configuration conf = HBaseConfiguration.create();
步骤2:通过连接工厂创建连接; Connection conn = ConnectionFactory.createConnection(conf);
步骤3://获取hbase管理员Admin admin = conn.getAdmin();
步骤4:通过构建器模式,创建namespace描述符
@Test
public void createNS() throws Exception {
//初始化hbase 的conf
Configuration conf = HBaseConfiguration.create();
//通过连接工厂创建连接
Connection conn = ConnectionFactory.createConnection(conf);
//获取hbase管理员
Admin admin = conn.getAdmin();
//通过构建器模式,创建namespace描述符
NamespaceDescriptor nsd = NamespaceDescriptor.create("test2").build();
admin.createNamespace(nsd);
admin.close();
conn.close();
}
/**
* 创建表
*/
@Test
public void createTable() throws Exception {
//初始化hbase 的conf
Configuration conf = HBaseConfiguration.create();
//通过连接工厂创建连接
Connection conn = ConnectionFactory.createConnection(conf);
//获取hbase管理员
Admin admin = conn.getAdmin();
TableName table = TableName.valueOf("test:t1");
HTableDescriptor htd = new HTableDescriptor(table);
htd.addFamily(new HColumnDescriptor("f1"));
htd.addFamily(new HColumnDescriptor("f2"));
admin.createTable(htd);
admin.close();
conn.close();
}
/**
* 插入数据
*/
@Test
public void putData() throws Exception {
//初始化hbase 的conf
Configuration conf = HBaseConfiguration.create();
//通过连接工厂创建连接
Connection conn = ConnectionFactory.createConnection(conf);
HTable table = (HTable) conn.getTable(TableName.valueOf("test:t1"));
//设置自动刷写为false
table.setAutoFlush(false,false);
//put列表
List<Put> list = new LinkedList<Put>();
DecimalFormat df = new DecimalFormat("000");
long start = System.currentTimeMillis();
for (int i = 1; i < 100; i++) {
String str = df.format(i);
//Bytes.toBytes()可以将任意类型转换成字节数组
Put put = new Put(Bytes.toBytes("row" + str));
put.addColumn(Bytes.toBytes("f2"), Bytes.toBytes("name"), Bytes.toBytes("tomas"+str));
put.addColumn(Bytes.toBytes("f2"), Bytes.toBytes("id"), Bytes.toBytes(str));
put.addColumn(Bytes.toBytes("f2"), Bytes.toBytes("age"), Bytes.toBytes(i % 100 +""));
list.add(put);
}
table.put(list);
table.flushCommits();
table.close();
conn.close();
System.out.println(System.currentTimeMillis() - start);
}
/**
* 删除数据
*/
@Test
public void delData() throws Exception {
//初始化hbase 的conf
Configuration conf = HBaseConfiguration.create();
//通过连接工厂创建连接
Connection conn = ConnectionFactory.createConnection(conf);
Table table = conn.getTable(TableName.valueOf("test:t1"));
//Bytes.toBytes()可以将任意类型转换成字节数组
Delete delete = new Delete(Bytes.toBytes("row1"));
delete.addColumn(Bytes.toBytes("f1"), Bytes.toBytes("name"));
table.delete(delete);
table.close();
conn.close();
}
5.理解并描述Hbase表与Region与HDFS的关系。



6.理解并描述Hbase的三级寻址。
Hbase寻址机制
1、寻址示意图
2、-ROOT-和.META.表结构
-ROOT-表结构
.META.行记录结构
3、寻址流程
现在假设我们要从Table2里面查询一条RowKey是RK10000的数据。那么我们应该遵循以下步骤:
1. 从.META.表里面查询哪个Region包含这条数据。
2. 获取管理这个Region的RegionServer地址。
3. 连接这个RegionServer, 查到这条数据。
系统如何找到某个row key (或者某个 row key range)所在的region
bigtable 使用三层类似B+树的结构来保存region位置。
第一层: 保存zookeeper里面的文件,它持有root region的位置。
第二层:root region是.META.表的第一个region其中保存了.META.表其它region的位置。通过root region,我们就可以访问.META.表的数据。
第三层: .META.表它是一个特殊的表,保存了hbase中所有数据表的region 位置信息。
说明:
(1) root region永远不会被split,保证了最需要三次跳转,就能定位到任意region 。
(2).META.表每行保存一个region的位置信息,row key 采用表名+表的最后一行编码而成。
(3) 为了加快访问,.META.表的全部region都保存在内存中。
(4) client会将查询过的位置信息保存缓存起来,缓存不会主动失效,因此如果client上的缓存全部失效,则需要进行最多6次网络来回,才能定位到正确的region(其中三次用来发现缓存失效,另外三次用来获取位置信息)。
4、总结
Region定位流程:
寻找RegionServer
ZooKeeper–> -ROOT-(单Region)–> .META.–> 用户表
-ROOT-表
表包含.META.表所在的region列表,该表只会有一个Region;
Zookeeper中记录了-ROOT-表的location。
.META.表
表包含所有的用户空间region列表,以及RegionServer的服务器地址。
7.假设.META.表的每行(一个映射条目)在内存中大约占用1KB,并且每个Region限制为2GB,通过HBase的三级寻址方式,理论上Hbase的数据表最大有多大?

根据算式可得一个root表可寻址2048MB/1KB=2^22个,同理每个META表的Region可以寻址的用户数据表的Region个数2^22
最终,三层结构可以保存的region数目是(2^22)*(2^22)= 2^44
8.MapReduce的架构,各部分的功能,以及和集群其他组件的关系。
MapReduce基本架构
一句话——整体依旧主从构,map加redu(reduce简写)。 map、split入磁盘,数据对分partition。shuffle、sort、key-value,一个redu(reduce)一 tion(partition)透。注:最后一句,一个reduce解析一个partition。
一堆话——如下:
和HDFS一样,MapReduce也是采用Master/Slave的架构,其架构如下图所示:

MapReduce包含四个组成部分,分别为Client,JobTracker,TaskTracker,Task。
a)client客户端
每一个Job都会在用户端通过Client类将应用程序以及参数配置Configuration打包成Jar文件存储在HDFS,并把路径提交到JobTracker的master服务,然后由master创建每一个Task(即MapTask和ReduceTask),将它们分发到各个TaskTracker服务中去执行。
b)JobTracker
JobTracker负责资源监控和作业调度。JobTracker监控所有的TaskTracker与job的健康状况,一旦发现失败,就将相应的任务转移到其它节点;同时JobTracker会跟踪任务的执行进度,资源使用量等信息,并将这些信息告诉任务调度器,而调度器会在资源出现空闲时,选择合适的任务使用这些资源。在Hadoop中,任务调度器是一个可插拔的模块,用于可以根据自己的需要设计相应的调度器。
c)TaskTracker
TaskTracker会周期性地通过HeartBeat将本节点上资源的使用情况和任务的运行进度汇报给JobTracker,同时执行JobTracker发送过来的命令 并执行相应的操作(如启动新任务,杀死任务等)。TaskTracker使用“slot”等量划分本节点上的资源量。“slot”代表计算资源(cpu,内存等) 。一个Task获取到一个slot之后才有机会运行,而Hadoop调度器的作用就是将各个TaskTracker上的空闲slot分配给Task使用。slot分为MapSlot和ReduceSlot两种,分别提供MapTask和ReduceTask使用。TaskTracker通过slot数目(可配置参数)限定Task的并发度。
d)Task
Task分为MapTask和Reduce Task两种,均由TaskTracker启动。HDFS以固定大小的block为基本单位存储数据,而对于MapReduce而言,其处理单位是split。split是一个逻辑概念,它只包含一些元数据信息,比如数据起始位置、数据长度、数据所在节点等。它的划分方法完全由用户自己决定。但需要注意的是,split的多少决定了MapTask的数目,因为每一个split只会交给一个MapTask处理。split与block的关系如下图:

MapTask的执行过程如下图所示:由下图可知,Map Task 先将对应的split迭代解析成一个个key-value对,依次调用用户自定义的map()函数进行处理,最终将临时结果存放到本地磁盘上。其中,临时数据被分成若干个partition,每个partition将被一个Reduce Task处理。

Reduce Task 的执行过程如下图所示。该过程分为三个阶段:
1. 从远程节点上读取Map Task中间结果(称为“Shuffle 阶段”)
2. 按照 key 对 key-value 对进行排序(称为 “Sort 阶段”)
3. 依次读取< key, value list >,调用用户自定义的Reduce函数处理,并将最终结果存到HDFS上(称为“Reduce阶段”)
过程如下图:

9.MapReduce的工作过程,用自己词频统计的例子,将split, map, partition,sort,spill,fetch,merge reduce整个过程梳理并用图形表达出来。
1. 在map task执行时,它的输入数据来源于HDFS的block,当然在MapReduce概念中,map task只读取split。Split与block的对应关系可能是多对一,默认是一对一。在WordCount例子里,假设map的输入数据都是像“aaa”这样的字符串。
2. 在经过mapper的运行后,我们得知mapper的输出是这样一个key/value对: key是“aaa”, value是数值1。因为当前map端只做加1的操作,在reduce task里才去合并结果集。前面我们知道这个job有3个reduce task,到底当前的“aaa”应该交由哪个reduce去做呢,是需要现在决定的。
MapReduce提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上。
在我们的例子中,“aaa”经过Partitioner后返回0,也就是这对值应当交由第一个reducer来处理。接下来,需要将数据写入内存缓冲区中,缓冲区的作用是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然写入之前,key与value值都会被序列化成字节数组。
整个内存缓冲区就是一个字节数组,它的字节索引及key/value存储结构我没有研究过。如果有朋友对它有研究,那么请大致描述下它的细节吧。
3. 这个内存缓冲区是有大小限制的,默认是100MB。当map task的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写,字面意思很直观。这个溢写是由单独线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size * spillpercent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程。Map task的输出结果还可以往剩下的20MB内存中写,互不影响。
当溢写线程启动后,需要对这80MB空间内的key做排序(Sort)。排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序。
在这里我们可以想想,因为map task的输出是需要发送到不同的reduce端去,而内存缓冲区没有对将发送到相同reduce端的数据做合并,那么这种合并应该是体现是磁盘文件中的。从官方图上也可以看到写到磁盘中的溢写文件是对不同的reduce端的数值做过合并。所以溢写过程一个很重要的细节在于,如果有很多个key/value对需要发送到某个reduce端去,那么需要将这些key/value值拼接到一块,减少与partition相关的索引记录。
在针对每个reduce端而合并数据时,有些数据可能像这样:“aaa”/1, “aaa”/1。对于WordCount例子,就是简单地统计单词出现的次数,如果在同一个map task的结果中有很多个像“aaa”一样出现多次的key,我们就应该把它们的值合并到一块,这个过程叫reduce也叫combine。但MapReduce的术语中,reduce只指reduce端执行从多个maptask取数据做计算的过程。除reduce外,非正式地合并数据只能算做combine了。其实大家知道的,MapReduce中将Combiner等同于Reducer。
如果client设置过Combiner,那么现在就是使用Combiner的时候了。将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用。那哪些场景才能使用Combiner呢?从这里分析,Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果。所以从我的想法来看,Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。
4. 每次溢写会在磁盘上生成一个溢写文件,如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个溢写文件存在。当maptask真正完成时,内存缓冲区中的数据也全部溢写到磁盘中形成一个溢写文件。最终磁盘中会至少有一个这样的溢写文件存在(如果map的输出结果很少,当map执行完成时,只会产生一个溢写文件),因为最终的文件只有一个,所以需要将这些溢写文件归并到一起,这个过程就叫做Merge。Merge是怎样的?如前面的例子,“aaa”从某个map task读取过来时值是5,从另外一个map 读取时值是8,因为它们有相同的key,所以得merge成group。什么是group。对于“aaa”就是像这样的:{“aaa”, [5, 8, 2, …]},数组中的值就是从不同溢写文件中读取出来的,然后再把这些值加起来。请注意,因为merge是将多个溢写文件合并到一个文件,所以可能也有相同的key存在,在这个过程中如果client设置过Combiner,也会使用Combiner来合并相同的key。
至此,map端的所有工作都已结束,最终生成的这个文件也存放在TaskTracker够得着的某个本地目录内。每个reduce task不断地通过RPC从JobTracker那里获取maptask是否完成的信息,如果reduce task得到通知,获知某台TaskTracker上的map task执行完成,Shuffle的后半段过程开始启动。
简单地说,reduce task在执行之前的工作就是不断地拉取当前job里每个map task的最终结果,然后对从不同地方拉取过来的数据不断地做merge,也最终形成一个文件作为reduce task的输入文件

如map 端的细节图,Shuffle在reduce端的过程也能用图上标明的三点来概括。当前reduce copy数据的前提是它要从JobTracker获得有哪些map task已执行结束,这段过程不表,有兴趣的朋友可以关注下。Reducer真正运行之前,所有的时间都是在拉取数据,做merge,且不断重复地在做。如前面的方式一样,下面我也分段地描述reduce 端的Shuffle细节:
1. Copy过程,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求map task所在的TaskTracker获取map task的输出文件。因为map task早已结束,这些文件就归TaskTracker管理在本地磁盘中。
2. Merge阶段。这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活,它基于JVM的heapsize设置,因为Shuffle阶段Reducer不运行,所以应该把绝大部分的内存都给Shuffle用。这里需要强调的是,merge有三种形式:1)内存到内存 2)内存到磁盘 3)磁盘到磁盘。默认情况下第一种形式不启用,让人比较困惑,是吧。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件。
3. Reducer的输入文件。不断地merge后,最后会生成一个“最终文件”。为什么加引号?因为这个文件可能存在于磁盘上,也可能存在于内存中。对我们来说,当然希望它存放于内存中,直接作为Reducer的输入,但默认情况下,这个文件是存放于磁盘中的。至于怎样才能让这个文件出现在内存中,之后的性能优化篇我再说。当Reducer的输入文件已定,整个Shuffle才最终结束。然后就是Reducer执行,把结果放到HDFS上。

浙公网安备 33010602011771号