前记
公司内部使用的是mapr版本的hadoop生态系统,因而从mapr的凯发k8网页登录官网看到了这篇文文章:,原本想翻译全文,然而如果翻译就需要各种咬文嚼字,太麻烦,因而本文大部分使用了自己的语言,并且加入了其他资源的参考理解以及本人自己读源码时对其的理解,属于半翻译、半原创吧。
hbase架构组成
hbase采用master/slave架构搭建集群,它隶属于hadoop生态系统,由一下类型节点组成:hmaster节点、hregionserver节点、zookeeper集群,而在底层,它将数据存储于hdfs中,因而涉及到hdfs的namenode、datanode等,总体结构如下:
其中
hmaster节点用于:
- 管理hregionserver,实现其负载均衡。
- 管理和分配hregion,比如在hregion split时分配新的hregion;在hregionserver退出时迁移其内的hregion到其他hregionserver上。
- 实现ddl操作(data definition
language,namespace和table的增删改,column
familiy的增删改等)。
- 管理namespace和table的元数据(实际存储在hdfs上)。
- 权限控制(acl)。
hregionserver节点用于:
- 存放和管理本地hregion。
- 读写hdfs,管理table中的数据。
- client直接通过hregionserver读写数据(从hmaster中获取元数据,找到rowkey所在的hregion/hregionserver后)。
zookeeper集群是协调系统,用于:
- 存放整个
hbase集群的元数据以及集群的状态信息。
- 实现hmaster主从节点的failover。
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上实现数据的本地化。
hregion
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个会引起性能问题?
来回答这个问题:感觉这个1000的数字是从bigtable的论文中来的(5 implementation节):each tablet server manages a set of tablets(typically we have somewhere between ten to a thousand tablets per tablet server))。
hmaster
hmaster没有单点故障问题,可以启动多个hmaster,通过zookeeper的master election机制保证同时只有一个hmaster出于active状态,其他的hmaster则处于热备份状态。一般情况下会启动两个hmaster,非active的hmaster会定期的和active hmaster通信以获取其最新状态,从而保证它是实时更新的,因而如果启动了多个hmaster反而增加了active hmaster的负担。前文已经介绍过了hmaster的主要用于hregion的分配和管理,ddl(data definition language,既table的新建、删除、修改等)的实现等,既它主要有两方面的职责:
- 协调hregionserver
- 启动时hregion的分配,以及负载均衡和修复时hregion的重新分配。
- 监控集群中所有hregionserver的状态(通过heartbeat和监听zookeeper中的状态)。
- admin职能
- 创建、删除、修改table的定义。
zookeeper:协调者
zookeeper为hbase集群提供协调服务,它管理着hmaster和hregionserver的状态(available/alive等),并且会在它们宕机时通知给hmaster,从而hmaster可以实现hmaster之间的failover,或对宕机的hregionserver中的hregion集合的修复(将它们分配给其他的hregionserver)。zookeeper集群本身使用一致性协议(paxos协议)保证每个节点状态的一致性。
how the components work together
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.(如中的设计),其中-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的流程就变成了:
- 从zookeeper(/hbase/meta-region-server)中获取hbase:meta的位置(hregionserver的位置),缓存该位置信息。
- 从hregionserver中查询用户table对应请求的rowkey所在的hregionserver,缓存该位置信息。
- 从查询到hregionserver中读取row。
从这个过程中,我们发现客户会缓存这些位置信息,然而第二步它只是缓存当前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详解
hregionserver一般和datanode在同一台机器上运行,实现数据的本地性。hregionserver包含多个hregion,由wal(hlog)、blockcache、memstore、hfile组成。
- 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之后,通过实现了多个wal并行写(multiwal),该实现采用hdfs的多个管道写,以单个hregion为单位。关于wal可以参考wikipedia的。顺便吐槽一句,英文版的维基百科竟然能毫无压力的正常访问了,这是某个gfw的疏忽还是以后的常态?
- blockcache是一个读缓存,即“引用局部性”原理(也应用于cpu,,空间局部性是指cpu在某一时刻需要某个数据,那么有很大的概率在一下时刻它需要的数据在其附近;时间局部性是指某个数据在被访问过一次后,它有很大的概率在不久的将来会被再次的访问),将数据预读取到内存中,以提升读的性能。hbase中提供两种blockcache的实现:默认on-heap lrublockcache和bucketcache(通常是off-heap)。通常bucketcache的性能要差于lrublockcache,然而由于gc的影响,lrublockcache的延迟会变的不稳定,而bucketcache由于是自己管理blockcache,而不需要gc,因而它的延迟通常比较稳定,这也是有些时候需要选用bucketcache的原因。这篇文章对on-heap和off-heap的blockcache做了详细的比较。
- 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组成。
- memstore是一个写缓存(in memory sorted buffer),所有数据的写在完成wal日志写后,会
写入memstore中,由memstore根据一定的算法将数据flush到地层hdfs文件中(hfile),通常每个hregion中的每个
column family有一个自己的memstore。
- hfile(storefile)
用于存储hbase的数据(cell/keyvalue)。在hfile中的数据是按rowkey、column family、column排序,对相同的cell(即这三个值都一样),则按timestamp倒序排列。
虽然上面这张图展现的是最新的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会引起性能问题?具体原因有待考证。
- 当一个hregion中的所有memstore的大小总和超过了hbase.hregion.memstore.flush.size的大小,默认128mb。此时当前的hregion中所有的memstore会flush到hdfs中。
- 当全局memstore的大小超过了hbase.regionserver.global.memstore.upperlimit的大小,默认40%的内存使用量。此时当前hregionserver中所有hregion中的memstore都会flush到hdfs中,flush顺序是memstore大小的倒序(一个hregion中所有memstore总和作为该hregion的memstore的大小还是选取最大的memstore作为参考?有待考证),直到总体的memstore使用量低于hbase.regionserver.global.memstore.lowerlimit,默认38%的内存使用量。
- 当前hregionserver中wal的大小超过了hbase.regionserver.hlog.blocksize * hbase.regionserver.max.logs的数量,当前hregionserver中所有hregion中的memstore都会flush到hdfs中,flush使用时间顺序,最早的memstore先flush直到wal的数量少于hbase.regionserver.hlog.blocksize * hbase.regionserver.max.logs。说这两个相乘的默认大小是2gb,查代码,hbase.regionserver.max.logs默认值是32,而hbase.regionserver.hlog.blocksize是hdfs的默认blocksize,32mb。但不管怎么样,因为这个大小超过限制引起的flush不是一件好事,可能引起长时间的延迟,因而这篇文章给的建议:“hint: keep hbase.regionserver.hlog.blocksize * hbase.regionserver.maxlogs just a bit above hbase.regionserver.global.memstore.lowerlimit * hbase_heapsize.”。并且需要注意,给的描述是有错的(虽然它是官方的文档)。
在memstore flush过程中,还会在尾部追加一些meta数据,其中就包括flush时最大的wal sequence值,以告诉hbase这个storefile写入的最新数据的序列,那么在recover时就直到从哪里开始。在hregion启动时,这个sequence会被读取,并取最大的作为下一次更新时的起始sequence。
hfile格式
hbase的数据以keyvalue(cell)的形式顺序的存储在hfile中,在memstore的flush过程中生成hfile,由于memstore中存储的cell遵循相同的排列顺序,因而flush过程是顺序写,我们直到磁盘的顺序写性能很高,因为不需要不停的移动磁盘指针。
hfile参考bigtable的sstable和hadoop的实现,从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
posted on 2015-08-22 17:44
dlevin 阅读(60420)
评论(0) 编辑 收藏 所属分类:
hbase 、
architecture