HBase深入理解

可能有的图片显示不出来,如有需要,请留邮箱我发给你!

1、Hbase基本介绍

1.1 什么是HBase

  Hbase是一种分布式、可扩展、支持海量数据存储的NoSQL数据库

  • Nosql = no sql : NoSQL作为一种相比新型结构化存储单元,基本上不使用sql
  • Nosql = not only sql :不仅仅是sql,它不一定遵循传统数据库的一些基本要求

1.2 Hbase有什么特点

  1. **大:**一个表可以有上十亿行,上百万列
  2. **面向列:**列可以灵活指定,面向列(族)的存储和权限控制
  3. **稀疏:**对于为null的列,不占用空间
  4. **无严格模式:**每行都有一个可排序的主键和任意多的列,列可以根据需要动态的增加,同一行可以有不同列

1.3 Hbase表结构

image-20200630160528263 image-20200630160916787
  1. nameSpace: 命名空间,相当于关系型数据库的database概念。
  2. **region 😗*类似于关系型数据库表的概念,
  3. **row:**一行数据,每行数据由一个rowkey和多个Column(列)组成,数据是按照rowkey的字典顺序排序的
  4. Column: HBase中的每个列都由column family(列族)和Column Qualifier(列限定符)进行限定
  5. TimeStamp: 用户标识数据的不同版本(version),每条数据写入时,如果不指定,系统会自动为其加上该字段,其值为写入HBase的时间

1.4 Hbase基本架构

  从物理结构上,Hbase包含了三种类型的server:zookeeper,HMaster,HRegion server,采用一种主从模式的结构。

image-20200704122234296
  • region server主要用来服务读写操作。当用户通过client访问数据是,会和HBase Region server直接通信
  • HMaster主要进行regeion server的管理、DDL(创建、删除表)操作等
  • Zookeeper是HDFS的一部分,主要用来维持整个集群的存活,保障HA,故障自动转移

而底层的存储,还是依赖于HDFS的。

  • Hadoop的DataNode存储Region server所管理的数据,以hfile的形式,所有hbase的数据存储在hdfs中
  • Hadoop的NameNode维护所有物理数据块的metadata
1.4.1 Region Server

HBase的tables根据rowkey的范围进行水平切分,切分成region后分配到各个region server。一个region包含一个表在start key和end key所有行。而用户都是跟region server进行读写交互。一个region建议大小在5~10G。可通过参数 hbase.hregion.max.filesize去设置

image-20200704124127531
1.4.2 HBase Master

一般也叫做HMaster ,HMaster主要职责包括三个方面:

  • 与Region server的交互,对region server进行统一管理
  • 启动时region的分配 崩溃后恢复的region重新分配 负载均衡region的重新分配
  • 创建、删除、更新表结构的DDL操作
image-20200704124824569
1.4.3 Zookeeper

Hbase使用zookeeper作为分布式协调服务,来维护集群内的server状态。(通过heartbeat维护那些server是存活并可用的)。负责HMaster的选举工作,通过创建临时节点,并监控状态,来实现HA。

1.4.4 这些组件如何一起协调工作
image-20200704125641357

1)HMaster的高可用

  多个HMaster会去竞争zookeeper上的临时节点,而zookeeper会将第一个创建成功的HMaster作为当前active的HMaster,其他HMaster进入stand by的状态。这个active的HMaster会不断发送heartbeat给zk,其他stand by状态的HMaster节点会监听这个active HMaster的故障信息,一旦发现active HMaster宕机了,就会重新竞争新的active HMaster

2) HMaster监控HRegion Server

  每个regeion server会创建一个临时节点。HMaster会监控这些节点来确认那些region server是可用的,如果region server没有发送heartbeat给zk,zk会删除这个临时节点,其他监听这会受到这个故障节点被删除的信息。比如HMaster会监控RS的消息,如果发现某个RS下线了,那么就会重新分配RS来恢复相应的region数据。

2、深入理解HBase的原理

2.1 client 访问HBase的数据

  有一张特殊的HBase目录表,叫做META table,保存了集群中各个region的位置。zookeeper中保存了这样meta data的位置信息。(在0.96之前,zookeeper保存的是root table 里面存的是meta table的位置)

image-20200704162501890

基本流程如下:

  1. client 先访问zookeeper,获取meta表位于哪个region server中
  2. 访问对应的region server,获取.meta表,根据读请求的table/rowkey,查询目标数据位于哪个region server的哪个region 中,并将该table的region信息以及meta表的位置信息缓存在client的meta cache.
  3. 与目标region server进行通讯
  4. 分别在block cache(读缓存),memstore和store file(hfile)中查询目标数据,并将所有数据进行合并。
  5. 将合并的最终结果返回给客户端。

.meta. table的存储结构

  • meta table 保存了所有region信息的一张表
  • Meta table 存储的数据形式类似一颗b树
  • 以keyvalue形式保存数据
  • key:region的table name ,start key等信息Values:region server 的相关信息
image-20200704155853321

2.2 深入Region Server

一个region server运行在一个HDFS的data node上,并且拥有以下组件:

image-20200704160345058
  • WAL:全称Write Ahead Log,属于分布式系统上的文件。主要用来存储还未被持久化到磁盘的新数据。如果新数据还未持久化,节点发生宕机,那么就可以用WAL来恢复这些数据。
  • BlockCache:是一个读缓存。它存储了高频访问的数据。当这个缓存满了后,会清除最近最少访问的数据
  • MenStore:是一个写缓存。它存储了还未被写入磁盘的数据。它在写入磁盘前,对自身数据进行排序,从而保证数据的顺序写入。每个region的每个column family 会对应一份mensotre
  • HFiles:按照字典排序存储各个row的键值

2.3 Client 写数据到HBase

image-20200704161748324

基本流程:

  1. client 先访问zookeeper,获取.meta表位于哪个region server
  2. 访问对应的region server,获取.meta表,根据读请求的namespace:table/rowkey,查询出目标数据位于哪个region server的哪个region 中。并将该table的region信息以及meta表的位置信息缓存在客户端得meta cache,方便下次访问。
  3. 与目标Region server进行通讯
  4. 将数据顺序写入(追加)到WAL
  5. 将数据写入对应的MemStore,数据会在Memstore进行排序
  6. 向客户端发送ack
  7. 等达到Memsotre的刷写时机后,将数据写到HFile

2.4 MenStore Flush

Memstore刷写时机:

  1. 当某个memstore的大小达到了hbase.hregion.memsotre.flush.size(默认128M)

  2. 当region server中 memstore的总大小达到

    java_headpsize*hbase.regionserver.global.memstore.size(默认值0.4) *hbase.regionserver.global.memsotre.size.lower.limit(默认值0.5)
    

    region会按照其所有memstore的大小顺序(由大到下)一次进行刷写。直到region server中所有memstore的总大小减小到上述值一下。

  3. 到达自动刷写时间,也会触发memstore。hbase.regionserver.optionalcacheflushinterval(1h)

  4. 当region server中的memsotre的总大小达到以下值时时,会阻止往所有的memstore写数据。

java_heapsize*hbase.regionserver.global.memstore.size(默认值0.4)

2.5 深入 HFile

在HBase中,数据以有序kv的形式,存储在HFile中。当MemStore存储足够的数据,全部kv对被写入HFile存入HDFS。这里写文件时顺序写,避免了磁盘大量移动磁头的过程,比随机写高效很多。

image-20200704172141937

HFile的逻辑结构主要包括6个部分

  1. Data Block段:保存表中的数,这部分可以被压缩
  2. Meta block段(可选的):保存用户自定义的kv对,可以被压缩
  3. File info : HFile的元数据信息,不被压缩,用户也可以在这一部分添加自己的元信息
  4. Data block index: Data block的索引。每条索引的key是被索引的block的第一条记录的key
  5. Meta block index(可选的): Meta block 的索引
  6. Trailer: 这一段是定长的。保存了每一段的偏移量和寻址信息,还包括其他信息,比如布隆过滤器和时间范围信息。读取一个HFile时,先读取Trailer,找到datablock index段的微信并读取到内存中,当检索某个key时,不需要扫描整个Hfile 。

2.6 StoreFile Compaction

2.6.1 为什么需要Compactioin

   由于memstore每次刷写都会生成一个新的HFile,且同一个字段的不同版本(timestamp)和不同类型(put/delete)有可能会分布在不同的HFile中,因此查询时需要遍历所有的HFile。为了减少HFile的文件个数,清理过期数据,所以HBase提供了Compaction机制,将小文件合并成大文件,目的是提高查询性能。compaction分为两种 :Minor Compaction和major Compaction

2.6.2 Minor Compaction和major Compaction
  • Minor Compaction会将临近的若干个较小的HFile合并成一个较大的Hfile,但不会清理过期和删除数据
  • Major Compcation会将一个Store下的所有HFile合并成一个大HFile,并且会清理掉过期和删除的数据。
image-20200704180112936
2.6.3 触发条件
  1. MemStore Flush :应该说compaction操作源头就来自flush操作,memstore flush 会产生Hfile文件,文件越多就需要compaction。一旦文件数>=hbase.hstore.compaction.min (default is 3),就会触发compaction。
  2. 后台线程周期性检查:后台线程compaction checker定期触发检查是否需要执行compaction,检查周期为hbase.server.thread.wakefrequency*hbase.server.compactchecker.interval.multiplier
  3. 手动触发:一般来讲,手动触发compaction通常是为了执行major compaction。原因有三:1)很多业务担心自动major compaction影响读写性能 2)用户执行完alter操作后希望立刻生效 3)hbase管理员发现磁盘容量不够的情况下手动触发major compaction删除大量过期数据。

2.7 Region split

  默认情况下,每个Table起初只有一个Region,随着数据的不断写入,Region会自动进行拆分。刚拆分时,两个子Region 都位于当前的 RS,但处于负载均衡考虑,HMaster有可能会将某个region转移给其他的Region server

Region split时机:

  1. 当1个region中的某个store下所有storefile的总大小超过hbase.hregion.max.filesize(默认是10G),该Region就会进行拆分(0.94版本之前)
  2. 当1个region中的某个store下所有storefile的总大小超过min(R ^2 * hbase.hregion.memstore.flush.size,hbase.hregion.max.filesize),该region就会进行拆分,其中R为当前Region Server中属该Table的个数(0.94版本之后)

2.8 Region Merge

  • Region的合并不是为了性能,而是出于维护的目的
  • 比如删除大量的数据,这个时候每个Region都变的很小,存储多个Region就浪费了,这个时候可以把Region合并起来,进而减少一些Region服务器节点

实现方法

  1. 通过Merge类冷合并Region

    1. 执行前,需要关闭HBase集群
    2. 直接执行 Hase org.apache.hadoop.hbase.util.Merge region_name
  2. 通过online_mege 热合并Region

    1. 在线进行合并

    2. online_merge传参是Region的hash值,而Region的hash值就是region名称最后那段在两个.之间的字符串部分

    3. 需要进入Hbase shell

      merge_region 'c2212a3956b814a6f0d57a90983a8515','553dd4db667814cf2f050561167ca030'
      

3、HBase的shell 命令 和 API

3.1 Hbase 的shell命令

1、显示HBase的表列表  list
2、创建表  create '表名','列族','列族' ..
3、查看表的信息: desc '表名'
4、往表里插入数据:  put '表名','rowkey','cell...'
5、查询数据 : get '表名','rowkey'
6、查询数据 : scan '表名'
7、删除数据: delte '表名','rowkey'
8、修改表结构: alter 
9、修改数据: hbase没有修改数据的显示操作,重复插入就相当于是修改数据
10、清空表 truncate '表名' ,首先要disable 表  启动表:enable table_name
11、删除表 drop table , 先要disable12、过滤器操作:
get 'user', 'rk0001', {FILTER => "ValueFilter(=, 'binary:中国')"}
get 'user', 'rk0001', {FILTER => "(QualifierFilter(=,'substring:a'))"}

3.2 HBase API

public class HbaseUtil {
    public static Configuration conf = null ;
    public static Connection connection = null ;
    public static Admin admin = null ;
    /*
    * @desc 取得连接
    * */
    public static void setConf(String quorum,String port){
        try {
            conf = HBaseConfiguration.create();
            conf.set("hbase.zookeeper.quorum",quorum); //zookeeper地址  qyl01,qyl02,qyl03
            conf.set("hbase.zookeeper.property.clientPort",port);  //2181 
            connection = ConnectionFactory.createConnection(conf);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /*
    * @desc 关闭连接
    * */
    public static void closeHBaseConnection(){
        try {
            if (connection != null) {
                connection.close();
            }
            if(admin != null){
                admin.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    /*
    * @desc 创建表
    * */
    public static void createTable(String tableName,String columnFamily){
        try {
            TableName tbName = TableName.valueOf(tableName);
            if(!admin.tableExists(tbName)){
                HTableDescriptor hTableDescriptor = new HTableDescriptor(tbName);
                HColumnDescriptor hColumnDescriptor = new HColumnDescriptor(columnFamily);
                hTableDescriptor.addFamily(hColumnDescriptor);
                admin.createTable(hTableDescriptor);
            }else{
                System.out.println(tableName+" exists !");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /*
    * @desc 添加多条记录
    * */
    public static void addMoreCord(String tableName, String columnFamily, String qualifier, List<String> rowList, String value){
        Table table = null ;
        try{
            table = connection.getTable(TableName.valueOf(tableName));
            List<Put> puts = new ArrayList<Put>();
            Put put = null;
            for(int i = 0; i < rowList.size(); i++){
                put = new Put(Bytes.toBytes(rowList.get(i)));
                put.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(qualifier),Bytes.toBytes(value));
                puts.add(put);
            }
            table.put(puts);
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            if(table != null){
                try {
                    table.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
    /*
     * @desc 查询rowkey下多列值,一起返回
     * */
    public static String[] getValue(String tableName,String rowKey,String family,String[] qualifier){
        Table table = null;
        try {
            table = connection.getTable(TableName.valueOf(tableName));
            Get get = new Get(rowKey.getBytes());
            //返回指定列族、列名,避免rowKey下所有数据
            get.addColumn(family.getBytes(), qualifier[0].getBytes());
            get.addColumn(family.getBytes(), qualifier[1].getBytes());
            Result rs = table.get(get);
            // 返回最新版本的Cell对象
            Cell cell = rs.getColumnLatestCell(family.getBytes(), qualifier[0].getBytes());
            Cell cell1 = rs.getColumnLatestCell(family.getBytes(), qualifier[1].getBytes());
            String[] value = new String[qualifier.length];
            if (cell!=null) {
                value[0] = Bytes.toString(CellUtil.cloneValue(cell));
                value[1] = Bytes.toString(CellUtil.cloneValue(cell1));
            }
            return value;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        } finally {
            if (table!=null){
                try {
                    table.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
    /*
     * @desc 查询rowkey下某一列值
     */
    public static String getValue(String tableName, String rowKey, String family, String qualifier) {
        Table table = null;
        try {
            table = connection.getTable(TableName.valueOf(tableName));
            Get get = new Get(rowKey.getBytes());
            //返回指定列族、列名,避免rowKey下所有数据
            get.addColumn(family.getBytes(), qualifier.getBytes());
            Result rs = table.get(get);
            Cell cell = rs.getColumnLatestCell(family.getBytes(), qualifier.getBytes());
            String value = null;
            if (cell!=null) {
                value = Bytes.toString(CellUtil.cloneValue(cell));
            }
            return value;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (table!=null){
                try {
                    table.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /*
     * 全表扫描
     * @param tableName
     * @return
     * @desc qualifier 指的是某列
     */
    public static ResultScanner scan(String tableName,String family,String qualifier) {
        Table table = null;
        try {
            table = connection.getTable(TableName.valueOf(tableName));
            Scan scan = new Scan();
            ResultScanner rs = table.getScanner(scan);
//			一般返回ResultScanner,遍历即可
//			if (rs!=null){
//				String row = null;
//				String quali = null;
//    			String value = null;
//				for (Result result : rs) {
//					row = Bytes.toString(CellUtil.cloneRow(result.getColumnLatestCell(family.getBytes(), qualifier.getBytes())));
//					quali =Bytes.toString(CellUtil.cloneQualifier(result.getColumnLatestCell(family.getBytes(), qualifier.getBytes())));
//					value =Bytes.toString(CellUtil.cloneValue(result.getColumnLatestCell(family.getBytes(), qualifier.getBytes())));
//					System.out.println(row+"-"+quali+"-"+value);
//				}
//			}
            return rs;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (table!=null){
                try {
                    table.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

}

3.3 HBase 过滤器

3.3.1 说明

  要完成一个过滤操作,至少需要两个参数,一个是抽象操作符,另一个就是具体的比较器了(Comparator)

抽象操作符

LESS <
LESS_OR_EQUAL <=
EQUAL =
NOT_EQUAL <>
GREATER_OR_EQUAL >=
GREATER >
NO_OP 排除所有

比较器(指定比较机制)

BinaryComparator 按字节索引顺序比较指定字节数组,采用 Bytes.compareTo(byte[])
SubstringComparator 判断提供的子串是否出现在 value 中
3.3.2 分类
  • 行键过滤器 rowFilter
  • 列簇过滤器 familyFilter
  • 列过滤器 QualifierFilter
  • 值过滤器 valueFilter
  • 时间过滤器 TimeStampsFilter
  • 单列值过滤器 SingleColumnValueFilter --会返回满足条件的整行
3.3.3 使用
Filter rowFilter = new RowFilter(CompareOp.GREATER, new BinaryComparator("95007".getBytes()));
scan.setFilter(rowFilter);

4、HBase调优

4.1 表的设计

1、pre-Creating-Regions(预分区)

  默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到很大的时候才进行切分,提前创建分区,可以加快批量数据写入。(列如以手机号)

2、Row Key的设计

  越小越好,rowkey的设计是要根据实际业务来 ,散列性(取反,用hash值对某个数取余)

3、column family

  不要在一张表里定义太多的column family,建议2~3个,因为某个column family在flush的时候,它临近的column family 也会因关联效应被触发flush,最终导致系统产生更多个I/0。

4、compaction & Split

  关闭 hbase.hregion.majorcompaction(默认7天),手动编写脚本避开业务高峰期进行major compaction

5、BlockCache

  一个rs上的block和memstore大小不能超过heapsize * 0.8 ,对于注重读响应时间的系统,可以将blockcache设置大写 如 blockcache=0.4 memstore = 0.39 ,以加大缓存命中率(默认blockcache=0.2 memstore=0.4)

4.2 写表的优化

1、多个HTable并发写: 创建多个HTable客户端用于写操作,提高写数据的吞吐量

2、使用bulkload写入

3、Auto Flash: 调用HTable.setAutoFlushTo(false) 方法可以将HTable写客户端自动flush关闭,默认是开启的

4、Compression压缩:推荐使用snappy hcd.setCompressionType(Algorithm.SNAPPY)

4.3 读表优化

1、批量读 : 通过调用HTable.get(List)方法可以根据一个指定的rowkey列表,批量获取多行记录,减少IO

2、缓存查询的结果:创建表的时候,可以通过HColumnDEscriptor.setInMemory(true)建表放在rs的缓存中。

5、Hbase的bulkLoad

  简单来说 Bulkload 就是利用 HBase 的数据信息按照特定格式存储在 HDFS 内这一原理,直接在 HDFS 中生成持久化的 HFile 数据格式文件,然后上传至合适位置,即完成巨量数据快速入库的办法。

实现步骤:

  1. 从数据源(文本文件或其他的数据库)提取数据并上传到HDFS
  2. 利用MapReduce作业处理先准备的数据,需要我们编写map函数,reduce函数由Hbase提供,rowkey作为输出key,keyvalue,put或者delete作为输出value。需要HFileOutputFormat2来生成HBase数据文件。
  3. 告诉Region Servers数据的位置并导入数据。通常使用 LoadIncrementalHFiles

代码

使用mapReduce生成HFile文件

public class IteblogBulkLoadMapper extends Mapper<LongWritable, Text, ImmutableBytesWritable, Put>{
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            String line = value.toString();
            String[] items = line.split("\t");
  
            ImmutableBytesWritable rowKey = new ImmutableBytesWritable(items[0].getBytes());
            Put put = new Put(Bytes.toBytes(items[0]));   //ROWKEY
            put.addColumn("f1".getBytes(), "url".getBytes(), items[1].getBytes());
            put.addColumn("f1".getBytes(), "name".getBytes(), items[2].getBytes());
            
            context.write(rowkey, put);
        }
}

通过BlukLoad方式加载HFile文件

public class LoadIncrementalHFileToHBase {
    public static void main(String[] args) throws Exception {
            Configuration configuration = HBaseConfiguration.create();
            HBaseConfiguration.addHbaseResources(configuration);
            LoadIncrementalHFiles loder = new LoadIncrementalHFiles(configuration);
            HTable hTable = new HTable(configuration, "blog_info");
            loder.doBulkLoad(new Path("hdfs://iteblog:9000/user/iteblog/output"), hTable);
    }
}

驱动程序

public class IteblogBulkLoadDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        final String SRC_PATH= "hdfs://iteblog:9000/user/iteblog/input";
        final String DESC_PATH= "hdfs://iteblog:9000/user/iteblog/output";
        Configuration conf = HBaseConfiguration.create();
       
        Job job=Job.getInstance(conf);
        job.setJarByClass(IteblogBulkLoadDriver.class);
        job.setMapperClass(IteblogBulkLoadMapper.class);
        job.setMapOutputKeyClass(ImmutableBytesWritable.class);
        job.setMapOutputValueClass(Put.class);
        job.setOutputFormatClass(HFileOutputFormat2.class);
        HTable table = new HTable(conf,"blog_info");
        HFileOutputFormat2.configureIncrementalLoad(job,table,table.getRegionLocator());
        FileInputFormat.addInputPath(job,new Path(SRC_PATH));
        FileOutputFormat.setOutputPath(job,new Path(DESC_PATH));
          
        System.exit(job.waitForCompletion(true)?0:1);
    }
}

6、HBase面试题

  1. Hbase读写流程

  2. rowkey设计

  3. 简述Hbase filter的实现原理是什么?

  4. 简述Hbase性能优化的思路。

  5. Hbase和传统数据库的区别?

  6. hbaes 的bulkload

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值