Phoenix使用建议


转自:阿里云社区

 

Phoenix是什么

 

Phoenix查询引擎支持使用SQL进行HBase数据的查询,会将SQL查询转换为一个或多个HBase API,协同处理器与自定义过滤器的实现,并编排执行。使用Phoenix进行简单查询,其性能量级是毫秒。

 

更多的信息可以参考官网

 

Phoenix使用建议

1、 二级索引使用指南

二级索引是Phoenix中非常重要的功能,用户在使用前需要深入了解,可以参考资料:二级索引社区文档二级索引中文文档全局索引设计实践

下面介绍一些在使用二级索引过程中的注意事项:

 

1)是否需要使用覆盖索引?

覆盖索引需要将查询返回字段加入到索引表中,这样在命中索引时,只需要查询一次索引表即可,非覆盖索引,要想拿到完整结果则需要回查主表。不难理解,覆盖索引查询性能更好,但是会浪费一定存储空间,影响一定写性能。非覆盖索引使用时,有时执行计划并不能默认命中索引,此时,用户需要加索引Hint

 

2)应该使用local Index还是global Index?实现上,一个global index表对应着一个hbase 表,local index是在主表上新增一列存储索引数据。

适用场景上,global index 适用于多读的场景,但存在同步索引时带来网络开销较大的问题。而local由于和原数据存储在一张表中同步索引数据会相对快一点。虽然local index也有一定适用场景,但仍然推荐使用global index, 其原因有以下几点:

  1. 当前版本的phoneix的local index的实现相对global index不太完善,问题较多,使用存在一定的风险。
  2. local index不太完善,大的改动后,可能会存在不兼容,升级流程比较复杂。
  3. 在大数据量下,原始数据和索引数据放在一起会加剧region分裂,且分裂后索引数据的本地性也会丧失。

因此,在阿里云HBase SQL服务中LOCAL INDEX功能已经被禁止!

 

3)索引表最多可以创建多少个?

索引会保证实时同步,也会引来写放大问题,一般建议不超过10个,如果超过建议使用HBase全文索引功能

 

4) 构建索引需要注意哪些事项?

使用创建索引语句(CREATE INDEX)时,如果指定async参数,则为异步构建,语句完成时,会在SYSTEM.CATALOG表中简历索引表的元信息,并建立跟主表的关系,但是状态是building,索引表中没有数据,也不可查,需要后续用REBUILD语句来构建,或者借助MR工具来构建索引,并将状态更新为active。如果不指定,则为同步构建,一般建议数据量小于1亿行都使用同步构建即可,比较简单。

2、 加盐注意事项

注意:本特性有副作用,不建议使用。使用前请确保您已经充分了解其原理与适用场景。如果您无法确定您的场景是否适合,请钉钉与我们联系,我们会帮您进行评估。

加盐通常用来解决数据热点和范围查询同时存在的场景。原理介绍可以参考:Phoenix社区文档中文文档

加盐有较强的适用场景要求,场景不合适将适得其反:

  • 写热点或写不均衡:比如以时间作为第一列主键,永远写表头或者表尾
  • 需要范围查询:要按第一列主键进行范围查询,故不能使用hash打散

有热点就要打散,但打散就难以做范围查询。因此,要同时满足这对相互矛盾的需求,必须有一种折中的方案:既能在一定程度上打散数据,又能保证有序。这个解决方案就是加盐,亦称分桶(salt buckets)。数据在桶内保序,桶之间随机。写入时按桶个数取模,数据随机落在某个桶里,保证写请求在桶之间是均衡的。查询时读取所有的桶来保证结果集的有序和完备。

一般来说,严格满足上述条件的业务场景并不常见。大多数场景都可以找到其他的业务字段来协助散列。考虑到其严重的副作用,我们不建议使用这个特性。


副作用:

  • 写瓶颈:一般全表只有buckets个region用于承担写。当业务体量不断增长时,因为无法调整bucket数量,不能有更多的region帮助分担写,会导致写入吞吐无法随集群扩容而线性增加。导致写瓶颈,从而限制业务发展。
  • 读扩散:select会按buckets数量进行拆分和并发,每个并发都会在执行时占用一个线程。select本身一旦并发过多会导致线程池迅速耗尽或导致QueryServer因过高的并发而FGC。同时,本应一个RPC完成的简单查询,现在也会拆分成多个,使得查询RT大大增加。

这两个副作用会制约业务的发展,尤其对于大体量的、发展快速的业务。因为桶个数不能修改,写瓶颈会影响业务的扩张。读扩散带来的RT增加也大大降低了资源使用效率。


常见的使用误区

  • 预分区:用分桶来实现建表的预分区最常见的误用。这是因为Phoenix提供的split on预分区语法很难使用。目前可用hbase shell的建表,指定预分区,之后关联为Phoenix表。在海量数据场景,合理的预分区是一个很有挑战的事情,我们后续会对此进行专题讨论,敬请期待。
  • 伪热点:写入热点或不均衡大多数情况都是假象,通常还有其他字段可用于打散数据。比如监控数据场景,以metric名字的hash值做首列主键可有效解决写入均衡的问题。

一定不要为了预分区而使用加盐特性,要结合业务的读写模式来进行表设计。如果您无法做出准确的判断,可以钉钉联系云HBase值班咨询。

Buckets个数跟机型配置和数据量有关系,可以参考下列方式计算,其中 N 为 Core/RS 节点数量:

  • 单节点内存 8G: 2*N
  • 单节点内存 16G: 3*N
  • 单节点内存 32G: 4*N
  • 单节点内存 64G: 5*N
  • 单节点内存 128G: 6*N

注意:索引表默认会继承主表的盐值;bucket的数目不能超过256;一个空的Region在内存中的数据结构大概2MB,用户可以评估下单个RegionServer承载的总Region数目,有用户发生过在低配置节点上,建大量加盐表直接把集群内存耗光的问题。

3、 慎用扫全表、OR、Join和子查询

虽然Phoenix支持各种Join操作,但是Phoenix主要还是定位为在线数据库,复杂Join,比如子查询返回数据量特别大或者大表Join大表,在实际计算过程中十分消耗系统资源,会严重影响在线业务,甚至导致OutOfMemory异常。对在线稳定性和实时性要求高的用户,建议只使用Phoenix的简单查询,且查询都命中主表或者索引表的主键。另外,建议用户在运行SQL前都执行下explain,确认是否命中索引,或者主键,参考explain社区文档explain中文文档

4、 Phoenix不支持复杂查询

Phoenix的二级索引本质还是前缀匹配,用户可以建多个二级索引来增加对数据的查询模式,二级索引的一致性是通过协处理器实现的,索引数据可以实时可见,但也会影响写性能,特别是建多个索引的情况下。对于复杂查询,比如任意条件的and/or组合,模糊查找,分词检索等Phoenix不支持。

5、 Phoenix不支持复杂分析

Phoenix定位为操作型分析(operational analytics),对于复杂分析,比如前面提到的复杂join则不适合,这种建议用Spark这种专门的大数据计算引擎来实现,参考X-Pack Spark分析服务, HBase SQL(Phoenix)与Spark的选择

6、 Phoenix是否支持映射已经存在的HBase表?

支持,参考社区相关文档。用户可以通过Phoenix创建视图或者表映射已经存在的HBase表,注意如果使用表的方式映射HBase表,在Phoenix中执行DROP TABLE语句同样也会删除HBase表。另外,由于column family和列名是大小写敏感的,必须一一对应才能映射成功。另外,Phoenix的字段编码方式大部分跟HBase的Bytes工具类不一样,一般建议如果只有varchar类型,才进行映射,包含其他类型字段时不要使用映射。

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
 
import com.google.common.base.Strings;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.RegionLocator;
import org.apache.hadoop.hbase.mapreduce.TableInputFormat;
import org.apache.hadoop.hbase.mapreduce.TableSplit;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
 
public class SaltRangeTableInputFormat extends TableInputFormat {
 
     @Override
     public List<InputSplit> getSplits(JobContext context) throws IOException {
         Configuration conf = context.getConfiguration();
 
         String tableName = conf.get(TableInputFormat.INPUT_TABLE);
         if (Strings.isNullOrEmpty(tableName)) {
             throw new IOException( "tableName must be provided." );
         }
 
         Connection connection = ConnectionFactory.createConnection(conf);
         val table = TableName.valueOf(tableName)
         RegionLocator regionLocator = connection.getRegionLocator(table);
 
 
         String scanStart = conf.get(TableInputFormat.SCAN_ROW_START);
         String scanStop = conf.get(TableInputFormat.SCAN_ROW_STOP);
 
         Pair< byte [][], byte [][]> keys = regionLocator.getStartEndKeys();
         if (keys == null || keys.getFirst() == null || keys.getFirst().length == 0 ) {
             throw new RuntimeException( "At least one region is expected" );
         }
         List<InputSplit> splits = new ArrayList<>(keys.getFirst().length);
         for ( int i = 0 ; i < keys.getFirst().length; i++) {
             String regionLocation = getTableRegionLocation(regionLocator, keys.getFirst()[i]);
             String regionSalt = null ;
             if (keys.getFirst()[i].length > 0 ) {
                 regionSalt = Bytes.toString(keys.getFirst()[i]).split( "-" )[ 0 ];
             }
 
             byte [] startRowKey = Bytes.toBytes(regionSalt + "-" + scanStart);
             byte [] endRowKey = Bytes.toBytes(regionSalt + "-" + scanStop);
 
             InputSplit split = new TableSplit(TableName.valueOf(tableName),
                     startRowKey, endRowKey, regionLocation);
             splits.add(split);
         }
         return splits;
     }
 
     private String getTableRegionLocation(RegionLocator regionLocator,
                                           byte [] rowKey) throws IOException {
         return regionLocator.getRegionLocation(rowKey).getHostname();
     }
}


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM