Hbase

目录

1 概述

1.1 HBase 数据模型

1.1.1 HBase 逻辑结构

1.1.2 HBase 物理存储结构

1.1.3 数据模型

1.2 HBase 基本架构

2 HBase 快速入门

2.1 HBase 安装部署

hadoop3.X和Hbase2.X不兼容问题:

2.2 基本操作

3 HBase API

3.1 环境准备

3.2 创建连接

3.2.1 单线程创建连接

3.2.2 多线程创建连接(推荐)

3.3.3 DDL

3.3.4 DML

4 深层

4.1 Master

4.2 RegionServer 架构

4.3 写流程

4.4 刷写机制

4.5 读流程

4.5.1 HFile 结构

4.5.2 读流程

4.5.3 合并读取数据优化

4.6 StoreFile Compaction

4.7 Region Split

5 优化(调参)

5.1 RowKey 设计

5.4 HBase 使用经验法则

6 整合 Phoenix

6.1 安装

6.2 Phoenix Shell 操作

!table 或 !tables 显示所有表

创建表

插入数据

查询记录

删除记录

删除表

退出命令行 !quit

6.3 表的映射

6.4 数字类型说明

6.5 jdbc连接

7 二级索引

7.1 二级索引配置文件

7.2 全局索引(global index)

8 整合hive

1 概述

Apache HBase™ 是以 hdfs 为数据存储的,一种分布式、可扩展的 NoSQL 数据库

1.1 HBase 数据模型

HBase 的设计理念依据 Google 的 BigTable 论文,论文中对于数据模型的首句介绍:

Bigtable 是一个稀疏的、分布式的、持久的多维排序 map

之后对于映射的解释如下:

该映射由行键、列键和时间戳索引;映射中的每个值都是一个未解释的字节数组

最终 HBase 关于数据模型和 BigTable 的对应关系如下:

        HBase 使用与 Bigtable 非常相似的数据模型。用户将数据行存储在带标签的表中。数据行具有可排序的键和任意数量的列。该表存储稀疏,因此如果用户喜欢,同一表中的行可以具有疯狂变化的列。

        最终理解 HBase 数据模型的关键在于稀疏、分布式、多维、排序的映射。其中映射 map 指代非关系型数据库的 key-Value 结构。

1.1.1 HBase 逻辑结构

HBase 可以用于存储多种结构的数据,以 JSON 为例,存储的数据原貌为

{
  "row_key1": {
    "personal_info": {
      "name": "zhangsan",
      "city": "北京",
      "phone": "131********"
    },
    "office_info": {
      "tel": "010-1111111",
      "address": "atguigu"
    }
  },
  "row_key11": {
    "personal_info": {
      "city": "上海",
      "phone": "132********"
    },
    "office_info": {
      "tel": "010-1111111"
    }
  },
  "row_key2": {
    ......
  }
}

        存储数据稀疏,数据存储多维,不同的行具有不同的列。 数据存储整体有序,按照RowKey的字典序排列,RowKey为Byte数组

1.1.2 HBase 物理存储结构

物理存储结构即为数据映射关系,而在概念视图的空单元格,底层实际根本不存储

1.1.3 数据模型

1)Name Space

        命名空间,类似于关系型数据库的 database 概念,每个命名空间下有多个表。HBase 两个自带的命名空间,分别是 hbase 和 default,hbase 中存放的是 HBase 内置的表,default表是用户默认使用的命名空间。

2)Table

        类似于关系型数据库的表概念。不同的是,HBase 定义表时只需要声明列族即可,不需 要声明具体的列。因为数据存储时稀疏的,所有往 HBase 写入数据时,字段可以动态、按需 指定。因此,和关系型数据库相比,HBase 能够轻松应对字段变更的场景。

3)Row

        HBase 表中的每行数据都由一个 RowKey 和多个 Column(列)组成,数据是按照 RowKey 的字典顺序存储的,并且查询数据时只能根据 RowKey 进行检索,所以 RowKey 的设计十分重 要。

4)Column

        HBase 中的每个列都由 Column Family(列族)和 Column Qualifier(列限定符)进行限 定,例如 info:name,info:age。建表时,只需指明列族,而列限定符无需预先定义。

5)Time Stamp

        用于标识数据的不同版本(version),每条数据写入时,系统会自动为其加上该字段, 其值为写入 HBase 的时间。

6)Cell

        由{rowkey, column Family:column Qualifier, timestamp} 唯一确定的单元。cell 中的数 据全部是字节码形式存贮。

1.2 HBase 基本架构

1)Master

        实现类为 HMaster,负责监控集群中所有的 RegionServer 实例。主要作用如下:

(1)管理元数据表格 hbase:meta,接收用户对表格创建修改删除的命令并执行

(2)监控 region 是否需要进行负载均衡,故障转移和 region 的拆分。

通过启动多个后台线程监控实现上述功能:

①LoadBalancer 负载均衡器

        周期性监控 region 分布在 regionServer 上面是否均衡,由参数 hbase.balancer.period 控 制周期时间,默认 5 分钟。

②CatalogJanitor 元数据管理器

        定期检查和清理 hbase:meta 中的数据。meta 表内容在进阶中介绍。

③MasterProcWAL master 预写日志处理器

        把 master 需要执行的任务记录到预写日志 WAL 中,如果 master 宕机,让 backupMaster 读取日志继续干。

2)Region Server

Region Server 实现类为 HRegionServer,主要作用如下:

(1)负责数据 cell 的处理,例如写入数据 put,查询数据 get 等

(2)拆分合并 region 的实际执行者,有 master 监控,有 regionServer 执行。

3)Zookeeper

        HBase 通过 Zookeeper 来做 master 的高可用、记录 RegionServer 的部署信息、并且存储 有 meta 表的位置信息。

        HBase 对于数据的读写操作时直接访问 Zookeeper 的,在 2.3 版本推出 Master Registry 模式,客户端可以直接访问 master。使用此功能,会加大对 master 的压力,减轻对 Zookeeper 的压力。

4)HDFS

        HDFS 为 Hbase 提供最终的底层数据存储服务,同时为 HBase 提供高容错的支持

2 HBase 快速入门

2.1 HBase 安装部署

1. zookeeper、hadoop部署

2. hbase解压,配置

https://www.apache.org/dyn/closer.lua/hbase/2.4.17/hbase-2.4.17-bin.tar.gz

需要哪个版本直接改数字

hadoop3.X和Hbase2.X不兼容问题:

<property>
  <name>hbase.wal.provider</name>
  <value>filesystem</value>
</property>

3. 环境变量

#HBASE_HOME

export HBASE_HOME=/opt/module/hbase

export PATH=$PATH:$HBASE_HOME/bin

4. 配置文件

hbase-env.sh 修改内容,可以添加到最后:

export JAVA_HOME=/自己的java路径

export HBASE_MANAGES_ZK=false

hbase-site.xml 修改内容

  </configuration>
  <property>
    <name>hbase.cluster.distributed</name>
    <value>true</value>
  </property>

  <property>
    <name>hbase.zookeeper.quorum</name>
    <value>hadoop1,hadoop2,hadoop3</value>
    <description>The directory shared by RegionServers.</description>
  </property>

  <property>
    <name>hbase.rootdir</name>
    <value>hdfs://hadoop1:8020/hbase</value>
    <description>The directory shared by RegionServers.</description>
  </property>
<!-- <property>-->
<!-- <name>hbase.zookeeper.property.dataDir</name>-->
<!-- <value>/export/zookeeper</value>-->
<!-- <description> 记得修改 ZK 的配置文件 -->
<!-- ZK 的信息不能保存到临时文件夹-->
<!-- </description>-->
<!-- </property>-->
  <property>
    <name>hbase.wal.provider</name>
    <value>filesystem</value>
  </property>
</configuration>

regionservers

hadoop1

hadoop2

hadoop3

 解决 HBase 和 Hadoop 的 log4j 兼容性问题,修改 HBase 的 jar 包,使用 Hadoop 的 jar 包

mv $HBASE_HOME/lib/client-facing-thirdparty/slf4j-reload4j-1.7.33.jar $HBASE_HOME/lib/client-facing-thirdparty/slf4j-reload4j-1.7.33.jar.bak

分发

5. 原神启动

先启动hadoop 启动zk

1)单点启动

bin/hbase-daemon.sh start master

bin/hbase-daemon.sh start regionserver

2)群启

bin/start-hbase.sh

bin/stop-hbase.sh

http://hadoop1:16010/master-status

2.2 高可用(可选)

        在 HBase 中 HMaster 负责监控 HRegionServer 的生命周期,均衡 RegionServer 的负载, 如果 HMaster 挂掉了,那么整个 HBase 集群将陷入不健康的状态,并且此时的工作状态并不 会维持太久。所以 HBase 支持对 HMaster 的高可用配置。

2.2 基本操作

用不到的shell

进入命令行

bin/hbase shell

 help "COMMAND" 帮助命令

namespace

Group name: namespace
Commands: alter_namespace, create_namespace, describe_namespace, drop_namespace, list_namespace, list_namespace_tables

系统的命名空间不使用 

ddl

  Group name: ddl
  Commands: alter, alter_async, alter_status, clone_table_schema, create, describe, disable, disable_all, drop, drop_all, enable, enable_all, exists, get_table, is_disabled, is_enabled, list, list_regions, locate_region, show_filters

list自动过滤掉了系统两个表

创建表语句,不写命名空间默认default空间

甄姬8复杂,需要自己help看去

删除数据的方法有两个:delete 和 deleteall。

delete 表示删除一个版本的数据,即为 1 个 cell,不填写版本默认删除最新的一个版本。

hbase:026:0> delete 'bigdata:student','1001','info:name'

deleteall 表示删除所有版本的数据,即为当前行当前列的多个 cell。(执行命令会标记 数据为要删除,不会直接将数据彻底删除,删除数据只在特定时期清理磁盘时进行)

3 HBase API

3.1 环境准备

    <dependency>
      <groupId>org.apache.hbase</groupId>
      <artifactId>hbase-server</artifactId>
      <version>2.4.11</version>
      <exclusions>
        <exclusion>
          <groupId>org.glassfish</groupId>
          <artifactId>javax.el</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.glassfish</groupId>
      <artifactId>javax.el</artifactId>
      <version>3.0.1-b06</version>
    </dependency>

3.2 创建连接

        HBase 的客户端连接由 ConnectionFactory 类来创建,用户使用完成 之后需要手动关闭连接。同时连接是一个重量级的,推荐一个进程使用一个连接,对 HBase 的命令通过连接中的两个属性 Admin 和 Table 来实现。

3.2.1 单线程创建连接

package org.example;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import java.io.IOException;

public class c1_connection1 {
    public static void main(String[] args) throws IOException {
        Configuration conf = new Configuration();
        conf.set("hbase.zookeeper.quorum", "hadoop1,hadoop2,hadoop3");

//        同步连接
        Connection connection = ConnectionFactory.createConnection(conf);

//        异步连接
//        CompletableFuture<AsyncConnection> asyncConnection = ConnectionFactory.createAsyncConnection();

        System.out.println(connection);

        connection.close();
    }
}

hconnection-0x4802796d
 

3.2.2 多线程创建连接(推荐)

使用类单例模式,确保使用一个连接,可以同时用于多个线程。

public class c2_connection_mul {
    static Connection connection = null;
    static {
        // 把linux的hbase-site.xml导入resources就可以不用配置了
//        Configuration conf = new Configuration();
//        conf.set("hbase.zookeeper.quorum", "hadoop1,hadoop2,hadoop3");
        try {
            connection = ConnectionFactory.createConnection();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void closeConnection() throws IOException {
        if (connection != null) {
            connection.close();
        }
    }

    public static void main(String[] args) throws IOException {
        System.out.println(connection);
        closeConnection();

    }
}

3.3.3 DDL

package org.example;

import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos;
import org.apache.hadoop.hbase.util.Bytes;

import java.io.IOException;
import java.io.StringReader;

public class c3_DDL {
    static Connection connection = c2_connection_mul.connection;

    public static void createNamespace(String namespace) throws IOException {
        Admin admin = connection.getAdmin();
        NamespaceDescriptor.Builder builder = NamespaceDescriptor.create(namespace);
        builder.addConfiguration("user","yuange");  // 没实际含义

        try {
            admin.createNamespace(builder.build());
        }catch (IOException e){
            System.out.println("admin.createNamespace(builder.build());  error");
            e.printStackTrace();
        }

        admin.close();
    }

    public static boolean isTableExist(String namespace, String tablename) throws IOException {
        Admin admin = connection.getAdmin();
        boolean b = false;
        try {
            b = admin.tableExists(TableName.valueOf(namespace, tablename));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        admin.close();
        return b;
    }

    public static void createTable(String namespace, String tablename, String... columnFamilies) throws IOException {
        if (columnFamilies.length == 0){
            System.out.println("妹有列族");
            return;
        }
        if(isTableExist(namespace, tablename)){
            System.out.println("表格存在了");
            return;
        }

        Admin admin = connection.getAdmin();

        TableDescriptorBuilder tableDescriptorBuilder =
                TableDescriptorBuilder.newBuilder(TableName.valueOf(namespace, tablename));
        for (String columnFamily : columnFamilies) {
            ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder =
                    ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamily));
            columnFamilyDescriptorBuilder.setMaxVersions(3);

            tableDescriptorBuilder.addColumnFamily(columnFamilyDescriptorBuilder.build());
        }
        try {
            admin.createTable(tableDescriptorBuilder.build());
        } catch (IOException e) {
            System.out.println("表格已经存在");
            throw new RuntimeException(e);
        }
    }

    public static void modifyTable(String namespace, String tablename, String columnFamily, int ver) throws IOException {
        if (!isTableExist(namespace, tablename)){
            System.out.println("表格不存在");
            return;
        }

        Admin admin = connection.getAdmin();
        try {
            TableDescriptor descriptor = admin.getDescriptor(TableName.valueOf(namespace, tablename));



            // 2  (如果填写tablename的方法是创建了一个新的表格描述建造器,妹有之前的信息)
//        TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TableName.valueOf(namespace, tablename));
            TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(descriptor);


            // 4
            ColumnFamilyDescriptor columnFamily1 = descriptor.getColumnFamily(Bytes.toBytes(columnFamily));

            ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder = ColumnFamilyDescriptorBuilder.newBuilder(columnFamily1);
            columnFamilyDescriptorBuilder.setMaxVersions(ver);
            // 3 这个上面有两个 准备句 4 (如果新创建,那别的参数就初始化了,改上边两句)
            tableDescriptorBuilder.modifyColumnFamily(columnFamilyDescriptorBuilder.build());
            // 1 这个上面有两个 2 3
            admin.modifyTable(tableDescriptorBuilder.build());
        } catch (IOException e) {
            System.out.println("admin.modifyTable(tableDescriptorBuilder.build()); error");
            throw new RuntimeException(e);
        }

        admin.close();

    }

    public static boolean deleteTable(String namespace, String tablename) throws IOException {
        if(!isTableExist(namespace, tablename)){
            System.out.println("表格不存在  无法删除");
            return false;
        }

        Admin admin = connection.getAdmin();
        try {
            TableName tableName = TableName.valueOf(namespace, tablename);
            // 标记删除之前要标记为不可用
            admin.disableTable(tableName);
            admin.deleteTable(tableName);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


        admin.close();
        return true;
    }



    public static void main(String[] args) throws IOException {
        // 创建命名空间
//        createNamespace("test");
        // 判断表格存在
//        boolean exist = isTableExist("yuange", "student");
//        System.out.println(exist);

//        createTable("yuange", "student2", "info", "msg");

        deleteTable("yuange", "student2");
        c2_connection_mul.closeConnection();
    }
}

3.3.4 DML

写入删除查找数据

package org.example;

import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.filter.ColumnValueFilter;
import org.apache.hadoop.hbase.filter.FilterList;
import org.apache.hadoop.hbase.filter.SingleColumnValueFilter;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.yarn.webapp.hamlet.Hamlet;

import java.io.IOException;

public class c4_DML {
    public static Connection connection = c2_connection_mul.connection;
    public static String namespace = "yuange";

    public static void putCell(String namespace, String tablename,
                               String rowkey, String colFamily, String colName, String value) throws IOException {
        // 对表格内容进行修改使用Table
        Table table = connection.getTable(TableName.valueOf(namespace, tablename));
        // 开调api 放一个行键,在放列族 列名 值
        Put put = new Put(Bytes.toBytes(rowkey));
        put.addColumn(Bytes.toBytes(colFamily), Bytes.toBytes(colName), Bytes.toBytes(value));
        table.put(put);
        table.close();
    }
    public static void get(String namespace, String tablename,
                           String rowkey, String colFamily, String colName) throws IOException {
        Table table = connection.getTable(TableName.valueOf(namespace, tablename));

        Get get = new Get(Bytes.toBytes(rowkey));
        // 不加就读一整行
//        get.addColumn(Bytes.toBytes(colFamily), Bytes.toBytes(colName));
        get.readAllVersions();
        Result result = null;
        try {
            result = table.get(get);
            Cell[] cells = result.rawCells();
            for (Cell cell : cells) {
                String s = new String(CellUtil.cloneValue(cell));
                System.out.println(s);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        table.close();
    }

    public static void scan(String namespace, String tablename,
                            String startrow, String endrow) throws IOException {
        Table table = connection.getTable(TableName.valueOf(namespace, tablename));
        Scan scan = new Scan();
        // scan不配置,直接扫描整个表
        scan.withStartRow(Bytes.toBytes(startrow));
        scan.withStopRow(Bytes.toBytes(endrow), false);

        ResultScanner scanner = null;
        try {
            scanner = table.getScanner(scan);
            for (Result result : scanner) {
                for (Cell cell : result.rawCells()) {
                    System.out.print(new String(CellUtil.cloneRow(cell))+"-"+new String(CellUtil.cloneFamily(cell))+"-"+
                            new String(CellUtil.cloneQualifier(cell))+"-"+ new String(CellUtil.cloneValue(cell)));
                }
                System.out.println();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        table.close();
    }

    public static void filterScan(String namespace, String tablename, String colFamily, String colname, String value,
                            String startrow, String endrow) throws IOException {
        Table table = connection.getTable(TableName.valueOf(namespace, tablename));

        // 添加过滤多个
        FilterList filterList = new FilterList();

        // 添加过滤器1
        // 结果只保留当前列
//        ColumnValueFilter columnValueFilter = new ColumnValueFilter(Bytes.toBytes(colFamily), Bytes.toBytes(colname),
//                CompareOperator.EQUAL, Bytes.toBytes(value));
//        filterList.addFilter(columnValueFilter);

        // 添加过滤器2
        // 结果只保留当前行
        // 并且放弃过滤没有这个值的行,也打出来
        SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter(Bytes.toBytes(colFamily), Bytes.toBytes(colname),
                CompareOperator.EQUAL, Bytes.toBytes(value));
        filterList.addFilter(singleColumnValueFilter);

        Scan scan = new Scan();

        // scan不配置,直接扫描整个表
        scan.withStartRow(Bytes.toBytes(startrow));
        scan.withStopRow(Bytes.toBytes(endrow), false);
        scan.setFilter(filterList);

        ResultScanner scanner = null;
        try {
            scanner = table.getScanner(scan);
            for (Result result : scanner) {
                for (Cell cell : result.rawCells()) {
                    System.out.print(new String(CellUtil.cloneRow(cell))+"-"+new String(CellUtil.cloneFamily(cell))+"-"+
                            new String(CellUtil.cloneQualifier(cell))+"-"+ new String(CellUtil.cloneValue(cell)));
                }
                System.out.println();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        table.close();
    }


    public static void delCol(String namespace, String tablename, String rowkey, String colFamily, String colname) throws IOException {
        Table table = connection.getTable(TableName.valueOf(namespace, tablename));

        Delete delete = new Delete(Bytes.toBytes(rowkey));
        // 删除一个版本
        delete.addColumn(Bytes.toBytes(colFamily), Bytes.toBytes(colname));
        // 删除所有ver
//        delete.addColumns(Bytes.toBytes(colFamily), Bytes.toBytes(colname));
        table.delete(delete);
        table.close();
    }

    public static void main(String[] args) throws IOException {

        scan(namespace, "student", "1", "4");
        delCol(namespace, "student", "3", "name", "ming");
        scan(namespace, "student", "1", "4");


        System.out.println("asda");
        connection.close();
    }
}

4 深层

4.1 Master

元数据表:

        全称 hbase:meta,只是在 list 命令中被过滤掉了,本质上和 HBase 的其他表格一样。 RowKey:

        ([table],[region start key],[region id]) 即 表名,region 起始位置和 regionID。

列:

         info:regioninfo 为 region 信息,存储一个 HRegionInfo 对象。

        info:server 当前 region 所处的 RegionServer 信息,包含端口号。

        info:serverstartcode 当前 region 被分到 RegionServer 的起始时间。

        如果一个表处于切分的过程中,即 region 切分,还会多出两列info:splitA 和 info:splitB, 存储值也是 HRegionInfo 对象,拆分结束后,删除这两列。

注意:在客户端对元数据进行操作的时候才会连接 master,如果对数据进行读写,直接连接zookeeper 读取目录/hbase/meta-region-server 节点信息,会记录 meta 表格的位置。直接读 取即可,不需要访问 master,这样可以减轻 master 的压力,相当于 master 专注 meta 表的 写操作,客户端可直接读取 meta 表

        在 HBase 的 2.3 版本更新了一种新模式:Master Registry。客户端可以访问 master 来读取 meta 表信息。加大了 master 的压力,减轻了 zookeeper 的压力。

4.2 RegionServer 架构

数据有序,所以需要中间的缓存缓冲区

1)MemStore

        写缓存,由于 HFile 中的数据要求是有序的,所以数据是先存储在 MemStore 中,排好 序后,等刷写时才会刷写到 HFile,每次刷写都会形成一个新的 HFile,写入到对应的 文件夹 store 中。

2)WAL

        由于数据要经 MemStore 排序后才能刷写到 HFile,但把数据保存在内存中会有很高的 概率导致数据丢失,为了解决这个问题,数据会先写在一个叫做 Write-Ahead logfile 的文件 中,然后再写入 MemStore 中。所以在系统出现故障的时候,数据可以通过这个日志文件重建。

3)BlockCache

        读缓存,每次查询出的数据会缓存在 BlockCache 中,方便下次查询。

4.3 写流程

写流程从客户端创建指令开始, 到最终刷写落盘到hdfs上结束

(1)首先访问 zookeeper,获取 hbase:meta 表位于哪个 Region Server;

(2)访问对应的 Region Server,获取 hbase:meta 表,将其缓存到连接中,作为连接属 性 MetaCache,由于 Meta 表格具有一定的数据量,导致了创建连接比较慢; 之后使用创建的连接获取 Table,这是一个轻量级的连接,只有在第一次创建的时候会 检查表格是否存在访问 RegionServer,之后在获取 Table 时不会访问 RegionServer;

(3)调用Table的put方法写入数据,此时还需要解析RowKey,对照缓存的MetaCache, 查看具体写入的位置有哪个 RegionServer;

(4)将数据顺序写入(追加)到 WAL,此处写入是直接落盘的,并设置专门的线程控 制 WAL 预写日志的滚动(类似 Flume);

(5)根据写入命令的 RowKey 和 ColumnFamily 查看具体写入到哪个 MemStory,并且 在 MemStory 中排序;

(6)向客户端发送 ack;

(7 )等达到 MemStore 的刷写时机后,将数据刷写到对应的 story 中。

4.4 刷写机制

MemStore 刷写由多个线程控制,条件互相独立:

主要的刷写规则是控制刷写文件的大小,在每一个刷写线程中都会进行监控:

(1)当某个 memstroe 的大小达到了 hbase.hregion.memstore.flush.size(默认值 128M), 其所在 region 的所有 memstore 都会刷写。

当 memstore 的大小达到了

hbase.hregion.memstore.flush.size(默认值 128M)

* hbase.hregion.memstore.block.multiplier(默认值 4)

时,会刷写,同时阻止继续往该 memstore 写数据(由于线程监控是周期性的,所有有可能面对数据洪峰,尽管可能性比较小)

(2)由 HRegionServer 中的属性 MemStoreFlusher 内部线程 FlushHandler 控制。标准为 LOWER_MARK(低水位线)和 HIGH_MARK(高水位线),意义在于避免写缓存使用过多的内 存造成 OOM

        当 region server 中 memstore 的总大小达到低水位线,region 会按照其所有 memstore 的大小顺序(由大到小)依次进行刷写。直到 region server 中所有 memstore 的总大小减小到上述值以下。

        当 region server 中 memstore 的总大小达到高水位线时,会同时阻止继续往所有的 memstore 写数据。

(3)为了避免数据过长时间处于内存之中,到达自动刷写的时间,也会触发 memstore flush。由 HRegionServer 的属性 PeriodicMemStoreFlusher 控制进行,由于重要性比较低,5min才会执行一次。自动刷新的时间间隔由该属性进行配置 hbase.regionserver.optionalcacheflushinterval(默认 1 小时)。

(4)当 WAL 文件的数量超过 hbase.regionserver.max.logs,region 会按照时间顺序依次 进行刷写,直到 WAL 文件数量减小到 hbase.regionserver.max.log 以下(该属性名已经废弃, 现无需手动设置,最大值为 32)。

4.5 读流程

4.5.1 HFile 结构

HFile 是存储在 HDFS 上面每一个 store 文件夹下实际存储数据的文件。

包括数据本身(keyValue 键值对)、元数据记录、文件信息、数据索引、元数据索引和 一个固定长度的尾部信息(记录文件的修改情况)。

        键值对按照块大小(默认 64K)保存在文件中,数据索引按照块创建,块越多,索引越 大。每一个 HFile 还会维护一个布隆过滤器(就像是一个很大的地图,文件中每有一种 key, 就在对应的位置标记,读取时可以大致判断要 get 的 key 是否存在 HFile 中)。

由于 HFile 存储经过序列化,所以无法直接查看。可以通过 HBase 提供的命令来查看存 储在 HDFS 上面的 HFile 元数据内容。

bin/hbase hfile -m -f /hbase/data/命名空间/表名/regionID/列族/HFile 名

4.5.2 读流程

4.5.3 合并读取数据优化

每次读取数据都需要读取三个位置,最后进行版本的合并。效率会非常低,所有系统需 要对此优化。

(1)HFile 带有索引文件,读取对应 RowKey 数据会比较快。

(2)Block Cache 会缓存之前读取的内容和元数据信息,如果 HFile 没有发生变化(记录在 HFile 尾信息中),则不需要再次读取。

(3)使用布隆过滤器能够快速过滤当前 HFile 不存在需要读取的 RowKey,从而避免读 取文件。(布隆过滤器使用 HASH 算法,不是绝对准确的,出错会造成多扫描一个文件,对 读取数据结果没有影响)

4.6 StoreFile Compaction

        由于 memstore 每次刷写都会生成一个新的 HFile,文件过多读取不方便,所以会进行文 件的合并,清理掉过期和删除的数据,会进行 StoreFile Compaction。

分为两种,分别是 Minor Compaction 和 Major Compaction

  • Minor Compaction 会将临近的若干个较小的 HFile 合并成一个较大的 HFile,并清理掉部分过期和删除的数据, 有系统使用一组参数自动控制,
  • Major Compaction 会将一个 Store 下的所有的 HFile 合并成 一个大 HFile,并且会清理掉所有过期和删除的数据,由参数 hbase.hregion.majorcompaction 控制,默认 7 天。

4.7 Region Split

Region 切分分为两种,创建表格时候的预分区即自定义分区,同时系统默认还会启动一 个切分规则,避免单个 Region 中的数据量太大。

每一个 region 维护着 startRow 与 endRowKey,如果加入的数据符合某个 region 维护的 rowKey 范围,则该数据交给这个 region 维护。那么依照这个原则,我们可以将数据所要投 放的分区提前大致的规划好,以提高 HBase 性能。

        Region 的拆分是由 HRegionServer 完成的,在操作之前需要通过 ZK 汇报 master,修改 对应的 Meta 表信息添加两列 info:splitA 和 info:splitB 信息。之后需要操作 HDFS 上面对 应的文件,按照拆分后的 Region 范围进行标记区分,实际操作为创建文件引用,不会挪动数据。刚完成拆分的时候,两个 Region 都由原先的 RegionServer 管理。之后汇报给 Master, 由Master将修改后的信息写入到Meta表中。等待下一次触发负载均衡机制,才会修改Region 的管理服务者,而数据要等到下一次压缩时,才会实际进行移动。

5 优化(调参)

5.1 RowKey 设计

        一条数据的唯一标识就是 rowkey,那么这条数据存储于哪个分区,取决于 rowkey 处于 哪个一个预分区的区间内,设计 rowkey的主要目的 ,就是让数据均匀的分布于所有的 region 中,在一定程度上防止数据倾斜。

  • 1)生成随机数、hash、散列值
  • 2)时间戳反转
  • 3)字符串拼接

5.4 HBase 使用经验法则

官方给出了权威的使用法则:

(1)Region 大小控制 10-50G

(2)cell 大小不超过 10M(性能对应小于 100K 的值有优化),如果使用 mob(Mediumsized Objects 一种特殊用法)则不超过 50M。

(3)1 张表有 1 到 3 个列族,不要设计太多。最好就 1 个,如果使用多个尽量保证不 会同时读取多个列族。

(4)1 到 2 个列族的表格,设计 50-100 个 Region。

(5)列族名称要尽量短,不要去模仿 RDBMS(关系型数据库)具有准确的名称和描述。

(6)如果 RowKey 设计时间在最前面,会导致有大量的旧数据存储在不活跃的 Region 中,使用的时候,仅仅会操作少数的活动 Region,此时建议增加更多的 Region 个数。

(7)如果只有一个列族用于写入数据,分配内存资源的时候可以做出调整,即写缓存 不会占用太多的内存。

6 整合 Phoenix

6.1 安装

Overview | Apache Phoenix

        Phoenix 在 5.0 版本默认提供有两种客户端使用(瘦客户端和胖客户端),在 5.1.2 版本 安装包中删除了瘦客户端,本文也不再使用瘦客户端。而胖客户端和用户自己写 HBase 的 API 代码读取数据之后进行数据处理是完全一样的。

解压、把里面的server包复制到hbase的lib

cp phoenix-server-hbase-2.4-5.1.2.jar /opt/module/hbase/lib/

幻景变量 

#phoenix

export PHOENIX_HOME=/opt/module/phoenix

export PHOENIX_CLASSPATH=$PHOENIX_HOME export PATH=$PATH:$PHOENIX_HOME/bin

重启 HBase

连接 Phoenix

/opt/module/phoenix/bin/sqlline.py hadoop1,hadoop2,hadoop3:2181

6.2 Phoenix Shell 操作

!table 或 !tables 显示所有表

创建表

直接指定单个列作为 RowKey

CREATE TABLE IF NOT EXISTS student(
id VARCHAR primary key,
name VARCHAR,
age BIGINT,
addr VARCHAR);

在 phoenix 中,表名等会自动转换为大写,若要小写,使用双引号,如"us_population"。

指定多个列的联合作为 RowKey

CREATE TABLE IF NOT EXISTS student1 (
id VARCHAR NOT NULL,
name VARCHAR NOT NULL,
age BIGINT,
addr VARCHAR
CONSTRAINT my_pk PRIMARY KEY (id, name));

注:Phoenix 中建表,会在 HBase 中创建一张对应的表。为了减少数据对磁盘空间的占 用,Phoenix 默认会对 HBase 中的列名做编码处理

若不想对列名编码,可在建表语句末尾加 上 COLUMN_ENCODED_BYTES = 0;

插入数据

upsert into student values('1001','zhangsan', 10, 'beijing');

查询记录

select * from student;

select * from student where id='1001';

删除记录

delete from student where id='1001';

删除表

drop table student;

退出命令行 !quit

6.3 表的映射

默认情况下, HBase 中已存在的表,通过 Phoenix 是不可见的。如果要在 Phoenix 中操 作 HBase 中已存在的表,可以在 Phoenix 中进行表的映射。

映射方式有两种:视图映射和表 映射。

        Phoenix 创建的视图是只读的,所以只能用来做查询,无法通过视图对数据进行修改等操作。在 phoenix 中创建关联 test 表的视图

create view "test"(
id varchar primary key,
"info1"."name" varchar, 
"info2"."address" varchar);

        在 Pheonix 创建表去映射 HBase 中已经存在的表,是可以修改删除 HBase 中已经存在 的数据的。而且,删除 Phoenix 中的表,那么 HBase 中被映射的表也会被删除。

注:进行表映射时,不能使用列名编码,需将 column_encoded_bytes 设为 0。

create table"test"(
id varchar primary key,
"info1"."name" varchar, 
"info2"."address" varchar) 
column_encoded_bytes=0;

6.4 数字类型说明

        HBase 中的数字,底层存储为补码,而 Phoenix 中的数字,底层存储为在补码的基础上, 将符号位反转。故当在 Phoenix 中建表去映射 HBase 中已存在的表,当 HBase 中有数字类型 的字段时,会出现解析错误的现象。

        Phoenix 种提供了 unsigned_int,unsigned_long 等无符号类型,其对数字的编码解 码方式和 HBase 是相同的,如果无需考虑负数,那在 Phoenix 中建表时采用无符号类型是最 合适的选择。

        如需考虑负数的情况,则可通过 Phoenix 自定义函数,将数字类型的最高位,即 符号位反转即可,自定义函数可参考如下链接:https://phoenix.apache.org/udf.html。???3202年还有bug

6.5 jdbc连接

    <dependency>
      <groupId>org.apache.phoenix</groupId>
      <artifactId>phoenix-client-hbase-2.4</artifactId>
      <version>5.1.2</version>
    </dependency>
package org.example;

import java.sql.*;
import java.util.Properties;

public class c5_jdbc {
    public static void main(String[] args) throws SQLException {
        String url = "jdbc:phoenix:hadoop1,hadoop2,hadoop3:2181";

        Properties properties = new Properties();

        Connection connection = DriverManager.getConnection(url, properties);

        PreparedStatement preparedStatement = connection.prepareStatement(
                "select * from student"
        );

        ResultSet resultSet = preparedStatement.executeQuery();

        while (resultSet.next()) {
            System.out.println(resultSet.getString(1) + resultSet.getString(2) + resultSet.getString(3));

        }

        connection.close();

    }
}

7 二级索引

7.1 二级索引配置文件

添加如下配置到 HBase 的 HRegionserver 节点的 hbase-site.xml。

<!-- phoenix regionserver 配置参数-->
<property>
  <name>hbase.regionserver.wal.codec</name>
  <value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value>
</property>

分发,重启hbase

7.2 全局索引(global index)

        Global Index 是默认的索引格式,创建全局索引时,会在 HBase 中建立一张新表。也就 是说索引数据和数据表是存放在不同的表中的,因此全局索引适用于多读少写的业务场景。

         写数据的时候会消耗大量开销,因为索引表也要更新,而索引表是分布在不同的数据节 点上的,跨节点的数据传输带来了较大的性能消耗。

在读数据的时候 Phoenix 会选择索引表来降低查询消耗的时间。

CREATE INDEX my_index ON my_table (my_col);
#例如
0: jdbc:phoenix:hadoop102,hadoop103,hadoop104> 
create index
my_index on student1(age);

#删除索引
DROP INDEX my_index ON my_table
0: jdbc:phoenix:hadoop102,hadoop103,hadoop104> 
drop index my_index
on student1;

如果想查询的字段不是索引字段的话索引表不会被使用,也就是说不会带来查询速度的 提升。若想解决上述问题,可采用如下方案:

(1)使用包含索引

创建携带其他字段的全局索引(本质还是全局索引)

(2)本地索引

Local Index 适用于写操作频繁的场景。 索引数据和数据表的数据是存放在同一张表中(且是同一个 Region),避免了在写操作 的时候往不同服务器的索引表中写索引带来的额外开销。

my_column 可以是多个。

CREATE LOCAL INDEX my_index ON my_table (my_column);

本地索引会将所有的信息存在一个影子列族中,虽然读取的时候也是范围扫描,但是没有全局索引快,优点在于不用写多个表了。

8 整合hive

在 hive-site.xml 中添加 zookeeper 的属性,如下:

<property>
 <name>hive.zookeeper.quorum</name>
 <value>hadoop102,hadoop103,hadoop104</value>
</property>
<property>
 <name>hive.zookeeper.client.port</name>
 <value>2181</value>
</property>

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值