前言
HBase作为一款历史悠久且具有代表性的NOSQL的数据库,其优点与缺点同样的明显,所以在确认了使用场景适合后下一步就是如何用好HBase了,不夸张的说,HBase使用的好与坏可能会造成天差地别,所以当你抱怨HBase如何的差劲不如XXX的时候,一定要先确认下自己是不是真的很好的使用了HBase,让其充分发挥出自己的亮点
正文
想要用好HBase,首先要确认自己的业务场景是不是适合使用HBase,这个我在另外一篇博文中已经有所描述HBase的使用场景,大家可以参考下。如果确认业务和数据场景适合使用HBase,那么下一步就是如何用好HBase了,这个大体上来说分为两部分,第一部分就是通过配置优化的方式来根据业务场景打造合适的集群,这个后续会单独讲解;第二部分就是如何把HBase的核心也就是表(Table)设计好,这个就是本文要着重讲解的内容。
HBase的表设计包含如下几个方面:
- 表空间设计
- rowkey设计
- 列族设计
- 列设计
下面就从这四个方面来详细讨论
表空间设计
HBase 1.X以来,HBase从原始的0.9X版本的table一级设计,升级到了namespace:table的两级设计。这样做的好处不仅使HBase的逻辑结构更加的清晰,也为后续的权限管理以及资源隔离等高级属性提供了很好的基础。HBase有默认有两个namespace,一个是hbase,用来存放HBase元数据相关的内容;另一个是default,用来存放未指定namespace的业务数据。也就是说在入库的时候如果只指定tablename,未指定namespace的话,表就会被创建在default的这个namespace中。在这里建议根据业务或者数据特点划分namespace,方便后续进行数据管理:
- 针对hbase使用较多数据量较大的业务,可以将数据放在单独的namespace中单独管理,入库的时候tablename使用namespace:tablename的方式来处理
- 针对hbase使用较少数据量较少的业务,可以将数据放在default的namespace中统一管理,入库的时候直接单独使用tablename方式来处理
rowkey设计
HBase的rowkey设计是HBase表设计中最重要的一部分,rowkey设计既要满足业务查询的功能需求,也要满足业务查询的性能需求,基本上rowkey设计的合理,HBase的性能和效果就不会太差,下面就详细说说rowkey设计的原则:
rowkey的作用
- HBase的源生查询就三种:单rowkey的get操作;范围rowkey的scan操作以及全表的scan操作,都是根据rowkey发起的,上述表明HBase的查询依赖rowkey
- HBase的rowkey在一个表中是唯一的,即一个rowkey能唯一代表一条数据,如果rowkey相同,HBase会认为是数据更新操作,会将原数据覆盖,上述表明HBase的逻辑存储依赖rowkey
- HBase的table在逻辑上被分为多个region,region被分散到多个regiongserver上,以达到横向扩展和高可用的目的。而region的划分是根据rowkey的字典序来进行的,每个region都有startKey和endKey,在此范围内的rowkey都会集中存储在一个region内,提升HBase按rowkey范围scan的效率,上述表明HBase的物理存储依赖rowkey
rowkey设计原则
rowkey设计根本上还是需要解决或者避免可能出现的各种问题,下面就各种问题以及使用场景来进行说明:
- get or scan?
get和scan是HBase两种源生的查询api,一个是单条数据的获取,一种是多条数据的获取,这两种业务需求会对表设计带来很大的影响
- 如何避免数据热点?
数据热点包含数据写热点和读热点,HBase的一个很大的特点就是高并发高吞吐,所以在这种背景下热点的问题必须要得到解决,否则会严重影响性能
- 如何优化HBase硬盘占用量?
当数据积累到一定程度后,HBase的数据硬盘占用量会达到一个相当可观的地步,所以硬盘使用量优化也是需要考虑的一个问题
- 对于时序数据,如何能快速的取到最新一条?
对于时序数据,由于有时间的概念,所以很多业务上可能会有取某个人或者设备最新一条数据的需求
rowkey设计方案
上述的几种情况就是HBase设计中通常要考虑的积累问题,下面就上面的问题进行方案设计:
- 首先HBase新建一张表默认只有1个region,当region大小达到一定的阈值后会进行自动的split操作,一个region变成两个region,后续按照这个规则继续迭代。当吞吐量过大的时候,单一的region肯定会成为性能瓶颈,所以建表的时候可以通过传入region的startKey和endKey的数据来自定义table的region个数,这个过程就是table预分区的过程,table合理的预分区规则也是解决数据热点的关键也是最有效的方法。对应的HBase的api就是Admin接口的void createTable(HTableDescriptor descriptor, byte[][] splitKeys) throws IOException;即可。
- 说到HBase的预分区,有两种常用的预分区方式:第一种就是按照Hash对table进行预分区,对写入数据的rowkey进行加盐操作,即对rowkey进行Hash操作取固定位数追加到rowkey的头部,这样做的好处是可以完全解决读写的热点,但是缺点是数据完全分散,相关的数据无法存储在相邻的位置,这对于get操作来说没有影响,但是对于scan这种操作来说,效率会有很大的影响,无法顺序的取数据,甚至无法按照业务需求进行scan,这种预分区适合仅get的场景;第二种是按照业务标识字段进行预分区,如一张表中存放的是各地的数据,rowkey的开头是地区编码,则可以按照地区编码进行预分区,这么做的好处是可以增加数据读写的并行度,但是不能完全解决读写热点问题,毕竟每个地区的数据量是不一样的,但是这样做的好处是对get无影响,对scan很友好,因为这种设计可以使业务相关的数据可以顺序存储,能够发挥HBase后模糊查询高效顺序取数据的特点,这种预分区适合scan操作频繁的场景。
- HBase底层是使用cell来进行存储的,一个cell包含了rowkey、columnFamily、column以及timestamp等属性,即如果每一列都有值的话,那一条数据的rowkey会被重复存储columnFamily*column*timestamp(多版本,取决于maxVersion个数),所以虽然rowkey最大长度64kb,但是实际应用中一般为10-100bytes,以byte[]形式保存,一般设计成定长。设计过长会降低memstore内存的利用率和硬盘的使用率
- 对于时序数据,为了展示时间轨迹的数据特点,在HBase中一般的rowkey存储方式是id+timestamp,但是某些业务场景需要取到每个id的最新一条数据,这时候可以考虑将timestamp进行处理,用LONG.MAX-timestamp来标识时间,即rowkey的设计为id+(LONG.MAX-timestamp),这样最新一条数据就会存储在id开头rowkey范围内最上面的一条数据。取数据时,直接用id进行scan取第一条数据,即是最新的一条数据。
从上面可见,想要完全避免热点,数据就会完全打散,关联的数据不再一起,scan就很难正常进行;而想使scan的效率和效果达到一个理想的状态,数据就必须集中,尤其是业务相关的数据,这样势必会有造成热点的风险。所以HBase的rowkey在设计上肯定会有相互矛盾的需求和场景,在这种情况下,一定要抓住主要矛盾,进行设计上的取舍,达到最佳的效果。
列族设计
列族是设计是在建表阶段需要完成的,也是表属性需要集中配置的维度,主要有下面的内容供参考:
VERSIONS
数据保留的版本数,默认是1,可以在建表的时候指定版本数量,对有保留多版本数据需求的场景可以进行指定,业务上可以用来实现保存某条数据或者某个业务实体最新的几条数据的需求,在查询时可以通过指定版本或者迭代来获取对应版本的数据
BLOOMFILTER
BLOOMFILTER(布隆过滤器)是用来加速HBase查询性能的一个组件,在HBase的配置中有三个选项:NONE、ROW 和 ROWCOL ,默认为 ROW。
布隆过滤器是以HFile为单位存在的,即一个HFile一个布隆过滤器。默认情况下的ROW形式的布隆过滤器的原理就是通过一定的算法将HBase的rowkey存储在一个数据结构里,当HBase查询一个HFile中是否有某个rowkey时,不需要去迭代查询,而是通过布隆过滤器直接可以判断该文件中是否有该rowkey,有的话去查询,没有的话直接跳过。
另外两种配置NONE是关闭布隆过滤器,而ROWCOL是针对随机读+指定列这种场景存在的,如果没有此种需求,则默认配置即可。虽然说开启布隆过滤器会占用部分内存和硬盘,但是其对HBase随机读的加速效果带来的好处是在是太诱人了,所以正常情况下建议开启布隆过滤器
此处普及一个知识点,准备小本本:布隆过滤器能快速判断一个rowkey是否存在的代价是这个算法有一定的误差率,即如果它认为rowkey不存在的话,则该rowkey一定不存在;反之如果它认为该rowkey存在的话,则有很小的概率不存在。误差率对于数据正确性要求极高的数据库来说是不可接受的,那么HBase是如何规避这个问题的呢?
答案就是不必处理,因为即使出现了误判,即布隆过滤器认为这个rowkey存在,那最多就是去HFile中查询一次,查询的结果就是没有查出数据,返回空,这对整个的最终结果来说是正确的,唯一的代价就是多查了一次,但是和节省了无数次查询的性能提升来说,这个性能损耗微不足道,毕竟没有布隆过滤器,这个操作是一定要执行的。
TTL(Time To Live)
即数据的生命周期,数据如果存储达到了TTL的最大值,就会被自动删除,这对数据滚动存储的场景来说很有意义。也可以作成冷热数据分离的实现,热数据在HBase中按TTL滚动,供实时业务查询和处理;全量数据存储在HDFS上,供数据分析和数据挖掘使用。
COMPRESSION & DATA_BLOCK_ENCODING
这个内容我在另外一篇文章中详细介绍了,大家可以参考下:HBase数据压缩 Column family Compress & Data Block Encoding
BLOCKSIZE
HBase 数据块大小。Block 是 HBase 系统文件层写入、读取的最小粒度,默认块大小为 64K。
对于不同的业务数据,块大小的合理设置对读写性能有很大的影响。通常来说,如果业务请求以 get 请求为主,可以考虑将块设置较小;如果以 scan 请求为主,可以将块调大;默认的 64K 块大小是在 scan 和 get 之间取得的一个平衡,适合绝大多数的业务场景。但是如果确定要调整,调整之前一定要进行充分测试,再决定是否部署生产。
DFS_REPLICATION
数据 Block 在 HDFS 上存储的副本数,默认为 HDFS 系统设置的值(dfs.replication),除非HBase的数据量过大且硬盘资源紧张,否则不建议修改此值。
IN_MEMORY
传说中的内存列族,即将该列族的所有数据都放在内存中。如果表中某些列的数据量不大,不会进行频繁的增删改但是进行 get 和 scan 操作的频率又特别高,同时业务要求延时低,此时可以采用 IN_MEMORY 效果比较好。
列设计
由于HBase是弱schema的结构,所以列相关的配置不在建表时指定,而是在数据写入的时候自动指定。而HBase的稀疏存储更是支持业务上定义大量的列,然后查询的时候指定列名进行精确查询,取到需要的数据。这种设计很符合HBase的特点以及设计理念。但是按照上文所说,HBase底层是按照cell存储的,cell的越多,rowkey以及column family的冗余存储就越大,浪费了缓存资源和硬盘资源,所以给出的建议是在充分发挥HBase的特点的同时尽量减少列的数量,将业务相关的列存放在一起,使用类似protocolbuffer等协议将数据集成在一起,这样查询的时候即可以节省迭代列的个数,又可以节省硬盘和内存资源,一举两得。
总结
俗话说得好,一千个人眼里有一千个哈姆雷特,那一千个HBase使用场景可能会有一千个HBase设计方案。HBase的设计从来不是非黑即白,也没有一套万能的配置能应对一切场景,我们能做的只有在掌握了HBase的特点和设计原则后尽量的理解业务,去设计满足业务需求的技术方案,在多种选择中进行取舍,在方案不断完善的同时自己的技术不断地成长。
传送门:HBase完整文档:HBase生产环境从入门到熟练使用,这一篇文章就够了