hbase是一分布式的、面向列的开源nosql海量库存储系统,它的理论原型是 google 的 bigtable 论文。通俗地讲,hbase 是一个具有高可靠性、高性能、面向列、可伸缩的分布式存储系统。它可以处理分布在数千台服务器上的pb级海量数据。
hbase是基于hdfs存储数据的,hdfs是部署在商业服务器上的,并且具有高容错性。基于hdfs,就意味着hbase具有超强的扩展性和容错性。
在详细介绍hbase之前,我们一起来看看hbase的架构,如下图所示:
hbase的系统架构
hbase采用的是key/value的存储格式,这就能保证,即使数据量增大,也不会导致查询性能大幅度下降。因为 hbase 是一个面向列存储的数据库,当表的字段很多时,可以把其中几个字段放在一部分机器上,而另外几个字段放到另一部分机器上,充分分散负载的压力。如此复杂的存储结构和分布式存储方式,带来的代价就是即便是存储很少的数据,也不会很快。
我们可以看出,hbase是那种既不快,又慢的不明显的数据库,因此,它主要应用在以下两种情况的查询:
单表数据量不能太大(千万级别),并发量不能太高。对数据需求分析不要求特别及时,同时也不要求太灵活。
因为hbase是基于hadoop存储的,所以,hbase适合存储 pb 级别以上的海量数据,可以部署在普通廉价商用服务器上,能够在几十到百毫秒内返回数据。这都得力于hbase的良好扩展性,为海量数据的存储提供了便利。
hbase是根据列族来存储数据的。列族下面可以有非常多的列,在建表时,必须指定列族,但是不用指定列。
稀疏主要体现在hbase的列的灵活性上面,在hbase的列族中,可以指定任意多的列,在列数据为空的情况下,hbase表是不会占用存储空间的。
一个是基于上层处理能力(regionserver)的扩展,通过横向添加 regionsever 的机器,进行水平扩展,提升 hbase 上层的处理能力,提升hbase服务更多 region 的能力。另外一个是基于存储能力(hdfs)的扩展。
hbase和数据库的比较
在下图中,列簇(column family)对应的值就是 info 和 area ,列( column 或者称为 qualifier )对应的就是 name 、 age 、 country 和 city ,row key 对应的就是 row1 和 row2,cell 对应的就是具体的值。
row key :表的主键,按照字典序排序。列簇:在 hbase 中,列簇将表进行横向切割。列:属于某一个列簇,在 hbase 中可以进行动态的添加。cell : 是指具体的 value 。version :在这张图里面没有显示出来,这个是指版本号,用时间戳(timestamp )来表示。
hbase的逻辑结构
看完这张图,是不是有点疑惑,怎么获取其中的一条数据呢?既然 hbase 是 kv 的数据库,那么当然是以获取 key 的形式来获取到 value 啦。在 hbase 中的 key 组成是这样的:
cell的结构
key 是由 row key 、cf(column family) 、column 和 timestamp 组成的。
timestamp 在 hbase 中的作用就是版本号,因为在 hbase 中有着数据多版本的特性,所以同一个 key 可以有多个版本的 value 值(可以通过配置来设置多少个版本)。查询的话是默认取回最新版本的那条数据,但是也可以进行查询多个版本号的数据。
hbase的物理结构
hregionserver 就是一个机器节点,包含多个 hregion ,但是这些 hregion 不一定是来自于同一个 table。直接面对用户的读写请求,是真正干活的节点。它的主要功能如下:
为table分配hregion。处理来自客户端的读写请求,和底层的hdfs进行交互,并将数据存储到hdfs中。负责单个hregion变大后的拆分。负责storefile的合并工作。
zookeeper会监控hregionserver的上下线情况,当zk发现某个hregionserver下线之后,会告知master进行失效处理。下线hregionserver以及他负责的hregion暂时对外提供服务,master会将该hregionserver所负责的hregion转移到其他hregionserver上。
每一个hregion都包含多个store,一个store就对应一个列族的数据,而一个store可以有多个storefile。hregion 是 hbase 中分布式存储和负载均衡的最小单元,但不是存储的最小单元。每一个hregion都有开始的rowkey和结束的rowkey,代表着存储的row的范围。
store 对应着的是 table 里面的 column family,不管有 cf 中有多少的数据,都会存储在 store 中,这也是为了避免访问不同的 store 而导致的效率低下。一个 cf 组成一个 store ,默认是 10 g,如果大于 10g 会进行分裂。store 是 hbase 的核心存储单元,一个 store 由 memstore 和 storefile 组成。
每个store都包含一个memstore实例,memstore是内存的存储对象,当 memstore 的大小达到一个阀值(默认大小是 128m)时,如果超过了这个大小,那么就会进行刷盘,把内存里的数据刷进到 storefile 中,即生成一个快照。目前hbase 会有一个线程来负责memstore 的flush操作。
storefile底层是以 hfile 的格式保存数据。
storefile 以 hfile 格式保存在 hdfs 上,hfile 文件是不定长的,长度固定的只有其中的两块:trailer 和 fileinfo。
storefile的物理结构
hfile 里面的每个 keyvalue 对就是一个简单的 byte 数组。但是这个 byte 数组里面包含了很 多项,并且有固定的结构。
hfile的物理结构
分别表示key 的长度和 value 的长度。紧接着是rowkey 的长度,紧接着是 rowkey,然后是 family 的长度,然后是 family,接着是 qualifier,然后是两个固定长度的数值,表示 time stamp 和 key type(put/delete)。value 部分没有这么复杂的结构,就是纯粹的二进制数据了。
hbase的底层架构
从hbase的架构图上可以看出,hbase中的组件包括client、zookeeper、hmaster、hregionserver、hregion、store、memstore、storefile、hfile、hlog等。
client在访问hbase之前,先访问zookeeper找到数据所在的hregion。client中有访问hbase的接口,另外client还维护了对应的cache来加速hbase的访问,比如缓存元数据。
① hbase 通过zookeeper来做 master 的高可用
通过zookeeper来保证集群中只有一个master在运行,为hbase提供了failover机制,如果master发生意外会通过竞争机制产生选举新的master,避免master节点的单点故障问题。
② 通过zookeeper监控hregionserver的状态,当hregionserver有异常,或者有新的hregionserver上线时,会通过回调方式告诉masterregionserver有节点的上下线信息
③ 存储hbase的schema,包括有哪些table,每个table有哪些column family信息。
master 是集群的主节点,可以配置成ha的形式。master 的工作并不高,因为client 访问 hbase 上数据的过程并不需要 master 参与(寻址访问 zookeeper 和 regioneserver,数据读写访问 regioneserver),master 的负载很低。他的主要工作如下:
负责hregionserver的负载均衡。发现失效的hregionserver之后,将该hregionserver上的region分配给其他hregionserver。为hregionserver分配region。负责hdfs上的hbase的垃圾文件的回收。维护table和region的元数据,处理 schema 更新请求(表的创建,删除,修改,列簇的增加等等)
hregionserver的功能参见 2.3.1 hregionserver
hregion的功能参见 2.3.2 hregion
store的功能参见 2.3.3 store
hfile是存储在hdfs中的二进制文件,实际上,storefile就是对hfile做了轻量级包装,storefile底层是hfile。
hlog(wal log):wal(write-ahead-log)意为预写日志,在 regionserver 在插入和删除数据的过程中,用来记录操作内容的一种日志,主要用来做灾难恢复使用,hlog记录数据的所有变更,一旦region server 宕机,就可以从log中进行恢复。
wal是保存在hdfs上的持久化hadoop sequence file文件。数据到达 region 时先写入wal,然后被加载到memstore中。这样就算region宕机了,操作没来得及执行持久化,在重启的时候从wal开始加载数据并执行。跟redis的aof类似。
在每个hregionserver上,所有的hregion都共享一份hlog,在写入数据时先写入wal,成功之后再写入memstore。当memstore的大小达到一个阀值(默认大小是 128m)时,就会形成一个一个的storefile。wal的状态是可以关闭的,关闭之后增删改的操作会快一些,但是会牺牲掉数据的可靠性。当然,我们也可以采用异步的方式写入wal(默认间隔是1秒钟)。hbase中的wal文件是一个滚动日志数据结构,一个wal实例包含多个wal文件,在wal的大小超过一定的阀值,或者wal所在的hdfs文件块要满了的时候,wal会触发滚动操作。
我们在对hbase进行读写操作的时候,不需要和hmasterregionserver打交道。客户端只需要从zookeeper中获取hbase表数据的地址,然后直接从hregionserver中进行读写操作。
客户端通过 zookeeper 集群,根据-root-表和.meta.表,找到目标数据所在的 regionserver(就是要找到数据所在的 region 的主机地址)与目标的 regionserver 进行通信,查询目标数据。regionserver 定位到目标数据所在的 region,发出查询请求。region 分别在block cache(读缓存),memstore 和storefile(hfile)中查找目标数据,并将查询到的所有数据进行合并,此处的所有数据是指,同一条数据包含不同的版本(timestamp)的数据。将从hfile中查询到的数据块(block,hfile的数据存储单元,默认大小是128mb)缓存到block cache中,然后将最新的数据返回给客户端。
data block 是hbase读写的基本单元,为了提高查询效率,hregionserver 基于 lru 的 block cache 机制进行读操作。hbase的读取数据的机制是:hbase 同时读取磁盘和内存中的数据,然后把磁盘上的数据放到 blockcache中,blockcache 是磁盘数据的缓存。
hbase的读机制
client 先访问zookeeper,根据 rowkey 查询目标数据位于哪个regionserver对应的 region 中。client 向目标regionserver 进行通信,并提交写请求。regionserver 找到目标 region,region 检查数据的格式是否与 schema 一致。如果客户端没有指定版本,则获取当前系统时间作为数据版本。将数据顺序写入(追加)到 wal log将数据更新写入 memstore,数据会在memstore中排序。判断 memstore 的是否需要 flush 为 storefile 文件。
hbase的写机制
hbase能够提供实时计算服务的根本原因是其架构和底层数据结构决定的,hbase底层的存储引擎为lsm-tree(log-structured merge-tree)。
lsm的核心思想是放弃部分读能力,换取写入的最大化能力。lsm的原则就是,先将最新的数据驻留在内存中,等到积累足够多时,再使用归并排序的方式将内存中的数据追加都磁盘的队尾。另外,lsm的写入是磁盘的顺序写,数据写入速度也很稳定。我们知道磁盘的顺序写和内存写性能相差不大,但是顺序写磁盘速度要比随机写磁盘快至少三个数量级!
不过读取的时候稍微麻烦,需要合并磁盘中历史数据和内存中最近修改操作,这样磁盘在寻址耗时就远远大于磁盘顺序读取的诗句;另外数据读操作的时候,还要看数据在内存中是否命中,否则需要访问更多的磁盘文件。基于lsm树实现的hbase的写性能比mysql高一个数量级,但是读数据的性能要比mysql低一个数量级。
lsm树原理把一棵大树拆分成n棵小树,它首先写入内存中,随着小树越来越大,内存中的小树会flush到磁盘中,磁盘中的树定期可以做merge操作,合并成一棵大树,以优化读性能。
lsm树
补充:lsm-tree全称是log structured merge tree,是一种分层有序,面向磁盘的数据结构,其核心思想是充分了利用了,磁盘顺序写要远比随机写性能高出很多的特性,如下图示:
随机性和顺序写的性能比对
围绕lsm-tree的原理进行设计和优化,以此让写性能达到最优,当然有得就有舍,这种结构虽然大大提升了数据的写入能力,却是以牺牲部分读取性能为代价的,故此这种结构通常适合于写多读少的场景,这是hbase写比读速度快的根本原因所在。
一个 hregion 只能分配给一个 hregionserver,也就是说hregionserver和hregion的关系是一对多的关系。master 记录了hbase集群中哪些 hregionserver 是可用的。以及哪些 hregion 已经分配给了哪些 hregionserver,哪些 hregion 还没有分配。
当hregionserver需要分配的新的 hregion时,master 就会给这个 hregionserver 发送一个装载请求,把 hregion 分配给这个 hregionserver。hregionserver 得到请求后,就开始对此 hregion 提供服务。
master使用zookeeper来跟踪hregionserver的状态。当某个hregionserver启动时,首先在zookeeper上的/hbase/rs目录下建立代表自己的znode。由于master订阅了/hbase/rs目录上的变更消息,当/hbase/rs目录下的文件出现新增或删除操作时,master可以得到来自zookeeper的实时通知。因此一旦hregionserver上线,master能马上得到消息。
当regionserver下线时,它和zookeeper的会话就会断开。当master连续几次和regionserver都无法通信时,就可以确定hregionserver和zookeeper之间的网络断开了,或者是这个regionserver挂了。
我们知道,在hbase中,是通过4个维度一起来定位一条数据:行键(rowkey)、列族(column family)、列限定符(column qualifier)、时间戳(timestamp)。其中rowkey是最容易出问题的:
① 首先是单点集中问题,我所见过的单点集中问题主要有下面几种情况:
rowkey前面的字符比较集中固定。集群节点过少。
集群节点少是硬件不足的表现,我们主要考虑rowkey的设计问题。我们建议rowkey的设计方式如下:
随机字符(2位) 时间位(14位) 业务编码(4位)
亲身检测过:前后两种方案对比,前者的mr程序跑了2个小时,后者只花了10分钟。
② rowkey的长度过于太长
在hbase中,rowkey、列族、列名等都是以byte[]的形式传输的。rowkey的上限长度是64kb,但是我们使用hbase主要是为了让它快,因此在实际的应用中,rowkey的大小不会超过100b。这主要是从下面两个方面考虑的。
hbase的数据是存储在hfile中的,rowkey是keyvalue结构中的一个域。假设rowkey的大小是100b,那么1000万条数据,rowkey可能就占用了1gb的空间,也会影响hbase的响应速度的。
rowkey的设计原则
hbase中的memstore和blockcache,分别对应列族在store级别的写入缓存和regionserver级别的读取缓存。如果rowkey过长,缓存中存储数据的密度就会降低,影响数据落地或查询效率。
目前服务器一般都安装64位操作系统,内存按照8b对齐,因此,在设计rowkey时候,一般考虑做成8b的整数倍,例如如16b或者24b。同理,列族、列名的命名在保证可读的情况下尽量短。hbase官方不推荐使用3个以上列族,因此实际上列族命名几乎都用一个字母,比如‘c’或‘f’。
hbase支持很多压缩算法,而且能够做到从列簇级别上进行压缩。压缩可以减少网络带宽,同时也能够加快数据的读取,因此使用压缩算法通常能带来可观的性价比。
hbase支持的压缩算法
注意:如果一张表已经使用了某种压缩算法,那么现在想更改这张表的压缩格式,要先将该表disable才能修改,之后再enable重新上线。