一、高可用

HBase的优化 hbase优化方法_数据


HBase的优化 hbase优化方法_数据_02


HBase的优化 hbase优化方法_java_03


HBase的优化 hbase优化方法_java_04


HBase的优化 hbase优化方法_apache_05

二、预分区

建立分区方式

1、手动设定预分区

create '表名','列族名',SPLITS=>['1000','2000','3000']

HBase的优化 hbase优化方法_java_06


HBase的优化 hbase优化方法_apache_07

2、生成十六进制序列预分区

create '表名','列族名',NUMREGIONS=>15,SPLITLGO=>{'HexStringSplit'}
create 'staff2','info','partition2',{NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'}

HBase的优化 hbase优化方法_HBase的优化_08


HBase的优化 hbase优化方法_HBase的优化_09

3、按照文件设置的规则预分区

创建split.txt文件

举例
aaa
bbb
ddd
ccc
会自动比较顺序分区aaa bbb ccc ddd

然后执行

create '表名', '列族名', {SPLITS_FILE => '绝对路径'}
create 'staff3','partition3',SPLITS_FILE => '/root/data/split.txt'

HBase的优化 hbase优化方法_java_10


HBase的优化 hbase优化方法_HBase的优化_11

4、使用JavaAPI创建预分区

//自定义算法,产生一系列 hash 散列值存储在二维数组中
byte[][] splitKeys = 某个散列值函数
//创建 HbaseAdmin 实例
HBaseAdmin hAdmin = new HBaseAdmin(HbaseConfiguration.create());
//创建 HTableDescriptor 实例
HTableDescriptor tableDesc = new HTableDescriptor(tableName);
//通过 HTableDescriptor 实例和散列值二维数组创建带有预分区的 Hbase 表
hAdmin.createTable(tableDesc, splitKeys);

案例1 学生按学号分区

1、创建分区文件

HBase的优化 hbase优化方法_数据_12

2、建立预分区表
create 'split_table_test', 'cf', {SPLITS_FILE => '/usr/local/soft/hbase-1.4.6/region_split_info.txt'}

HBase的优化 hbase优化方法_apache_13

3、API上传数据
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class Demo2API {

    Connection conn;
    Admin admin;

    @Before
    public void createConn() throws IOException {
        // 1、创建一个配置文件
        Configuration conf = HBaseConfiguration.create();
        // 配置ZK的地址,通过ZK可以找到HBase
        conf.set("hbase.zookeeper.quorum", "master:2181,node1:2181,node2:2181");

        // 2、创建连接
        conn = ConnectionFactory.createConnection(conf);
        // 3、创建Admin对象
        admin = conn.getAdmin();
    }

 @Test
    /**
     * putAll 读取学生信息数据并写入HBase的student表
     */
    public void putAll() throws IOException {
        /**
         * 读取学生信息数据
         */

        // Junit 和 main方法运行时的工作路径不一样
        // 这里传入的相对路径要动态调整
        BufferedReader br = new BufferedReader(new FileReader("data/students.txt"));

        // 与HBase中的student表建立连接
        Table student = conn.getTable(TableName.valueOf("split_table_test"));

        String line = null;

        // 创建Put的集合
        ArrayList<Put> puts = new ArrayList<>();
        int batchSize = 11;
        while ((line = br.readLine()) != null) {
            // 写入HBase
            String[] splits = line.split(",");
            String id = splits[0];
            String name = splits[1];
            String age = splits[2];
            String gender = splits[3];
            String clazz = splits[4];

            Put put = new Put(id.getBytes());
            byte[] info = "cf".getBytes();
            put.addColumn(info, "name".getBytes(), name.getBytes());
            put.addColumn(info, "age".getBytes(), age.getBytes());
            put.addColumn(info, "gender".getBytes(), gender.getBytes());
            put.addColumn(info, "clazz".getBytes(), clazz.getBytes());


            // 每条数据都会执行一次,效率很慢
//            student.put(put);

            // 将每个Put对象加入puts集合
            puts.add(put);
            // 当puts集合的大小同batchSize大小一致时,则调用HTable的put方法进行批量写入
            if (puts.size() == batchSize) {
                student.put(puts);
                // 清空集合
                puts.clear();
            }

        }
        System.out.println(puts.isEmpty());
        System.out.println(puts.size());
        // 当batchSize的大小同数据的条数不成整比的时候 可能会造成最后几条数据未被写入
        // 手动去判断puts集合是否为空,不为空则将其写入HBase
        if (!puts.isEmpty()) {
            student.put(puts);
        }
        br.close();
    }

  @After
    public void close() throws IOException {
        admin.close();
        conn.close();
    }

}

HBase的优化 hbase优化方法_数据_14


HBase的优化 hbase优化方法_数据_15

案例2 电信表找规律分区

HBase的优化 hbase优化方法_java_16

创建分区表
create 'dianxin_split','cf1',{SPLITS=>['1|','3|','5|','7|','9|','B|','D|']}

HBase的优化 hbase优化方法_java_17

导入数据(使用Javaapi)
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
public class Demo3DianXin {
Connection conn;
    Admin admin;
    TableName dianxinTN;

    @Before
    public void createConn() throws IOException {
        // 1、创建一个配置文件
        Configuration conf = HBaseConfiguration.create();
        // 配置ZK的地址,通过ZK可以找到HBase
        conf.set("hbase.zookeeper.quorum", "master:2181,node1:2181,node2:2181");

        // 2、创建连接
        conn = ConnectionFactory.createConnection(conf);
        // 3、创建Admin对象
        admin = conn.getAdmin();
        dianxinTN = TableName.valueOf("dianxin_split");

    }
 @Test
    /**
     * 插入数据
     */
    public void putAll() throws IOException {
        Table dianxin = conn.getTable(dianxinTN);

        BufferedReader br = new BufferedReader(new FileReader("data/DIANXIN.csv"));

        String line;

        int batchSize = 1000;
        ArrayList<Put> puts = new ArrayList<>();
        while ((line = br.readLine()) != null) {

            String[] splits = line.split(",");
            String mdn = splits[0];
            String start_time = splits[1];
            String lg = splits[4];
            String lat = splits[5];

            Put put = new Put(mdn.getBytes());

            put.addColumn("cf1".getBytes(), "lg".getBytes(), Long.parseLong(start_time), lg.getBytes());
            put.addColumn("cf1".getBytes(), "lat".getBytes(), Long.parseLong(start_time), lat.getBytes());

            puts.add(put);
            if (puts.size() == batchSize) {
                dianxin.put(puts);
                puts.clear();
            }

        }

        if (!puts.isEmpty()) {
            dianxin.put(puts);
        }

    }
 @After
    public void close() throws IOException {
        admin.close();
        conn.close();
    }
}

HBase的优化 hbase优化方法_apache_18

三、Rowkey设计原则

在HBase中,row key可以是任意字符串,最大长度64KB,实际应用中一般为 10~100bytes,存为byte[]字节数组,一般设计成定长的。
row key是按照字典序存储,因此,设计row key时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。

rowkey检索表中的记录三种方式:

1. 通过get方式,指定rowkey获取唯一一条记录

2. 通过scan方式,设置startRow和stopRow参数进行范围匹配

3. 全表扫描,即直接扫描整张表中所有行记录

rowkey长度原则

建议越短越好,不要超过16个字节

原因如下:
数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光rowkey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率;
MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。

rowkey散列原则

如果rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息,所有的数据都会集中在一个RegionServer上,这样在数据检索的时候负载会集中在个别的RegionServer上,造成热点问题,会降低查询效率。

rowkey唯一原则

必须在设计上保证其唯一性,rowkey是按照字典顺序排序存储的,因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。

四、热点问题

  HBase中的行是按照rowkey的字典顺序排序的,这种设计优化了scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。然而糟糕的rowkey设计是热点的源头。 热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用,这也会影响同一个RegionServer上的其他region,由于主机无法服务其他region的请求。 设计良好的数据访问模式以使集群被充分,均衡的利用。
  为了避免写热点,设计rowkey使得不同行在同一个region,但是在更多数据情况下,数据应该被写入集群的多个region,而不是一个。
下面是一些常见的避免热点的方法以及它们的优缺点:

加盐(随机)

这里所说的加盐不是密码学中的加盐,而是在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同。分配的前缀种类数量应该和你想使用数据分散到不同的region的数量一致。加盐之后的rowkey就会根据随机生成的前缀分散到各个region上,以避免热点。

哈希(可以预测,通过key获取哈希,还是可以通过get获取数据)

哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据

反转

固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
反转rowkey的例子以手机号为rowkey,可以将手机号反转后的字符串作为rowkey,这样的就避免了以手机号那样比较固定开头导致热点问题

时间戳反转

一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为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、尽量减少rowkey和列的大小,当具体的值在系统间传输时,它的rowkey,列簇、列名,时间戳也会一起传输。如果你的rowkey、列簇名、列名很大,甚至可以和具体的值相比较,那么将会造成大量的冗余,不利于数据的储存与传输
2、列族尽可能越短越好,最好是一个字符
3、列名也尽可能越短越好,冗长的列名虽然可读性好,但是更短的列名存储在HBase中会更好

四、参数调优

hbase.regionserver.global.memstore.size

默认值0.4,RS所有memstore占用内存在总内存中的比例,当达到该值,则会从整个RS中找出最需要flush的region进行flush,直到总内存比例降至该数限制以下,并且在降至限制比例前,将阻塞所有的写memstore的操作,在以写为主的集群中,可以调大该配置项,不建议太大,因为block cache和memstore cache的总大小不会超过0.8,而且不建议这两个cache的大小总和达到或者接近0.8,避免OOM,在偏向写的业务时,可配置为0.45

hbase.regionserver.global.memstore.size.lower.limit

默认值0.95,相当于上一个参数的0.95

如果有 16G 堆内存,默认情况下:

HBase的优化 hbase优化方法_数据_19

hfile.block.cache.size

RS的block cache的内存大小限制,默认值0.4,在偏向读的业务中,可以适当调大该值,具体配置时需试hbase集群服务的业务特征,结合memstore的内存占比进行综合考虑。

hbase.hregion.memstore.flush.size

默认值128M,单位字节,超过将被flush到hdfs,该值比较适中,一般不需要调整。

hfile.block.cache.size

RS的block cache的内存大小限制,默认值0.4,在偏向读的业务中,可以适当调大该值,具体配置时需试hbase集群服务的业务特征,结合memstore的内存占比进行综合考虑。

hbase.hregion.max.filesize

配置region大小,默认是10G,region大小一般控制在几个G比较合适,可以在建表时规划好region数量,进行预分区,做到一定时间内,每个region的数据大小在一定的数据量之下,当发现有大的region,或者需要对整个表进行region扩充时再进行split操作,一般提供在线服务的hbase集群均会弃用hbase的自动split,转而自己管理split。

hbase.hstore.blockingStoreFiles

默认值 10,一个列族下HFile数量达到该值,flush操作将会受到阻塞,阻塞时间为hbase.hstore.blockingWaitTime,默认90000,即1.5分钟,在这段时间内,如果compaction操作使得HFile下降到blockingStoreFiles配置值,则停止阻塞。另外阻塞超过时间后,也会恢复执行flush操作。这样做可以有效地控制大量写请求的速度,但同时这也是影响写请求速度的主要原因之一。生产环境中默认值太小了,一般建议设置大点比如100,避免出现阻塞更新的情况

hbase.regionserver.maxlogs

默认值32,HLOG最大的数量

hbase.regionserver.hlog.blocksize

默认为 2 倍的HDFS block size(128MB),即256MB

JVM调整

内存大小:master默认为1G,可增加到2G,regionserver默认1G,可调大到10G,或者更大,zk并不耗资源,可以不用调整,需要注意的是,调整了rs的内存大小后,需调整hbase.regionserver.maxlogs和hbase.regionserver.hlog.blocksize这两个参数,WAL的最大值由hbase.regionserver.maxlogs * hbase.regionserver.hlog.blocksize决定(默认32*2*128M=8G),一旦达到这个值,就会被触发flush memstore,如果memstore的内存增大了,但是没有调整这两个参数,实际上对大量小文件没有任何改进,调整策略:hbase.regionserver.hlog.blocksize * hbase.regionserver.maxlogs 设置为略大于hbase.regionserver.global.memstore.size* HBASE_HEAPSIZE。

MemStore Flush触发

Region 中任意一个 MemStore 占用的内存超过相关阈值

当一个 Region 中所有 MemStore 占用的内存大小超过刷写阈值的时候会触发一次刷写,这个阈值由 hbase.hregion.memstore.flush.size 参数控制,默认为128MB。我们每次调用 put、delete 等操作都会检查的这个条件的。
但是如果我们的数据增加得很快,达到了 hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier 的大小,hbase.hregion.memstore.block.multiplier 默认值为4,也就是128*4=512MB的时候,那么除了触发 MemStore 刷写之外,HBase 还会在刷写的时候同时阻塞所有写入该 Store 的写请求!这时候如果你往对应的 Store 写数据,会出现 RegionTooBusyException 异常。

整个 RegionServer 的 MemStore 占用内存总和大于相关阈值

如果达到了 RegionServer 级别的 Flush,那么当前 RegionServer 的所有写操作将会被阻塞,而且这个阻塞可能会持续到分钟级别。

WAL数量大于相关阈值或WAL的大小超过一定阈值

如果设置了 hbase.regionserver.maxlogs,那就是这个参数的值;否则是 max(32, hbase_heapsize * hbase.regionserver.global.memstore.size * 2 / logRollSize) (logRollSize 默认大小为:0.95 * HDFS block size)
如果某个 RegionServer 的 WAL 数量大于 maxLogs 就会触发 MemStore 的刷写。
WAL的最大值由hbase.regionserver.maxlogs * hbase.regionserver.hlog.blocksize决定(默认32*2*128M=8G),一旦达到这个值,就会被触发flush memstore,如果memstore的内存增大了,但是没有调整这两个参数,实际上对大量小文件没有任何改进,调整策略:hbase.regionserver.hlog.blocksize * hbase.regionserver.maxlogs 设置为略大于hbase.regionserver.global.memstore.size* HBASE_HEAPSIZE。

定期自动刷写

如果我们很久没有对 HBase 的数据进行更新,这时候就可以依赖定期刷写策略了。RegionServer 在启动的时候会启动一个线程 PeriodicMemStoreFlusher 每隔 hbase.server.thread.wakefrequency 时间(服务线程的sleep时间,默认10000毫秒)去检查属于这个 RegionServer 的 Region 有没有超过一定时间都没有刷写,这个时间是由 hbase.regionserver.optionalcacheflushinterval 参数控制的,默认是 3600000,也就是1小时会进行一次刷写。如果设定为0,则意味着关闭定时自动刷写。
为了防止一次性有过多的 MemStore 刷写,定期自动刷写会有 0 ~ 5 分钟的延迟

数据更新超过一定阈值

如果 HBase 的某个 Region 更新的很频繁,而且既没有达到自动刷写阀值,也没有达到内存的使用限制,但是内存中的更新数量已经足够多,比如超过 hbase.regionserver.flush.per.changes 参数配置,默认为30000000,那么也是会触发刷写的。

手动触发刷写

分别对某张表、某个 Region 进行刷写操作。
可以在 Shell 中执行 flush 命令

五、基础调优(配置文件)

1.允许在 HDFS 的文件中追加内容

hdfs-site.xml、hbase-site.xml
属性:dfs.support.append
解释:开启 HDFS 追加同步,可以优秀的配合 HBase 的数据同步和持久化。默认值为 true。

2.优化 DataNode 允许的最大文件打开数

hdfs-site.xml
属性:dfs.datanode.max.transfer.threads
解释:HBase 一般都会同一时间操作大量的文件,根据集群的数量和规模以及数据动作,设置为 4096 或者更高。默认值:4096

3.优化延迟高的数据操作的等待时间

hdfs-site.xml
属性:dfs.image.transfer.timeout
解释:如果对于某一次数据操作来讲,延迟非常高,socket 需要等待更长的时间,建议把该值设置为更大的值(默认 60000 毫秒),以确保 socket 不会被 timeout 掉。

4.优化数据的写入效率

mapred-site.xml
属性:
mapreduce.map.output.compress
mapreduce.map.output.compress.codec
解释:开启这两个数据可以大大提高文件的写入效率,减少写入时间。第一个属性值修改为true,第二个属性值修改为:org.apache.hadoop.io.compress.GzipCodec 或者其他压缩方式。

5.设置 RPC 监听数量

hbase-site.xml
属性:Hbase.regionserver.handler.count
解释:默认值为 30,用于指定 RPC 监听的数量,可以根据客户端的请求数进行调整,读写请求较多时,增加此值。

6.优化 HStore 文件大小

hbase-site.xml
属性:hbase.hregion.max.filesize
解释:默认值 10737418240(10GB),如果需要运行 HBase 的 MR 任务,可以减小此值,因为一个 region 对应一个 map 任务,如果单个 region 过大,会导致 map 任务执行时间过长。该值的意思就是,如果 HFile 的大小达到这个数值,则这个 region 会被切分为两个 Hfile。

7.优化 HBase 客户端缓存

hbase-site.xml
属性:hbase.client.write.buffer
解释:用于指定 Hbase 客户端缓存,增大该值可以减少 RPC 调用次数,但是会消耗更多内存,反之则反之。一般我们需要设定一定的缓存大小,以达到减少 RPC 次数的目的。

8.指定 scan.next 扫描 HBase 所获取的行数

hbase-site.xml
属性:hbase.client.scanner.caching
解释:用于指定 scan.next 方法获取的默认行数,值越大,消耗内存越大。

9.flush、compact、split 机制

当 MemStore 达到阈值,将 Memstore 中的数据 Flush 进 Storefile;compact 机制则是把 flush出来的小文件合并成大的 Storefile 文件。split 则是当 Region 达到阈值,会把过大的Region一分为二。
涉及属性:
即:128M 就是 Memstore 的默认阈值
hbase.hregion.memstore.flush.size:134217728
即:这个参数的作用是当单个 HRegion 内所有的 Memstore 大小总和超过指定值时,flush该HRegion 的所有 memstore。RegionServer 的 flush 是通过将请求添加一个队列,模拟生产消费模型来异步处理的。那这里就有一个问题,当队列来不及消费,产生大量积压请求时,可能会导致内存陡增,最坏的情况是触发 OOM。
hbase.regionserver.global.memstore.upperLimit:0.4
hbase.regionserver.global.memstore.lowerLimit:0.38
即:当 MemStore 使用内存总量达到hbase.regionserver.global.memstore.upperLimit 指定值时,将会有多个 MemStores flush 到文件中,MemStore flush 顺序是按照大小降序执行的,直到刷新到 MemStore 使用内存略小于 lowerLimit