原文出处: DLevin(@雪地脚印_) php
公司内部使用的是MapR版本的Hadoop生态系统,于是从MapR的官网看到了这篇文文章:An In-Depth Look at the HBase Architecture,本来想翻译全文,然而若是翻译就须要各类咬文嚼字,太麻烦,于是本文大部分使用了本身的语言,而且加入了其余资源的参考理解以及本人本身读源码时对其的理解,属于半翻译、半原创吧。html
HBase采用Master/Slave架构搭建集群,它隶属于Hadoop生态系统,由一下类型节点组成:HMaster节点、HRegionServer节点、ZooKeeper集群,而在底层,它将数据存储于HDFS中,于是涉及到HDFS的NameNode、DataNode等,整体结构以下:java
其中HMaster节点用于:算法
HRegionServer节点用于:apache
ZooKeeper集群是协调系统,用于:数组
HBase Client经过RPC方式和HMaster、HRegionServer通讯;一个HRegionServer能够存放1000个HRegion;底层Table数据存储于HDFS中,而HRegion所处理的数据尽可能和数据所在的DataNode在一块儿,实现数据的本地化;数据本地化并非总能实现,好比在HRegion移动(如因Split)时,须要等下一次Compact才能继续回到本地化。缓存
本着半翻译的原则,再贴一个《An In-Depth Look At The HBase Architecture》的架构图:架构
这个架构图比较清晰的表达了HMaster和NameNode都支持多个热备份,使用ZooKeeper来作协调;ZooKeeper并非云般神秘,它通常由三台机器组成一个集群,内部使用PAXOS算法支持三台Server中的一台宕机,也有使用五台机器的,此时则能够支持同时两台宕机,既少于半数的宕机,然而随着机器的增长,它的性能也会降低;RegionServer和DataNode通常会放在相同的Server上实现数据的本地化。负载均衡
HBase使用RowKey将表水平切割成多个HRegion,从HMaster的角度,每一个HRegion都纪录了它的StartKey和EndKey(第一个HRegion的StartKey为空,最后一个HRegion的EndKey为空),因为RowKey是排序的,于是Client能够经过HMaster快速的定位每一个RowKey在哪一个HRegion中。HRegion由HMaster分配到相应的HRegionServer中,而后由HRegionServer负责HRegion的启动和管理,和Client的通讯,负责数据的读(使用HDFS)。每一个HRegionServer能够同时管理1000个左右的HRegion(这个数字怎么来的?没有从代码中看到限制,难道是出于经验?超过1000个会引发性能问题?)。oop
HMaster没有单点故障问题,能够启动多个HMaster,经过ZooKeeper的Master Election机制保证同时只有一个HMaster出于Active状态,其余的HMaster则处于热备份状态。通常状况下会启动两个HMaster,非Active的HMaster会按期的和Active HMaster通讯以获取其最新状态,从而保证它是实时更新的,于是若是启动了多个HMaster反而增长了Active HMaster的负担。前文已经介绍过了HMaster的主要用于HRegion的分配和管理,DDL(Data Definition Language,既Table的新建、删除、修改等)的实现等,既它主要有两方面的职责:
1.协调HRegionServer
2.Admin职能
ZooKeeper为HBase集群提供协调服务,它管理着HMaster和HRegionServer的状态(available/alive等),而且会在它们宕机时通知给HMaster,从而HMaster能够实现HMaster之间的failover,或对宕机的HRegionServer中的HRegion集合的修复(将它们分配给其余的HRegionServer)。ZooKeeper集群自己使用一致性协议(PAXOS协议)保证每一个节点状态的一致性。
ZooKeeper协调集群全部节点的共享信息,在HMaster和HRegionServer链接到ZooKeeper后建立Ephemeral节点,并使用Heartbeat机制维持这个节点的存活状态,若是某个Ephemeral节点实效,则HMaster会收到通知,并作相应的处理。
另外,HMaster经过监听ZooKeeper中的Ephemeral节点(默认:/hbase/rs/*)来监控HRegionServer的加入和宕机。在第一个HMaster链接到ZooKeeper时会建立Ephemeral节点(默认:/hbasae/master)来表示Active的HMaster,其后加进来的HMaster则监听该Ephemeral节点,若是当前Active的HMaster宕机,则该节点消失,于是其余HMaster获得通知,而将自身转换成Active的HMaster,在变为Active的HMaster以前,它会建立在/hbase/back-masters/下建立本身的Ephemeral节点。
HBase的第一次读写
在HBase 0.96之前,HBase有两个特殊的Table:-ROOT-和.META.(如BigTable中的设计),其中-ROOT- Table的位置存储在ZooKeeper,它存储了.META. Table的RegionInfo信息,而且它只能存在一个HRegion,而.META. Table则存储了用户Table的RegionInfo信息,它能够被切分红多个HRegion,于是对第一次访问用户Table时,首先从ZooKeeper中读取-ROOT- Table所在HRegionServer;而后从该HRegionServer中根据请求的TableName,RowKey读取.META. Table所在HRegionServer;最后从该HRegionServer中读取.META. Table的内容而获取这次请求须要访问的HRegion所在的位置,而后访问该HRegionSever获取请求的数据,这须要三次请求才能找到用户Table所在的位置,而后第四次请求开始获取真正的数据。固然为了提高性能,客户端会缓存-ROOT- Table位置以及-ROOT-/.META. Table的内容。以下图所示:
但是即便客户端有缓存,在初始阶段须要三次请求才能直到用户Table真正所在的位置也是性能低下的,并且真的有必要支持那么多的HRegion吗?或许对Google这样的公司来讲是须要的,可是对通常的集群来讲好像并无这个必要。在BigTable的论文中说,每行METADATA存储1KB左右数据,中等大小的Tablet(HRegion)在128MB左右,3层位置的Schema设计能够支持2^34个Tablet(HRegion)。即便去掉-ROOT- Table,也还能够支持2^17(131072)个HRegion, 若是每一个HRegion仍是128MB,那就是16TB,这个貌似不够大,可是如今的HRegion的最大大小都会设置的比较大,好比咱们设置了2GB,此时支持的大小则变成了4PB,对通常的集群来讲已经够了,于是在HBase 0.96之后去掉了-ROOT- Table,只剩下这个特殊的目录表叫作Meta Table(hbase:meta),它存储了集群中全部用户HRegion的位置信息,而ZooKeeper的节点中(/hbase/meta-region-server)存储的则直接是这个Meta Table的位置,而且这个Meta Table如之前的-ROOT- Table同样是不可split的。这样,客户端在第一次访问用户Table的流程就变成了:
从这个过程当中,咱们发现客户会缓存这些位置信息,然而第二步它只是缓存当前RowKey对应的HRegion的位置,于是若是下一个要查的RowKey不在同一个HRegion中,则须要继续查询hbase:meta所在的HRegion,然而随着时间的推移,客户端缓存的位置信息愈来愈多,以致于不须要再次查找hbase:meta Table的信息,除非某个HRegion由于宕机或Split被移动,此时须要从新查询而且更新缓存。
hbase:meta表
hbase:meta表存储了全部用户HRegion的位置信息,它的RowKey是:tableName,regionStartKey,regionId,replicaId等,它只有info列族,这个列族包含三个列,他们分别是:info:regioninfo列是RegionInfo的proto格式:regionId,tableName,startKey,endKey,offline,split,replicaId;info:server格式:HRegionServer对应的server:port;info:serverstartcode格式是HRegionServer的启动时间戳。
HRegionServer通常和DataNode在同一台机器上运行,实现数据的本地性。HRegionServer包含多个HRegion,由WAL(HLog)、BlockCache、MemStore、HFile组成。
1.WAL即Write Ahead Log,在早期版本中称为HLog,它是HDFS上的一个文件,如其名字所表示的,全部写操做都会先保证将数据写入这个Log文件后,才会真正更新MemStore,最后写入HFile中。采用这种模式,能够保证HRegionServer宕机后,咱们依然能够从该Log文件中读取数据,Replay全部的操做,而不至于数据丢失。这个Log文件会按期Roll出新的文件而删除旧的文件(那些已持久化到HFile中的Log能够删除)。WAL文件存储在/hbase/WALs/${HRegionServer_Name}的目录中(在0.94以前,存储在/hbase/.logs/目录中),通常一个HRegionServer只有一个WAL实例,也就是说一个HRegionServer的全部WAL写都是串行的(就像log4j的日志写也是串行的),这固然会引发性能问题,于是在HBase 1.0以后,经过HBASE-5699实现了多个WAL并行写(MultiWAL),该实现采用HDFS的多个管道写,以单个HRegion为单位。关于WAL能够参考Wikipedia的Write-Ahead Logging。顺便吐槽一句,英文版的维基百科居然能毫无压力的正常访问了,这是某个GFW的疏忽仍是之后的常态?
2.BlockCache是一个读缓存,即“引用局部性”原理(也应用于CPU,分空间局部性和时间局部性,空间局部性是指CPU在某一时刻须要某个数据,那么有很大的几率在一下时刻它须要的数据在其附近;时间局部性是指某个数据在被访问过一次后,它有很大的几率在不久的未来会被再次的访问),将数据预读取到内存中,以提高读的性能。HBase中提供两种BlockCache的实现:默认on-heap LruBlockCache和BucketCache(一般是off-heap)。一般BucketCache的性能要差于LruBlockCache,然而因为GC的影响,LruBlockCache的延迟会变的不稳定,而BucketCache因为是本身管理BlockCache,而不须要GC,于是它的延迟一般比较稳定,这也是有些时候须要选用BucketCache的缘由。这篇文章BlockCache101对on-heap和off-heap的BlockCache作了详细的比较。
3.HRegion是一个Table中的一个Region在一个HRegionServer中的表达。一个Table能够有一个或多个Region,他们能够在一个相同的HRegionServer上,也能够分布在不一样的HRegionServer上,一个HRegionServer能够有多个HRegion,他们分别属于不一样的Table。HRegion由多个Store(HStore)构成,每一个HStore对应了一个Table在这个HRegion中的一个Column Family,即每一个Column Family就是一个集中的存储单元,于是最好将具备相近IO特性的Column存储在一个Column Family,以实现高效读取(数据局部性原理,能够提升缓存的命中率)。HStore是HBase中存储的核心,它实现了读写HDFS功能,一个HStore由一个MemStore 和0个或多个StoreFile组成。
虽然上面这张图展示的是最新的HRegionServer的架构(可是并非那么的精确),可是我一直比较喜欢看如下这张图,即便它展示的应该是0.94之前的架构。
HRegionServer中数据写流程图解
当客户端发起一个Put请求时,首先它从hbase:meta表中查出该Put数据最终须要去的HRegionServer。而后客户端将Put请求发送给相应的HRegionServer,在HRegionServer中它首先会将该Put操做写入WAL日志文件中(Flush到磁盘中)。
写完WAL日志文件后,HRegionServer根据Put中的TableName和RowKey找到对应的HRegion,并根据Column Family找到对应的HStore,并将Put写入到该HStore的MemStore中。此时写成功,并返回通知客户端。
MemStore Flush
MemStore是一个In Memory Sorted Buffer,在每一个HStore中都有一个MemStore,即它是一个HRegion的一个Column Family对应一个实例。它的排列顺序以RowKey、Column Family、Column的顺序以及Timestamp的倒序,以下所示:
每一次Put/Delete请求都是先写入到MemStore中,当MemStore满后会Flush成一个新的StoreFile(底层实现是HFile),即一个HStore(Column Family)能够有0个或多个StoreFile(HFile)。有如下三种状况能够触发MemStore的Flush动做,须要注意的是MemStore的最小Flush单元是HRegion而不是单个MemStore。听说这是Column Family有个数限制的其中一个缘由,估计是由于太多的Column Family一块儿Flush会引发性能问题?具体缘由有待考证。
在MemStore Flush过程当中,还会在尾部追加一些meta数据,其中就包括Flush时最大的WAL sequence值,以告诉HBase这个StoreFile写入的最新数据的序列,那么在Recover时就直到从哪里开始。在HRegion启动时,这个sequence会被读取,并取最大的做为下一次更新时的起始sequence。
HBase的数据以KeyValue(Cell)的形式顺序的存储在HFile中,在MemStore的Flush过程当中生成HFile,因为MemStore中存储的Cell遵循相同的排列顺序,于是Flush过程是顺序写,咱们直到磁盘的顺序写性能很高,由于不须要不停的移动磁盘指针。
HFile参考BigTable的SSTable和Hadoop的TFile实现,从HBase开始到如今,HFile经历了三个版本,其中V2在0.92引入,V3在0.98引入。首先咱们来看一下V1的格式:
V1的HFile由多个Data Block、Meta Block、FileInfo、Data Index、Meta Index、Trailer组成,其中Data Block是HBase的最小存储单元,在前文中提到的BlockCache就是基于Data Block的缓存的。一个Data Block由一个魔数和一系列的KeyValue(Cell)组成,魔数是一个随机的数字,用于表示这是一个Data Block类型,以快速监测这个Data Block的格式,防止数据的破坏。Data Block的大小能够在建立Column Family时设置(HColumnDescriptor.setBlockSize()),默认值是64KB,大号的Block有利于顺序Scan,小号Block利于随机查询,于是须要权衡。Meta块是可选的,FileInfo是固定长度的块,它纪录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等。Data Index和Meta Index纪录了每一个Data块和Meta块的其实点、未压缩时大小、Key(起始RowKey?)等。Trailer纪录了FileInfo、Data Index、Meta Index块的起始位置,Data Index和Meta Index索引的数量等。其中FileInfo和Trailer是固定长度的。
HFile里面的每一个KeyValue对就是一个简单的byte数组。可是这个byte数组里面包含了不少项,而且有固定的结构。咱们来看看里面的具体结构:
开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,而后是固定长度的数值,表示Family的长度,而后是Family,接着是Qualifier,而后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。随着HFile版本迁移,KeyValue(Cell)的格式并未发生太多变化,只是在V3版本,尾部添加了一个可选的Tag数组。
HFileV1版本的在实际使用过程当中发现它占用内存多,而且Bloom File和Block Index会变的很大,而引发启动时间变长。其中每一个HFile的Bloom Filter能够增加到100MB,这在查询时会引发性能问题,由于每次查询时须要加载并查询Bloom Filter,100MB的Bloom Filer会引发很大的延迟;另外一个,Block Index在一个HRegionServer可能会增加到总共6GB,HRegionServer在启动时须要先加载全部这些Block Index,于是增长了启动时间。为了解决这些问题,在0.92版本中引入HFileV2版本:
在这个版本中,Block Index和Bloom Filter添加到了Data Block中间,而这种设计同时也减小了写的内存使用量;另外,为了提高启动速度,在这个版本中还引入了延迟读的功能,即在HFile真正被使用时才对其进行解析。
FileV3版本基本和V2版本相比,并无太大的改变,它在KeyValue(Cell)层面上添加了Tag数组的支持;并在FileInfo结构中添加了和Tag相关的两个字段。关于具体HFile格式演化介绍,能够参考这里。
对HFileV2格式具体分析,它是一个多层的类B+树索引,采用这种设计,能够实现查找不须要读取整个文件:
Data Block中的Cell都是升序排列,每一个block都有它本身的Leaf-Index,每一个Block的最后一个Key被放入Intermediate-Index中,Root-Index指向Intermediate-Index。在HFile的末尾还有Bloom Filter用于快速定位那么没有在某个Data Block中的Row;TimeRange信息用于给那些使用时间查询的参考。在HFile打开时,这些索引信息都被加载并保存在内存中,以增长之后的读取性能。
这篇就先写到这里,未完待续。。。。
https://www.mapr.com/blog/in-depth-look-hbase-architecture#.VdNSN6Yp3qx
http://jimbojw.com/wiki/index.php?title=Understanding_Hbase_and_BigTable
http://hbase.apache.org/book.html
http://www.searchtb.com/2011/01/understanding-hbase.html
http://research.google.com/archive/bigtable-osdi06.pdf