超详细梳理HBase核心知识点(上)--建议收藏

之前我的公众号名字叫做:''Java不睡觉'',原因就是当时看了一本书,名字是《HBase不睡觉书》。这本书正如其名字一样,是一本让人读起来根本不会发困的书,very奈斯。本文就是整理了这本书上的知识点而形成的文章,准备分为上下两篇文章系统梳理HBase核心知识点,如果你想了解HBase,那么这篇文章不会让你失望的,同时推荐阅读一下原书。让我们开始吧

前言

HBase 是一个开源的、面向列的非关系型分布式数据库,目前是Hadoop体系中非常关键的一部分。
在最初,HBase是基于谷歌的 BigTable 原型实现的,许多技术来自于Fay Chang在2006年所撰写的Google论文"BigTable"。与 BigTable基于Google文件系统(File System)一样,HBase则是基于HDFS(Hadoop的分布式文件系统)之上而开发的。

HBase 采用 Java 语言实现,在其内部实现了BigTable论文提到的一些压缩算法、内存操作和布隆过滤器等,这些能力使得HBase 在海量数据存储、高性能读写场景中得到了大量应用,如 Facebook 在 2010年11 月开始便一直选用 HBase来作为消息平台的存储层技术。
HBase 以 Apache License Version 2.0开源,这是一种对商业应用友好的协议,同时该项目当前也是Apache软件基金会的顶级项目之一。

有什么特性?

  • 基于列式存储模型,对于数据实现了高度压缩,节省存储成本
  • 采用 LSM 机制而不是B(+)树,这使得HBase非常适合海量数据实时写入的场景
  • 高可靠,一个数据会包含多个副本(默认是3副本),这得益于HDFS的复制能力,由RegionServer提供自动故障转移的功能
  • 高扩展,支持分片扩展能力(基于Region),可实现自动、数据均衡
  • 强一致性读写,数据的读写都针对主Region上进行,属于CP型的系统
  • 易操作,HBase提供了Java API、RestAPI/Thrift API等接口
  • 查询优化,采用Block Cache 和 布隆过滤器来支持海量数据的快速查找

一、整体架构

整个HBase 集群主要由 Zookeeper、HBase Master、HBase RegionServer和HDFS构成。HBase集群架构图如下:

其中 Master 节点是允许存在多个的,当多个 Master 节点共存时,只有一个 Master 是提供服务的,这种主备角色的"仲裁"由 ZooKeeper 实现。

RegionServer是直接负责存储数据的服务器,RegionServer保存的表数据直接存储在Hadoop的HDFS上。RegionServer非常依赖ZooKeeper服务。ZooKeeper管理了HBase中所有的RegionServer的信息,包括具体的数据段存放在哪个 RegionServer上。 客户端每次与HBase连接,其实都是先与ZooKeeper通信,查询出具体需要连接哪个RegionServer,然后再连接到RegionServer。

1.1 Region

Region就是一段数据的集合。HBase中的表一般拥有一个到多个Region。Region具有以下特性:

  • Region不能跨服务器,一个RegionServer上有一个或者多个 Region。
  • 数据量小的时候,一个Region足以存储所有数据;但是,当数据 量大的时候,HBase会拆分Region。
  • 当HBase在进行负载均衡的时候,也有可能会从一台 RegionServer上把Region移动到另一台RegionServer上。
  • Region是基于HDFS的,它的所有数据存取操作都是调用了HDFS的 客户端接口来实现的。

1.2 RegionServer

RegionServer就是存放Region的容器,直观上说就是服务器上的一 个服务。当客户端从ZooKeeper获取RegionServer的地址后,它会直接从 RegionServer获取数据。

1.3 Master

在1.2小节中提到过,客户端从 ZooKeeper获取了RegionServer的地址后,会直接从RegionServer获取数据。其实不光是获取数据,包括插入、删除等所有的数据操作都是直接操作RegionServer,而不需要经过Master。

不像Hadoop等其他分布式系统,在HBase中,Master更像是一个打杂的。Master只负责各种协调工作,比如建表、删表、 移动Region、合并等操作。它们的共性就是需要跨RegionServer,这些 操作由哪个RegionServer来执行都不合适,所以HBase就将这些操作放 到了Master上了。

这种结构的好处是大大降低了集群对Master的依赖。Master节点一般只有一个到两个,一旦宕机,如果集群对Master的依赖度很大,那么就会产生单点故障问题。在HBase中,即使Master宕机了,集群依然 可以正常地运行,依然可以存储和删除数据。

1.4 Zookeeper

Zookeeper 对于 HBase的作用是至关重要的。

  • Zookeeper 提供了 HBase Master 的高可用实现,并保证同一时刻有且仅有一个主 Master 可用。
  • Zookeeper 保存了 Region 和 Region Server 的关联信息(提供寻址入口),并保存了集群的元数据(Schema/Table)。
  • Zookeeper 实时监控Region server的上线和下线信息,并实时通知Master。

除了 HBase之外,有许多分布式大数据相关的开源框架,都依赖于 Zookeeper 实现 HA。

1.5 微观架构

HBase是一个分布式列式数据库,最基本的存储单位是列(column),一个列或者多个列形成一行(row)。在HBase中,这一行有三个列a、b、 c,下一个行也许是有4个列a、e、f、g。行跟行的列可以完全不一样,这个行的数据跟另外一个行的数据也可以存储在不同的机器上,甚至同一行内的列也可以存储在完全不同的机器上!

每个行(row)都拥有唯一的行键(row key)来标定这个行的唯一 性。每个列都有多个版本,多个版本的值存储在单元格(cell)中。

综上,HBase的存储结构可以表示成下图所示的结构:

二、常用Shell命令与API

2.1 常用Shell命令

①进入hbase命令行:

$HBASE_HOME/bin/hbase shell

②create命令建表
在hbase shell下执行:

create 'zhb_test', 'cf'

创建一个名为'zhb_test'的表,并且带有一个名为'cf'的列族。

补充一下列族的知识点:
前面提到过,HBase是一个分布式列式数据库。

HBase的表都是由列族(Column Family)组成的;
没有列族的表是没有意义的;
列并不是依附于表上,而是依附于列族上;如下图所示:

通过刚才的命令,我们现在建立的表有一个列族,叫cf,但是我们没有指定这个列族里面有什么列。向表TableA中插入数据时,你只是向HBase中插入了一个单元格(Cell),而这个单元格是由表:列族:行:列来定位的,而别的行有没有此列HBase并不知道。

HBase的所有数据属性都是定义在列族上的。同一个表的不同列族可以定义完全不同的两套属性,所以从这个意义上来说,列族更像是传统关系数据库中的表,而表本身反倒变成只是存放列族的空壳了。

③ list命令看到整个库中有哪些表

可以看到库里共有15个表,包括我们刚才创建的'zhb_test'

④ describe命令来查看表属性

输入命令alter 'zhb_test', 'cf2'再添加一个列族,然后再describe看一下:

可以看到现在describe输出的是两个元素,分别对应cf和cf2两 个列族。也印证了我们之前说的数据属性是存在于列族上的。

补充:
在执行alter命令之前,最好先停用(disable)这个表 。因为对列族的所有操作都会同步到所有拥有这个表的RegionServer上,你在执行命令的时候可以看到总共 有多少个RegionServer,当前执行了几个RegionServer。当有很多客户端都在连着的时候,直接新增一个列族对性能的影响较大。

⑤ put命令来插入数据

在HBase中,如果你的一行有10列,那存储一行的数据得写10行的 语句。这是因为HBase中行的每一个列都存储在不同的位置,你必须指 定你要存储在哪个单元格;而单元格需要根据表、行、列这几个维度来 定位。

执行命令:
put 'zhb_test', 'rowkey1', 'cf:name', 'zhb'

表示:
往'zhb_test'表插入一个单元格。这个单元格的rowkey为'rowkey1',也就是说它是属于'rowkey1'这个行中的 一个列,该单元格的列族为'cf',该单元格的列名为'name'。数据值为'zhb'。

之后我们用scan命令扫描一下表,就可以看到我们刚才插入的数据了:
scan 'zhb_test'

看这条记录的时候,你会看到时间戳属性。每一个单元格都可 以存储多个版本(version)的值。HBase的单元格并没有version这个 属性,它用timestamp来存储该条记录的时间戳,这个时间戳就用来当 版本号使用。如果你在写put语句的时候不指定时间戳,系统就会自动用当前时 间帮你指定它。有意思的是,这个timestamp虽然说是时间的标定,其 实你可以输入任意的数字,比如1、2、3都可以存储进去。当你用scan命令的时候HBase会显示拥有最大(最新)的timestamp的数据版本。可以指定列族中保存的Cell版本数。

⑥ get命令获取单元格数据

过get只能查询一个单元格的记录,在表的数据很大的时候,get查询 的速度远远高于scan。

get 'zhb_test', 'rowkey1', 'cf:name'

2.2 Java API

Hbase API 文档:https://hbase.apache.org/apidocs/index.html

创建一个Maven项目,在pom.xml中添加如下依赖:

<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-client</artifactId>
    <version>2.0.0</version>
</dependency>

HbaseClient.java

package javaa.sg.bigo;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HbaseClient {
    // config zookeeper
    static private org.apache.hadoop.conf.Configuration configuration = null;
    static private Connection connection = null;
    private static Logger logger = LoggerFactory.getLogger(HbaseClient.class);
    static private Lock lock = new ReentrantLock();
 
    static Connection getConnectionInstance() {
        if (null == connection) {
            lock.lock();
            try {
                if (null == connection) {
                    configuration = HBaseConfiguration.create();
                    configuration.set("hbase.zookeeper.quorum", "zk1:2182,zk2:2182,zk3:2182");
                    configuration.set("hbase.client.keyvalue.maxsize", "100000000");
                    connection = ConnectionFactory.createConnection(configuration);
                }
            } catch (IOException e) {
                logger.error("create hbase error ", e);
            } finally {
                lock.unlock();
            }
        }
        return connection;
    }
}

使用demo

// 懒加载单例模式
    static private Connection connection = HbaseClient.getConnectionInstance();
    /**
     * 创建表
     *
     * @param tableName
     */
    public static void createTable(String tableStr, String[] familyNames) {
        System.out.println("start create table ......");
        try {
            Admin admin = connection.getAdmin();
            TableName tableName = TableName.valueOf(tableStr);
            if (admin.tableExists(tableName)) {// 如果存在要创建的表,那么先删除,再创建
                admin.disableTable(tableName);
                admin.deleteTable(tableName);
                System.out.println(tableName + " is exist,detele....");
            }
            HTableDescriptor tableDescriptor = new HTableDescriptor(tableName);
            // 添加表列信息
            if (familyNames != null && familyNames.length > 0) {
                for (String familyName : familyNames) {
                    tableDescriptor.addFamily(new HColumnDescriptor(familyName));
                }
            }
            admin.createTable(tableDescriptor);
        } catch (MasterNotRunningException e) {
            e.printStackTrace();
        } catch (ZooKeeperConnectionException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("end create table ......");
    }
    /**
     * 添加行列数据数据
     *
     * @param tableName
     * @throws Exception
     */
    public static void insertData(String tableName, String rowId, String familyName,String qualifier, String value) throws Exception {
        System.out.println("start insert data ......");
        Table table = connection.getTable(TableName.valueOf(tableName));
        Put put = new Put(rowId.getBytes());// 一个PUT代表一行数据,再NEW一个PUT表示第二行数据,每行一个唯一的ROWKEY,此处rowkey为put构造方法中传入的值
        put.addColumn(familyName.getBytes(), qualifier.getBytes(), value.getBytes());// 本行数据的第一列
        try {
            table.put(put);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("end insert data ......");
    }
    /**
     * 添加行列数据数据
     *
     * @param tableName
     * @throws Exception
     */
    public static void batchInsertData(String tableName, String rowId, List<String> familyNames,
                                  String qualifier, List<String> values) throws Exception {
        if (null == qualifier) qualifier = "tmp";
        Table table = connection.getTable(TableName.valueOf(tableName));
        Put put = new Put(rowId.getBytes());// 一个PUT代表一行数据,再NEW一个PUT表示第二行数据,每行一个唯一的ROWKEY,此处rowkey为put构造方法中传入的值
        for (int i = 0; i < familyNames.size(); ++i) {
            put.addColumn(familyNames.get(i).getBytes(),
                    qualifier.getBytes(), values.get(i).getBytes());// 本行数据的第一列
        }
        try {
            table.put(put);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 删除行
     *
     * @param tablename
     * @param rowkey
     */
    public static void deleteRow(String tablename, String rowkey) {
        try {
            Table table = connection.getTable(TableName.valueOf(tablename));
            Delete d1 = new Delete(rowkey.getBytes());
            table.delete(d1);//d1.addColumn(family, qualifier);d1.addFamily(family);
            System.out.println("删除行成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 查询所有数据
     *
     * @param tableName
     * @throws Exception
     */
    public static void queryAll(String tableName) throws Exception {
        Table table = connection.getTable(TableName.valueOf(tableName));
        try {
            ResultScanner rs = table.getScanner(new Scan());
            for (Result r : rs) {
                System.out.println("获得到rowkey:" + new String(r.getRow()));
                for (Cell keyValue : r.rawCells()) {
                    System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
                            new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
                }
            }
            rs.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 根据rowId查询
     *
     * @param tableName
     * @throws Exception
     */
    public static void queryByRowId(String tableName, String rowId) throws Exception {
        Table table = connection.getTable(TableName.valueOf(tableName));
        try {
            Get scan = new Get(rowId.getBytes());// 根据rowkey查询
            Result r = table.get(scan);
            System.out.println("获得到rowkey:" + new String(r.getRow()));
            for (Cell keyValue : r.rawCells()) {
                System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
                        new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 根据列条件查询
     *
     * @param tableName
     */
    public static void queryByCondition(String tableName, String familyName,String qualifier,String value) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            Filter filter = new SingleColumnValueFilter(Bytes.toBytes(familyName),
                    Bytes.toBytes(qualifier), CompareOp.EQUAL, Bytes.toBytes(value)); // 当列familyName的值为value时进行查询
            Scan s = new Scan();
            s.setFilter(filter);
            ResultScanner rs = table.getScanner(s);
            for (Result r : rs) {
                System.out.println("获得到rowkey:" + new String(r.getRow()));
                for (Cell keyValue : r.rawCells()) {
                    System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":"+
                            new String(CellUtil.cloneQualifier(keyValue)) + "====值:" + new String(CellUtil.cloneValue(keyValue)));
                }
            }
            rs.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 多条件查询
     *
     * @param tableName
     */
    public static void queryByConditions(String tableName, String[] familyNames, String[] qualifiers,String[] values) {
        try {
            Table table = connection.getTable(TableName.valueOf(tableName));
            List<Filter> filters = new ArrayList<Filter>();
            if (familyNames != null && familyNames.length > 0) {
                int i = 0;
                for (String familyName : familyNames) {
                    Filter filter = new SingleColumnValueFilter(Bytes.toBytes(familyName),
                            Bytes.toBytes(qualifiers[i]), CompareOp.EQUAL, Bytes.toBytes(values[i]));
                    filters.add(filter);
                    i++;
                }
            }
            FilterList filterList = new FilterList(filters);
            Scan scan = new Scan();
            scan.setFilter(filterList);
            ResultScanner rs = table.getScanner(scan);
            for (Result r : rs) {
                System.out.println("获得到rowkey:" + new String(r.getRow()));
                for (Cell keyValue : r.rawCells()) {
                    System.out.println("列:" + new String(CellUtil.cloneFamily(keyValue))+":" +
                            new String(CellUtil.cloneQualifier(keyValue)) +
                            "====值:" + new String(CellUtil.cloneValue(keyValue)));
                }
            }
            rs.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 删除表
     *
     * @param tableName
     */
    public static void dropTable(String tableStr) {
        try {
            Admin admin = connection.getAdmin();
            TableName tableName = TableName.valueOf(tableStr);
            admin.disableTable(tableName);
            admin.deleteTable(tableName);
            admin.close();
        } catch (MasterNotRunningException e) {
            e.printStackTrace();
        } catch (ZooKeeperConnectionException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

三、存储机制

这一节我们来看一下HBase这个数据库是怎么存储数据的,数据是怎么存储到磁盘上的。我们会从最宏观的Master和RegionServer结构一直 到最小的单元格(Cell)。

再把宏观架构图拿出来复习一下:

从这张图上可以看出一个HBase集群由一个Master(也可以把两个 Master做成 HighAvailable)和多个RegionServer组成。右下角是其中 一个RegionServer的内部构造图,我们先看完这幅图的图解后,再解剖 它。

RegionServer上有一个或者多个Region。我们读 写的数据就存储在Region上。HBase是一个会自动分片的数据库。 一个Region就相当于关系型数据库中分区表的一个分区。

3.1 RegionServer结构

放大RegionServer,如下图:

从这幅图中我们可以看出一个RegionServer包含有:

  • 一个WAL:
    预写日志,WAL是Write-Ahead Log的缩写。从名字就可以看出它的用途,就是:预先写入。当操作到达Region的时候,HBase先不管三七二十一把操作写到WAL里面去。HBase会先把数据放到基于内存实现的Memstore里,等数据达到一定的数量 时才刷写(flush)到最终存储的HFile内。而如果在这个过程中 服务器宕机或者断电了,那么数据就丢失了。WAL是一个保险机 制,数据在写到Memstore之前,先被写到WAL了。这样当故障恢复的时候可以从WAL中恢复数据。

  • 多个Region:
    Region相当于一个数据分片。每一个Region都有起始rowkey和结束rowkey,代表了它所存储的row范围。

3.2 Region内部结构

接下来我们来看单个Region内部的结构

一个Region包含有:

  • 多个Store:每一个Region内都包含有多个Store实例。一个 Store对应一个列族的数据,举个例子,如果一个表有两个列族,那么在一 个Region里面就有两个Store。在最右边的单个Store的解剖图上,我们可以看到Store内部有MemStore和HFile这两个组成部分。

3.3 WAL(预写日志)

预写日志(Write-ahead log,WAL)就是设计来解决宕机之后的操作恢复问题的,数据到达 Region 的时候是先写入 WAL,然后再被加载到 Memstore,就算 Region 的机器宕掉了,由于 WAL 的数据是存储在 HDFS 上的,所以数据并不会丢失。

WAL 是默认开启的,可以通过下面的代码关闭 WAL。

Mutation.setDurability(Durability.SKIP_WAL);

Put、Append、Increment、Delete 都是 Mutation 的子类,所以他们都有 setDurability 方法,这样可以让该数据操作快一点,但是最好不要这样做,因为当服务器宕机时,数据就会丢失。

如果你实在想不惜通过关闭 WAL 来提高性能,可以选择异步写入 WAL。

Mutation.setDurability(Durability.ASYNC WAL);

这样设定后 Region 会等到条件满足的时候才把操作写入 WAL,这里提到的条件主要指的是时间间隔 hbase.regionserver.optionallogflushinterval,这个时间间隔的意思是 HBase 间隔多久会把操作从内存写入 WAL,默认值是 1s。

3.4 Store内部结构

在 Store 中有两个重要组成部分:

  • MemStore:每个 Store 中有一个 MemStore 实例,数据写入 WAL 之后就会被放入 MemStore。MemStore 是内存的存储对象,只有当 MemStore 满了的时候才会将数据刷写(flush)到 HFile 中;

  • HFile:在 Store 中有多个 HFile,当 MemStore 满了之后 HBase 就会在 HDFS 上生成一个新的 HFile,然后把 MemStore 中的内容写到这个 HFile 中。HFile 直接跟 HDFS 打交道,它是数据的存储实体。

WAL 是存储在 HDFS 上的,Memstore 是存储在内存中的,HFile 又是存储在 HDFS 上的;数据是先写入 WAL,再被放入 Memstore,最后被持久化到 HFile 中。数据在进入 HFile 之前已经被存储到 HDFS 一次了,为什么还需要被放入 Memstore?

这是因为 HDFS 上的文件只能创建、追加、删除,但是不能修改。对于一个数据库来说,按顺序地存放数据是非常重要的,这是性能的保障,所以我们不能按照数据到来的顺序来写入硬盘。

可以使用内存先把数据整理成顺序存放,然后再一起写入硬盘,这就是 Memstore 存在的意义。虽然 Memstore 是存储在内存中的,HFile 和 WAL 是存储在 HDFS 上的,但由于数据在写入 Memstore 之前,要先被写入 WAL,所以增加 Memstore 的大小并不能加速写入速度。Memstore 存在的意义是维持数据按照 rowkey 顺序排列,而不是做一个缓存。

3.5 MemStore

设计 MemStore 的原因有以下几点:

  • 由于 HDFS 上的文件不可修改,为了让数据顺序存储从而提高读取效率,HBase 使用了 LSM 树结构来存储数据,数据会先在 Memstore 中整理成 LSM 树,最后再刷写到 HFile 上。

  • 优化数据的存储,比如一个数据添加后就马上删除了,这样在刷写的时候就可以直接不把这个数据写到 HDFS 上。

不过不要想当然地认为读取也是先读取 Memstore 再读取磁盘哟!读取的时候是有专门的缓存叫 BlockCache,这个 BlockCache 如果开启了,就是先读 BlockCache,读不到才是读 HFile+Memstore。

3.6 HFile(StoreFile)

HFile是数据存储的实际载体,我们创建的所有表、列等数据都存储在HFile里面。HFile类似 Hadoop的TFile类,它模仿了BigTable的SSTable格式。HFile中组成部分如下图所示:

我们可以看到HFile是由一个一个的块组成的。在HBase中一个块的 大小默认为64KB,由列族上的BLOCKSIZE属性定义。这些块区分了不同 的角色:

  • Data:数据块。每个 HFile 有多个 Data 块,我们存储在 HBase 表中的数据就在这里,Data 块其实是可选的,但是几乎很难看到不包含 Data 块的 HFile。

  • Meta:元数据块。Meta 块是可选的,Meta 块只有在文件关闭的时候才会写入。Meta 块存储了该 HFile 文件的元数据信息,在 v2 之前布隆过滤器(Bloom Filter)的信息直接放在 Meta 里面存储,v2 之后分离出来单独存储。

  • FileInfo:文件信息,其实也是一种数据存储块。FileInfo 是 HFile 的必要组成部分,是必选的,它只有在文件关闭的时候写入,存储的是这个文件的信息,比如最后一个 Key(LastKey),平均的 Key 长度(AvgKeyLen)等;

  • DataIndex:存储 Data 块索引信息的块文件。索引的信息其实也就是 Data 块的偏移值(offset),DataIndex 也是可选的,有 Data 块才有 DataIndex;

  • MetaIndex:存储 Meta 块索引信息的块文件。MetaIndex 块也是可选的,有 Meta 块才有 MetaIndex;

  • Trailer:必选的,它存储了 FileInfo、DataIndex、MetaIndex 块的偏移值。

其实叫 HFile 或者 StoreFile 都没错,在物理存储上我们管 MemStore 刷写而成的文件叫 HFile,StoreFile 就是 HFile 的抽象类而已。

3.7 Data数据块

刚刚我们讲完了HBase的底层存储HFile,不过我们现在还没有达到 HBase架构的最微观结构。接下来,我们继续解剖Data数据块。

Data数据块的第一位存储的是块的类型,后面存储的是多个 KeyValue键值对,也就是单元格(Cell)的实现类。Cell是一个接口, KeyValue是它的实现类。

3.8 KeyValue类

让我们来看看单元格最重要的实现类KeyValue类的结构,如下图所示:

一个 KeyValue 类里面最后一个部分是存储数据的 Value,而前面的部分都是存储跟该单元格相关的元数据信息。如果你存储的 value 很小,那么这个单元格的绝大部分空间就都是 rowkey、column family、column 等的元数据,所以大家的列族和列的名字如果很长,大部分的空间就都被拿来存储这些数据了。

不过如果采用适当的压缩算法就可以极大地节省存储列族、列等信息的空间了,所以在实际的使用中,可以通过指定压缩算法来压缩这些元数据。不过压缩和解压必然带来性能损耗,所以使用压缩也需要根据实际情况来取舍。如果你的数据主要是归档数据,不太要求读写性能,那么压缩算法就比较适合你。

至此,我们终于将HBase剖析到了最小的不可分割的数据结构 KeyValue。

3.9 数据增删改查的真面目

HBase 是一个可以随机读写的数据库,而它所基于的持久化层 HDFS 却是要么新增,要么整个删除,不能修改的系统。那 HBase 怎么实现我们的增删查改的?真实的情况是这样的:HBase 几乎总是在做新增操作。

  • 当你新增一个单元格的时候,HBase 在 HDFS 上新增一条数据;

  • 当你修改一个单元格的时候,HBase 在 HDFS 又新增一条数据,只是版本号比之前那个大(或者你自己定义);

  • 当你删除一个单元格的时候,HBase 还是新增一条数据!只是这条数据没有 value,类型为 DELETE,这条数据叫墓碑标记(Tombstone)。

由于数据库在使用过程中积累了很多增删查改操作,数据的连续性和顺序性必然会被破坏。为了提升性能,HBase 每间隔一段时间都会进行一次合并(Compaction),合并的对象为 HFile 文件。

合并分为 minor compaction 和 major compaction,在 HBase 进行 major compaction 的时候,它会把多个 HFile 合并成 1 个 HFile,在这个过程中,一旦检测到有被打上墓碑标记的记录,在合并的过程中就忽略这条记录,这样在新产生的 HFile 中,就没有这条记录了,自然也就相当于被真正地删除了。

3.10 Hbase数据结构总结

HBase 数据的内部结构大体如下:

  • 一个 RegionServer 包含多个 Region,划分规则是:一个表的一段键值在一个 RegionServer 上会产生一个 Region。不过当某一行的数据量太大了(要非常大),HBase 也会把这个 Region 根据列族切分到不同的机器上去;

  • 一个 Region 包含多个 Store,划分规则是:一个列族分为一个 Store,如果一个表只有一个列族,那么这个表在这个机器上的每一个 Region 里面都只有一个 Store;

  • 一个 Store 里面只有一个 Memstore;

  • 一个 Store 里面有多个 HFile,每次 Memstore 的刷写(flush)就产生一个新的 HFile 出来。

对应关系如下图:

四、KeyValue的写入和读出

4.1 写入

一个KeyValue被持久化到HDFS的过程的总结见下图:

  • WAL:数据被发出之后第一时间被写入WAL。由于WAL是基于HDFS 来实现的,所以也可以说现在单元格就已经被持久化了,但是 WAL只是一个暂存的日志,它是不区分Store的。这些数据是不能
    被直接读取和使用。
  • Memstore:数据随后会立即被放入Memstore中进行整理。 Memstore会负责按照LSM树的结构来存放数据。这个过程就像我 们在打牌的时候,抓牌之后在手上对牌进行整理的过程。
  • HFile:最后,当Memstore太大了达到尺寸上的阀值,或者达到了刷写时间间隔阀值的时候,HBaes会被这个Memstore的内容刷 写到HDFS系统上,称为一个存储在硬盘上的HFile文件。至此, 我们可以称为数据真正地被持久化到硬盘上,就算宕机,断电, 数据也不会丢失了。

4.2 读出

由于有 MemStore(基于内存)和 HFile(基于HDFS)这两个机制,你一定会立马想到先读取 MemStore,如果找不到,再去 HFile 中查询。这是显而易见的机制,可惜 HBase 在处理读取的时候并不是这样的。实际的读取顺序是先从 BlockCache 中找数据,找不到了再去 Memstore 和 HFile 中查询数据。

墓碑标记和数据不在一个地方,读取数据的时候怎么知道这个数据要删除呢?如果这个数据比它的墓碑标记更早被读到,那在这个时间点真是不知道这个数据会被删 除,只有当扫描器接着往下读,读到墓碑标记的时候才知道这个数据是被标记为删除的,不需要返回给用户。

所以 HBase 的 Scan 操作在取到所需要的所有行键对应的信息之后还会继续扫描下去,直到被扫描的数据大于给出的限定条件为止,这样它才能知道哪些数据应该被返回给用户,而哪些应该被舍弃。所以你增加过滤条件也无法减少 Scan 遍历的行数,只有缩小 STARTROW 和 ENDROW 之间的行键范围才可以明显地加快扫描的速度。

在 Scan 扫描的时候 store 会创建 StoreScanner 实例,StoreScanner 会把 MemStore 和 HFile 结合起来扫描,所以具体从 MemStore 还是 HFile 中读取数据,外部的调用者都不需要知道具体的细节。当 StoreScanner 打开的时候,会先定位到起始行键(STARTROW)上,然后开始往下扫描。

其中红色块部分都是属于指定row的数据,Scan要把所有符合条件 的StoreScanner都扫描过一遍之后才会返回数据给用户。

五、 Region的定位

Region 的查找,早期的设计(0.96.0)之前是被称为三层查询架构:

  • Region:查找的数据所在的 Region;

  • .META.:是一张元数据表,它存储了所有 Region 的简要信息,.META. 表中的一行记录就是一个 Region,该行记录了该 Region 的起始行、结束行和该 Region 的连接信息,这样客户端就可以通过这个来判断需要的数据在哪个 Region 上;

  • ROOT-:是一张存储 .META. 表的表,.META. 可以有很多张,而 -ROOT- 就是存储了 .META. 表在什么 Region 上的信息(.META. 表也是一张普通的表,也在 Region 上)。通过两层的扩展最多可以支持约 171 亿个 Region。

-ROOT- 表记录在 ZooKeeper 上,路径为:/hbase/root-region-server;Client 查找数据的流程从宏观角度来看是这样的:

  • 用户通过查找 zk(ZooKeeper)的 /hbase/root-regionserver 节点来知道 -ROOT- 表在什么 RegionServer 上;

  • 访问 -ROOT- 表,看需要的数据在哪个 .META. 表上,这个 .META. 表在什么 RegionServer 上;

  • 访问 .META. 表来看要查询的行键在什么 Region 范围里面;

  • 连接具体的数据所在的 RegionServer,这回就真的开始用 Scan 来遍历 row 了。

从 0.96 版本之后这个三层查询架构被改成了二层查询架构,-ROOT- 表被去掉了,同时 zk 中的 /hbase/root-region-server 也被去掉了,直接把 .META. 表所在的 RegionServer 信息存储到了 zk 中的 /hbase/meta-region-server。再后来引入了 namespace,.META. 表被修改成了 hbase:meta。

新版 Region 查找流程:

  • 客户端先通过 ZooKeeper 的 /hbase/meta-region-server 节点查询到哪台 RegionServer 上有 hbase:meta 表。

  • 客户端连接含有 hbase:meta 表的 RegionServer,hbase:meta 表存储了所有 Region 的行键范围信息,通过这个表就可以查询出要存取的 rowkey 属于哪个 Region 的范围里面,以及这个 Region 又是属于哪个 RegionServer;

  • 获取这些信息后,客户端就可以直连其中一台拥有要存取的 rowkey 的 RegionServer,并直接对其操作;

  • 客户端会把 meta 信息缓存起来,下次操作就不需要进行以上加载 hbase:meta 的步骤了。

二层查询架构如下图所示:

下半篇主要介绍:
① Region的拆分和合并,及相关经验总结。
② HFile的合并(compaction)
③ Region自动均衡
④ BlockCache 和 BloomFilter
等。

参考资料:
《HBase不睡觉书》https://www.cnblogs.com/littleatp/p/11946199.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叹了口丶气

觉得有收获就支持一下吧~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值