简介:
HBase是一个典型的NOsql数据库,以其独特的列式存储和顺序读写(磁盘的顺序读写比内存的随机读写还要高效),能做到高效读取和存储海量数据,是大数据存储和数仓建设中很重要的工具
在讲rowkey设计和预分区之前,让我们来看看hbase数据是如何根据rowkwy找到属于自己的region进行存储
一、Hbase寻址和读写原理
架构分析
1、HMaster
负责管理HBase元数据,即表的结构、表存储的Region等元信息。
负责表的创建,删除和修改(因为这些操作会导致HBase元数据的变动)。
负责为HRegionServer分配Region,分配好后也会将元数据写入相应位置(后面会详细讲述放在哪)。
如果对可用性要求较高,它需要做HA高可用(通过Zookeeper)。但是HMaster不会去处理Client端的数据读写请求,因为这样会加大其负载压力,具体的读写请求它会交给HRegionServer来做。
2、HRegionServer
一个RegionServer里有多个Region。
处理Client端的读写请求(根据从HMaster返回的元数据找到对应的Region来读写数据)。
管理Region的Split分裂、StoreFile的Compaction合并。
一个RegionServer管理着多个Region,在HBase运行期间,可以动态添加、删除HRegionServer。
3、HRegion
一个HRegion里可能有1个或多个Store。
HRegionServer维护一个HLog。
HRegion是分布式存储和负载的最小单元。
表通常被保存在多个HRegionServer的多个Region中。
因为HBase用于存储海量数据,故一张表中数据量非常之大,单机一般存不下这么大的数据,故HBase会将一张表按照行水平将大表划分为多个Region,每个Region保存表的一段连续数据。 初始只有1个Region,当一个Region增大到某个阈值后,便分割为两个。
4、Store
Store是存储落盘的最小单元,由内存中的MemStore和磁盘中的若干StoreFile组成。
一个Store里有1个或多个StoreFile和一个memStore。
每个Store存储一个列族。
读写原理
写过程:
1、Client访问ZK,根据ROOT表获取meta表所在Region的位置信息,并将该位置信息写入Client Cache。
(注:为了加快数据访问速度,我们将元数据、Region位置等信息缓存在Client Cache中。)
2、Client读取meta表,再根据meta表中查询得到的Namespace、表名和RowKey等相关信息,获取将要写入Region的位置信息(此过程即Region三层定位,如下图),最后client端会将meta表写入Client Cache。
3、Client向上一步HRegionServer发出写请求,HRegionServer先将操作和数据写入HLog(预写日志,Write Ahead Log,WAL),再将数据写入MemStore,并保持有序。
(联想:HDFS中也是如此,EditLog写入时机也是在真实读写之前发生)
4、当单个regiion里的多个MemStore的数据缓存大小超过阈值时(默认是128M,老版本是64M),将数据溢写磁盘,生成一个StoreFile文件。
当Store中StoreFile的数量超过阈值时,将若干小StoreFile合并(Compact)为一个大StoreFile。
当Region中最大Store的大小超过阈值时,Region分裂(Split),等分成两个子Region。
读过程:
1、获取将要读取Region的位置信息(同读的1、2步)。
2、Client向HRegionServer发出读请求。
3、HRegionServer先从MemStore读取数据,如未找到,再从StoreFile中读取。
寻址机制!!!(重要):
首先:rowkey寻址和数据存放是根据字典排序
现在假设我们要从用户表
里面查询一条RowKey是rk00888的数据。那么我们应该遵循以下步骤:
- 从.META.表里面查询哪个Region包含这条数据:有多个region,每个region存储的数据有startkey和endkey,是一个范围(例如:第一个region的数据范围是rk00000~rk99999,那么数据rk00888所在的位置就在第一个region里)
- 获取管理这个Region的RegionServer地址。
- 连接这个RegionServer, 查到这条数据。
二、rowkey设计和预分区
rowkey的设计和预分区的startkey、endkey决定数据存放是否均匀
rowkey设计:
前面了解到,hbase中数据存放是根据rowkey的字典顺序而存放进不同的region里面,所以合理的设计rowkey来优化读写就显得尤为重要:
rowkey唯一原则
必须在设计上保证其唯一性,rowkey是按照字典顺序排序存储的,因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。
rowkey长度原则
rowkey是一个二进制码流,可以是任意字符串,最大长度 64kb ,实际业务中一般为10-100bytes,以 byte[] 形式保存,一般设计成定长。
建议越短越好,不要超过16个字节,原因如下:
数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光rowkey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率;
MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。
目前操作系统都是64位系统,内存8字节对齐,控制在16个字节,8字节的整数倍利用了操作系统的最佳特性。
rowkey散列原则
如果rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息,所有的数据都会集中在一个RegionServer上,这样在数据检索的时候负载会集中在个别的RegionServer上,造成热点问题,会降低查询效率。
预分区:
1、为什么要进行预分区?
HBase默认建表时有一个region,这个region的rowkey是没有边界的,即没有startkey和endkey,在数据写入时,所有数据都会写入这个默认的region,当一个region中数据超过阈值时(默认10G),此region已经不能承受不断增长的数据量,会进行split,分成2个region。在此过程中,会产生两个问题:
a.我们的数据会不断的往一个region上写,会有写热点问题。
b.region split会消耗宝贵的集群I/O资源。
c.如果在任务执行时region分裂,会导致任务失败
基于此我们可以控制在建表的时候,创建多个空region,并确定每个region的起始和终止rowky,这样只要我们的rowkey设计能均匀的命中各个region,就不会存在写热点问题。自然split的几率也会大大降低。当然随着数据量的不断增长,该split的还是要进行split。像这样预先创建hbase表分区的方式,称之为预分区
首先看没有预分区的:
一个region里面就保存了7000w(虽然7000w对hbase来说是小case)的数据,读写压力全在这一个region上,会很容易造成热点问题
2、使用Hbase shell进行预分区
前面说到了rowkey的排列是根据字典排序,所以rowkey寻址的时候是和startkey、endkey进行比较,在字典排序在哪个region的startkey、endkey之间,就落在哪个region
create 't1','f1',SPLITS => ['10','20','30']
创建了一个列簇 f 的表 t1,预分4个区,第一个 region 包含从 ‘\x00’ - ‘\x30’ 的所有 key(’\x31’ 是 ASCII码中的 1)
四个分区,rowkey如果是‘0232534’,就落在第一个分区,‘130034
24’落在第二个分区(字典排序!!!!)
自定义分区:
hbase>create 't14','f',SPLITS_FILE=>'splits.txt'
可以使用 SPLITS_FILE 来指定一个文本文件,文件内写入拆分点(即startkey,endkey),通过这种方式来自定义拆分点
splits.txt文件截图:
预分区截图:
3、预分区的实际应用
在实际的业务场景中,因为数据量多,正常预分区个数在200~2000的范围,分区个数参考:
hbase数据日增1G:200region
hbase自带的分区算法:
# 基于随机算法创建一个有4默认个分区的表
hbase>create 't2','f1', { NUMREGIONS => 200 , SPLITALGO => 'UniformSplit' }
# 基于 hex keys 创建一个有500个默认分区的表
hbase>create 't3','f1', { NUMREGIONS => 500, SPLITALGO => 'HexStringSplit' }
HexStringSplit、UniformSplit 说明:
UniformSplit(占用空间小):将可能的键的空间平均分割的聚合体。当键是近似一致的随机字节时(例如散列),建议使用这个。行是范围为 00 => FF 的原始字节值,用0右填充以保持相同的 memcmp()顺序。对于byte[]环境来说,这是一种自然的算法,可以节省空间,但是对于可读性来说,它并不一定是最简单的。
HexStringSplit(占用空间大):HexStringSplit 是一个典型的 RegionSplitter.SplitAlgorithm来选择 region 边界。HexStringSplit region 边界的格式是MD5校验和或任何其他均匀分布的十六进制值的ASCII表示形式。Row是十六进制编码的长值,其范围为“00000000”=>“FFFFFFFF”,并左填充0,以使其在字典上保持与二进制相同的顺序。由于这种分割算法使用十六进制字符串作为键,所以在 shell 中方便读写,但是占用更多的空间,而且可能不够直观。
但是这两种可能都不适合你们的业务场景:
我一开始使用的是HexStringSplit算法,贴上截图
这种算法的确做到了把字符尽可能的分散,但是对分布不均匀的业务数据来说,还是不够随机,以上就是,当字符排序大于最后一个分区的startkey时,数据就会都集中在最后一个分区,还是没有解决数据均匀存储的目的
一个通用的解决办法:(全文重点!!!!!!)
自定义分区+rowkey随机前缀
1、我们先自定义500个分区,切分点是100~600的字符(自定义分区在上边)
2、rowkey随机前缀:
因为rowkey是根据字典顺序寻址,👎我们预分区的startkey、endkey是100~600的字符
第一个region:··· ~100
第二个region:100 ~101
第三个region:101 ~102
第n个region:···~···
第601个region:599 ~600
最后一个region:600~···所以我们只要保证,rowkey的前三个字符是在100~600之间的随机值,那么数据就可以随机且均匀的落在500个分区上了。。嘿嘿(我好了)
然后,数据就会像这个样子,特别均匀的落在500个region上(大家都找到了属于自己的region,而你还是个没有region的野孩子)