这是我参与「第四届青训营 」笔记创作活动的的第10天
一、HBase适用场景
HBase概述
HBase是一个分布式存储、数据库引擎,可以支持千万的QPS、PB级别的存储,这些都已经在生产环境验证,并且在广大的公司已经验证。特别是阿里、小米、京东、滴滴内部都有数千、上万台的HBase集群。选择一个技术的首要条件是对齐大公司,大公司会投入大量的人力去维护、改进、贡献社区。
关于NewSQL与NoSQL的关系
技术总是不断向前发展的,如今都在提NewSQL,其实NewSQL在笔者看来是NoSQL之上的一个封装,一个子场景。NoSQL中的大表,典型就是提供了KV1V2……Vn,其中每个V可以是1b,也可以是100MB。可以说是一个元的存在,就类似于数字世界的01,可以任意组合。在以HBase为代表的NoSQL中,HBase可以组合出任意的场景,NewSQL可以是之上加了SQL层或者更近一层添加事务的子场景。
HBase场景
HBase可以说是一个数据库,也可以说是一个存储。拥有双重属性的HBase天生就具备广阔的应用场景。在2.0中,引入了OffHeap降低了延迟,可以满足在线的需求。引入MOB,可以存储10M左右的对象,完全适应了对象存储。另外由于自身的并发能力、存储能力,可以说是具有最为竞争力的引擎
- 对象存储:我们知道不少的头条类、新闻类的的新闻、网页、图片存储在HBase之中,一些病毒公司的病毒库也是存储在HBase之中
- 时序数据:HBase之上有OpenTSDB模块,可以满足时序类场景的需求
- 推荐画像:特别是用户的画像,是一个比较大的稀疏矩阵,蚂蚁的风控就是构建在HBase之上
- 时空数据:主要是轨迹、气象网格之类,滴滴打车的轨迹数据主要存在HBase之中,另外在技术所有大一点的数据量的车联网企业,数据都是存在HBase之中
- CubeDB OLAP:Kylin一个cube分析工具,底层的数据就是存储在HBase之中,不少客户自己基于离线计算构建cube存储在hbase之中,满足在线报表查询的需求
- 消息/订单:在电信领域、银行领域,不少的订单查询底层的存储,另外不少通信、消息同步的应用构建在HBase之上
- Feeds流:典型的应用就是xx朋友圈类似的应用
- NewSQL:之上有Phoenix的插件,可以满足二级索引、SQL的需求,对接传统数据需要SQL非事务的需求
二、HBase架构设计
HBase数据模型
RowKey:用于唯一标识Hbase中的一条数据(记录),不可重复,按照字典顺序排序,只能存储64k的字节数据,因此一般要求设计RowKey要简短。
TimeStamp:HBase自动赋值时间戳(也可自己实现),作为版本号,64位整型。Hbase每个cell存储单元(k:v)对同一份数据有多个版本,通过实践戳来区分版本之间的差异,按时间倒叙排序。
CF(ColumnFamily):列族,可包含多(成千上万)列。权限控制,存储以及调优都是在列族层面进行的。HBase把同一列族里面的数据存储在同一目录下,由几个文件保存(StoreFile)。
新的列族成员(列)可以随后按需、动态加入,Family下面可以有多个Qualifier,所以可以简单的理解为,HBase中的列是二级列,也就是说Family是第一级列,Qualifier是第二级列。两个是父子关系。
Value:数据。
Cell:对应单元格具体值,是未解析的字节数组。由{rowkey, column(=+),version}唯一确定的单元。由rowkey,列族,列,版本(自动赋予的时间戳)唯一确定一个单元。Cell中的数据,没有类型,全部是字节码形式存储。
HBase是KV的数据库,如何通过K获取Value?
KEY 的组成是以 Row key 、CF(Column Family) 、Column 和 TimeStamp 组成的。
TimeStamp 在 HBase 中充当的作用就是版本号,因为在 HBase 中有着数据多版本的特性,所以同一个 KEY 可以有多个版本的 Value 值(可以通过配置来设置多少个版本)。查询的话是默认取回最新版本的那条数据,但是也可以进行查询多个版本号的数据。
三、大数据支撑
1、表的设计
1、Pre-Creating Regions
默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分。一种可以加快批量写入速度的方法是通过预先创建一些空的regions,这样当数据写入HBase时,会按照region分区情况,在集群内做数据的负载均衡。
2、Rowkey设计
HBase中row key用来检索表中的记录,支持以下三种方式:
1、通过单个row key访问:即按照某个row key键值进行get操作;
2、通过row key的range进行scan:即通过设置startRowKey和endRowKey,在这个范围内进行扫描;
3、全表扫描:即直接扫描整张表中所有行记录。
在HBase中,rowkey可以是任意字符串,最大长度64KB,实际应用中一般为10~100bytes,存为byte[]字节数组,一般设计成定长的。 rowkey是按照字典序存储,因此,设计row key时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
Rowkey设计原则:
1、越短越好,提高效率
(1)数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如操作100字节,1000万行数据,单单是存储rowkey的数据就要占用10亿个字节,将近1G数据,这样会影响HFile的存储效率。
(2)HBase中包含缓存机制,每次会将查询的结果暂时缓存到HBase的内存中,如果rowkey字段过长,内存的利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。
2、散列原则–实现负载均衡
如果Rowkey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将Rowkey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个Regionserver实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息将产生所有新数据都在一个 RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。
(1)加盐:添加随机值
(2)hash:采用md5散列算法取前4位做前缀
(3)反转:将手机号反转
3、唯一原则–字典序排序存储
必须在设计上保证其唯一性,rowkey是按照字典顺序排序存储的,因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。
3、列族的设计
不要在一张表里定义太多的column family。目前Hbase并不能很好的处理超过2~3个column family的表。因为某个column family在flush的时候,它邻近的column family也会因关联效应被触发flush,最终导致系统产生更多的I/O。
原因:
1、当开始向hbase中插入数据的时候,数据会首先写入到memstore,而memstore是一个内存结构,每个列族对应一个memstore,当包含更多的列族的时候,会导致存在多个memstore,每一个memstore在flush的时候会对应一个hfile的文件,因此会产生很多的hfile文件,更加严重的是,flush操作时region级别,当region中的某个memstore被flush的时候,同一个region的其他memstore也会进行flush操作,当某一张表拥有很多列族的时候,且列族之间的数据分布不均匀的时候,会产生更多的磁盘文件。
2、当hbase表的某个region过大,会被拆分成两个,如果我们有多个列族,且这些列族之间的数据量相差悬殊的时候,region的split操作会导致原本数据量小的文件被进一步的拆分,而产生更多的小文件
3、与 Flush 操作一样,目前 HBase 的 Compaction 操作也是 Region 级别的,过多的列族也会产生不必要的 IO。
4、HDFS 其实对一个目录下的文件数有限制的(dfs.namenode.fs-limits.max-directory-items)。如果我们有 N 个列族,M 个 Region,那么我们持久化到 HDFS 至少会产生 NM 个文件;而每个列族对应底层的 HFile 文件往往不止一个,我们假设为 K 个,那么最终表在 HDFS 目录下的文件数将是 NM*K,这可能会操作 HDFS 的限制。
4、in memory
hbase在LRU缓存基础之上采用了分层设计,整个blockcache分成了三个部分,分别是single、multi和inMemory。
三者区别如下: single:如果一个block第一次被访问,放在该优先队列中; multi:如果一个block被多次访问,则从single队列转移到multi队列 inMemory:优先级最高,常驻cache,因此一般只有hbase系统的元数据,如meta表之类的才会放到inMemory队列中。
5、Max Version
创建表的时候,可以通过ColumnFamilyDescriptorBuilder.setMaxVersions(int maxVersions)设置表中数据的最大版本,如果只需要保存最新版本的数据,那么可以设置setMaxVersions(1),保留更多的版本信息会占用更多的存储空间。
6、Time to Live
创建表的时候,可以通过ColumnFamilyDescriptorBuilder.setTimeToLive(int timeToLive)设置表中数据的存储生命期,过期数据将自动被删除,例如如果只需要存储最近两天的数据,那么可以设置setTimeToLive(2 * 24 * 60 * 60)。