Hbase优化
1、表的设计
- 1.1 预先创建region
- 默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分。一种可以加快批量写入速度的方法是通过预先创建一些空的regions,这样当数据写入Hbase时,会按照region分区情况,在集群内做数据的负载均衡。
- 1.2 row key:HBase中rowkey用来检索表中的记录,支持以下三种方式
- 检索方式
- 通过单个row key访问:即按照某个row key键值进行get操作
- 通过row key的range进行scan:即通过设置startRowKey和stopRowKey,在这个范围内进行扫描
- 全表扫描:即直接扫描整张表中所有行记录 (该方法不用。 涉及到blockcache中大量的IO)
- 在HBase中,row key可以是任意字符串,最大长度是64KB,实际中一般为10-100bytes,存为byte字节数组,一般设计为定长的。
- row key是按照字典序存储,因此设计row key时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。(顺序IO的速度是快于随机IO的)
- 比如:如果最近写入HBase表中的数据是最可能被访问的,可以考虑将时间戳作为row key的一部分,由于是字典序排序,所有可以使用Long.MAX_VALUE-timestamp作为rowkey,这样可以保证新写入的数据在读取时可以被快速命中。
- Rowkey规则:
- 越小越好(节省存储空间)
- rowkey的设计是要根据实际业务来
- 散列性(尽可能分配到不同的regionserver中)
- 取反(001 取反100 002取反200)
- 哈希
- 检索方式
- 1.3 Column family列族
- 不要在一张表里定义太多的列族。 目前hbase中并不能很好的处理超过2~3个cf的表。
- 因为某个cf在flush的时候,它邻近的cf也会因关联效应被触发flush,而邻近的cf可能当前只有很少一部分数据,(并不需要flush,)因此导致产生了大量的小文件,需要进行合并操作,从而导致系统产生更多的IO。
- 不要在一张表里定义太多的列族。 目前hbase中并不能很好的处理超过2~3个cf的表。
- 1.4 In Memory 1.5 Time To Live 1.6 MAX Version
- 列族是能够进行权限管理、存储的最小单元。 某个列族中的所有列共享以上所有属性(4/5/6)
- COLUMN FAMILIES DESCRIPTION
{NAME => ‘cf’, BLOOMFILTER => ‘ROW’, VERSIONS => ‘1’, IN_MEMORY => ‘false’, KEEP_DELETED_CELLS => ‘FAL
SE’, DATA_BLOCK_ENCODING => ‘NONE’, TTL => ‘FOREVER’, COMPRESSION => ‘NONE’, MIN_VERSIONS => ‘0’, BLOC
KCACHE => ‘true’, BLOCKSIZE => ‘65536’, REPLICATION_SCOPE => ‘0’}
- COLUMN FAMILIES DESCRIPTION
- 建表时,
- 可以通过HColumnDescriptor的setInMemory(true)方法将表放到regionserver的缓存中,保证在读取的时候被cache命中;
- 可以通过HColumnDescriptor的setTimeToLive(int timeToLive)方法设置表中数据的存储生命期,过期数据将自动被删除。
- 例如如果只需要存储最近两天的数据,那么可以设置为setTimeToLive(22460*60)
- 可以通过HColumnDescriptor的setMaxVersions(int maxVersions)方法设置表中数据的最大版本,如果只需要保存最新版本的数据,参数设为1即可。
- 列族是能够进行权限管理、存储的最小单元。 某个列族中的所有列共享以上所有属性(4/5/6)
- 1.7 合并compaction和切分spilt
- hbase为了防止小文件(被刷到磁盘的memstore)过多,以保证查询效率,hbase需要在必要的时候将这些小的storefile合并成相对较大的storefile,这个过程称之为合并(compaction)。compaction存在两种类型:minor和major
- 在HBase中,数据在更新时首先写入hlog日志和内存memstore中,memstore中的数据是排序的,当memstore累计到一定阈值时,就会创建一个新的memstore,并且将老的memstore添加到flush队列,由单独的线程flush到磁盘上,成为一个storefile。于此同时,系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了(minor compact)。
- storeFile是只读的,一旦创建后就不可以再修改。因此HBase中的更新其实是不断追加的操作。当一个store中的storefile达到一定的阈值后,就会进行一次合并(major compact),将对同一个key的修改合并到一起,形成一个大的storefile,当storefile的大小达到一定阈值后,又会对storefile进行分割,等分为两个storefile。
- 由于对表的更新是不断追加的,处理读请求时,需要访问store中全部的storefile和memstore,将他们按照rowkey进行合并(合并时又会涉及到排序),由于storefile和memstore都是经过排序的,并且storefile带有内存中索引,通常合并过程还是比较快的。
- 实际应用中,可以考虑必要时进行手动major compact,将同一个row key的修改进行合并形成一个大的storefile。同时可以将storefile设置大些,减少spilt的发生。
2、写表操作
-
2.1多HTable并发写
- 创建多个HTable客户端用于写操作,不再浪费单个的HTable启动过程,提高写数据的吞吐量。
-
2.2 HTable参数设置
- Auto Flush
- 通过调用HTable.setAutoFlush(false)方法可以将HTable写客户端的自动flush关闭,这样可以批量写入数据到HBase,而不是有一条put就执行一次更新,只有当put填满客户端写缓存时,才实际向HBase服务端发起写请求。默认情况下auto flush是开启的。
- Write Buffer
- 通过调用HTable.setWriteBufferSize(writeBufferSize)方法可以设置HTable客户端的写buffer大小,如果新设置的buffer小于当前写buffer中的数据时,buffer将会被flush到服务端。其中,writeBufferSize的单位是byte字节数,可以根据实际写入数据量的多少来设置该值。
- WAL Flag
- 在HBae中,客户端向集群中的RegionServer提交数据时(Put/Delete操作),首先会先写WAL(Write Ahead Log)日志(即HLog,一个RegionServer上的所有Region共享一个HLog),只有当WAL日志写成功后,再接着写MemStore,然后客户端被通知提交数据成功;如果写WAL日志失败,客户端则被通知提交失败。这样做的好处是可以做到RegionServer宕机后的数据恢复。
- 因此,对于相对不太重要的数据,可以在Put/Delete操作时,通过调用Put.setWriteToWAL(false)或Delete.setWriteToWAL(false)函数,放弃写WAL日志,从而提高数据写入的性能。
- 值得注意的是:谨慎选择关闭WAL日志,因为这样的话,一旦RegionServer宕机,Put/Delete的数据将会无法根据WAL日志进行恢复。
- Auto Flush
-
2.3 批量写
- List<Put> 只需要一次网络IO开销
-
2.4 多线程并发写
3、读表操作
- 3.1多HTable并发读
- 3.2 HTable参数设置
-
Scanner Caching
- hbase.client.scanner.caching配置项可以设置HBase scanner一次从服务端抓取的数据条数,默认情况下一次一条。通过将其设置成一个合理的值,可以减少scan过程中next()的时间开销,代价是scanner需要通过客户端的内存来维持这些被cache的行记录。
- 有三个地方可以进行配置:
- 1)在HBase的conf配置文件中进行配置;
- 2)通过调用HTable.setScannerCaching(int scannerCaching)进行配置;
- 3)通过调用Scan.setCaching(int caching)进行配置。三者的优先级越来越高。Scan更细粒度。
-
Scan Attribute Selection
- scan时指定需要的Column Family,可以减少网络传输数据量,否则默认scan操作会返回整行所有Column Family的数据。
Get get=new Get("1111".getBytes());//传入rowkey //添加要获取的列名和列族,减少网络的io,相当于在服务器端做了过滤 get.addColumn("cf".getBytes(), "name".getBytes()); get.addColumn("cf".getBytes(), "age".getBytes()); get.addColumn("cf".getBytes(), "sex".getBytes());
-
Close ResultScanner
- 通过scan取完数据后,记得要关闭ResultScanner,否则RegionServer可能会出现问题(对应的Server资源 [内存、内核CPU核心数、网络带宽、磁盘] 无法释放)。
-
3.3 批量读
-
3.4 多线程并发读
-
3.5 缓存查询结果
- 对于频繁查询HBase的应用场景,可以考虑在应用程序中(memecache,redis,代码逻辑)做缓存,当有新的查询请求时,首先在缓存中查找,如果存在则直接返回,不再查询HBase;否则对HBase发起读请求查询,然后在应用程序中将查询结果缓存起来。至于缓存的替换策略,可以考虑LRU等常用的策略。先查redis查不到再找hbase
-
3.6 BlockCache
- HBase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写(也承担部分读的压力);另外一部分作为BlockCache,主要用于读。
- 写请求会先写入Memstore,Regionserver会给每个region提供一个Memstore,当Memstore满64MB以后,会启动 flush刷新到磁盘。当Memstore的总大小超过限制时(heapsize * hbase.regionserver.global.memstore.upperLimit * 0.9),会强行启动flush进程,从最大的Memstore开始flush直到低于限制
- 读请求先到Memstore中查数据,查不到就到BlockCache中查,再查不到就会到磁盘上读,并把读的结果放入BlockCache。由于BlockCache采用的是LRU策略,因此BlockCache达到上限(heapsize * hfile.block.cache.size * 0.85)后,会启动淘汰机制,淘汰掉最老的一批数据。
- 一个Regionserver上有一个BlockCache和N个Memstore,它们的大小之和不能大于等于heapsize [每一个节点服务(比如hbase)分配的内存大小]* 0.8,否则HBase不能启动。默认BlockCache为0.2,而Memstore为0.4(尽可能分配memstore多一点,否则可能会导致大量小文件的产生)。对于注重读响应时间的系统,可以将 BlockCache设大些,比如设置BlockCache=0.4,Memstore=0.39,以加大缓存的命中率。
-
数据库连接池 :尽量避免多次连接的开启和关闭操作
HTable
HTable是HBase客户端与HBase服务端通讯的Java API对象,客户端可以通过HTable对象与服务端进行CRUD操作(增删改查)。
Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "tablename");
//TODO CRUD Operation……
- 规避HTable对象的创建开销
- 因为客户端创建HTable对象后,需要进行一系列的操作:检查.META.表确认指定名称的HBase表是否存在,表是否有效等等,整个时间开销比较重,可能会耗时几秒钟之长,因此最好在程序启动时一次性创建完成需要的HTable对象,如果使用Java API,一般来说是在构造函数中进行创建,程序启动后直接重用。
- HTable对象不是线程安全的
- HTable对象对于客户端读写数据来说不是线程安全的,因此多线程时,要为每个线程单独创建一个HTable对象,不同对象间不要共享HTable对象使用,特别是在客户端auto flash被置为false时,由于存在本地write buffer,可能导致数据不一致。
- (ThreadLocal)。在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本
- HTable对象之间共享Configuration,这样的好处在于:
- 共享ZooKeeper的连接:每个客户端需要与ZooKeeper建立连接,查询用户的table regions位置,这些信息可以在连接建立后缓存起来共享使用;
- 共享公共的资源:客户端需要通过ZooKeeper查找-ROOT-和.META.表,这个需要网络传输开销,客户端缓存这些公共资源后能够减少后续的网络传输开销,加快查找过程速度。
- 因此,与以下这种方式相比:
HTable table1 = new HTable(“table1”);
HTable table2 = new HTable(“table2”);
下面的方式更有效些:
Configuration conf = HBaseConfiguration.create();
HTable table1 = new HTable(conf, “table1”);
HTable table2 = new HTable(conf, “table2”);
备注:即使是高负载的多线程程序,也并没有发现因为共享Configuration而导致的性能问题;如果你的实际情况中不是如此,那么可以尝试不共享Configuration。
HTablePool
HTablePool可以解决HTable存在的线程不安全问题,同时通过维护固定数量的HTable对象,能够在程序运行期间复用这些HTable资源对象。
Configuration conf = HBaseConfiguration.create();
HTablePool pool = new HTablePool(conf, 10);
- HTablePool可以自动创建HTable对象,而且对客户端来说使用上是完全透明的,可以避免多线程间数据并发修改问题。
- HTablePool中的HTable对象之间是公用Configuration连接的,能够可以减少网络开销。
- HTablePool的使用很简单:每次进行操作前,通过HTablePool的getTable方法取得一个HTable对象,然后进行put/get/scan/delete等操作,最后通过HTablePool的putTable方法将HTable对象放回到HTablePool中。
Hbase和DBMS比较:
- 查询数据不灵活:
- 不能使用column之间过滤查询
- 不支持全文索引。使用ES和hbase整合完成全文搜索。
- a. 使用MR批量读取hbase中的数据,在ES里面建立索引(no store)之保存rowkey的值。
- b. 根据关键词从索引中搜索到rowkey(分页)
- c. 根据rowkey从hbase查询所有数据