欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

大数据面试之HBase

程序员文章站 2022-07-06 10:50:37
...


说明,感谢亮哥长期对我的帮助,此处多篇文章均为亮哥带我整理。以及参考诸多博主的文章。如果侵权,请及时指出,我会立马停止该行为;如有不足之处,还请大佬不吝指教,以期共同进步。

1.HBase

1.1 HBase的架构模型?

1、HMaster
    负责管理HBase元数据,即表的结构、表存储的Region等元信息。
    负责表的创建,删除和修改(因为这些操作会导致HBase元数据的变动)。
    负责为HRegionServer分配Region,分配好后也会将元数据写入相应位置。

2、HRegionServer
    一个RegionServer里有多个Region。
    处理Client端的读写请求(根据从HMaster返回的元数据找到对应的Region来读写数据)。
    管理Region的Split分裂、StoreFile的Compaction合并。

一个RegionServer管理着多个Region,在HBase运行期间,可以动态添加、删除HRegionServer。

3、HRegion
    一个HRegion里可能有1个或多个Store。
    HRegionServer维护一个HLog。
    HRegion是分布式存储和负载的最小单元。
    表通常被保存在多个HRegionServer的多个Region中。

4、Store
    Store是存储落盘的最小单元,由内存中的MemStore和磁盘中的若干StoreFile组成。
    一个Store里有1个或多个StoreFile和一个memStore。
    每个Store存储一个列族。

参考

Region是HBase数据存储和管理的基本单位。在HBase的一个表中,可以包含一个或多个region。

对于一个region,每个列族都会对应一个store,用来存储该列族的数据。

每个store都有一个写缓存memstore,用于缓存写入的数据。

1.2 HBase读写流程

1.2.1 写过程

(1) Client先从缓存中定位region,如果没有缓存则需访问zookeeper,从.META.表获取要写入的region信息

(2) 找到小于rowkey并且最接近rowkey的startkey对应的region

(3) 将更新写入WAL中。当客户端发起put/delete请求时,考虑到写入内存会有丢失数据的风险,
    因此在写入缓存前,HBase会先写入到Write Ahead Log(WAL)中(WAL存储在HDFS中),
    那么即使发生宕机,也可以通过WAL还原初始数据。
    
(4) 将更新写入memstore中,当增加到一定大小,达到预设的Flush size阈值时,会触发flush memstore,
    把memstore中的数据写出到hdfs上,生成一个storefile。
    
(5) 随着Storefile文件的不断增多,当增长到一定阈值后,触发compact合并操作,
    将多个storefile合并成一个,同时进行版本合并和数据删除。
    
(6) storefile通过不断compact合并操作,逐步形成越来越大的storefile。

(7) 单个stroefile大小超过一定阈值后,触发split操作,把当前region拆分成两个,
    新拆分的2个region会被hbase master分配到相应的2个regionserver上。

1.2.2 读过程

(1) Client先从缓存中定位region,如果没有缓存则需访问zookeeper,查询.-ROOT-.表,
    获取.-ROOT-.表所在的regionserver地址。

(2) 通过查询.-ROOT-.的region服务器 获取 含有.-META-.表所在的regionserver地址。

(3) clinet会将保存着regionserver位置信息的元数据表.META.进行缓存,
    然后在表中确定待检索rowkey所在的regionserver信息。

(4) client会向在.META.表中确定的regionserver发送真正的数据读取请求。

(5) 先从memstore中找,如果没有,再到storefile上读。

参考

1.3 HBase查询速度为什么快

采用了LSM树型结构(日志结构合并树),而不是B或B+树。

B+树最大的性能问题是会产生大量的随机I/O,低下的磁盘寻道速度。

为了读性能的提高,数据在磁盘中必须有序,这就是B+树的原理。但是写就悲剧了,key跨度很大的情况,
新插入的数据存储在磁盘上相隔很远的地方,会产生大量的随机IO,磁盘寻道速度跟不上。

对于LSM树来说,牺牲了部分读性能,来大幅提高写性能。

原理是:将一颗大树拆分成N颗小树,首先写入内存(内存中的数据是有序的,内存没有寻道速度的问题,随机写的性能得到大幅提升),
在内存中构建一颗有序小树(memstore),随着其越来越大,flush到磁盘上,成为一个storefile,该storefile中的数据是有序的。

参考

1.4 Hbase数据热点的措施

hbase的表的多个region中有一个region的读写并发很高,其他的region相对来说读写少,造成热点的region
防止数据热点的有效措施
1. 加盐
这里所说的加盐不是密码学中的加盐,而是在 rowkey 的前面增加随机数,具体就是给rowkey 分配一个随机前缀以使得它和之前的rowkey 的开头不同。分配的前缀种类数量应该和你想使用数据分散到不同的 region 的数量一致。加盐之后的 rowkey就会根据随机生成的前缀分散到各个region上,以避免热点。
2.哈希
哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的 rowkey,可以使用 get 操作准确获取某一个行数据
3. 反转
第三种防止热点的方法是反转固定长度或者数字格式的 rowkey。这样可以使得 rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机 rowkey,但是牺 牲了 rowkey 的有序性。
反转 rowkey 的例子以手机号为 rowkey,可以将手机号反转后的字符串作为 rowkey,这 样的就避免了以手机号那样比较固定开头导致热点问题
4. 时间戳反转
一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为 rowkey的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp 追加到 key 的末尾,例 如 [key][reverse_timestamp] , [key] 的最新值可以通过 scan [key]获得[key]的第一条记录,因 为 HBase 中 rowkey 是有序的,第一条记录是最后录入的数据。比如需要保存一个用户的操作记录,按照操作时间倒序排序,在设计 rowkey 的时候,可以这样设计[userId 反转][Long.Max_Value - timestamp],在查询用户的所有操作记录数据的时候,直接指 定反转后的 userId,startRow 是[userId 反转][000000000000],stopRow 是[userId 反 转][Long.Max_Value - timestamp]如果需要查询某段时间的操作记录,startRow 是[user 反转][Long.Max_Value - 起始时间], stopRow 是[userId 反转][Long.Max_Value - 结束时间]


参考
参考

1.5 HBase的特点是什么?

(1)hbase是一个分布式的基于列式存储的数据库,基于hadoop的HDFS存储,zookeeper进行管理。

(2)hbase适合存储半结构化或非结构化数据,对于数据结构字段不够确定或者杂乱无章很难按一个概念去抽取的数据。

(3)hbase为null的记录不会被存储。

(4)基于的表包括rowkey,时间戳和列族。新写入数据时,时间戳更新,同时可以查询到以前的版本。

(5)hbase是主从结构。Hmaster作为主节点,hregionserver作为从节点。

1.6 HBase中region太小和region太大带来的结果

Region过大会发生多次compaction,将数据读一遍并写一遍到hdfs上,占用io

Region过小会造成多次split,region会下线,影响访问服务,调整hbase.heregion.max.filesize为256m。

1.6 JAVA访问HBase、JPA访问HBase

JAVA访问HBase

 //第一步,设置HBsae配置信息
        Configuration configuration = HBaseConfiguration.create();    
        //注意。这里这行目前没有注释掉的,这行和问题3有关系  是要根据自己zookeeper.znode.parent的配置信息进行修改。
       configuration.set("zookeeper.znode.parent","/hbase-unsecure"); //与 hbase-site-xml里面的配置信息 zookeeper.znode.parent 一致  
        configuration.set("hbase.zookeeper.quorum","192.168.8.30");  //hbase 服务地址
        configuration.set("hbase.zookeeper.property.clientPort","2181"); //端口号
        //这里使用的是接口Admin   该接口有一个实现类HBaseAdmin   也可以直接使用这个实现类
        // HBaseAdmin baseAdmin = new HBaseAdmin(configuration);
        Admin admin = ConnectionFactory.createConnection(configuration).getAdmin();
        if(admin !=null){
            try {
                //获取到数据库所有表信息
                HTableDescriptor[] allTable = admin.listTables();
                for (HTableDescriptor hTableDescriptor : allTable) {
                    System.out.println(hTableDescriptor.getNameAsString());
                }
            }catch (IOException e) {
                e.printStackTrace();
            }

参考
参考
参考
参考
参考
使用kundera jpa操作hbase

kundera是一个兼容jpa接口的对象映射器。当前kundera支持的数据库有:
Cassandra,MongoDB,HBase,Redis,OracleNoSQL,Neo4j,CouchDB,Dudu,Relational databases,Apache Spark

persistence.xml内容如下  
<!--指定供应商地址,固定的使用kundera  -->
<provider>com.impetus.kundera.KunderaPersistence</provider>
<!-- 指定映射的类的全路径 -->
<class>com.zxz.entity.Person</class>
<properties>
<!-- 配置zookeeper节点服务器ip -->
<property name="kundera.nodes" value="192.168.14.125"/>
<!-- 配置zookeeper rpc连接端口 -->
<property name="kundera.port" value="2181"/>
<!-- 配置hbase名字空间和表内容 -->
<property name="kundera.keyspace" value="zxz:testZ"/>
<!-- 配置hbase方言 -->
<property name="kundera.dialect" value="hbase"/>
<!-- 配置hbase依赖类 -->
<property name="kundera.client.lookup.class" value="com.impetus.client.hbase.HBaseClientFactory"/>
</properties>
编写实体类也就是上面的配置映射的类,hbase的表一一对应
@Entity
//指定hbase表中的列族
@Table(name="zxz")
public class Person {
//指定rowkey
@Id	
private int id;
//指定列
@Column
private String name;
@Column
private int age; 
public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public String toString() {
		return "Person [age=" + age + ", id=" + id + ", name=" + name + "]";
	}
}

首先在代码前介绍1个类和两个接口,他们就是来衔接hbase表和实体类进行底层实现的
1,Persistence(持久)类
   引导类,用于在Java SE环境中获取EntityManagerFactory接口。
2,EntityManagerFactory(实体管理工厂)接口
   用于与持久单元的实体管理器工厂交互的接口。
3,ManagerFactory(实体管理)接口
   用于与持久上下文交互的接口EntityManager实例与持久化上下文关联。持久性上下文是一组实体类实例,其 
   中对于任何持久实体标识,都有一个惟一的实体类实例。在持久化上下文中,管理实体实例及其生命周期。 
   EntityManager API用于创建和删除持久实体实例,通过其主键查找实体,并查询实体。前面两个都是为这
   接口铺垫的。
   
以上都是从源码摘下来的

public class hbaseJpa {
//CRUD(增C删D改U查R)
//1,C 增加
	@Test
	public void insert() {
		// 创建出实体管理对象进行对对象的操作来实现前两行是固定写法
		// persistence类调用抽象方法createEntityManagerFactory("参数为当前项目名")方法来获取EntityManagerFactory接口
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("mykundera");
		// 大家可能会有疑惑说接口能调用方法呢,有空的同学可以看一下他其实返回的是EntityManagerFactoryImpl这个实现类,
		// 只不过利用了多态的方式返回了实现类的接口,其实和List<> list=new ArrayList<>();差不多
		// EntityManger接口其实和EntityManagerFactory一样的实现方式都是利用多态方式调用createEntityManager()方法获取来的
		EntityManager em = emf.createEntityManager();
		// 创建person实体类对象,下面的操作打家都很熟悉了,给属性赋值,反过来想就是设置rowkey,列,值
		Person p = new Person();
		p.setId(2);
		p.setName("cat");
		p.setAge(20);
		// persist方法就是传入一个对象,他会判断是否存在,如果存在抛异常,否则创建对象实例,也就是把值添加到hbase相应的表中
		em.persist(p);
		// 关闭实体管理接口
		em.close();
	}
 
//2,D 删除
	@Test
	public void delete() {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("mykundera");
		EntityManager em = emf.createEntityManager();
		Person p = new Person();
		// 如果不指定属性值,他会把这个整个实体上指定的列族数据全删掉
		// em.remove(p);
		p.setId(2);
		p.setAge(19);
		p.setName("cat");
		em.close();
	}
 
//3,U 修改
	@Test
	public void update() {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("mykundera");
		EntityManager em = emf.createEntityManager();
		Person p = new Person();
		p.setId(2);
		p.setAge(50);
		// 必须指定rowkey和列,调用merge("实体类.class")
		em.merge(Person.class);
		em.close();
	}
 
//4,R 查
	@Test
	public void select() {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("mykundera");
		EntityManager em = emf.createEntityManager();
		// 调用find(实体类.class,rowkey)方法返回实体类对象,直接打印对象就行了,因为我重写过toString了
		Person p = em.find(Person.class, 1);
		System.out.println(p);
		em.close();
	}
 
//使用querysql方式高级查询,面向对象的sql语言
	@Test
	public void selectSql() {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("mykundera");
		EntityManager cm = emf.createEntityManager();
		// 调用createQuery("面向对象的sql表名是对象名,列是属性")
		Query query = cm.createQuery("select p from Person p where p.id=2");
		List<Person> re = query.getResultList();
		for (Person person : re) {
			System.out.println(person);
		}
	}
 
//使用过滤器实现querysql
	@Test
	public void filterselectSql() {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("mykundera");
		EntityManager cm = emf.createEntityManager();
		// 使用filter过滤器步骤
		// 1。创建EntityManager实例并使用getDelegate方法来获取客户机的映射
		Map<String, Client> clli = (Map<String, Client>) cm.getDelegate();
		// 2。从hbase持久化单元的客户机映射中获取客户机。
		HBaseClient hc = (HBaseClient) clli.get("mykundera");
		// 3。最后,想要使用的任何过滤器创建一个过滤器对象,并将其设置为HbaseClient对象:
		// 值过滤,使用小于比较符,使用的字节比较器过滤小于2下的数据,之前博客有专门讲过过滤器使用
		ValueFilter vf = new ValueFilter(CompareOp.LESS, new BinaryComparator(Bytes.toBytes("10")));
		hc.setFilter(vf);
		Query query = cm.createQuery("select p from Person p ");
		List<Person> re = query.getResultList();
		for (Person person : re) {
			System.out.println(person);
		}
	}

参考

1.7 Hbase行键列族的概念,物理模型,表的设计原则?

行键:是hbase表自带的,每个行键对应一条数据。

列族:是创建表时指定的,为列的集合,每个列族作为一个文件单独存储,存储的数据都是字节数组,其中数据可以有很多,通过时间戳来区分。

物理模型:整个hbase表会拆分成多个region,每个region记录着行键的起始点保存在不同的节点上,查询时就是对各个节点的并行查询,
    当region很大时使用.META表存储各个region的起始点,-ROOT又可以存储.META的起始点。

Rowkey的设计原则:各个列族数据平衡,长度原则、相邻原则,创建表的时候设置表放入regionserver缓存中,避免自动增长和时间,
    使用字节数组代替string,最大长度64kb,最好16字节以内,按天分表,两个字节散列,四个字节存储时分毫秒。

列族的设计原则:尽可能少(按照列族进行存储,按照region进行读取,不必要的io操作),经常和不经常使用的两类数据放入不同列族中,列族名字尽可能短。

参考

相关标签: NoSQL Hbase