HBase

  1. 简介
    1. 概述
      1. HBASE是Apache提供的基于Hadoop的、分布式的、可扩展的、能够存储大量数据的非关系型数据库
      2. 基于Google的论文《BigTable》设计实现
      3. 区别于传统数据库的行存储,HBASE是面向列进行存储,底层基于Key-Value结构存储
      4. HBASE能够提供低延迟的数据查询能力,其原因是底层充分利用了缓存机制以及复杂的数据结构和算法来实现
    2. 行存储和列存储
      1. 行存储在磁盘上的存储是连续的;列存储在磁盘上的存储是不连续的
      2. 从写入性能上对比,写入次数越少性能越高。因为针对磁盘的每一次写入,都要发生磁头调度,产生寻道时间。因为行存储是只写一次而列存储要写多次,所以行存储在写入性能上更有优势
      3. 从读取性能上对比:
        1. 如果读取的是整表,则行存储性能较高
        2. 如果是读取指定的列,则行存储会产生冗余列,而冗余列的消除是在内存中发生。而列存储则不会存在冗余列
      4. 在存储数据的时候,如果基于行存储,由于一行数据的字段类型可能不同,所以会产生频繁的数据类型转换;如果是基于列存储,由于同一列数据的类型一般一致,则可以避免频繁的数据类型转换,同时可以考虑一些更好的压缩算法对一列数据进行压缩
    3. 特点
      1. 分布式架构:HBASE是通过集群来存储数据,数据最终要落地到HDFS上
      2. 是一种NoSQL的非关系型数据库,不符合关系型数据库的范式
      3. 面向列存储,底层基于key-value结构
      4. 适合存储半结构化、非结构化的数据
      5. 适合存储稀疏的数据,空的数据不占用空间
      6. 提供实时的增删改查的能力,但是不提供严格的事务机制,只能在行级别提供事务
    4. 关系型和非关系型数据库
      1. 传统关系型数据库的缺陷:
        1. 高并发读写的瓶颈:Web 2.0网站要根据用户个性化信息来实时生成动态页面和提供动态信息,所以基本上无法使用静态化技术,因此数据库并发负载非常高,可能峰值会达到每秒上万次读写请求。关系型数据库应付上万次SQL查询还勉强顶得住,但是应付上万次SQL写数据请求,硬盘I/O却无法承受。其实对于普通的BBS网站,往往也存在相对高并发写请求的需求,例如,人人网的实时统计在线用户状态,记录热门帖子的点击次数,投票计数等,这是一个相当普遍的业务需求
        2. 可扩展性的限制:在基于Web的架构中,数据库是最难以进行横向扩展的,当应用系统的用户量和访问量与日俱增时,数据库系统却无法像Web Server和App Server那样简单地通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供24小时不间断服务的网站来说,对数据库系统进行升级和扩展是非常痛苦的事情,往往需要停机维护和数据迁移,而不能通过横向添加节点的方式实现无缝扩展
        3. 事务一致性的负面影响:事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。保证数据库一致性是指当事务完成时,必须使所有数据都具有一致的状态。在关系型数据库中,所有的规则必须应用到事务的修改上,以便维护所有数据的完整性,这随之而来的是性能的大幅度下降。很多Web系统并不需要严格的数据库事务,对读一致性的要求很低,有些场合对写一致性要求也不高。因此数据库事务管理成了高负载下的一个沉重负担
        4. 复杂SQL查询的弱化:任何大数据量的Web系统都非常忌讳几个大表间的关联查询,以及复杂的数据分析类型的SQL查询,特别是SNS类型的网站,从需求以及产品设计角度就避免了这种情况的产生。更多的情况往往只是单表的主键查询,以及单表的简单条件分页查询,SQL的功能被极大地弱化了,所以这部分功能不能得到充分发挥
      2. NoSQL数据库的优势:
        1. 扩展性强:NoSQL数据库种类繁多,但是一个共同的特点就是去掉关系型数据库的关系特性,数据之间是弱关系,非常容易扩展。一般来说,NoSql数据库的数据结构都是Key-Value字典式存储结构。例如,HBase、Cassandra等系统的水平扩展性能非常优越,非常容易实现支撑数据从TB到PB级别的过渡
        2. 并发性能好:NoSQL数据库具有非常良好的读写性能,尤其在大数据量下,同样表现优秀。当然这需要有优秀的数据结构和算法做支撑
        3. 数据模型灵活:NoSQL无须事先为要存储的数据建立字段,随时可以存储自定义的数据格式。而在关系型数据库中,增删字段是一件非常麻烦的事情。对于数据量非常大的表,增加字段简直就是一场噩梦。NoSQL允许使用者随时随地添加字段,并且字段类型可以是任意格式。
    5. 概述:HBase以表的形式存储数据。表有行和列族组成。列族划分为若干个列
    6. Row Key:行键
      1. hbase本质上也是一种Key-Value存储系统。Key相当于RowKey,Value相当于列族数据的集合
      2. 与nosql数据库们一样,row key是用来检索记录的主键
      3. 访问hbase table中的行,只有三种方式:
        1. 通过单个row key访问
        2. 通过row key的range
        3. 全表扫描
      4. Row key行键 (Row key)可以是任意字符串(最大长度是 64KB,实际应用中长度一般为 10-100bytes),在hbase内部,row key保存为字节数组
      5. 存储时, 数据按照Rowkey的字典序(byte order)排序存储。设计key时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。(位置相关性)
    7. 列族(列簇)
      1. hbase表中的每个列,都归属于某个列族
      2. 列族是表的schema的一部分(而列不是),列族必须在使用表之前定义
      3. 列名都以列族作为前缀。例如courses:history , courses:math 都属于 courses 这个列族
      4. 访问控制、磁盘和内存的使用统计都是在列族层面进行的。实际应用中,列族上的控制权限能帮助管理不同类型的应用:允许一些应用可以添加新的基本数据、一些应用可以读取基本数据并创建继承的列族、一些应用则只允许浏览数据(甚至可能因为隐私的原因不能浏览所有数据)
    8. Cell与时间戳
      1. 由{row key, column( =<family> + < label>), version} 唯一确定的单元
      2. cell中的数据是没有类型的,全部是字节码形式存贮
      3. 每个 cell都保存着同一份数据的多个版本,版本通过时间戳来索引
      4. 时间戳的类型是 64位整型。时间戳可以由hbase(在数据写入时自动 )赋值,此时时间戳是精确到毫秒的当前系统时间。时间戳也可以由客户显式赋值
      5. 如果应用程序要避免数据版本冲突,就必须自己生成具有唯一性的时间戳
      6. 每个 cell中,不同版本的数据按照时间倒序排序,即最新的数据排在最前面
      7. 为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担,hbase提供了两种数据版本回收方式:
        1. 保存数据的最后n个版本
        2. 保存最近一段时间内的版本(比如最近七天)
      8. 用户可以针对每个列族进行设置

  2. 安装
    1. 单机安装
      1. 特点:不依赖于Hadoop的HDFS,配置完既可使用,好处是便于测试。坏处是不具备分布式存储数据的能力
      2. 安装步骤
        1. 安装JDK
        2. 上传或者下载HBASE的安装包
        3. 解压安装包:tar -xvf hbase-0.98.17-hadoop2-bin.tar.gz
        4. 进入HBASE安装目录下的子目录conf:cd hbase-0.98.17-hadoop2/conf
        5. 修改配置文件hbase-site.xml:vim hbase-site.xml
        6. 添加如下配置:
        7. <!--指定HBASE的数据存储目录-->

          <property>

              <name>hbase.rootdir</name>

              <value>file:///home/software/hbase/tmp</value>

          </property>

        8. 进入HBASE的安装目录的子目录bin下:cd ../bin
        9. 启动服务器端,执行sh start-hbase.sh
        10. 启动完成之后可以通过jps命令查看是否有HMaster进程

        11. 启动客户端  ./hbase shell
    2. 伪分布式安装
      	1. 安装JDK
      	2. 安装Hadoop的伪分布式或者完全分布式集群
      	3. 上传或者下载HBASE的安装包
      	4. 解压安装包:tar -xvf hbase-0.98.17-hadoop2-bin.tar.gz
      	5. 进入HBASE安装目录下的子目录conf:cd hbase-0.98.17-hadoop2/conf
      	6. 修改conf/hbase-env.sh:vim hbase-env.sh
      	7. 添加JAVA_HOME:export JAVA_HOME=JDK的实际安装路径
      	8. 重新生效:source hbase-env.sh
      	9. 修改配置文件hbase-site.xml:vim hbase-site.xml
      	10. 配置使用hdfs:
      	<property>
      	    <name>hbase.rootdir</name>
      	    <value>hdfs://hadoop01:9000/hbase</value>
      	</property>
      	<property>
      	    <name>dfs.replication</name>
      	    <value>1</value>
      	</property>
      	11. 启动Hadoop。如果是使用的Hadoop完全分布式集群,则还需要启动Zookeeper
      	12. 进入HBASE的安装目录的子目录bin下:cd ../bin
      	13. 启动服务器端,执行sh start-hbase.sh
      	启动完成之后可以通过jps命令查看是否有HMaster进程
      	14. 启动客户端  ./hbase shell
      

       

    3. 完全分布式安装
      	1. 安装和配置:Hadoop+JDK+Zookeeper
      	2. 安装Hbase
      	3. 修改conf/hbase-env.sh
      	#修改JAVA_HOME:export JAVA_HOME=xxxx
      	#修改Zookeeper和Hbase的协调模式,hbase默认使用自带的zookeeper,如果需要使用外部zookeeper,需要先关闭:export HBASE_MANAGES_ZK=false
      	4. 修改hbase-site.xml,配置开启完全分布式模式
      	<property>
      	    <name>hbase.rootdir</name>
      	    <value>hdfs://hadoop01:9000/hbase</value>
      	</property> 
      	<property>
      	    <name>hbase.cluster.distributed</name>
      	    <value>true</value>
      	</property>
      	<!--#配置Zookeeper的连接地址与端口号-->
      	<property>
      	    <name>hbase.zookeeper.quorum</name>
      	    <value>hadoop01:2181,hadoop02:2181,hadoop03:2181</value>
      	</property>
      	5. 配置region服务器,修改conf/regionservers文件,每个主机名独占一行,hbase启动或关闭时会按照该配置顺序启动或关闭主机中的hbase:
      	hadoop01
      	hadoop02
      	hadoop03
      	6. 将01节点配置好的hbase通过远程复制拷贝到其他节点上
      	7. 启动Zookeeper服务
      	8. 启动Hadoop
      	9. 启动Hbase
      	10. 查看各节点的java进程是否正确,或者通过浏览器访问http://xxxxx:60010来访问web界面,通过web见面管理hbase
      	11. 关闭Hmaster,进入到hbase安装目录下的bin目录,执行:stop-hbase.sh
      	12. 关闭regionserver,进入到hbase安装目录下的bin目录,执行:sh hbase-daemon.sh stop regionserver
      

       

  3. 基本指令

    指令

    说明

    示例

    create

    创建表,t1指表名,c1,c2 列族名

    create 'tab1','colfamily1','colfamily2'

    list

    查看一共有哪些表

    list

    put

    t1指表名,r1指行键名,c1指列名,value指单元格值。ts1指时间戳,一般都省略掉了。注意,行键名在一张表里要全局唯一

    put 'tab1','row-1','colfamily1:co11','aaa'

    put 'tab1','row-1','colfamily1:co12','bbb'

    put 'tab1','row-1','colfamily2:co11','ccc'

    put 'tab1','row-1','colfamily2:co12','ddd'

     

    get 

    根据表名和行键查询

    get 'tab1','row-1' 

    get 'tab1','row-1','colfamily1'

    get 'tab1','row-1','colfamily1','colfamily2'

    get 'tab1','row-1','colfamily1:co11'

     

    scan 

    扫描所有数据,也可以跟指定条件

     

     

     

    scan 'tab1' #扫描整表数据,会查询出所有的行数据

     

    scan 'tab1',{COLUMNS=>['colfamily1']}

    scan 'tab1',{COLUMNS=>['cf1:name']}

    scan 'tab1',{COLUMNS=>['cf1:name','cf2:salary']}

    scan 'tab1',{COLUMNS=>['colfamily1','colfamily2']}

     

    scan 'tab1',{RAW=>true,VERSIONS=>3}

    可以在查询时加上RAW=>true来开启对历史版本数据的查询,VERSIONS=>3指定查询最新的几个版本的数据
     

     

     

     

    deleteall 

    根据表名、行键删除整行数据

    deleteall 'tab1','row-1'

    drop 

    删除表,前提是先禁用表

    drop  'tab1'

    disable

    禁用表

    disable 'tab1'

    create 指令补充

    建表时可以指定VERSIONS,配置的是当前列族在持久化到文件系统中时,要保留几个最新的版本数据,这并不影响内存中的历史数据版本

    create 'tab1',{NAME=>'c1',VERSIONS=>3},{NAME=>'c2',VERSIONS=>3}
     

    exit

    推出shell客户端

     

    enable

    启用表

    enable 'tab1'

  4. API操作
    创建表
    // 创建表
    @Test
    public void create() throws MasterNotRunningException, ZooKeeperConnectionException, IOException {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取连接
    	HBaseAdmin admin = new HBaseAdmin(conf);
    	// 指定表名
    	HTableDescriptor table = new HTableDescriptor(TableName.valueOf("student"));
    	// 指定列族名
    	HColumnDescriptor basic = new HColumnDescriptor("basic");
    	HColumnDescriptor info = new HColumnDescriptor("info");
    	// 指定历史版本上限
    	basic.setMaxVersions(3);
    	info.setMaxVersions(5);
    	// 将列族添加到表中
    	table.addFamily(basic);
    	table.addFamily(info);
    	// 创建表
    	admin.createTable(table);
    	// 关闭连接
    	admin.close();
    }
    
    添加数据
    /*
     * 添加数据
     */
    @Test
    public void put() throws MasterNotRunningException, ZooKeeperConnectionException, IOException {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取表
    	HTable table = new HTable(conf, "student");
    	// 添加行键
    	Put put = new Put("1".getBytes());
    	// 向指定的列族中添加列值
    	put.add("basic".getBytes(), "name".getBytes(), "Sam".getBytes());
    	put.add("info".getBytes(), "address".getBytes(), "info".getBytes());
    	// 将数据添加到table中
    	table.put(put);
    	// 关闭流
    	table.close();
    	
    }
    
    百万条数据写入
    /*
     * 测试百万条数据添加
     */
    @Test
    public void putmillion() throws IOException {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取表
    	HTable table = new HTable(conf, "student");
    	long begin = System.currentTimeMillis();
    	List<Put> list = new ArrayList<>();
    	for (int i = 1; i <= 100000; i++) {
    		Put put = new Put(("" + i).getBytes());
    		put.add("basic".getBytes(), "id".getBytes(), ("id-" + i).getBytes());
    		list.add(put);
    		if (i % 10000 == 0) {
    			table.put(list);
    			list = new ArrayList<>();
    		}
    	}
    	long end = System.currentTimeMillis();
    	System.out.println(end - begin);
    	table.close();
    }
    
    获取数据
    /*
     * 获取数据
     */
    @Test
    public void get() throws IOException {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取表
    	HTable table = new HTable(conf, "student");
    	// 获取指定行键的数据
    	Get get = new Get("100".getBytes());
    	// 获取结果
    	Result r = table.get(get);
    	byte[] bs = r.getValue("basic".getBytes(), "id".getBytes());
    	System.out.println(new String(bs));
    	// 关流
    	table.close();
    	
    }
    
    获取结果集
    /*
     * 获取结果集
     */
    @Test
    public void scan() throws IOException {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取表
    	HTable table = new HTable(conf, "student");
    	// 获取迭代器
    	Scan s = new Scan("99950".getBytes());
    	ResultScanner rs = table.getScanner(s);
    	Iterator<Result> it = rs.iterator();
    	// 迭代遍历
    	while (it.hasNext()) {
    		Result result = (Result) it.next();
    		byte[] bs = result.getValue("basic".getBytes(), "id".getBytes());
    		System.out.println(new String(bs));
    	}
    	// 关流
    	table.close();
    }
    
    删除数据
    /*
     * 删除数据
     */
    @Test
    public void delete() throws IOException {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取表
    	HTable table = new HTable(conf, "student");
    	// 进行删除
    	Delete del = new Delete("2".getBytes());
    	table.delete(del);
    	// 关流
    	table.close();
    }
    
    删除表
    /*
     * 删除表
     */
    @Test
    public void deleteTable() throws IOException{
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取管理权限
    	HBaseAdmin admin = new HBaseAdmin(conf);
    	// 禁用表
    	admin.disableTable("student");
    	// 删除表
    	admin.deleteTable("student");
    	// 关流
    	admin.close();
    }
    
    正则过滤器
    /*
     * 正则过滤器
     */
    @Test
    public void scanData() throws Exception {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取表
    	HTable table = new HTable(conf, "student");
    	// 获取迭代器
    	Scan scan = new Scan();
    	// 正则过滤器,匹配行键含3的行数据
    	Filter filter = new RowFilter(CompareOp.EQUAL, new RegexStringComparator(".*3.*"));
    	// 加入过滤器
    	scan.setFilter(filter);
    	ResultScanner scanner = table.getScanner(scan);
    	// 获取结果的迭代器
    	Iterator<Result> it = scanner.iterator();
    	while (it.hasNext()) {
    		Result result = it.next();
    		// 通过result对象获取指定列族的列的数据
    		byte[] value = result.getValue("basic".getBytes(), "id".getBytes());
    		System.out.println(new String(value));
    	}
    	scanner.close();
    	table.close();
    }
    
    行键过滤器
    @Test
    public void rowKeyCompare() throws Exception {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取表
    	HTable table = new HTable(conf, "student");
    	Scan scan = new Scan();
    	// 行键比较过滤器,下例是匹配小于或等于指定行键的行数据
    	Filter filter = new RowFilter(CompareOp.LESS_OR_EQUAL, new BinaryComparator("100".getBytes()));
    	scan.setFilter(filter);
    	ResultScanner scanner = table.getScanner(scan);
    	Iterator<Result> it = scanner.iterator();
    	while (it.hasNext()) {
    		Result result = it.next();
    		byte[] value = result.getValue("basic".getBytes(), "id".getBytes());
    		System.out.println(new String(value));
    	}
    	scanner.close();
    	table.close();
    }
    
    行键前缀过滤器
    @Test
    public void prefix() throws Exception {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取表
    	HTable table = new HTable(conf, "student");
    	Scan scan = new Scan();
    	// 行键前缀过滤器
    	Filter filter = new PrefixFilter("3".getBytes());
    	scan.setFilter(filter);
    	ResultScanner scanner = table.getScanner(scan);
    	Iterator<Result> it = scanner.iterator();
    	while (it.hasNext()) {
    		Result result = it.next();
    		byte[] value = result.getValue("basic".getBytes(), "id".getBytes());
    		System.out.println(new String(value));
    	}
    	scanner.close();
    	table.close();
    }
    
    列值过滤器
    @Test
    public void colScan() throws Exception {
    	// 获取配置
    	Configuration conf = HBaseConfiguration.create();
    	// 设置zookeeper的地址
    	conf.set("hbase.zookeeper.quorum", "192.168.232.129:2181,192.168.232.130:2181,192.168.232.131:2181");
    	// 获取表
    	HTable table = new HTable(conf, "student");
    	Scan scan = new Scan();
                  // --列值过滤器
    	Filter filter = new SingleColumnValueFilter("basic".getBytes(), "name".getBytes(), CompareOp.EQUAL,
    			"rose".getBytes());
    	scan.setFilter(filter);
    	ResultScanner scanner = table.getScanner(scan);
    // --获取结果的迭代器
    Iterator<Result> it = scanner.iterator();
    while (it.hasNext()) {
    	Result result = it.next();
    	byte[] name = result.getValue("basic".getBytes(), "name".getBytes());
    	byte[] age = result.getValue("basic".getBytes(), "age".getBytes());
    	System.out.println(new String(name) + ":" + new String(age));
    }
    scanner.close();
    table.close();
    }
    

     

  5. 原理
    1. 物理存储原理
      1. 概述:
        1. Hbase里的一个Table 在行的方向上分割为一个或者多个HRegion,即HBase中一个表的数据会被划分成一个或者很多的HRegion,每一个HRegion会被存储在一个节点上。这样做的目的是为了能做到数据的分布式存储
        2. HRegion可以动态扩展(即增加或者删除节点,并且会进行分裂)并且HBase保证HRegion的负载均衡
        3. HRegion实际上是行键排序(默认是字典排序)后的按规则分割的连续的存储空间
        4. 一张Hbase表,可能有多个HRegion,每个HRegion达到一定大小(默认是10GB)时,进行分裂

      2. 分裂

        1. HRegion是按大小分割的,每个表一开始只有一个HRegion,随着数据不断插入表,HRegion不断增大,当增大到一个阀值的时候,HRegion就会等分两个新的等大的HRegion。因此随着table中的行不断增多,就会有越来越多的HRegion
        2. 按照现在主流硬件的配置,每个HRegion的大小可以是1~20GB。这个大小由hbase.hregion.max.filesize指定,默认为10GB
        3. HRegion的拆分和转移是由HBase(HMaster)自动完成的,用户感知不到
        4. HRegion是Hbase中分布式存储和负载均衡的最小单元
        5. HRegion虽然是分布式存储的最小单元,但并不是存储的最小单元
        6. HRegion由一个或者多个HStore组成,每个HStore保存一个columns family
        7. 每个HStore又由一个memStore(写缓存,默认是128M)以及0个或者多个StoreFile组成,StoreFile以HFile格式保存在HDFS上
        8. 总结:HRegion是分布式的存储最小单位,StoreFile(Hfile)是存储最小单位
    2. 系统架构
      1. HBase架构组成
        1. Base采用Master/Slave(主从)架构搭建集群,它隶属于Hadoop生态系统,由以下类型节点组成:
          1. HMaster节点:
            1. 管理HRegionServer,实现其负载均衡,保证每一个HRegionServer上的数据量差别不大
            2. 管理和分配HRegion,比如在HRegion分裂时分配新的HRegion;在HRegionServer退出时迁移其内的HRegion到其他HRegionServer上
            3. 实现DDL操作(Data Definition Language,namespace和table的增删改,column familiy的增删改等)
            4. 管理namespace和table的元数据(实际存储在HDFS上)
            5. 权限控制(ACL)
          2. HRegionServer节点:
            1. 存放和管理本地HRegion
            2. 读写HDFS,管理Table中的数据
            3. Client直接通过HRegionServer读写数据(从HMaster中获取元数据,找到RowKey所在的HRegion/HRegionServer后)
          3. ZooKeeper集群
            1. 存放整个 HBase集群的元数据、集群的状态信息以及RS服务器的运行状态
            2. 实现HMaster主备节点的failover
          4. HBase的数据存储于HDFS中,因而涉及到HDFS的NameNode、DataNode等。RegionServer和DataNode一般会放在相同的Server上实现数据的本地化(避免或减少数据在网络中的传输,节省带宽)
        2. HBase Client通过RPC方式和HMaster、HRegionServer通信;一个HRegionServer可以存放1000个HRegion(1000个数字的由来是来自于Google的Bigtable论文);底层Table数据存储于HDFS中,而HRegion所处理的数据尽量和数据所在的DataNode在一起,实现数据的本地化;数据本地化并不是总能实现,比如在HRegion移动(如因Split)时,需要等下一次Compact才能继续回到本地化
    3. 架构原理详解
      1. HRegion
        1. HBase使用RowKey将表水平切割成多个HRegion
        2. 从HMaster的角度,每个HRegion都纪录了它的StartKey和EndKey
        3. 由于RowKey是排序的,因而Client可以通过HMaster快速的定位每个RowKey在哪个HRegion中
        4. HRegion由HMaster分配到相应的HRegionServer中,然后由HRegionServer负责HRegion的启动和管理、和Client的通信以及负责数据的读(使用HDFS)
        5. 每个HRegionServer可以同时管理1000个左右的HRegion
      2. HMaster
        1. HMaster没有单点故障问题,因为在HBase集群中可以启动多个HMaster
        2. 通过ZooKeeper的Master Election机制保证同时只有一个HMaster处于Active状态,其他的HMaster则处于热备份状态
        3. 一般情况下会启动两个HMaster,Backup的HMaster会定期的和Active HMaster通信以获取其最新状态,从而保证它是实时更新的,因而如果启动了多个HMaster反而增加了Active HMaster的负担
        4. HMaster的主要用于HRegion的分配和管理,DDL(Data Definition Language,既Table的新建、删除、修改等)的实现等,既它主要有两方面的职责:
          1. 协调HRegionServer:
            1. 启动时HRegion的分配,以及负载均衡和修复时HRegion的重新分配
            2. 监控集群中所有HRegionServer的状态(通过Heartbeat和监听ZooKeeper中的状态)
          2. Admin职能
            1. 管理创建、删除、修改Table的操作
      3. Zookeeper
        1. ZooKeeper为HBase集群提供协调服务,它管理着HMaster和HRegionServer的状态(available/alive等)
        2. ZooKeeper协调集群所有节点的共享信息,在HMaster和HRegionServer连接到ZooKeeper后创建Ephemeral( 临时)节点,并使用Heartbeat机制维持这个节点的存活状态,如果某个Ephemeral节点实效,则HMaster会收到通知,并做相应的处理
        3. 当HMaster宕机的时候实现HMaster之间的failover(失败恢复)
        4. 在HRegionServer宕机时通知给HMaster,从而对宕机的HRegionServer中的HRegion集合的修复(将它们分配给其他的HRegionServer)
        5. 另外,HMaster通过监听ZooKeeper中的Ephemeral节点(默认:/hbase/rs/*)来监控HRegionServer的加入和宕机。在第一个HMaster连接到ZooKeeper时会创建Ephemeral节点(默认:/hbasae/master)来表示Active的HMaster,其后加进来的HMaster则监听该Ephemeral节点,如果当前Active的HMaster宕机,则该节点消失,因而其他HMaster得到通知,而将自身转换成Active的HMaster,在变为Active的HMaster之前,它会创建在/hbase/back-masters/下创建自己的Ephemeral节点。

      4. HBase的第一次读写

        1. HBase 0.96以前,HBase有两个特殊的Table:-ROOT-和.META.,
          1. -ROOT- 表的存储位置存储在ZooKeeper,它存储了.META.表的RegionInfo信息,并且它只能存在一个HRegion
          2. .META. 表则存储了用户定义的Table的RegionInfo信息,它可以被切分成多个HRegion
          3. 第一次访问Table时,首先从ZooKeeper中读取-ROOT- 表所在HRegionServer;然后从该HRegionServer中根据请求的TableName,RowKey读取.META. 表所在HRegionServer;最后从该HRegionServer中读取.META. 表的内容而获取此次请求需要访问的HRegion所在的位置,然后访问该HRegionSever获取请求的数据,这需要三次请求才能找到用户Table所在的位置,然后第四次请求开始获取真正的数据。当然为了提升性能,客户端会缓存-ROOT- Table位置以及-ROOT-/.META. Table的内容
          4. 可是即使客户端有缓存,在初始阶段需要三次请求才能获取到用户自定义的Table真正所在的位置

        2. HBase 0.96以后去掉了-ROOT- Table只剩下这个特殊的目录表叫做Meta Table(hbase:meta),它存储了集群中所有用户HRegion的位置信息,而ZooKeeper的节点中(/hbase/meta-region-server)存储的则直接是这个Meta Table的位置,并且这个Meta Table如以前的-ROOT- Table一样是不可split的。这样,客户端在第一次访问用户Table的流程是:
          1. 从ZooKeeper(/hbase/meta-region-server)中获取hbase:meta的位置(HRegionServer的位置),缓存该位置信息
          2. 从HRegionServer中查询用户Table对应请求的RowKey所在的HRegionServer,缓存该位置信息
          3. 从查询到HRegionServer中读取Row
        3. 从这个过程中,客户端会缓存这些位置信息,然而第二步它只是缓存当前RowKey对应的HRegion的位置,因而如果下一个要查的RowKey不在同一个HRegion中,则需要继续查询hbase:meta所在的HRegion,然而随着时间的推移,客户端缓存的位置信息越来越多,以至于不需要再次查找hbase:meta Table的信息
        4. 当某个HRegion因为宕机或Split被移动,此时需要重新查询并且更新缓存。
      5. HRegionServer详解
        1.  
        2. HRegionServer一般和DataNode在同一台机器上运行,实现数据的本地性
        3. HRegionServer存储和管理多个HRegion,由WAL(HLog)、BlockCache、Region组成:
          1. WAL即Write Ahead Log
            1. 在早期版本中称为HLog,它是HDFS上的一个文件,所有写操作都会先保证将写操作写入这个Log文件后,才会真正更新MemStore,最后写入HFile中
            2. 采用这种模式,可以保证HRegionServer宕机后,依然可以从该Log文件中恢复数据,重新执行所有的操作,而不至于数据丢失
            3. HLog文件就是一个普通的Hadoop Sequence File,Sequence File 的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是”写入时间”,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。HLog Sequece File的Value是HBase的KeyValue对象,即对应HFile中的KeyValue
            4. 这个Log文件会定期Roll出新的文件而删除旧的文件(那些已持久化到HFile中的Log可以删除)。WAL文件存储在/hbase/WALs/${HRegionServer_Name}的目录中(在0.94之前,存储在/hbase/.logs/目录中),一般一个HRegionServer只有一个WAL实例,也就是说一个HRegionServer的所有WAL写都是串行的(就像log4j的日志写也是串行的)
            5. 一个RS服务器只有一个HLOG文件,在0.94版本之前,写HLOG的操作是串行的,所以效率很低,所以1.0版本之后,Hbase引入多管道并行写技术,从而提高性能
          2. BlockCache是一个读缓存
            1. 采用“引用局部性”原理(也应用于CPU,分空间局部性和时间局部性,空间局部性是指CPU在某一时刻需要某个数据,那么有很大的概率在一下时刻它需要的数据在其附近;时间局部性是指某个数据在被访问过一次后,它有很大的概率在不久的将来会被再次的访问),将数据预读取到内存中,以提升读的性能
            2. 这样设计的目的是为了提高读缓存的命中率
            3. HBase中默认采用on-heap LruBlockCache策略(LRU -evicted,是一种数据的回收策略,  LRU– 最近最少使用的:移除最长时间不被使用的对象)
          3. HRegion是一个Table中的一个Region在一个HRegionServer中的表达
            1. 一个Table可以有一个或多个HRegion,它们可以在一个相同的HRegionServer上,也可以分布在不同的HRegionServer上
            2. 一个HRegionServer可以有多个HRegion,他们分别属于不同的Table
            3. HRegion由一个或者多个Store(HStore)构成,每个HStore对应了一个Table在这个HRegion中的一个Column Family,即每个Column Family就是一个集中的存储单元,因而最好将具有相近I/O特性的Column存储在一个Column Family,以实现高效读取(数据局部性原理,可以提高缓存的命中率)。HStore是HBase中存储的核心,它实现了读写HDFS功能,一个HStore由一个MemStore 和0个或多个StoreFile组成
            4. MemStore是一个写缓存(In Memory Sorted Buffer),所有数据的写在完成WAL日志写后,会写入MemStore中,由MemStore根据一定的算法(LSM-TREE算法,这个算法的作用是将数据顺序写入磁盘,而不是随机写,减少磁头调度时间,从而提高写入性能)将数据Flush到底层的HDFS文件中(HFile),通常每个HRegion中的每个 Column Family有一个自己的MemStore
            5. HFile(StoreFile) 用于存储HBase的数据(Cell/KeyValue)。在HFile中的数据是按RowKey、Column Family、Column排序,对相同的Cell(即这三个值都一样),则按timestamp倒序排列(即最新的数据在最前面)
        4. 因为Hbase的HFile是存到HDFS上,所以Hbase实际上是具备数据的副本冗余机制的
    4. HBASE写流程
      1. 当客户端发起一个Put请求时,首先它从hbase:meta表中查出该Put数据最终需要去的HRegionServer。然后客户端将Put请求发送给相应的HRegionServer,在HRegionServer中它首先会将该Put操作写入WAL日志文件中(Flush到磁盘中)
      2.  

      3. 写完WAL日志文件后,然后会将数据写到Memstore,在Memstore按Rowkey排序,以及用LSM-TREE对数据做合并处理。HRegionServer根据Put中的TableName和RowKey找到对应的HRegion,并根据Column Family找到对应的HStore,并将Put写入到该HStore的MemStore中。此时写成功,并返回通知客户端
      4.  

      5. MemStore是一个In Memory Sorted Buffer,在每个HStore中都有一个MemStore,一个HRegion的一个Column Family对应一个HStore实例。在MemStore中,数据的排列顺序以RowKey、Column Family、Column的顺序以及Timestamp的倒序
      6.  

      7. 每一次Put/Delete请求都是先写入到MemStore中,当MemStore满后会Flush成一个新的StoreFile(底层实现是HFile),即一个HStore(Column Family)可以有0个或多个StoreFile(HFile)。有以下三种情况可以触发MemStore的Flush动作:
        1. 当一个HRegion中的MemStore的大小超过了:hbase.hregion.memstore.flush.size的大小,默认128MB,此时当前的MemStore会Flush到HFile中
        2. 当RS服务器上所有的MemStore的大小超过了:hbase.regionserver.global.memstore.upperLimit的大小,默认35%的内存使用量,此时当前HRegionServer中所有HRegion中的MemStore可能都会Flush。一般从最大的Memostore开始flush
        3. 当前HRegionServer中WAL的大小超过了 1GB。hbase.regionserver.hlog.blocksize(32MB) * hbase.regionserver.max.logs(32)的数量,当前HRegionServer中所有HRegion中的MemStore都会Flush,这里指的是两个参数相乘的大小
      8. 此外,在MemStore Flush过程中,还会在尾部追加一些meta数据,其中就包括Flush时最大的WAL sequence值,以告诉HBase这个StoreFile写入的最新数据的序列,那么在Recover时就知道从哪里开始。在HRegion启动时,这个sequence会被读取,并取最大的作为下一次更新时的起始sequence
      9.  

      10. HBase的数据以KeyValue(Cell)的形式顺序的存储在HFile中,在MemStore的Flush过程中生成HFile,由于MemStore中存储的Cell遵循相同的排列顺序,因而Flush过程是顺序写,而磁盘的顺序写性能很高,因为不需要不停的移动磁盘指针。
      11.  

      12. HFile参考BigTable的SSTable和Hadoop的TFile实现,从HBase开始到现在,HFile经历了三个版本,其中V2在0.92引入
      13. HFile在V1的格式:
      14.  

      15. V1的HFile由多个Data Block、Meta Block、FileInfo、Data Index、Meta Index、Trailer组成:
        1. Data Block是HBase的最小存储单元,BlockCache就是基于Data Block的缓存的。一个Data Block由一个魔数(Magic)和一系列的KeyValue(Cell)组成,魔数是一个随机的数字,用于表示这是一个Data Block类型,以快速检测这个Data Block的格式,防止数据的破坏。Data Block的大小可以在创建Column Family时设置(HColumnDescriptor.setBlockSize()),默认值是64KB,大号的Block有利于顺序Scan,小号Block利于随机查询,因而需要权衡
        2. Meta块是可选的,FileInfo是固定长度的块,它纪录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等
        3. Data Index和Meta Index纪录了每个Data块和Meta块的起始点、未压缩时大小、Key(起始RowKey)等
        4. Trailer纪录了FileInfo、Data Index、Meta Index块的起始位置,Data Index和Meta Index索引的数量等。其中FileInfo和Trailer是固定长度的
      16. HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构
      17. 开始是两个固定长度的数值,分别表示Key的长度和Value的长度

      18. 然后是Key,key的开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete
      19. Value部分没有这么复杂的结构,就是纯粹的二进制数据了。随着HFile版本迁移,KeyValue(Cell)的格式并未发生太多变化,只是在V3版本,尾部添加了一个可选的Tag数组
      20. HFileV1版本的在实际使用过程中发现它占用内存多,因而增加了启动时间。为了解决这些问题,在0.92版本中引入HFileV2版本,在这个版本中,为了提升启动速度,还引入了延迟读的功能,即在HFile真正被使用时才对其进行解析:
      21.  

        对HFileV2格式具体分析,它是一个多层的类B+树索引,采用这种设计,可以实现查找不需要读取整个文件:

         

        Data Block中的Cell都是升序排列,每个block都有它自己的Leaf-Index,每个Block的最后一个Key被放入Intermediate-Index中,Root-Index指向Intermediate-Index。在HFile的末尾还有Bloom Filter(布隆过滤)用于快速定位那么没有在某个Data Block中的Row;TimeRange信息用于给那些使用时间查询的参考。在HFile打开时,这些索引信息都被加载并保存在内存中,以增加以后的读取性能。

    5. BloomFilter
      1. 背景说明
        1. Hash 函数在计算机领域,尤其是数据快速查找领域,加密领域用的极广
        2. 作用是将一个大的数据集映射到一个小的数据集上面(这些小的数据集叫做哈希值,或者散列值)
        3. Hash table(散列表,也叫哈希表),是根据哈希值(Key value)而直接进行访问的数据结构。也就是说,它通过把哈希值映射到表中一个位置来访问记录,以加快查找的速度。下面是一个典型的示意图:
        4. 这种简单的Hash Table存在一定的问题,就是Hash冲突的问题。假设 Hash 函数是良好的,如果位阵列长度为 m 个点,那么如果想将冲突率降低到例如 1%, 这个散列表就只能容纳 m * 1% 个元素。显然这就不叫空间有效了(Space-efficient)。

      2. Bloom Filter概述

        1. Bloom Filter是1970年由布隆(Burton Howard Bloom)提出的
        2. 它实际上是一个很长的二进制向量和一系列随机映射函数(Hash函数)
        3. 布隆过滤器可以用于检索一个元素是否在一个集合中
        4. 它的优点是空间效率和查询时间都远远超过一般的算法
        5. Bloom Filter广泛的应用于各种需要查询的场合中,如:Google 著名的分布式数据库 Bigtable 使用了布隆过滤器来查找不存在的行或列,以减少磁盘查找的IO次数
        6. 在很多Key-Value系统中也使用了布隆过滤器来加快查询过程,如 Hbase,Accumulo,Leveldb,一般而言,Value 保存在磁盘中,访问磁盘需要花费大量时间,然而使用布隆过滤器可以快速判断某个Key对应的Value是否存在,因此可以避免很多不必要的磁盘IO操作,只是引入布隆过滤器会带来一定的内存消耗
      3. Bloom Filter
        1. 如果想判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定,链表,树等等数据结构都是这种思路.。但是随着集合中元素的增加,需要的存储空间越来越大,检索速度也越来越慢。
        2. 一个Bloom Filter是基于一个m位的位向量(b1,…bm),这些位向量的初始值为0。另外,还有一系列的hash函数(h1,…hk)(默认是3个哈希函数),这些hash函数的值域属于1~m。下图是一个bloom filter插入x,y,z并判断某个值w是否在该数据集的示意图
        3. 布隆过滤器的缺点和优点一样明显。误算率(False Positive)是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣
        4. 总结:Bloom Filter 通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合。此外,引入布隆过滤器会带来一定的内存消耗
    6. HBASE读的实现
      1. 流程
        1. 首先对新写入的Cell,它会存在于MemStore中;然后对之前已经Flush到HFile中的Cell,它会存在于某个或某些StoreFile(HFile)中;最后,对刚读取过的Cell,它可能存在于BlockCache中
        2. 既然相同的Cell可能存储在三个地方,在读取的时候只需要扫瞄这三个地方,然后将结果合并即可(Merge Read),在HBase中扫瞄的顺序依次是:BlockCacheMemStoreStoreFile(HFile)(这个扫描顺序的目的也是为了减少磁盘的I/O次数)
        3. 其中StoreFile的扫瞄先会使用Bloom Filter(布隆过滤算法)过滤那些不可能符合条件的HFile,然后使用Block Index快速定位Cell,并将其加载到BlockCache中,然后从BlockCache中读取。
        4. 一个HStore可能存在多个StoreFile(HFile),此时需要扫瞄多个HFile,如果HFile过多又是会引起性能问题
      2. Compaction机制
        1. MemStore每次Flush会创建新的HFile,而过多的HFile会引起读的性能问题。为了解决这个问题,HBase采用Compaction机制来解决这个问题。在HBase中Compaction分为两种:Minor CompactionMajor Compaction
        2. Minor Compaction是指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经Deleted或Expired的Cell。一次Minor Compaction的结果是减少Store File的数量但是产生更大的StoreFile
        3. Major Compaction是指将所有的StoreFile合并成一个StoreFile,在这个过程中,标记为Deleted的Cell会被删除,而那些已经Expired的Cell会被丢弃,那些已经超过最多版本数的Cell会被丢弃。一次Major Compaction的结果是一个HStore只有一个StoreFile存在。Major Compaction可以手动或自动触发,然而由于它会引起很多的I/O操作而引起性能问题,因而它一般会被安排在周末、凌晨等集群比较闲的时间
        4. 实现Compaction的方式有2种:
          1. 通过API:
        5. // minor compact
          admin.compact("tab2".getBytes());
          // major compact
          admin.majorCompact("tab2".getBytes());

        6. 通过指令:
        7. compact('tab2') # minor compact

          major_compact('tab2') # major compact

        8. Hbase默认用的是Minor compaction。之所以默认不用Major Compaction的原因是在于,Major Compaction可能会代理大量的磁盘I/O,从而阻塞Hbase其他的读写操作。所以对于Major Compactoin,一般选择在业务峰值低的时候执行
  6. 扩展
    1. HBASE表的设计
      1. Rowkey设计
        1. 概述
          1. Rowkey是不可分割的字节数,按字典排序由低到高存储在表中
          2. 在设计HBase表时,Rowkey设计是最重要的事情,应该基于预期的访问模式来为Rowkey建模。Rowkey决定了访问HBase表时可以得到的性能,原因有两个:
            1. Region基于Rowkey为一个区间的行提供服务,并且负责区间的每一行
            2. HFile在硬盘上存储有序的行
          3. 这两个因素是相互关联的。当Region将内存中数据刷写为HFile时,这些行已经排过序,也会有序地写到硬盘上。Rowkey的有序特性和底层存储格式可以保证HBase表在设计Rowkey之后的良好性能
          4. 关系型数据库可以在多列上建立索引,但是HBase只能在Rowkey上建立索引。(可以通过ES为Hbase的列建立索引
        2. 设计方式
          1. Rowkey以字典顺序从大到小排序
          2. 原生HBase只支持从小到大的排序,但是现在有个需求想展现影片热度排行榜,这就要求实现从大到小排列,针对这种情况可以采用Rowkey=Integer.MAX_VALUE-Rowkey的方式将Rowkey进行转换,最大的变最小,最小的变最大,在应用层再转回来即可完成排序需求

          3. RowKey尽量散列设计
          4. 最重要的是要保证散列,这样就会保证所有的数据都不是在一个Region上,从而避免读写的时候负载会集中在个别Region上。比如ROWKEY_Random

          5. RowKey的长度尽量短
          6. 如果Rowkey太长,第一存储开销会增加,影响存储效率;第二内存中Rowkey字段过长,会导致内存的利用率降低,进而降低索引命中率

            Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节,不过建议是越短越好,不要超过16个字节

            原因如下:

          7. 数据的持久化文件HFile中是按照KeyValue存储的,如果Rowkey过长比如100个字节,1000万列数据光Rowkey就要占用100*1000万=10亿个字节,将近1G数据,这会极大影响HFile的存储效率
          8. MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统将无法缓存更多的数据,这会降低检索效率。因此Rowkey的字节长度越短越好
          9. RowKey唯一
          10. RowKey建议用String类型
          11. 虽然行键在HBase中是以byte[]字节数组的形式存储的,但是建议在系统开发过程中将其数据类型设置为String类型,保证通用性。

            常用的行键字符串有以下几种:

          12. 纯数字字符串,譬如9559820140512
          13. 数字+特殊分隔符,譬如95598-20140512
          14. 数字+英文字母,譬如city20140512
          15. 数字+英文字母+特殊分隔符,譬如city_20140512
          16. RowKey设计得最好有意义
          17. RowKey的主要作用是为了进行数据记录的唯一性标示,但是唯一性并不是其全部,具有明确意义的行键对于应用开发、数据检索等都具有特殊意义,譬如数字字符串:9559820140512,其实际意义是这样:95598(电网客服电话)+20140512(日期)

            行键往往由多个值组合而成,而各个值的位置顺序将影响到数据存储和检索效率,所以在设计行键时,需要对日后的业务应用开发有比较深入的了解和前瞻性预测,才能设计出可尽量高效率检索的行键

          18. 具有定长性
          19. 行键具有有序性的基础便是定长,譬如20140512080500、20140512083000,这两个日期时间形式的字符串是递增的,不管后面的秒数是多少,我们都将其设置为14位数字形式,如果我们把后面的0去除了,那么201405120805将大于20140512083,其有序性发生了变更。所以行键一定要设计成定长的

            此外,目前操作系统是都是64位系统,内存8字节对齐。控制在16个字节,8字节的整数倍利用操作系统的最佳特性

      2. 列族的设计
        1. 在设计hbase表时候,列族不宜过多,尽量的要少使用列族
        2. 经常要在一起查询的数据最好放在一个列族中,尽量的减少跨列族的数据访问
    2. HBASE表的优化
      1. 硬件和操作系统调优
        1. 配置内存
        2. HBase对于内存的消耗是非常大的,主要是其LSM树状结构、缓存机制和日志记录机制决定的,所以物理内存当然是越大越好

          在互联网领域,服务器内存方面的主流配置已经是64GB,所以一定要根据实际的需求和预算配备服务器内存。如果资源很紧张,推荐内存最小在32GB,如果再小会严重影响HBase集群性能

        3. 配置CPU
        4. HBase给使用者的印象可能更偏向于“内存型”NoSQL数据库,从而忽略了CPU方面的需求,其实HBase在某些应用上对CPU的消耗非常大,例如频繁使用过滤器,因为在过滤器中包含很多匹配、搜索和过滤的操作;多条件组合扫描的场景也是CPU密集型的;压缩操作很频繁等。如果服务器CPU不够强悍,会导致整个集群的负载非常高,很多线程都在阻塞状态(非网络阻塞和死锁的情况)。

          建议每台物理节点至少使用双路四核CPU(2×4),主流是2~8路,一般单颗CPU至少四核。对于CPU密集型的集群,当然是越多越好。

        5. 垃圾回收器(GC)的选择
        6. 对于运行HBase相关进程JVM的垃圾回收器,不仅仅关注吞吐量,还关注停顿时间,而且两者之间停顿时间更为重要,因为HBase设计的初衷就是解决大规模数据集下实时访问的问题。那么按照首位是停顿时间短,从这个方面CMS和G1有着非常大的优势

          而CMS作为JDK1.5已经出现的垃圾收集器,已经成熟应用在互联网等各个行业。所以,选用CMS作为老年代的垃圾回收器。与CMS搭配的新生代收集器有Serial和ParNew,而对比这两个收集器,明显ParNew具有更好的性能,所以新生代选用ParNew作为垃圾收集器。那么,最终选用的垃圾收集器搭配组合是CMS+ParNew。而且很多成熟应用已经验证了这种组合搭配的优势

          与CMS收集器相关的几个重要参数的具体含义、默认值和相关说明详见表

          配置方式:需要添加到hbase-env.sh文件中

          export HBASE_OPTS="-XX:+UseConcMarkSweepGC" -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSCompactAtFullCollection

        7. JVM堆大小设置
        8. 堆内存大小参数hbase-env.sh文件中设置:export HBASE_HEAPSIZE=16384,单位是MB,即默认是16GB。当然,这个值需要根据节点实际的物理内存来决定。一般不超过实际物理内存的1/2。服务器内存的分配,比如服务器内存64GB,为操作系统预留出8G~16GB。此外给Yarn留出8G~16GB,如果没有其他框架,把剩余的留给HBase

      2. HBase调优

        1. 	1. 调节数据块(data block)的大小
          	HFile数据块大小可以在列族层次设置。这个数据块不同于之前谈到的HDFS数据块,其默认值是65536字节,或64KB。数据块索引存储每个HFile数据块的起始键。数据块大小的设置影响数据块索引的大小。数据块越小,索引越大,从而占用更大内存空间。同时加载进内存的数据块越小,随机查找性能更好。但是,如果需要更好的序列扫描性能,那么一次能够加载更多HFile数据进入内存更为合理,这意味着应该将数据块设置为更大的值。相应地,索引变小,将在随机读性能上付出更多的代价
          	可以在表实例化时设置数据块大小:hbase(main):002:0> create 'mytable',{NAME => 'colfam1', BLOCKSIZE => '65536'}
          	2. 适当时机关闭数据块缓存
          	把数据放进读缓存,并不是一定能够提升性能。如果一个表或表的列族只被顺序化扫描访问或很少被访问,则Get或Scan操作花费时间长一点是可以接受的。在这种情况下,可以选择关闭列族的缓存
          	关闭缓存的原因在于:如果只是执行很多顺序化扫描,会多次使用缓存,并且可能会滥用缓存,从而把应该放进缓存获得性能提升的数据给排挤出去,所以如果关闭缓存,不仅可以避免上述情况发生,而且可以让出更多缓存给其他表和同一表的其他列族使用。数据块缓存默认是打开的
          	可以在新建表或更改表时关闭数据块缓存属性:hbase(main):002:0> create 'mytable', {NAME => 'colfam1', BLOCKCACHE => 'false'}
          	3. 开启布隆过滤器
          	布隆过滤器(Bloom Filter)允许对存储在每个数据块的数据做一个反向测验。当查询某行时,先检查布隆过滤器,看看该行是否不在这个数据块。布隆过滤器要么确定回答该行不在,要么回答不知道。因此称之为反向测验。布隆过滤器也可以应用到行内的单元格上,当访问某列标识符时先使用同样的反向测验
          	使用布隆过滤器也不是没有代价,相反,存储这个额外的索引层次占用额外的空间。布隆过滤器的占用空间大小随着它们的索引对象数据增长而增长,所以行级布隆过滤器比列标识符级布隆过滤器占用空间要少。当空间不是问题时,它们可以压榨整个系统的性能潜力
          	可以在列族上打开布隆过滤器: create 'mytable', {NAME => 'colfam1', BLOOMFILTER => 'ROWCOL'}
          	布隆过滤器参数的默认值是NONE。另外,还有两个值:ROW表示行级布隆过滤器;ROWCOL表示列标识符级布隆过滤器。行级布隆过滤器在数据块中检查特定行键是否不存在,列标识符级布隆过滤器检查行和列标识符联合体是否不存在。ROWCOL布隆过滤器的空间开销高于ROW布隆过滤器。
          	4. 开启数据压缩
          	HFile可以被压缩并存放在HDFS上,这有助于节省硬盘I/O,但是读写数据时压缩和解压缩会抬高CPU利用率。压缩是表定义的一部分,可以在建表或模式改变时设定。除非确定压缩不会提升系统的性能,否则推荐打开表的压缩。只有在数据不能被压缩,或者因为某些原因服务器的CPU利用率有限制要求的情况下,有可能需要关闭压缩特性
          	HBase可以使用多种压缩编码,包括LZO、SNAPPY和GZIP,LZO和SNAPPY是其中最流行的两种
          	当建表时可以在列族上打开压缩:create 'mytable', {NAME => 'colfam1', COMPRESSION => 'SNAPPY'}
          	注意,数据只在硬盘上是压缩的,在内存中(MemStore或BlockCache)或在网络传输时是没有压缩的
          	5. 设置Scan缓存
          	HBase的Scan查询中可以设置缓存,定义一次交互从服务器端传输到客户端的行数,设置方法是使用Scan类中setCaching()方法,这样能有效地减少服务器端和客户端的交互,更好地提升扫描查询的性能
          	HTable table = new HTable(config, Bytes.toBytes(tableName));
          Scan scanner = new Scan();
          	/* batch and caching */
          scanner.setBatch(0);
          scanner.setCaching(10000);
          	ResultScanner rsScanner = table.getScanner(scanner);
          	for (Result res : rsScanner) {
          	final List<KeyValue> list = res.list();
          	String rk = null;
          	StringBuilder sb = new StringBuilder();
          	for (final KeyValue kv : list) {
          		sb.append(Bytes.toStringBinary(kv.getValue()) + ",");
          		rk = getRealRowKey(kv);
          	}
          	if (sb.toString().length() > 0)
          		sb.setLength(sb.toString().length() - 1);
          	System.out.println(rk + "\t" + sb.toString());
          }
          	rsScanner.close();
          	6. 显式地指定列
          	当使用Scan或Get来处理大量的行时,最好确定一下所需要的列。因为服务器端处理完的结果,需要通过网络传输到客户端,而且此时,传输的数据量成为瓶颈,如果能有效地过滤部分数据,使用更精确的需求,能够很大程度上减少网络I/O的花费,否则会造成很大的资源浪费。如果在查询中指定某列或者某几列,能够有效地减少网络传输量,在一定程度上提升查询性能。下面代码是使用Scan类中指定列的addColumn()方法
          	HTable table = new HTable(config, Bytes.toBytes(tableName));
          Scan scanner = new Scan();
          	/* 指定列 */
          scanner.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(column));
          	ResultScanner rsScanner = table.getScanner(scanner);
          	for (Result res : rsScanner) {
          	final List<KeyValue> list = res.list();
          	String rk = null;
          	StringBuilder sb = new StringBuilder();
          	for (final KeyValue kv : list) {
          		sb.append(Bytes.toStringBinary(kv.getValue()) + ",");
          		rk = getRealRowKey(kv);
          	}
          	if (sb.toString().length() > 0)
          		sb.setLength(sb.toString().length() - 1);
          	System.out.println(rk + "\t" + sb.toString());
          }
          rsScanner.close();
          	7. 关闭ResultScanner
          	ResultScanner类用于存储服务端扫描的最终结果,可以通过遍历该类获取查询结果。但是,如果不关闭该类,可能会出现服务端在一段时间内一直保存连接,资源无法释放,从而导致服务器端某些资源的不可用,还有可能引发RegionServer的其他问题。所以在使用完该类之后,需要执行关闭操作。这一点与JDBC操作MySQL类似,需要关闭连接。代码的最后一行rsScanner.close()就是执行关闭ResultScanner。
          	8. 使用批量读
          	通过调用HTable.get(Get)方法可以根据一个指定的行键获取HBase表中的一行记录。同样HBase提供了另一个方法,通过调用HTable.get(List<Get>)方法可以根据一个指定的行键列表,批量获取多行记录。使用该方法可以在服务器端执行完批量查询后返回结果,降低网络传输的速度,节省网络I/O开销,对于数据实时性要求高且网络传输RTT高的场景,能带来明显的性能提升。
          	9. 使用批量写
          	通过调用HTable.put(Put)方法可以将一个指定的行键记录写入HBase,同样HBase提供了另一个方法,通过调用HTable.put(List<Put>)方法可以将指定的多个行键批量写入。这样做的好处是批量执行,减少网络I/O开销。
          	10. 关闭写WAL日志
          	在默认情况下,为了保证系统的高可用性,写WAL日志是开启状态。写WAL开启或者关闭,在一定程度上确实会对系统性能产生很大影响,根据HBase内部设计,WAL是规避数据丢失风险的一种补偿机制,如果应用可以容忍一定的数据丢失的风险,可以尝试在更新数据时,关闭写WAL。该方法存在的风险是,当RegionServer宕机时,可能写入的数据会出现丢失的情况,且无法恢复。关闭写WAL操作通过Put类中的writeToWAL()设置。可以通过在代码中添加:put.setWriteToWAL(false);
          	11. 设置AutoFlush
          	HTable有一个属性是AutoFlush,该属性用于支持客户端的批量更新。该属性默认值是true,即客户端每收到一条数据,立刻发送到服务端。如果将该属性设置为false,当客户端提交Put请求时,将该请求在客户端缓存,直到数据达到某个阈值的容量时(该容量由参数hbase.client.write.buffer决定)或执行hbase.flushcommits()时,才向RegionServer提交请求。这种方式避免了每次跟服务端交互,采用批量提交的方式,所以更高效。
          	但是,如果还没有达到该缓存而客户端崩溃,该部分数据将由于未发送到RegionServer而丢失。这对于有些零容忍的在线服务是不可接受的。所以,设置该参数的时候要慎重。
          	可以在代码中添加:table.setAutoFlush(false);table.setWriteBufferSize(12*1024*1024);
          	12. 预创建Region
          	在HBase中创建表时,该表开始只有一个Region,插入该表的所有数据会保存在该Region中。随着数据量不断增加,当该Region大小达到一定阈值时,就会发生分裂(Region Splitting)操作。并且在这个表创建后相当长的一段时间内,针对该表的所有写操作总是集中在某一台或者少数几台机器上,这不仅仅造成局部磁盘和网络资源紧张,同时也是对整个集群资源的浪费。这个问题在初始化表,即批量导入原始数据的时候,特别明显。为了解决这个问题,可以使用预创建Region的方法
          	Hbase内部提供了RegionSplitter工具:${HBASE_HOME}/bin/hbase  org.apache.hadoop.hbase.util.RegionSplitter test2  HexStringSplit  -c 10 -f cf1
          	其中,test2是表名,HexStringSplit表示划分的算法,参数-c 10表示预创建10个Region,-f cf1表示创建一个名字为cf1的列族。
          	13. 调整ZooKeeper Session的有效时长
          	参数zookeeper.session.timeout用于定义连接ZooKeeper的Session的有效时长,这个默认值是180秒。这意味着一旦某个RegionServer宕机,HMaster至少需要180秒才能察觉到宕机,然后开始恢复。或者客户端读写过程中,如果服务端不能提供服务,客户端直到180秒后才能觉察到。在某些场景中,这样的时长可能对生产线业务来讲不能容忍,需要调整这个值
          此参数在HBase-site.xml中,通过<property></property>

           

    3. 扩展-LSM-TREE
      1. 概述
        1. 传统磁盘I/O是比较耗性能的,优化系统性能往往需要和磁盘I/O打交道,而磁盘I/O产生的时延主要由下面3个因素决定:
          1. 寻道时间(将磁盘臂移动到适当的柱面上所需要的时间,寻道时移动到相邻柱面移动所需时间1ms,而随机移动所需时间位5~10ms)
          2. 旋转时间(等待适当的扇区旋转到磁头下所需要的时间)
          3. 实际数据传输时间(低端硬盘的传输速率为5MB/ms,而高速硬盘的速率是10MB/ms)
        2. 近20年平均寻道时间改进了7倍,传输速率改进了1300倍,而容量的改进则高达50000倍,这一格局主要是因为磁盘中运动部件的改进相对缓慢和渐进,而记录表面则达到了相当高的密度。对于一个块的访问完全由寻道时间和旋转延迟所决定,所以花费相同时间访问一个盘块,那么读取的数据越多越好
        3. 磁盘I/O瓶颈可能出现在seek(寻道)和transfer(数据传输)上面
        4. 根据磁盘I/O类型,关系型存储引擎中广泛使用的B树及B+树,而Bigtable的存储架构基础的会使用Log-Structured Merge Tree
      2. B-Tree和B+Tree:如果没有太多的写操作,B+树可以工作的很好,它会进行比较繁重的优化来保证较低的访问时间。而写操作往往是随机的,随机写到磁盘的不同位置上,更新和删除都是以磁盘seek的速率级别进行的。RDBMS通常都是Seek型的,主要是由用于存储数据的B树或者是B+树结构引起的,在磁盘seek的速率级别上实现各种操作,通常每个访问需要log(N)个seek操作
      3. LSM-Tree
        1. 概述
          1. LSM-tree工作在磁盘传输速率的级别上,可以更好地扩展到更大的数据规模上,保证一个比较一致的插入速率因为它会使用日志文件和一个内存存储结构,将随机写操作转化为顺序写
          2. 在传输等量数据场景下,随机写I/O的时延大部分花费在了seek操作上,数据库对磁盘进行零碎的随机写会产生多次seek操作;而顺序存取只需一次seek操作,便可以传输大量数据,针对批量写入大量数据的场景,顺序写比随机写具有明显的优势
          3. The Log-Structured Merge-Tree(LSM-Tree)的一个重要思想就是通过使用某种算法,该算法会对索引变更进行延迟及批量处理并通过一种类似于归并排序的方式高效地将更新迁移到磁盘进行批量写入利用磁盘顺序写性能远好于随机写这一特点,将随机写转变为顺序写,从而保证对磁盘的操作是顺序的,以提升写性能,同时建立索引,以获取较快的读性能,在读和写性能之间做一个平衡
          4. 插入100亿条数据,每条数据大约100kb,比如每次更新1%的数据,如果用B-tree,用时100天,如果用LSM-TREE,用时1天。

        2. LSM-Tree原理
        3. HBase实现
          1. MemStore

            MemStore是HBase中C0的实现,向HBase中写数据的时候,首先会写到内存中的MemStore,当达到一定阀值之后,flush(顺序写)到磁盘,形成新的StoreFile(HFile),最后多个StoreFile(HFile)又会进行Compact。

             

            memstore内部维护了一个数据结构:ConcurrentSkipListMap,数据存储是按照RowKey排好序的跳跃列表。跳跃列表的算法有同平衡树一样的渐进的预期时间边界,并且更简单、更快速和使用更少的空间。

        4. HFile:HFlile是lsm tree中C1的实现

  7. Phoenix
    1. Phoenix介绍和安装
      1. 概述:HBase基础上架构的SQL中间件,使得可以通过SQL/JDBC来操作HBase
      2. 安装
        	1. 上传/下载Phoenix安装包到linux服务器并解压,这台linux服务器最好是Hbase Master节点。所以,如果是Hbase集群,则不需要在全部的服务节点上来安装Phoenix,只需要在HMaster节点上安装即可
        	2. 将Phoneix安装目录下的两个jar包,拷贝到Hbase安装目录下的lib目录
        	cp  phoenix-4.8.1-HBase-0.98-server.jar  /home/software/hbase/lib
        	cp  phoenix-4.8.1-HBase-0.98-client.jar  /home/software/hbase/lib
        	3. 在 etc/profile文件中配置Hbase的目录路径
        	export HBASE_HOME=/home/software/hbase
        	export PATH=$PATH:$HBASE_HOME/bin
        	4. 重新生效,source  /etc/profile
        	5. 启动Hadoop、ZK、HBase集群
        	6. 进入Phoenix安装目录的bin目录
        	7. 执行:./sqlline.py hadoop01,hadoop02,hadoop03:2181
        	
        	如上图所以,证明Phoenix安装成功
        	此外,此时进入hbase,执行list查看,会发现多出如下的表:
        	
        	8. 如果需要kill掉Phoenix进程,则执行: pstree  -p 
        	查看 py进程,杀掉Sqlline的父进程
        	
        

         

    2. Phoenix使用
      	1. 创建表:
      	create table tab1(id integer primary key,name varchar);
      	注:
      	①Phoenix建表必须有声明主键,否则报错
      	②Phoenix建表的表名,在hbase里的表名是大写的,此外,列名也是大写的。
      	③这条建表语句,并未声明表的列族,则默认就一个列族,且列族的名字为:0。
      	④在列族0中,除主键列外,其余的列都属于0列族里的列
      	2. 查看所有表:
      	tables
      	
      
      	3. 插入数据:
      	upsert into tab1 values(1,'hello');
      	注:字符串类型用 '  '包起来,不要用“ ”,否则报错。
      
      	4. 查询数据:
      	select * from tab1;
      	
      
      	5. 删除数据:
      	delete from tab1 where id=2;
      
      	6. 删除表:
      	drop table tab1;
      
      	7. 创建小写的表名:
      	create table "tab2" (id integer primary key,name varchar);
      	create table "tab2" ("id" integer primary key,"name" varchar);
      	select * from  "tab2";
      	注:CRUD都以 "tab2"为表名来操作 
      	
      	8. 自定义列族名
      	create table tab3 (id integer primary key,info.name varchar,info.age integer);
      	upsert into tab3 values(1,'tom',23);
      	然后在hbase里查看会发现:describe 'TAB3'
      	
      	scan 'TAB3'
      	
      
      

       

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

宰祖宣

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值