最近在工作中用到了 Hbase 这个数据库,也顺便做了关于 Hbase 的知识记录来分享给大家。其实 Hbase的内容体系真的很多很多,这里介绍的是小羽认为在工作中会用到的一些技术点,希望可以帮助到大家。
可以这么说互联网都是建立在形形色色的数据库之上的,现在主流的数据库有这么几种:以 MySQL 为代表的关系型数据库以及其分布式解决方案,以 Redis 为代表的缓存数据库,以 ES 为代表的检索数据库,再就是分布式持久化 KV 数据库。而在开源领域,尤其是国内,HBase 几乎是分布式持久化KV数据库的首选方案。HBase 应用的业务场景非常之多,比如用户画像、实时(离线)推荐、实时风控、社交Feed流、商品历史订单、社交聊天记录、监控系统以及用户行为日志等等。
前言
我们每一个人无论使用什么科技产品,都会产生大量的数据,而这些数据的存储和查询对于小型数据库来说其实是很难满足我们的需求的,因此出现了 HBase 分布式大数据。HBase 是一个构建在 Hadoop 文件系统之上的面向列的数据库管理系统。HBase 是一种类似于 Google’s Big Table 的数据模型,它是 Hadoop 生态系统的一部分,它将数据存储在 HDFS 上,客户端可以通过 HBase 实现对 HDFS 上数据的随机访问。它主要有以下特性:
不支持复杂的事务,只支持行级事务,即单行数据的读写都是原子性的;
由于是采用 HDFS 作为底层存储,所以和 HDFS 一样,支持结构化、半结构化和非结构化的存储;
支持通过增加机器进行横向扩展;
支持数据分片;
支持 RegionServers 之间的自动故障转移;
易于使用的 Java 客户端 API;
支持 BlockCache 和布隆过滤器;
过滤器支持谓词下推。
HBase 原理
概念
HBase 是分布式、面向列的开源数据库(其实准确的说是面向列族)。HDFS 为 Hbase 提供可靠的底层数据存储服务,MapReduce 为 Hbase 提供高性能的计算能力,Zookeeper 为 Hbase 提供稳定服务和 Failover 机制,因此我们说 Hbase 是一个通过大量廉价的机器解决海量数据的高速存储和读取的分布式数据库解决方案。
列式存储
我们先来看一下之前的关系型数据库的按行来存储的。如下图:
可以看到只有第一行 ID:1 小羽的这一行的数据都填了,小娜和小智的数据都没有填完。在我们的行结构中,都是固定的,每一行都一样,就算不填,也要空着,不能没有。
来看一下使用了非关系型数据库的按列存储的效果图:
可以看到之前小羽的一列数据对应到了小羽现在的一行数据,原来小羽的七列数据变成了现在的七行。之前的七行数据在一行,共用过一个主键 ID:1 。在列式存储里,变成了七行,每一行都有一个主键与其对应,也就是为什么小羽的主键 ID:1 重复了七次。这样排列最大的好处就是,我们对于不需要的数据就不需要添加,会大大节省我们的空间资源。因为查询中的选择规则是通过列来定义的,整个数据库是自动索引化的。
NoSQL和关系型数据库对比
对比如下图:
RDBMS 与 Hbase 对比
Hbase 是根据列族来存储数据的。列族下面可以有非常多的列,列族在创建表的时候就必须指定。为了加深对 Hbase 列族的理解,下面是简单的关系型数据库的表和 Hbase 数据库的表:
主要区别:
HBase 架构
Hbase 是由 Client、Zookeeper、Master、HRegionServer、HDFS 等几个核心体系组成。
Client
Client 使用 HBase 的 RPC 机制与 HMaster、HRegionServer 进行通信。Client 与 HMaster 进行管理类通信,与 HRegion Server 进行数据操作类通信。
Zookeeper
Hbase 通过 Zookeeper 来做 master 的高可用、RegionServer 的监控、元数据的入口以及集群配置的维护等工作。具体工作如下:
1. 通过 Zoopkeeper 来保证集群中只有 1 个 master 在运行,如果 master 异常,会通过竞争机制产生新的 master 提供服务
2. 通过 Zoopkeeper 来监控 RegionServer 的状态,当 RegionSevrer 有异常的时候,通过回调的形式通知 Master RegionServer 上下限的信息
3. 通过 Zoopkeeper 存储元数据的统一入口地址。
客户端在使用 hbase 的时候,需要添加 zookeeper 的 ip 地址和节点路径,建立起与zookeeper的连接,建立连接的方式如下面的代码所示:
- Configuration configuration = HBaseConfiguration.create();
- configuration.set("hbase.zookeeper.quorum", "XXXX.XXX.XXX");
- configuration.set("hbase.zookeeper.property.clientPort", "2181");
- configuration.set("zookeeper.znode.parent", "XXXXX");
- Connection connection = ConnectionFactory.createConnection(configuration);
Hmaster
master 节点的主要职责如下:
1. 为 RegionServer 分配 Region
2. 维护整个集群的负载均衡
3. 维护集群的元数据信息,发现失效的 Region,并将失效的 Region 分配到正常RegionServer 上当 RegionSever 失效的时候,协调对应 Hlog 的拆分
HRegionServer
HRegionServer 内部管理了一系列 HRegion 对象,每个 HRegion 对应 Table中的一个 ColumnFamily 的存储,即一个 Store 管理一个 Region 上的一个列族(CF)。每个 Store 包含一个 MemStore 和 0 到多个 StoreFile。Store 是 HBase 的存储核心,由 MemStore 和 StoreFile 组成。
HLog
数据在写入时,首先写入预写日志(Write Ahead Log),每个 HRegionServer 服务的所有 Region 的写操作日志都存储在同一个日志文件中。数据并非直接写入 HDFS,而是等缓存到一定数量再批量写入,写入完成后在日志中做标记。
MemStore
MemStore 是一个有序的内存缓存区,用户写入的数据首先放入 MemStore,当 MemStore 满了以后 Flush 成一个 StoreFile(存储时对应为 File),当 StoreFile 数量增到一定阀值,触发 Compact 合并,将多个 StoreFile 合并成一个 StoreFile。StoreFiles 合并后逐步形成越来越大的 StoreFile,当 Region 内所有 StoreFiles(Hfile) 的总大小超过阀值(hbase.hregion.max.filesize)即触发分裂 Split,把当前的 Region Split 分成 2 个 Region,父 Region 下线,新 Spilt 出的 2 个孩子 Region 被 HMaster 分配到合适的 HRegionServer 上,使得原先 1 个 Region 的压力得以分流到 2 个 Region 上。
Region 寻址方式
通过 zookeeper.META,主要有以下几步:
1. Client 请求 ZK 获取.META.所在的 RegionServer 的地址。
2. Client 请求.META.所在的 RegionServer 获取访问数据所在的 RegionServer 地址,client 会将.META.的相关信息 cache 下来,以便下一次快速访问。
3. Client 请求数据所在的 RegionServer,获取所需要的数据。
HDFS
HDFS 为 Hbase 提供最终的底层数据存储服务,同时为 Hbase 提供高可用(Hlog 存储在HDFS)的支持。
HBase 组件
Column Family 列族
Column Family 又叫列族,Hbase 通过列族划分数据的存储,列族下面可以包含任意多的列,实现灵活的数据存取。Hbase 表的创建的时候就必须指定列族。就像关系型数据库创建的时候必须指定具体的列是一样的。Hbase 的列族不是越多越好,官方推荐的是列族最好小于或者等于 3。我们使用的场景一般是 1 个列族。
Rowkey
Rowkey 的概念和 mysql 中的主键是完全一样的,Hbase 使用 Rowkey 来唯一的区分某一行的数据。Hbase 只支持 3 种查询方式:基于 Rowkey 的单行查询,基于 Rowkey 的范围扫描,全表扫描。
Region 分区
Region:Region 的概念和关系型数据库的分区或者分片差不多。Hbase 会将一个大表的数据基于 Rowkey 的不同范围分配到不同的 Region 中,每个 Region 负责一定范围的数据访问和存储。这样即使是一张巨大的表,由于被切割到不同的 region,访问起来的时延也很低。
TimeStamp 多版本
TimeStamp 是实现 Hbase 多版本的关键。在 Hbase 中使用不同的 timestame 来标识相同 rowkey 行对应的不同版本的数据。在写入数据的时候,如果用户没有指定对应的 timestamp,Hbase 会自动添加一个 timestamp,timestamp 和服务器时间保持一致。在Hbase 中,相同 rowkey 的数据按照 timestamp 倒序排列。默认查询的是最新的版本,用户可通过指定 timestamp 的值来读取旧版本的数据。
Hbase 写逻辑
Hbase 写入流程
主要有三个步骤:
1. Client 获取数据写入的 Region 所在的 RegionServer
2. 请求写 Hlog, Hlog 存储在 HDFS,当 RegionServer 出现异常,需要使用 Hlog 来恢复数据。
3. 请求写 MemStore,只有当写 Hlog 和写 MemStore 都成功了才算请求写入完成。MemStore 后续会逐渐刷到 HDFS 中。
MemStore 刷盘
为了提高 Hbase 的写入性能,当写请求写入 MemStore 后,不会立即刷盘。而是会等到一定的时候进行刷盘的操作。具体是哪些场景会触发刷盘的操作呢?总结成如下的几个场景:
1. 这个全局的参数是控制内存整体的使用情况,当所有 memstore 占整个 heap 的最大比例的时候,会触发刷盘的操作。这个参数是hbase.regionserver.global.memstore.upperLimit,默认为整个 heap 内存的 40%。但这并不意味着全局内存触发的刷盘操作会将所有的 MemStore 都进行输盘,而是通过另外一个参数 hbase.regionserver.global.memstore.lowerLimit 来控制,默认是整个 heap 内存的 35%。当 flush 到所有 memstore 占整个 heap 内存的比率为35%的时候,就停止刷盘。这么做主要是为了减少刷盘对业务带来的影响,实现平滑系统负载的目的。
2. 当 MemStore 的大小达到 hbase.hregion.memstore.flush.size 大小的时候会触发刷盘,默认 128M 大小
3. 前面说到 Hlog 为了保证 Hbase 数据的一致性,那么如果 Hlog 太多的话,会导致故障恢复的时间太长,因此 Hbase 会对 Hlog 的最大个数做限制。当达到 Hlog 的最大个数的时候,会强制刷盘。这个参数是 hase.regionserver.max.logs,默认是 32 个。
4. 可以通过 hbase shell 或者 java api 手工触发 flush 的操作。
5. 在正常关闭 RegionServer 会触发刷盘的操作,全部数据刷盘后就不需要再使用 Hlog 恢复数据。
6. 当 RegionServer 出现故障的时候,其上面的 Region 会迁移到其他正常的 RegionServer 上,在恢复完 Region 的数据后,会触发刷盘,当刷盘完成后才会提供给业务访问。
HBase 中间层
Phoenix 是 HBase 的开源 SQL 中间层,它允许你使用标准 JDBC 的方式来操作 HBase 上的数据。在 Phoenix 之前,如果你要访问 HBase,只能调用它的 Java API,但相比于使用一行 SQL 就能实现数据查询,HBase 的 API 还是过于复杂。Phoenix 的理念是 we put sql SQL back in NOSQL,即你可以使用标准的 SQL 就能完成对 HBase 上数据的操作。同时这也意味着你可以通过集成 Spring Data JPA 或 Mybatis 等常用的持久层框架来操作 HBase。
其次 Phoenix 的性能表现也非常优异,Phoenix 查询引擎会将 SQL 查询转换为一个或多个 HBase Scan,通过并行执行来生成标准的 JDBC 结果集。它通过直接使用 HBase API 以及协处理器和自定义过滤器,可以为小型数据查询提供毫秒级的性能,为千万行数据的查询提供秒级的性能。同时 Phoenix 还拥有二级索引等 HBase 不具备的特性,因为以上的优点,所以 Phoenix 成为了 HBase 最优秀的 SQL 中间层。
HBase 安装使用
下载 HBase 压缩包,首先解压
- tar -zxvf hbase-0.98.6-hadoop2-bin.tar.gz
打开 hbase-env.sh 文件配置 JAVA_HOME:
- export JAVA_HOME=/opt/modules/jdk1.7.0_79
配置 hbase-site.xml:
- <configuration>
- <property>
- <name>hbase.rootdir</name>
- <value>hdfs://hadoop-senior.shinelon.com:8020/hbase</value>
- </property>
- <property>
- <name>hbase.cluster.distributed</name>
- <value>true</value>
- </property>
- <property>
- <name>hbase.zookeeper.quorum</name>
- <value>hadoop-senior.shinelon.com</value>
- </property>
- </configuration>
将上面的主机名换为自己的主机名,就可以启动HBase了。Web 页面访问如下:
HBase 命令
下面是小羽整理的一些关于 Hbase 的经常会使用到的命令:
HBase API 使用
API 如下:
- package com.initialize;
- import org.apache.hadoop.conf.Configuration;
- import org.apache.hadoop.hbase.HBaseConfiguration;
- import org.apache.hadoop.hbase.HColumnDescriptor;
- import org.apache.hadoop.hbase.HTableDescriptor;
- import org.apache.hadoop.hbase.TableName;
- import org.apache.hadoop.hbase.client.Admin;
- import org.apache.hadoop.hbase.client.Connection;
- import org.apache.hadoop.hbase.client.ConnectionFactory;
- import org.apache.hadoop.hbase.regionserver.BloomType;
- import org.junit.Before;
- import org.junit.Test;
- import java.io.IOException;
- /**
- *
- * 1、构建连接
- * 2、从连接中取到一个表DDL操作工具admin
- * 3、admin.createTable(表描述对象);
- * 4、admin.disableTable(表名);
- * 5、admin.deleteTable(表名);
- * 6、admin.modifyTable(表名,表描述对象);
- *
- */
- public class HbaseClientDemo {
- Connection conn = null;
- @Before
- public void getConn() throws IOException {
- //构建一个连接对象
- Configuration conf = HBaseConfiguration.create();//会自动加载hbase-site.xml
- conf.set("hbase.zookeeper.quorum","n1:2181,n2:2181,n3:2181");
- conn = ConnectionFactory.createConnection(conf);
- }
- /**
- * DDL
- * 创建表
- */
- @Test
- public void testCreateTable() throws Exception{
- //从连接中构造一个DDL操作器
- Admin admin = conn.getAdmin();
- //创建一个标定义描述对象
- HTableDescriptor hTableDescriptor = new HTableDescriptor(TableName.valueOf("user_info"));
- //创建列族定义描述对象
- HColumnDescriptor hColumnDescriptor_1 = new HColumnDescriptor("base_info");
- hColumnDescriptor_1.setMaxVersions(3);
- HColumnDescriptor hColumnDescriptor_2 = new HColumnDescriptor("extra_info");
- //将列族定义信息对象放入表定义对象中
- hTableDescriptor.addFamily(hColumnDescriptor_1);
- hTableDescriptor.addFamily(hColumnDescriptor_2);
- //用ddl操作器对象:admin来创建表
- admin.createTable(hTableDescriptor);
- //关闭连接
- admin.close();
- conn.close();
- }
- /**
- * 删除表
- */
- @Test
- public void testDropTable() throws Exception{
- Admin admin = conn.getAdmin();
- //停用表
- admin.disableTable(TableName.valueOf("user_info"));
- //删除表
- admin.deleteTable(TableName.valueOf("user_info"));
- admin.close();
- conn.close();
- }
- /**
- * 修改表定义--添加一个列族
- */
- @Test
- public void testAlterTable() throws Exception{
- Admin admin = conn.getAdmin();
- //取出旧的表定义信息
- HTableDescriptor tableDescriptor = admin.getTableDescriptor(TableName.valueOf("user_info"));
- //新构造一个列族定义
- HColumnDescriptor hColumnDescriptor = new HColumnDescriptor("other_info");
- hColumnDescriptor.setBloomFilterType(BloomType.ROWCOL);//设置该列族的布隆过滤器类型
- //将列族定义添加到表定义对象中
- tableDescriptor.addFamily(hColumnDescriptor);
- //将修改过的表定义交给admin去提交
- admin.modifyTable(TableName.valueOf("user_info"), tableDescriptor);
- admin.close();
- conn.close();
- }
- }
示例如下:
- package com.initialize;
- import org.apache.hadoop.conf.Configuration;
- import org.apache.hadoop.hbase.Cell;
- import org.apache.hadoop.hbase.CellScanner;
- import org.apache.hadoop.hbase.HBaseConfiguration;
- import org.apache.hadoop.hbase.TableName;
- import org.apache.hadoop.hbase.client.*;
- import org.apache.hadoop.hbase.util.Bytes;
- import org.junit.Before;
- import org.junit.Test;
- import java.io.IOException;
- import java.util.ArrayList;
- import java.util.Iterator;
- public class HbaseClientDML {
- Connection conn = null;
- @Before
- public void getConn() throws IOException {
- //构建一个连接对象
- Configuration conf = HBaseConfiguration.create();//会自动加载hbase-site.xml
- conf.set("hbase.zookeeper.quorum","n1:2181,n2:2181,n3:2181");
- conn = ConnectionFactory.createConnection(conf);
- }
- /**
- * 增,改:put来覆盖
- */
- @Test
- public void testPut() throws Exception{
- //获取一个操作指定表的table对象,进行DML操作
- Table table = conn.getTable(TableName.valueOf("user_info"));
- //构造要插入的数据为一个Put类型(一个put对象只能对应一个rowkey)的对象
- Put put = new Put(Bytes.toBytes("001"));
- put.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"),Bytes.toBytes("小羽"));
- put.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("age"), Bytes.toBytes("18"));
- put.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("addr"), Bytes.toBytes("成都"));
- Put put2 = new Put(Bytes.toBytes("002"));
- put2.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"), Bytes.toBytes("小娜"));
- put2.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("age"), Bytes.toBytes("17"));
- put2.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("addr"), Bytes.toBytes("成都"));
- ArrayList<Put> puts = new ArrayList<>();
- puts.add(put);
- puts.add(put2);
- //插进去
- table.put(puts);
- table.close();
- conn.close();
- }
- /***
- * 循环插入大量数据
- */
- @Test
- public void testManyPuts() throws Exception{
- Table table = conn.getTable(TableName.valueOf("user_info"));
- ArrayList<Put> puts = new ArrayList<>();
- for(int i=0;i<10000;i++){
- Put put = new Put(Bytes.toBytes(""+i));
- put.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("usernaem"), Bytes.toBytes("小羽" +i));
- put.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("age"), Bytes.toBytes((18+i) + ""));
- put.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("addr"), Bytes.toBytes("成都"));
- puts.add(put);
- }
- table.put(puts);
- }
- /**
- * 删
- */
- @Test
- public void testDelete() throws Exception{
- Table table = conn.getTable(TableName.valueOf("user_info"));
- //构造一个对象封装要删除的数据信息
- Delete delete1 = new Delete(Bytes.toBytes("001"));
- Delete delete2 = new Delete(Bytes.toBytes("002"));
- delete2.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("addr"));
- ArrayList<Delete> dels = new ArrayList<>();
- dels.add(delete1);
- dels.add(delete2);
- table.delete(dels);
- table.close();
- conn.close();
- }
- /**
- * 查
- */
- @Test
- public void testGet() throws Exception{
- Table table = conn.getTable(TableName.valueOf("user_info"));
- Get get = new Get("002".getBytes());
- Result result = table.get(get);
- //从结果中取用户指定的某个key和value的值
- byte[] value = result.getValue("base_info".getBytes(), "age".getBytes());
- System.out.println(new String(value));
- System.out.println("======================");
- //遍历整行结果中的所有kv单元格
- CellScanner cellScanner = result.cellScanner();
- while(cellScanner.advance()){
- Cell cell = cellScanner.current();
- byte[] rowArray = cell.getRowArray();//本kv所属行键的字节数组
- byte[] familyArray = cell.getFamilyArray();//列族名的字节数组
- byte[] qualifierArray = cell.getQualifierArray();//列名的字节数组
- byte[] valueArray = cell.getValueArray();//value字节数组
- System.out.println("行键:" + new String(rowArray, cell.getRowOffset(), cell.getRowLength()));
- System.out.println("列族名:" + new String(familyArray, cell.getFamilyOffset(), cell.getFamilyLength()));
- System.out.println("列名:" + new String(qualifierArray, cell.getQualifierOffset(), cell.getQualifierLength()));
- System.out.println("value:" + new String(valueArray, cell.getValueOffset(), cell.getValueLength()));
- }
- table.close();
- conn.close();
- }
- /**
- * 按行键范围查询数据
- */
- @Test
- public void testScan() throws Exception{
- Table table = conn.getTable(TableName.valueOf("user_info"));
- //包含起始行键,不包含结束行键,但是如果真的是想要查询出末尾的那个行键,可以在尾行键上拼接一个不可见的字符(\000)
- Scan scan = new Scan("10".getBytes(), "10000\001".getBytes());
- ResultScanner scanner =table.getScanner(scan);
- Iterator<Result> iterator = scanner.iterator();
- while(iterator.hasNext()){
- Result result =iterator.next();
- //遍历整行结果中的所有kv单元格
- CellScanner cellScanner = result.cellScanner();
- while(cellScanner.advance()){
- Cell cell = cellScanner.current();
- byte[] rowArray = cell.getRowArray();//本kv所属行键的字节数组
- byte[] familyArray = cell.getFamilyArray();//列族名的字节数组
- byte[] qualifierArray = cell.getQualifierArray();//列明的字节数组
- byte[] valueArray = cell.getValueArray();//value字节数组
- System.out.println("行键:" + new String(rowArray, cell.getRowOffset(), cell.getRowLength()));
- System.out.println("列族名:" + new String(familyArray, cell.getFamilyOffset(), cell.getFamilyLength()));
- System.out.println("列名:" + new String(qualifierArray, cell.getQualifierOffset(), cell.getQualifierLength()));
- System.out.println("value:" + new String(valueArray, cell.getValueOffset(), cell.getValueLength()));
- }
- System.out.println("----------------------");
- }
- }
- @Test
- public void test(){
- String a = "000";
- String b = "000\0";
- System.out.println(a);
- System.out.println(b);
- byte[] bytes = a.getBytes();
- byte[] bytes2 = b.getBytes();
- System.out.println("");
- }
- }
HBase 应用场景
对象存储系统
HBase MOB(Medium Object Storage),中等对象存储是 hbase-2.0.0 版本引入的新特性,用于解决 hbase 存储中等文件(0.1m~10m)性能差的问题。这个特性适合将图片、文档、PDF、小视频存储到 Hbase 中。
OLAP 的存储
Kylin 的底层用的是 HBase 的存储,看中的是它的高并发和海量存储能力。kylin 构建 cube 的过程会产生大量的预聚合中间数据,数据膨胀率高,对数据库的存储能力有很高要求。
Phoenix 是构建在 HBase 上的一个 SQL 引擎,通过 phoenix 可以直接调用 JDBC 接口操作 Hbase,虽然有 upsert 操作,但是更多的是用在 OLAP 场景,缺点是非常不灵活。
时序型数据
openTsDB 应用,记录以及展示指标在各个时间点的数值,一般用于监控的场景,是 HBase 上层的一个应用。
用户画像系统
动态列,稀疏列的特性。用于描述用户特征的维度数是不定的且可能会动态增长的(比如爱好,性别,住址等),不是每个特征维度都会有数据。
消息/订单系统
强一致性,良好的读性能,hbase 可以保证强一致性。
feed 流系统存储
feed 流系统具有读多写少、数据模型简单、高并发、波峰波谷式访问、持久化可靠性存储、消息排序这些特点,比如说 HBase 的 rowKey 按字典序排序正好适用于这个场景。
Hbase 优化
预先分区
默认情况下,在创建 HBase 表的时候会自动创建一个 Region 分区,当导入数据的时候,所有的 HBase 客户端都向这一个 Region 写数据,直到这个 Region 足够大了才进行切分。一种可以加快批量写入速度的方法是通过预先创建一些空的 Regions,这样当数据写入 HBase 时,会按照 Region 分区情况,在集群内做数据的负载均衡。
Rowkey 优化
HBase 中 Rowkey 是按照字典序存储,因此,设计 Rowkey 时,要充分利用排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
此外,Rowkey 若是递增的生成,建议不要使用正序直接写入 Rowkey,而是采用 reverse 的方式反转 Rowkey,使得 Rowkey 大致均衡分布,这样设计有个好处是能将 RegionServer 的负载均衡,否则容易产生所有新数据都在一个 RegionServer 上堆积的现象,这一点还可以结合 table 的预切分一起设计。
减少列族数量
不要在一张表里定义太多的 ColumnFamily。目前 Hbase 并不能很好的处理超过 2~3 个 ColumnFamily 的表。因为某个 ColumnFamily 在 flush 的时候,它邻近的 ColumnFamily 也会因关联效应被触发 flush,最终导致系统产生更多的 I/O。
缓存策略
创建表的时候,可以通过 HColumnDescriptor.setInMemory(true) 将表放到 RegionServer 的缓存中,保证在读取的时候被 cache 命中。
设置存储生命期
创建表的时候,可以通过 HColumnDescriptor.setTimeToLive(int timeToLive) 设置表中数据的存储生命期,过期数据将自动被删除。
硬盘配置
每台 RegionServer 管理 10~1000 个 Regions,每个 Region 在 1~2G,则每台 Server 最少要 10G,最大要1000*2G=2TB,考虑 3 备份,则要 6TB。方案一是用 3 块 2TB 硬盘,二是用 12 块 500G 硬盘,带宽足够时,后者能提供更大的吞吐率,更细粒度的冗余备份,更快速的单盘故障恢复。
分配合适的内存给 RegionServer 服务
在不影响其他服务的情况下,越大越好。例如在 HBase 的 conf 目录下的 hbase-env.sh 的最后添加 export HBASE_REGIONSERVER_OPTS="-Xmx16000m$HBASE_REGIONSERVER_OPTS”,其中 16000m 为分配给 RegionServer 的内存大小。
写数据的备份数
备份数与读性能成正比,与写性能成反比,且备份数影响高可用性。有两种配置方式,一种是将 hdfs-site.xml拷贝到 hbase 的 conf 目录下,然后在其中添加或修改配置项 dfs.replication 的值为要设置的备份数,这种修改对所有的 HBase 用户表都生效,另外一种方式,是改写 HBase 代码,让 HBase 支持针对列族设置备份数,在创建表时,设置列族备份数,默认为 3,此种备份数只对设置的列族生效。
WAL(预写日志)
可设置开关,表示 HBase 在写数据前用不用先写日志,默认是打开,关掉会提高性能,但是如果系统出现故障(负责插入的 RegionServer 挂掉),数据可能会丢失。配置 WAL 在调用 JavaAPI 写入时,设置 Put 实例的 WAL,调用 Put.setWriteToWAL(boolean)。
批量写
HBase 的 Put 支持单条插入,也支持批量插入,一般来说批量写更快,节省来回的网络开销。在客户端调用 JavaAPI 时,先将批量的 Put 放入一个 Put 列表,然后调用 HTable 的 Put(Put 列表) 函数来批量写。
最后
在理解 HBase 时,可以发现 HBase 的设计其实和 Elasticsearch 十分相似,如 HBase 的 Flush&Compact 机制等设计与 Elasticsearch 如出一辙,因此理解起来比较顺利。
从本质上来说,HBase 的定位是分布式存储系统,Elasticsearch 是分布式搜索引擎,两者并不等同,但两者是互补的。HBase 的搜索能力有限,只支持基于 RowKey 的索引,其它二级索引等高级特性需要自己开发。因此,有一些案例是结合 HBase 和 Elasticsearch 实现存储 + 搜索的能力。通过 HBase 弥补 Elasticsearch 存储能力的不足,通过 Elasticsearch 弥补 HBase 搜索能力的不足。
其实,不只是 HBase 和 Elasticsearch。任何一种分布式框架或系统,它们都有一定的共性,不同之处在于各自的关注点不同。小羽的感受是,在学习分布式中间件时,应先弄清其核心关注点,再对比其它中间件,提取共性和特性,进一步加深理解。