HBase笔记
HBase简介
- Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩、实时读写的分布式数据库
- 利用Hadoop HDFS作为其文件存储系统,利用Hadoop MapReduce来处理HBase中的海量数据,利用Zookeeper作为其分布式协同服务。
- 主要用来存储非结构化和半结构化的松散数据(列存NoSQL数据库)
HBase数据模型
-
RowKey
是一条记录的唯一标识,决定一行数据,类似于关系数据库中的id。
RowKey是按照字典顺序排序的,其只能存储64K的字节数据。
-
TimeStamp
在HBase每个cell存储单元对同一份数据有多个版本,根据唯一的时间戳在区分每个版本之间的差异,不同版本的数据按照时间倒序排序,最新的数据版本排在最前面。
时间戳的类型是64位整型。
时间戳可以由HBase在数据写入时自动赋值,此时间戳是精确到毫秒的当前系统时间,也可以是由客户显式赋值,如果应用程序要避免数据版本冲突,就必须自己生成具有唯一性的时间戳!
-
ColumnsFamily(列族,又叫列簇)和Qualifier(列)
HBase中,每个列都必须归属于某个列族,列族必须作为表模式(schema)定义的一部分预先给出。
列名是以列族作为前缀,每个列族都可以有多个动态列成员(Column);新的列族成员(列)可以随后按需、动态加入。
权限控制、存储以及调优都是在列族层面进行。
同一个列族里面的数据存储在同一目录下,由几个文件保存。
-
Cell单元格
由行和列的坐标交叉决定;
单元格是有版本的
单元格的内容是未解析的字节数组:
- 由**{RowKey, column(+),version}** 唯一确定的单元。
- cell中的数据是没有类型的,全部是字节数组形式存贮。
HBase架构
角色
-
Client
包含访问HBase的接口并维护cache来加快对HBase的访问。
-
Zookeeper
- 保证任何时候,集群中只有一个活跃的master;
- 存贮所有Region的寻址入口;
- 实时监控RegionServer的上线和下线信息,并实时通知Master;
- 存储HBase的schema和table元数据。
-
Master
- 为RegionServer分配Region;
- 负责RegionServer的负载均衡;
- 发现失效的RegionServer并重新分配其上的Region;
- 管理用户对table的增删改操作。
-
RegionServer
- 维护Region,处理对这些Region的I/O请求;
- 负责切分在运行过程中变得过大的region。
-
Region
- HBase自动把表水平划分成多个区域(region),每个region会保存一个表里面某段连续的数据;
- 每个表一开始只有一个region,随着数据不断插入表,region不断增大,当增大到一个阈值的时候,region就会等分为两个新的region(裂变);
- 当table中的行不断增多,就会有越来越多的region,这样一张完整的表被保存在多个RegionServer上。
-
MemStore与StoreFile
-
一个Region由多个store组成,一个store对应一个C(olumn)F(amily)(列族);
-
store包括位于内存中的MemStore和位于磁盘的StoreFile。
-
写操作时,先写入MemStore,当MemStore中的数据达到某个阈值时,RegionServer会启动flashCache进程写入StoreFile,每次写入形成单独的一个StoreFile;
当StoreFile文件的数量增长到一定的阈值后,系统会进行合并(minor、major compaction),在合并过程中会进行版本合并和删除工作(majar),形成更大的StoreFile。
当一个Region所有的StoreFile的大小和数量超过一定阈值后,会把当前的Region分割为两个,并由Master分配到相应的RegionServer服务器,实现负载均衡。
-
客户端检索数据,先在MemStore找,找不到再去blockCache,还找不到再找StoreFile。
-
-
读流程
- 在ZooKeeper中,存在这部分的元数据,所以第一步先在ZooKeeper中找,找到元数据表是存在哪一个RegionServer中,则拿到了该RegionServer的地址;
- 客户端根据获取到的RegionServer的地址,就通过对应的RegionServer拿到要查询的表对应的元数据;
- 通过表的元数据,再拿到存储该表数据的RegionServer;
- 先从MemStore中查找,如果没有,就去BlockCache中查找,还是没有,再从StoreFile中查找。
MenStore可以理解为写缓存,但其承担一部分读的压力,例如还没溢写到磁盘的数据,读的时候也需要检查。
!!!注意!!!
在官网的图中,并没有画出读缓存,实际上是有一个读缓存的,叫BlockCache。
读的过程中,从StoreFile查到的数据,会缓存到BlockCache中再返回客户端。
BlockCache中有一个更新缓存的环节,当BlockCache满了的时候,需要删除旧的数据,这时候就会用到一个原则,叫LRU原则,最近最少使用淘汰原则。
root目录和meta目录
root目录在0.92版本开始后,就不存在了!
细化读流程
一、构建scanner体系–组件施工队
scanner体系的核心在于三层scanner:RegionScanner、StoreScanner以及StoreFileScanner。三者是层级的关系,一个RegionScanner由多个StoreScanner构成,一张表由多个列族组成,就有多少个StoreScanner负责该列族的数据扫描。一个StoreScanner又是由多个StoreFileScanner组成。每个Store的数据由内存中的MemStore和磁盘上的StoreFile文件组成,相对应的,StoreScanner对象会雇佣一个MemStoreScanner和N个StoreFileScanner来进行实际的数据读取,每个StoreFile文件对应一个StoreFileScanner,注意:StoreFileScanner和MemstoreScanner是整个scan的最终执行者。
对应于建楼项目,一栋楼通常由好几个单元楼构成(每个单元楼对应于一个Store),每个单元楼会请一个监工(StoreScanner)负责该单元楼的建造。而监工一般不做具体的事情,他负责招募很多工人(StoreFileScanner),这些工人才是建楼的主体。下图是整个构建流程图:
- RegionScanner会根据列族构建StoreScanner,有多少列族就构建多少StoreScanner,用于负责该列族的数据检索
1.1 构建StoreFileScanner:每个StoreScanner会为当前该Store中每个HFile构造一个StoreFileScanner,用于实际执行对应文件的检索。同时会为对应Memstore构造一个MemstoreScanner,用于执行该Store中Memstore的数据检索。该步骤对应于监工在人才市场招募建楼所需的各种类型工匠。
1.2 过滤淘汰StoreFileScanner:根据Time Range以及RowKey Range对StoreFileScanner以及MemstoreScanner进行过滤,淘汰肯定不存在待检索结果的Scanner。上图中StoreFile3因为检查RowKeyRange不存在待检索Rowkey所以被淘汰。该步骤针对具体的建楼方案,裁撤掉部分不需要的工匠,比如这栋楼不需要地暖安装,对应的工匠就可以撤掉。
1.3 Seek rowkey:所有StoreFileScanner开始做准备工作,在负责的HFile中定位到满足条件的起始Row。工匠也开始准备自己的建造工具,建造材料,找到自己的工作地点,等待一声命下。就像所有重要项目的准备工作都很核心一样,Seek过程(此处略过Lazy Seek优化)也是一个很核心的步骤,它主要包含下面三步:
- 定位Block Offset:在Blockcache中读取该HFile的索引树结构,根据索引树检索对应RowKey所在的Block Offset和Block Size
- Load Block:根据BlockOffset首先在BlockCache中查找Data Block,如果不在缓存,再在HFile中加载
- Seek Key:在Data Block内部通过二分查找的方式定位具体的RowKey
1.4 StoreFileScanner合并构建最小堆:将该Store中所有StoreFileScanner和MemstoreScanner合并形成一个heap(最小堆),所谓heap是一个优先级队列,队列中元素是所有scanner,排序规则按照scanner seek到的keyvalue大小由小到大进行排序。这里需要重点关注三个问题,首先为什么这些Scanner需要由小到大排序,其次keyvalue是什么样的结构,最后,keyvalue谁大谁小是如何确定的:
为什么这些Scanner需要由小到大排序?
最直接的解释是scan的结果需要由小到大输出给用户,当然,这并不全面,最合理的解释是只有由小到大排序才能使得scan效率最高。举个简单的例子,HBase支持数据多版本,假设用户只想获取最新版本,那只需要将这些数据由最新到最旧进行排序,然后取队首元素返回就可以。那么,如果不排序,就只能遍历所有元素,查看符不符合用户查询条件。这就是排队的意义。
工匠们也需要排序,先做地板的排前面,做墙体的次之,最后是做门窗户的。做墙体的内部还需要再排序,做内墙的排前面,做外墙的排后面,这样,假如设计师临时决定不做外墙的话,就可以直接跳过外墙部分工作。很显然,如果不排序的话,是没办法临时做决定的,因为这部分工作已经可能做掉了。
HBase中KeyValue是什么样的结构?
HBase中KeyValue并不是简单的KV数据对,而是一个具有复杂元素的结构体,其中Key由RowKey,ColumnFamily,Qualifier ,TimeStamp,KeyType等多部分组成,Value是一个简单的二进制数据。Key中元素KeyType表示该KeyValue的类型,取值分别为Put/Delete/Delete Column/Delete Family等。KeyValue可以表示为如下图所示:
了解了KeyValue的逻辑结构后,我们不妨再进一步从原理的角度想想HBase的开发者们为什么如此对其设计。这个就得从HBase所支持的数据操作说起了,HBase支持四种主要的数据操作,分别是Get/Scan/Put/Delete,其中Get和Scan代表数据查询,Put操作代表数据插入或更新(如果Put的RowKey不存在则为插入操作、否则为更新操作),特别需要注意的是HBase中更新操作并不是直接覆盖修改原数据,而是生成新的数据,新数据和原数据具有不同的版本(时间戳);Delete操作执行数据删除,和数据更新操作相同,HBase执行数据删除并不会马上将数据从数据库中永久删除,而只是生成一条删除记录,最后在系统执行文件合并的时候再统一删除。
HBase中更新删除操作并不直接操作原数据,而是生成一个新纪录,那问题来了,如何知道一条记录到底是插入操作还是更新操作亦或是删除操作呢?这正是KeyType和Timestamp的用武之地。上文中提到KeyType取值为分别为Put/Delete/Delete Column/Delete Family四种,如果KeyType取值为Put,表示该条记录为插入或者更新操作,而无论是插入或者更新,都可以使用版本号(Timestamp)对记录进行选择;如果KeyType为Delete,表示该条记录为整行删除操作;相应的KeyType为Delete Column和Delete Family分别表示删除某行某列以及某行某列族操作;
- 不同KeyValue之间如何进行大小比较? 上文提到KeyValue中Key由RowKey,ColumnFamily,Qualifier ,TimeStamp,KeyType等5部分组成,HBase设定Key大小首先比较RowKey,RowKey越小Key就越小;RowKey如果相同就看CF,CF越小Key越小;CF如果相同看Qualifier,Qualifier越小Key越小;Qualifier如果相同再看Timestamp,Timestamp越大表示时间越新,对应的Key越小。如果Timestamp还相同,就看KeyType,KeyType按照DeleteFamily -> DeleteColumn -> Delete -> Put 顺序依次对应的Key越来越大。
- StoreScanner合并构建最小堆:上文讨论的是一个监工如何构建自己的工匠师团队以及工匠师如何做准备工作、排序工作。实际上,监工也需要进行排序,比如一单元的监工排前面,二单元的监工排之后… StoreScanner一样,列族小的StoreScanner排前面,列族大的StoreScanner排后面。
二、scan查询-层层建楼
构建Scanner体系是为了更好地执行scan查询,就像组建工匠师团队就是为了盖房子一样。scan查询总是一行一行查询的,先查第一行的所有数据,再查第二行的所有数据,但每一行的查询流程却没有什么本质区别。盖房子也一样,无论是盖8层还是盖18层,都需要一层一层往上盖,而且每一层的盖法并没有什么区别。所以实际上我们只需要关注其中一行数据是如何查询的就可以。
<