HBase 是什么
最近学习了 HBase, 正常来说写这篇文章, 应该从 DB 有什么缺点, HBase 如何弥补 DB 的缺点开始讲会更有体感, 但是本文这些暂时不讲, 只讲 HBase, 把 HBase 相关原理和使用讲清楚, 后面有一篇文章会专门讲 DB 与 NoSql 各自的优缺点以及使用场景.
HBase 是谷歌 Bigtable 的开源版本, 2006 年谷歌发布《Bigtable:A Distributed Storage System For Structured Data》论文之后, Powerset 公司就宣布 HBase 在 Hadoop 项目中成立, 作为子项目存在. 后来, 在 2010 年左右逐渐成为 Apache 旗下的一个顶级项目, 因此 HBase 名称的由来就是由于其作为 Hadoop Database 存在的, 用于存储非结构化, 半结构化的数据.
下图展示了 HBase 在 Hadoop 生态中的位置:
可以看到 HBase 建立在 HDFS 上, HBase 内部管理的文件全部都是存储在 HDFS 中, 同时 MapReduce 这个计算框架在 HBase 之上又提供了高性能的计算能力来处理海量数据.
HBase 的特点与不足
HBase 的基本特点概括大致如下:
海量数据存储 (PB) 级别, 在 PB 级别数据以及采用廉价 PC 存储的情况下, 数据能在几十到百毫秒内返回数据
高可用, WAL + Replication 机制保证集群异常不会导致写入数据丢失与数据损坏, 且 HBase 底层使用 HDFS,HDFS 本身也有备份
数据写入性能强劲
列式存储, 和传统数据库行式存储有本质的区别, 这个在之后 HBase 存储原理的时候详细解读
半结构化或非结构化数据存储
存储稀松灵活, 列数据为空的情况下不占据存储空间
同一份数据, 可存储多版本号数据, 方便历史数据回溯
行级别事务, 可以保证行级别数据的 ACID 特性
扩容方便, 无需数据迁移, 及扩即用
当然事事不是完美的, HBase 也存在着以下两个最大的不足:
无法做到条件查询, 这是最大的问题, 假如你的代码中存在多个查询条件, 且每次使用哪个 / 哪组查询条件不确定, 那么使用 HBase 是不合适的, 除非数据冗余, 设计多份 RowKey
做不了分页, 数据总记录数几乎无法统计, 因为 HBase 本身提供的表行数统计功能是一个 MapReduce 任务, 极为耗时, 既然拿不到总记录数, 分页总署也没法确定, 自然分页也无法做了
总的来说, 对于 HBase 需要了解以上的一些个性应该大致上就可以了, 根据 HBase 的特点与不足, 在合适的场景下选择使用 HBase, 接下来针对 HBase 的一些知识点逐一解读.
HBase 的基本架构
下图是 HBase 的基本架构:
从图上可以看到, HBase 中包含的一些组件如下:
Client---- 包含访问 HBase 的接口
Zookeeper---- 通过选举保证任何时候集群中只有一个 HMaster,HMaster 与 Region Server 启动时向注册, 存储所有 Region 的寻址入口, 实时监控 Region Server 的上下线信息并实时通知给 HMaster, 存储 HBase 的 Schema 与 Table 原数据
HMaster---- 为 Region Server 分配 Region, 负责 Region Server 的负载均衡, 发现失效的 Region Server 并重新分配其上的 Region, 管理用户对 Table 的增删改查
Region Server---- 维护 Region 并处理对 Region 的 IO 请求, 切分在运行过程中变得过大的 Region
其中, Region 是分布式存储和负载均衡中的最小单元, 不过并不是存储的最小单元. Region 由一个或者多个 Store 组成, 每个 Store 保存一个列簇; 每个 Store 又由一个 memStore 和 0~N 个 StoreFile 组成, StoreFile 包含 HFile,StoreFile 只是对 HFile 做了轻量级封装, 底层就是 HFile.
介于上图元素有点多, 我这边画了一张图, 把 HBase 架构中涉及的元素的关系理了一下:
HBase 的基本概念
接着看一下 HBase 的一些基本概念, HBase 是以 Table(表)组织数据的, 一个 Table 中有着以下的一些元素:
RowKey(行键)---- 即关系型数据库中的主键, 它是唯一的, 在 HBase 中这个主键可以是任意的字符串, 最大长度为 64K, 在内部存储中会被存储为字节数组, HBase 表中的数据是按照 RowKey 的字典序排列的. 例如 1,2,3,4,5,10, 按照自然数的顺序是这样的, 但是在 HBase 中 1 后面跟的是 10 而不是 2, 因此在设计 RowKey 的时候一定要充分利用字典序这个特性, 将一下经常读取的行存储到一起或者靠近, 减少 Scan 耗时, 提高读取的效率
Column Family(列族)---- 表 Schema 的一部分, HBase 表中的每个列都归属于某个列族, 即列族是由一系列的列组成的, 必须在创建表的时候就指定好. 列明都以列族作为前缀, 例如 courses:history,courses:math 都属于 courses 这个列族. 列族不是越多越好, 过多的列族会导致 io 增多及分裂时数据不均匀, 官方推荐列族数量为 1~3 个. 列族不仅能帮助开发者构建数据的语义边界, 还能有助于开发者设置某些特性, 例如可以指定某个列族内的数据压缩形式. 访问控制, 磁盘和内存怒的使用统计都是在列族层面进行的,
Column(列)---- 一般从属于某个列族, 列的数量一般没有强限制, 一个列族中可以有数百万列且这些列都可以动态添加
Version Number(版本号)----HBase 中每一列的值或者说每个单元格的值都是具有版本号的, 默认使用系统当前时间, 精确到毫秒, 也可以用户显式地设置. 每个单元格中, 不同版本的数据按照时间倒序排序, 即最新的数据排在最前面. 另外, 为了避免数据存在过多版本造成的管理 (存储 + 索引) 负担, HBase 提供了两种数据版本回收的方式, 一是保存数据的最后 n 个版本, 二是保存最近一段时间内的版本, 用户可以针对每个列族进行设置
Cell(单元格)---- 一个单元格就是由 RowKey,Column Family:Column,Version Number 唯一确定的, Cell 中的数据是没有类型的, 全部都是字节码
另外一个概念就是, 访问 HBase Table 中的行, 只有三种方式:
通过单个 Row Key 访问
通过 Row Key 的 range
全表扫描
这部分介绍的 Table,RowKey,Column Family,Column 等都属于逻辑概念, 而上部分中的 Region Server,Region,Store 等都属于物理概念, 下图展示了逻辑概念与物理概念之间的关系:
即: table 和 region 是一对多的关系, 因为 table 的数据可能被打在多个 region 中; region 和 columnFamily 是一对多的关系, 一个 store 对应一个 columnFamily, 一个 region 可能对应多个 store.
HBase 的逻辑表视图与物理表视图
接着看一下 HBase 中的表逻辑视图与物理视图. 首先是逻辑表视图:
看到这里定义了 2 个列族, 一个 Personal Info, 一个 Family Info, 对应到数据库中, 相当于把两张表合并到一个一起.
从逻辑视图看, 上图由 ZhangSan,LiSi 两行组成, 但是在实际物理存储上却不是按照这种方式进行的存储:
看到主要是有两点差别:
一行被拆开了, 按照列进行存储
空列不会被存储, 例如 LiSi 在 Peronal Info 中没有 Provice 与 Phone, 在 Family Info 中没有 Brother
HBase 的增删改查
光说不练假把式, 不能光讲理论, 代码也是要有的, 为了方便起见, 我用的是阿里云 HBase, 和 HBase 一样, 只是省去了运维成本. 当然虽然本人是内部员工, 但是工作之外的学习是不会占用公司资源的 ^_^ 悄悄告诉大家, 阿里云 HBase 有个福利, 第一个月免费试用, 想同样玩一下 HBase 的可以去阿里云搞一个.
首先添加一下 pom 依赖, 用阿里云指定的 HBase, 使用上和原生的 HBase API 一模一样:
- <dependency>
- <groupId>com.aliyun.hbase</groupId>
- <artifactId>alihbase-client</artifactId>
- <version>2.0.3</version>
- </dependency>
- <dependency>
- <groupId>jdk.tools</groupId>
- <artifactId>jdk.tools</artifactId>
- <version>1.8</version>
- <scope>system</scope>
- <systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
- </dependency>
注意一下第二个 dependency,jdk.tools 不添加 pom 文件可能会报错 "Missing artifact jdk.tools:jdk.tools:jar:1.8", 错误原因是 tools.jar 包是 JDK 自带的, pom.xml 中以来的包隐式依赖 tools.jar 包, 而 tools.jar 并未在库中, 因此需要将 tools.jar 包添加到 jdk 库中.
首先写个 HBaseUtil, 用单例模式来写, 好久没写了, 顺便练习一下:
- /**
- * 五月的仓颉 https://www.cnblogs.com/xrq730/p/11134806.html
- */
- public class HBaseUtil {
- private static HBaseUtil hBaseUtil;
- private Configuration config = null;
- private Connection connection = null;
- private Map<String, Table> tableMap = new HashMap<String, Table>();
- private HBaseUtil() {
- }
- public static HBaseUtil getInstance() {
- if (hBaseUtil == null) {
- synchronized (HBaseUtil.class) {
- if (hBaseUtil == null) {
- hBaseUtil = new HBaseUtil();
- }
- }
- }
- return hBaseUtil;
- }
- /**
- * 初始化 Configuration 与 Connection
- */
- public void init(String zkAddress) {
- config = HBaseConfiguration.create();
- config.set(HConstants.ZOOKEEPER_QUORUM, zkAddress);
- try {
- connection = ConnectionFactory.createConnection(config);
- } catch (IOException e) {
- e.printStackTrace();
- System.exit(0);
- }
- }
- /**
- * 创建 table
- */
- public void createTable(String tableName, byte[]... columnFamilies) {
- // HBase 创建表的时候必须创建指定列族
- if (columnFamilies == null || columnFamilies.length == 0) {
- return ;
- }
- TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));
- for (byte[] columnFamily : columnFamilies) {
- tableDescriptorBuilder.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(columnFamily).build());
- }
- try {
- Admin admin = connection.getAdmin();
- admin.createTable(tableDescriptorBuilder.build());
- // 这个 Table 连接存入内存中
- tableMap.put(tableName, connection.getTable(TableName.valueOf(tableName)));
- } catch (Exception e) {
- e.printStackTrace();
- System.exit(0);
- }
- }
- public Table getTable(String tableName) {
- Table table = tableMap.get(tableName);
- if (table != null) {
- return table;
- }
- try {
- table = connection.getTable(TableName.valueOf(tableName));
- if (table != null) {
- // table 对象存入内存
- tableMap.put(tableName, table);
- }
- return table;
- } catch (IOException e) {
- e.printStackTrace();
- return null;
- }
- }
- }
注意, HBase 中的数据一切皆二进制, 因此从上面代码到后面代码, 字符串全部都转换成了二进制.
接着定义一个 BaseHBaseUtilTest 类, 把一些基本的定义放在里面, 保持主测试类清晰:
- /**
- * 五月的仓颉 https://www.cnblogs.com/xrq730/p/11134806.html
- */
- public class BaseHBaseUtilTest {
- protected static final String TABLE_NAME = "student";
- protected static final byte[] COLUMN_FAMILY_PERSONAL_INFO = "personalInfo".getBytes();
- protected static final byte[] COLUMN_FAMILY_FAMILY_INFO = "familyInfo".getBytes();
- protected static final byte[] COLUMN_NAME = "name".getBytes();
- protected static final byte[] COLUMN_AGE = "age".getBytes();
- protected static final byte[] COLUMN_PHONE = "phone".getBytes();
- protected static final byte[] COLUMN_FATHER = "father".getBytes();
- protected static final byte[] COLUMN_MOTHER = "mother".getBytes();
- protected HBaseUtil hBaseUtil;
- }
第一件事情, 创建 Table, 注意前面说的, HBase 必须 Table 和列族一起创建:
- /**
- * 五月的仓颉 https://www.cnblogs.com/xrq730/p/11134806.html
- */
- public class HBaseUtilTest extends BaseHBaseUtilTest {
- @Before
- public void init() {
- hBaseUtil = HBaseUtil.getInstance();
- hBaseUtil.init("xxx");
- }
- /**
- * 创建表
- */
- @Test
- public void testCreateTable() {
- hBaseUtil.createTable(TABLE_NAME, COLUMN_FAMILY_PERSONAL_INFO, COLUMN_FAMILY_FAMILY_INFO);
- }
- }
我自己申请的 HBase,zk 地址就不给大家看啦, 如果同样申请了的, 替换一下就好了. testCreateTable 方法运行一下, 就创建好了 student 表. 接着利用 put 创建四条数据, 多创建几条, 等下 scan 可以测试:
- /**
- * 添加数据
- */
- @Test
- public void testPut() throws Exception {
- Table table = hBaseUtil.getTable(TABLE_NAME);
- // 用户 1, 用户 id:12345
- Put put1 = new Put("12345".getBytes());
- put1.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "Lucy".getBytes());
- put1.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "18".getBytes());
- put1.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE, "13511112222".getBytes());
- put1.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER, "LucyFather".getBytes());
- put1.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "LucyMother".getBytes());
- // 用户 2, 用户 id:12346
- Put put2 = new Put("12346".getBytes());
- put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "Lily".getBytes());
- put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "19".getBytes());
- put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE, "13522223333".getBytes());
- put2.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER, "LilyFather".getBytes());
- put2.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "LilyMother".getBytes());
- // 用户 3, 用户 id:12347
- Put put3 = new Put("12347".getBytes());
- put3.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "James".getBytes());
- put3.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "22".getBytes());
- put3.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER, "JamesFather".getBytes());
- put3.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "JamesMother".getBytes());
- // 用户 4, 用户 id:12447
- Put put4 = new Put("12447".getBytes());
- put4.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "Micheal".getBytes());
- put4.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "22".getBytes());
- put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE, "13533334444".getBytes());
- put4.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "MichealMother".getBytes());
- table.put(Lists.newArrayList(put1, put2, put3, put4));
- }
同样的, 运行一下 testPut 方法, 四条数据就创建完毕了. 注意为了提升处理效率, HBase 的 get,put 这些 API 都提供的批量处理方式, 这样一次提交可以提交多条数据, 发起一次请求即可, 不用发起请求.
接着看一下利用 Get API 查询数据:
- /**
- * 获取数据
- */
- @Test
- public void testGet() throws Exception {
- Table table = hBaseUtil.getTable(TABLE_NAME);
- // get1, 拿到全部数据
- Get get1 = new Get("12345".getBytes());
- // get2, 只拿 personalInfo 数据
- Get get2 = new Get("12346".getBytes());
- get2.addFamily(COLUMN_FAMILY_PERSONAL_INFO);
- Result[] results = table.get(Lists.newArrayList(get1, get2));
- if (results == null || results.length == 0) {
- return ;
- }
- for (Result result : results) {
- printResult(result);
- }
- }
- private void printResult(Result result) {
- System.out.println("==================== 分隔符 ====================");
- printBytes(result.getValue(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME));
- printBytes(result.getValue(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE));
- printBytes(result.getValue(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE));
- printBytes(result.getValue(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER));
- printBytes(result.getValue(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER));
- }
- private void printBytes(byte[] bytes) {
- if (bytes != null && bytes.length != 0) {
- System.out.println(new String(bytes));
- }
- }
HBase 查询数据比较灵活的是, 可以查询 RowKey 下对应的所有数据, 可以按照 RowKey-Column Family 的维度查询数据, 可以按照 RowKey-Column Family-Column 的维度查询数据, 也可以按照 RowKey-Column Family-Column-Timestamp 的维度查询数据, 可以查询 Timestamp 区间内的数据, 也可以查询 RowKey-Column Family-Column 下所有 Timestamp 数据. 上面的代码执行结果为:
==================== 分隔符 ====================
- Lucy
- 18
- 13511112222
- LucyFather
- LucyMother
==================== 分隔符 ====================
- Lily
- 19
- 13533334444
和我们的预期相符, 即 "12345" 这个 RowKey 查询出了所有数据,"12346" 这个 RowKey 只查了 personalInfo 这个列族的数据.
最后这一部分我们看一下更新, 更新的 API 和新增的 API 都是一样的, 都是 Put:
- @Test
- public void testUpdate() throws Exception {
- Table table = hBaseUtil.getTable(TABLE_NAME);
- // 用户 1, 用户 id:12345
- Put put = new Put("12346".getBytes());
- put.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, 1, "22".getBytes());
- table.put(put);
- }
Get 看一下执行 12346 这条数据的值:
- Lily
- 19
- 13533334444
看到 12346 对应的数据, 原本 Age 是 19, 更新到 22, 依然是 19, 这就是一个值得注意的点了. HBase 的更新其实是往 Table 里面新增一条记录, 按照 Timestamp 进行排序, 最新的数据在前面, 每次 Get 的时候将第一条数据取出来. 在这里我们指定的 Timestamp=1, 这个值落后于先前插入的 Timestamp, 自然就排在后面, 因此读取出来的 Age 依然是原值 19, 这个细节特别注意一下.
HBase 的 Scan
感觉前面篇幅有点大, 所以这里专门抽一个篇幅出来写一下 Scan,Scan 是 HBase 扫描数据的方式.
首先可以看一下最基本的 Scan:
- /**
- * 扫描
- */
- @Test
- public void testScan() throws Exception {
- Table table = hBaseUtil.getTable(TABLE_NAME);
- Scan scan = new Scan().withStartRow("12345".getBytes(), true).withStopRow("12347".getBytes(), true);
- ResultScanner rs = table.getScanner(scan);
- if (rs != null) {
- for (Result result : rs) {
- printResult(result);
- }
- }
- }
执行结果为:
==================== 分隔符 ====================
- Lucy
- 19
- 13511112222
- LucyFather
- LucyMother
==================== 分隔符 ====================
- Lily
- 19
- 13533334444
- LilyFather
- LilyMother
==================== 分隔符 ====================
- James
- 22
- JamesFather
- JamesMother
表示查询 12345~12347 这个范围内的所有 RowKey,withStartRow 的第二个参数 true 表示包含, 如果为 false 那么 12345 这个 RowKey 就查不出来了.
进阶的, HBase 为我们提供了带过滤器的 Scan, 一共有十来种, 我这边只演示两种以及组合的情况, 其他的查询一下 HBase API 文档即可, 2.1 版本的 API 文档地址为 http://hbase.apache.org/2.1/apidocs/index.html. 演示代码如下:
- @Test
- public void testScanFilter() throws Exception {
- Table table = hBaseUtil.getTable(TABLE_NAME);
- System.out.println("********************RowFilter 测试 ********************");
- Scan scan0 = new Scan().withStartRow("12345".getBytes(), true);
- scan0.setFilter(new RowFilter(CompareOperator.EQUAL, new BinaryComparator("12346".getBytes())));
- ResultScanner rs0 = table.getScanner(scan0);
- printResultScanner(rs0);
- System.out.println("********************PrefixFilter 测试 ********************");
- Scan scan1 = new Scan().withStartRow("12345".getBytes(), true);
- scan1.setFilter(new PrefixFilter("124".getBytes()));
- ResultScanner rs1 = table.getScanner(scan1);
- printResultScanner(rs1);
- System.out.println("******************** 两种 Filter 同时满足测试 ********************");
- Scan scan2 = new Scan().withStartRow("12345".getBytes(), true);
- Filter filter0 = new RowFilter(CompareOperator.EQUAL, new BinaryComparator("12447".getBytes()));
- Filter filter1 = new PrefixFilter("124".getBytes());
- FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL, filter0, filter1);
- scan2.setFilter(filterList);
- ResultScanner rs2 = table.getScanner(scan2);
- printResultScanner(rs2);
- }
执行结果为:
********************RowFilter 测试 ********************
==================== 分隔符 ====================
- Lily
- 19
- 13533334444
- LilyFather
- LilyMother
********************PrefixFilter 测试 ********************
==================== 分隔符 ====================
- Micheal
- 22
- MichealMother
******************** 两种 Filter 同时满足测试 ********************
==================== 分隔符 ====================
- Micheal
- 22
- MichealMother
总的来说, HBase 本质上是 KV 型 NoSql, 根据 Key 查询 Value 是最高效的, Scan 这个 API 还是慎用, 范围里面的数据量小倒无所谓, 一旦 RowKey 设计不合理, StartRow 和 EndRow 没有指定好, 可能会造成大范围的扫描, 降低 HBase 整体能力.
HBase 和 KV 型缓存的区别
看了上面的代码演示, 不知道大家有没有和我一开始有一样的疑问: HBase 看上去也是 K-V 形式的, 那么它和支持 KV 型数据的缓存 (例如 Redis,MemCache,Tair) 有什么区别?
我用一张表格总结一下二者的区别:
总的来说, 同样作为数据库的 NoSql 替代方案, HBase 更加适合用于海量数据的持久化场景, KV 型缓存更加适合用于对数据的高性能读写上.
HBase 的 Region 分裂及会导致的热点问题
经典问题, 首先看一下什么是 Region 分裂, 只把 Region 分裂讲清楚, 不讲具体 Region 分裂的实现方式, 理由也很简单, Region 分裂细节学得再清楚, 对工作中的帮助也不大, 没必要太过于追根究底.
Region 分裂是 HBase 能够拥有良好扩张性的最重要因素之一, 也必然是所有分布式系统追求无限扩展性的一副良药. 通过前面的部分我们知道 HBase 的数据以行为单位存储在 HBase 表中, HBase 表按照多行被分割为多个 Region, 这个 Region 分布在 HBase 集群中, 并且由 Region Server 进程负责讲这些 Region 提供给 Client 访问. 一个 Region 中, RowKey 是一个连续的范围, 也就是说表中的记录在 Region 中是按照 startKey 到 endKey 的范围为 RowKey 进行排序存储的. 通常一个表由多个 Region 构成, 这些 Region 分布在多个 Region Server 上, 也就是说, Region 是在 Region Server 中插入和查询数据时负载均衡的物理机制. 一张 HBase 表在刚刚创建的时候默认只有一个 Region, 所以关于这张表的请求都被路由到同一个 Region Server, 无论集群中有多少 Region Server, 而一旦某个 Region 的大小达到一定值, 就会自动分裂为两个 Region, 这也就是为什么 HBase 表在刚刚创建的阶段不能充分利用整个集群吞吐量的原因.
在 HBase 管理界面可以查看每个 Region,startKey 与 endKey 的范围, 例如(图片来自网络):
这里特别注意一个点, RowKey 是按照 Key 的字符自然顺序进行排序的, 因此 RowKey=9 的 Key, 会落在最后一个 Region Server 中而不是第一个 Region Server 中.
那么什么是热点问题应该也很好理解了:
虽然 HBase 的单机读写性能强劲, 但是当集群中成千上万的请求 RowKey 都落在 aaaaa-ddddd 之间, 那么这成千上万请求最终落到 Region Server1 这台服务器上, 一旦超出服务器自身承受能力, 那么必然导致服务器不可用甚至宕机. 因此我们说设计 RowKey 的时候千万把时间戳或者 id 自增的方式作为 RowKey 方案就是这个道理, 时间戳或者 id 自增的方式, 虽然最终可以让 RowKey 落到不同的 Region 中, 但是在当下或者当下往后的一段时间内, RowKey 一定是会落到同一个 Region 中的, 数据热点问题将严重影响 HBase 集群能力.
解决热点问题通常有两个方案, 最初级的方案是设置预分区, 即在 Table 创建的时候就先设置几个 Region, 为每个 Region 划分不同的 startKey 与 endKey, 但这么做有以下两个缺点:
高度依赖 RowKey, 必须事先知道插入数据的 RowKey 的分布
即使事先知道插入数据的 RowKey 分布, 但是如果数据分布不均匀或者存在热点行, 依然无法均匀分摊负载
但是无论如何, 设置预分区依然是一种解决热点问题的方案.
第二个解决方案是一劳永逸的解决方案也是使用 HBase 最核心的一个点: 合理设计 RowKey. 即让 RowKey 均匀分布在 Region 中, 大致有以下几个方案可供参考:
倒序. 例如手机号码 135ABCD,135EFGH,135IJKL 这种, 前缀没有区分度, 非常容易落到相同的 Region 中, 此时做倒序即 DCBA531,HGFE531,LKJI531, 将有区分度的部分放在前面, 就非常容易将数据散落在不同的 Region 中
原数据加密, 例如做 MD5, 因为 MD5 的随机性是非常强的, 因此做了 MD5 后, 数据将会非常分散
加随机前缀, 比如 ASCII 码中随机选 5 位作为数据前缀, 同样可以达到分散 RowKey 的效果, 但是缺点是必须记住每个原数据对应的前缀
无论如何, 还是那句话, 合理设计 RowKey 是 HBase 使用的核心.
WAL 机制
最后讲一下前面提到的 WAL 机制, WAL 的全称为 Write Ahead Log, 它是 HBase 的 RegionServer 在处理数据插入和删除的过程中用来记录操作内容的一种日志, 是用来做灾难恢复的.
其实 WAL 并不是什么新鲜思想, 在分布式领域很常见:
MySQL 有 binlog, 记录每一次数据变更
Redis 有 aof, 在开启 aof 的情况下, 每隔短暂时间, 将这段时间产生的操作记录文件
其核心都是, 变更数据前先写磁盘日志文件, 在系统发生异常的时候, 重放日志文件对数据进行恢复, HBase 的 WAL 机制也是一样的思想, 数据变更步骤为:
首先从之前的图上可以看到有 HLog,HLog 是实现 WAL 的类, 一个 RegionServer 对应一个 HLog, 多个 Region 共享一个 HLog, 不过从 HBase1.0 版本开始可以定义多个 HLog 以提高吞吐量
客户端的一次数据提交先写 HLog, 这个是告知客户端数据提交成功的前提
HLog 写入成功后写入 MemStore
当 MemStore 的值达到一定程度后, flush 到 hdfs, 形成一个一个的 StoreFile(HFile)
flush 过后, HLog 中对应的数据就没用了, 删除
因为有了 HLog, 即使在 MemStore 中的数据还没有 flush 到 hdfs 的时候系统发生了宕机或者重启, 数据都不会出现丢失.
来源: https://www.cnblogs.com/xrq730/p/11134806.html