阿里云HBase SQL(Phoenix)服务深度解读

阿里云HBase SQL服务简介

云HBase2.0是阿里云对社区HBase2.0的深度定制,在内核层面做了大量优化升级,并提供全球多活、备份恢复、冷存储等企业级特性,目前已被广泛应用于车联网、社交、推荐、画像等场景。阿里云HBase SQL基于Phoenix 5.0版本,为云HBase2.0赋予NewSQL特性,降低kv接口使用复杂性,并提供Schema、Secondary Indexes、View 、Bulk Loading(离线大规模load数据)、Atomic Upsert、Salted Tables、Dynamic Columns、Skip Scan等特性的能力,大大降低了用户的使用门槛。阿里云HBase团队将SQL能力服务化,在产品、内核上做了一系列优化升级,笔者将在本文中对这些特性做深度解读。

HBase SQL服务化

SQL_

HBase SQL服务如图所示,用户在阿里云HBase实例管理页面可以一键开通,并在界面进行配置、重启、升级,获取URL串之后直接可以访问。

Phoenix的用户都知道,Phoenix社区提供两种访问方式,重客户端和轻客户端。重客户端将Phoenix Core也就是Phoenix的大部分代码逻辑都放在了客户端中,好处是直接跟HBase集群交互,有一定性能优势,但是,用户在使用过程中经常会碰到很多问题,比如,依赖过重带来jar包冲突、服务端CP逻辑跟重客户端不一致导致难以升级、无法使用非Java语言、客户端CPU和内存占用较高等。轻客户端则采用QueryServer的方式将Phoenix Core的逻辑运行在单独的服务进程中,然后客户端可以保留最小的依赖、同时支持多种语言。

云HBase SQL服务采用分层的轻客户端架构,使用QueryServer做为服务端,其部署形态与RegionServer一一对应,由于无状态的设计,在上层架设SLB负载均衡器,可将客户端请求分发到每个QueryServer上,在降低客户端负载同时,也提高了Phoenix的并发扩展性。
thin_client

QueryServer借助Calcite的Avatica实现一套jdbc标准的RPC通信框架,将jdbc的接口封装成各类HTTP Request,并在内部嵌入Jetty做为其HTTP Server,处理客户端发送的Request并返回Response。目前QueryServer支持Json和Protobuf两种传输协议,由于传输高效性和兼容性,默认采用Protobuf做为传输协议。

在QueryServer中,主要jdbc接口对应的Rpc请求和Phoenix的action如下:

jdbc接口 Avatica请求 Phoenix动作
getConnection OpenConnectionRequest 创建phoenix重客户端连接并缓存在QueryServer
createStatement CreateStatementRequest 同步connection参数至PhoenixConnection,创建phoenixStatement并缓存
execute/executeUpdate/executeQuery(Statement) PrepareAndExecuteRequest 获取请求携带SQL语句,转发给PhoenixStatement执行
prepareStatement PrepareRequest 获取SQL,创建PhoenixPrepareStatement并缓存
execute/executeUpdate/executeQuery(PrepareStatement) ExecuteRequest PhoenixPrepareStatement设置请求携带预设参数值,并执行,返回执行结果
addBatch 数据缓存在客户端
executeBatch ExecuteBatchRequest 获取客户端缓存batch并设置在PhoenixPrepareStatement中,执行executeBatch返回结果
commit CommitRequest 在写数据时,如果autoCommit为false,写入数据首先会缓存在QueryServer中,commit时将缓存数据批量写入HBase

相对于云HBase1.x上的Phoenix重客户端架构,轻客户端架构更符合NewSQL数据库设计框架,并有以下优势:

  • QueryServer自身无状态,易于横向扩展
  • 计算资源移至Server端,客户端可运行在低配环境中
  • 真正面向jdbc接口,客户端不再访问HBase/Zookeeper
  • 支持更多native客户端语言访问,如python、go、.net等
  • 易于维护,升级时只对服务端升级,客户端无需修改

轻客户端优化

云HBase SQL团队针对社区Phoenix5.0版本进行深度测试,对轻客户端进行了一系列的优化和bug fix,并反馈给Phoenix和Avatica社区,推动社区共同发展。

  1. 轻客户端写性能优化
    在对轻客户端的写入性能测试时,我们发现社区轻客户端的写入性能比直接使用重客户端慢2~3倍,主要原因正式由于轻客户端相比重客户端多了一层HTTP传输链路,因此在尽量不修改业务代码的前提下,如何减少请求次数是优化写性能的重点。

在业务代码中经常使用jdbc的PreparedStatement的executeUpdate进行写入,社区轻客户端在autoCommit为false时会将数据传输至QueryServer并缓存,在commit时将数据batch写入到HBase中。阿里云Phoenix修改了executeUpdate执行逻辑,将数据缓存在客户端,commit时将批量数据发到QueryServer并直接写入HBase,在这一阶段减少了轻客户端到QueryServer的RPC请求次数,并避免缓存在QueryServer的数据在未提交前由于服务挂掉而丢失。
优化后在代码写入性能提升了2倍多,与重客户端性能基本没有差距。

  1. 支持upsert multi values语法
    原生Phoenix在写入时仅支持单次upsert一条数据的语法,这在对接Mybatis等不支持commit的框架时,每次写入一条数据并提交HBase性能上较差,为此阿里云Phoenix在SQL服务上支持了单次upsert写入多值功能,语法示例:upsert into tableName(col1,col2,...) values (v11,v12,...),(v21,v22,...),...,(vn1,vn2,...)

在使用时注意:

  • 为避免列次序混乱,必须指定写入列
  • 写入数据其中一条失败导致整批数据写入失败
  1. 支持QueryServer线程池最大线程和HTTP传输参数可配
    QueryServer使用Jetty做为HttpServer,在初始化时默认最大200个线程,在使用配置比较高的HBase集群时,往往由于处理线程数成为性能瓶颈,阿里云Phoenix修改了Avatica代码,通过增加配置方式使得线程数以及HTTP传输size大小等都可配,增加了Phoenix使用的灵活性,具体配置及说明如下:
参数名 描述
phoenix.queryserver.maximun.threads QueryServer最大线程数,默认200
phoenix.queryserver.minimum.threads QueryServer最小线程数,默认8
phoenix.queryserver.idle.timeout 线程空闲超时时间,默认60s
phoenix.queryserver.maximum.header.size HTTP请求包头大小,默认65536byte

以上参数在云HBase控制台都可以进行修改,并重启HBase SQL服务后生效。

  1. 轻客户端Connection超时Reopen时设置参数丢失问题
    轻客户端连接默认空闲10min会超时,下次使用时会进行重建,如果在超时前设置autoCommit、schema等连接属性会在reOpen时失效,这是社区avatica的一个bug,我们对此进行了修复,并贡献到社区,对应jira:https://issues.apache.org/jira/browse/CALCITE-2882 (贡献社区calcite-avatica 1.14.0版本)
  2. 轻客户端PrepareStatement预编译带有子查询/join语句时参数越界
    在使用轻客户端PrepareStatement预编译SQL时会将phoenix客户端的ParamMetaData同步到轻客户端的Connection,当SQL带有子查询或join语句时,Phoenix在实现上缺失了右表的Meta信息,导致出现参数越界的问题。

阿里云的Phoenix版本也对此进行了修复,详细见jira:https://issues.apache.org/jira/browse/PHOENIX-5192

  1. 轻客户端写入Array类型数据NPE
    轻客户端写入Array类型数据时由于获取Array中子元素的类型默认处理为空,导致在写入时出现NPE。问题修复见

jira:https://issues.apache.org/jira/browse/CALCITE-2939 (贡献社区calcite-avatica 1.14.0版本)

Phoenix Core优化与改进

我们对Phoenix Core做了大量稳定性、性能方面的优化和改进,提交了数十个patch给社区,团队的瑾谦同学也成为了Phoenix社区的Commiter。下面介绍几个比较有代表性的改进工作:

  1. 大表MetaCache缓存优化
    Phoenix每次查询都需要使用表的所有region信息,当表很大有上万个region时,读取meta表的region信息并缓存到客户端需要耗时几十秒,占用查询时间超过80%以上。阿里云Phoenix使用ZooKeeper订阅的方式,通过监控Region的状态变化,当Region发生balance/merge/split动作时,更新客户端缓存失效的region信息,这样可以大大减少客户端因为Region元数据更新不及时导致的查询失败,同时显著提高查询性能。

meta_cache

优化效果:经过测试对一张大表查询时如果出现Region状态变化,查询时延由40s降低到5s左右。

  1. 使用Lookup Join提升大表的索引表回查性能
    如果Phoenix二级索引不是覆盖索引,也就是说SELECT语句中的返回字段不在索引表中,那么在执行过程中会先查询索引表,获取RowKey,然后再用SemiJoin主表的方式获取最终结果。除了索引表本身的Scan开销外,还存在网络开销和SemiJoin本身的开销。我们采用Lookup Join的方式进行优化,在服务端scan索引表的时候,直接回查主表。经过测试,性能有大幅提高,500w数据量的查询,原有方式需要200s,而新的方式只需要10s,如果主表开启了bloomfilter,性能还能进一步提高到5s左右。
  2. 使用MultiGet取代SkipScan
    Phoenix在执行SemiJoin的时候会使用SkipScan的方式,该方式比较通用,scan的过程中会skip掉一定的读HFile开销,但是当SemiJoin的查询条件比较多并且比较分散时,就转变为了近似扫全表,这时性能就会下降严重,甚至超时。对于这种情况,我们采用了MultiGet的方式取代SkipScan会获得更好的性能。当用户对Primary Key做in查询时,可以通过添加“USE_GET”的hint使用该功能,可以支持10w级别的子查询结果。在查询条件比较多并且分散的情况,会有数量级的性能提升。
  3. 临时禁止掉存在风险的功能feature
    社区Phoenix功能很多很全,但是某些功能存在一定缺陷,或者会引入风险,在确保功能完善可用之前,我们会选择将部分功能禁掉,以避免用户误用,导致错误。目前涉及到的功能有:
  • ColumnDef中的DESC关键字。对于可变长度类型,比如varchar、decimal,该功能可能范围查询结果返回错误。
  • 全表扫描。对于没有加查询条件的查询语句,可能会触发全表扫描,导致系统不稳定。
  • Local Index。由于社区反馈和我们在实际支持用户的过程中发现的Local Index问题较多,而且大部分二级索引的场景,Global Index已经足够使用,因此我们临时禁掉了Local Index的功能。
  1. 时区问题解决
    社区Phoenix在时区处理上的逻辑会导致用户的不同用法,写入数据和查询结果不一致。详见文章:Phoenix关于时区的处理方式说明

总结展望

阿里云HBase团队在SQL服务上持续深耕,不断在稳定性、易用性、功能以及性能上继续增强优化,并不断反馈社区,积极推动Phoenx社区的发展。从HBase的架构上来看,相比于其他NoSQL存储引擎,其在数据规模,扩展性上具有明显优势。跟大数据其他组件,比如,搜索引擎、分析引擎、消息引擎,会更紧密的结合,SQL服务也会在这方面进行更多的探索,满足用户在实际使用过程中遇到的复杂查询、复杂分析等需求。在这里可以预告下,我们基于HBase和Solr双引擎,支持复杂查询的Phoenix Search Index功能即将上线。

猜你喜欢

转载自yq.aliyun.com/articles/703234