网易数帆旗下易数大数据团队开源的 Kyuubi,和 Spark 社区的Spark Thrift Server,都是通过纯 SQL 语言和 JDBC 接口的方式降低大数据使用门槛的项目。本文从企业大数据应用场景关注的问题出发,对比了 Kyuubi 与 Spark Thrift Server 的差异与优劣,并引入HiveServer2 进行全面的分析。
Spark Thrift Server 是Apache Spark社区基于HiveServer2实现的一个Thrift服务,旨在无缝兼容HiveServer2。它通过JDBC接口将Spark SQL的能力以纯SQL的方式提供给终端用户。这种“开箱即用”的模式可以最大化地降低用户使用Spark的障碍和成本。我们先从传统的 Spark 作业提交方式入手,谈谈 Spark Thrift Server 具备的优势。
在没有 Spark Thrift Server 的情况下,Spark 作为大数据处理工具,可能并不是对所有人都那么“友好”。
1.1.1 门槛高
用户通过Spark提供的 Scala/Java/Python 等接口使用 Spark时都需要一定的编程基础。同时,用户需要具备良好的大数据背景。举例来说,用户需要知道他们的程序最终提交哪个平台上,YARN、Kubernetes或者是别的平台?用户也需要知道如何去配置他们所提交作业的资源使用,需要多少的 Executor 数,每个 Executor 该配置多少的内存和CPU?他们自己又该知道怎么去合理的使用集群上的资源呢?用多了,会不会对其他关键任务造成影响?用少了,集群的资源是不是闲置浪费?成千上百的配置如何去设置?动态资源调度,自适应查询,推测执行,这些关键特性该如何无成本的应用到各个任务中去?
1.1.2 不安全
用户通过代码的方式访问元数据和数据,数据安全性无法保证,“删库跑路”轻而易举。所有的客户端配置需要直接或者间接的交到用户手中。一方面,这些配置本身就可能包含一些敏感信息;另一方面,后台的服务也基本上“裸露”在用户的面前。很多企业使用 Spark 的时候都会魔改加入一些适配自己场景的特性。例如在数据安全方面,可能大家都用过或者参考过网易开源出来 spark-authorizer 插件通过 Apache Ranger 来实现细粒度的 SQL Standard Based Authorization。但这种安全特性,通过 Spark 代码提交作业的方式,在会写代码的程序员面前,只是约束力不强的“君子协定”。
1.1.3 兼容性
客户端兼容性难以保证。一个用户的 Spark 作业最终被调度到集群上运行,面临着客户端环境和集群环境不一致,用户作业依赖和 Spark 或 Hadoop 依赖冲突等问题。如果团队选择维护一个内部魔改的 Spark 或者 Hadoop 版本,在这些版本的开发过程中,需要时刻注意是否能保持用户接口的兼容性。在各底层版本服务端的升级换代过程中,也需要尽可能的完成用户客户端的升级,这时候就可能引入许多不必要的兼容性测试工作,而且很容易出现测试覆盖不全的问题。
1.1.4 延时高
Spark Application的启动延时。按作业的生命周期维度分类,我们可以简单的将 Spark 任务简单的分为两种,常驻类型和非常驻类型。常驻类型作业的特点就是 Application的启动时间相对于总任务运行时长来讲忽略不计或者和计算过程完全解绑,如 Spark Structed Streaming任务,计算过程只有 Task 线程级别的调度,低延时快响应。而非常驻类型的作业,每次都需要启动 Spark 程序。相对而言,这个过程是非常耗时的,特别对于一些秒级分钟级的计算任务负担较重。
Spark ThriftServer本质上就是一个 Spark 应用在多线程场景下的应用。它在运行时启动一个由 Driver 和 Executor 组成的分布式 SQL 引擎。在 SQL 解析层,该服务可充分利用 Spark SQL 优化器的能力,在计算执行层,由于 Spark ThriftServer 是常驻类型的应用,没有启动开销,当没有开启动态分配的情况下,整个 SQL 的计算过程为纯的线程调度模式,性能极佳。
在这个架构的基础上,基于 HiveServer2 实现了 Session 相关操作,来响应客户端的连接和关闭请求,也实现了 Operation 相关操作,来响应客户端查询请求等。这些请求都由Server前端实现的线程池模型来响应。并调用Server后端的对应方法与SparkSession相关接口的绑定。然后Server后端通过一个异步线程池,将这些 Operation 提交到分布式 SQL 引擎上真正执行。
首先,在这种模式下用户通过简单的 SQL 语言和 JDBC 接口完成和 Spark ThriftServer 的交互,实现自己的业务逻辑。不需要对 Spark 本身有过多的技能基础,或者对 Spark 所运行的平台,或者底层数据的实现有过多的关注。Spark ThriftServer 基本的容量规划、底层服务的打通、以及后续的优化迭代都可以在服务端完成。当然也有同学会认为,SQL 的表达并不能满足所有的业务,但本身这个服务就是对标 HiveServer2 来给这类用户提供纯 SQL能力的。一些复杂的逻辑也可以尝试使用 UDF/UDAF 之类的进一步支持,基本上大部分的大数据处理工作,Spark ThriftServer都是可以胜任的。
其次,后台各类服务的打通都在 Spark ThriftServer 完成,不需要将其他服务的配置,如Hive Metastore Server, Hadoop 集群等的链接配置交到终端用户手中。这一定层度上保证了数据安全。在这基础上,服务端的开发和运维一般有能力去做一些认证鉴权之类的防护工作,保护数据安全。
最后,JDBC接口协议和C/S架构下服务端向下兼容的约束基本上保证了不会出现客户端兼容性的问题。用户只需选择合适版本的 JDBC 的驱动即可,服务端的升级不会造成接口的不兼容。至于版本升级中潜在的 SQL 兼容性的问题,在不使用 Spark ThriftServer 时也同样存在,且更难解决。而且在 Spark ThriftServer 模式下,服务端提前可以做全量的 SQL 采集工作,可以在升级前期就完成校验。
从上面 Spark ThriftServer的基本架构图我们也可以看出,其本质上是一个 Spark Application。用一个 Spark Application 去响应成千上万的客户端请求,一般会存在比较大的限制。
对于单线程的 Spark 应用来说,Driver 节点的单点效应,制约着它能起多少Executor提高并发能力及把数据分成多少个 Partition 来并行的处理。随着Executor数量的上升,Driver 需要处理更多控制面的 RPC 消息,而随着Partition的增加,Driver 也需要处理更多数据面的 RPC 消息。在 Spark ThriftServer 这个多线程模型下,它同时还要处理大量的客户端请求,单点的效应会更加的明显。另外,所有执行线程所依赖的Hive metastore client是一个,在访问HMS的时候也会有较明显的并发问题。
超大任务侵占过多或者所有 Spark ThriftServer 的计算资源,导致其他任务延时或者卡死。
如果使用Fair Scheduler Pools可以一定程度上解决计算侧的资源分配问题。但是在 Driver 调度侧,难以避免的还会遇到HMS,HDFS访问单点的问题,特别是读写动态分区表或者大量 Union 的场景。从本质上将,CPU/内存/IO等资源隔离就应该是YARN、Kubernetes这类资源管理器该干的事情。计算层做逻辑上的资源隔离,效果不可能理想,比如,这个问题在 Apache Impala 项目里也同样存在。
Spark ThriftServer 本身应该是一个支持多租户的系统,即它可以接受不用用户不同客户端的请求并在服务端贡献线程池中分配这些资源完成用户的请求返回结果。但从 Spark 自身设计角度出发,单 Spark 应用实现的 Spark ThriftServer 并不能完整支持多租户,因为整个 Application 只有全局唯一的用户名, 同时包括 Driver 端和 Executor 端。这个结合 HiveServer2 来讲更方便理解,Server端运行的时候一般使用的 Service Principal进行启动,客户端连接带有完整的用户信息,接受服务端认证通过后会由服务端“伪装执行”,所以每次执行任务的实际用户为客户端用户,这里包括了申请资源运行 MR 程序,访问 HMS 和 HDFS 等服务。回到 Spark ThriftServer 这边,这些资源申请和服务的访问都交给服务端用户去做。因此该服务端用户必然需要所有元数据和数据的超级权限。从数据安全的角度,这样做一方面造成潜在的权限泄露问题,另一方面 HMS 和 HDFS 也很难去根据不同的用户完成审计,因此线上出了问题一般很难追根溯源。从资源隔离和共享角度,Spark ThriftServer 占用的是单个资源队列(YARN Queue / Kubernetes Namespace, 这也导致很难细粒度或者弹性地控制每个用户可使用的资源池大小,如果有资源计费等需求就很难满足。
Spark ThriftServer 社区版本是不支持高可用(High Availability, HA)的。很难想象一个没有高可用的服务端应用能否支撑起 SLA 的目标,相信运维人员是肯定睡不安稳了。当然,要给 Spark ThriftServer 增加一个 HA 并不难,例如社区老早就有人提出这个问题,并附上了PR,详见SPARK-11100。对于 Spark ThriftServer 的 HA 实现,各个厂商魔改的方式大体上分两种思路,即“主从切换”和“负载均衡”。主从切换由Active Spark ThriftServer 和若干 Standby Spark ThriftServer 构成,当 Active Spark ThriftServer挂掉之后,Standby 节点触发选主成为 Active 节点接替。这里存在很多问题,第一,主从模式下只有一个 Active 节点,也就面临着严重的 Spark Driver 单点问题,并不能提供很高的并发能力;第二,因软硬件故障,而发生主从切换时,意味着“全部”的当前任务失败,各个 Spark 作业的Failover 并不轻量,这基本上已经可定义为P0级别的“事故”,SLA 肯定是要受到影响了;第三,Standby 节点造成严重的集群资源浪费,无论是是否开启 Spark 动态资源分配的特性,从主从切换的更加顺滑的目的出发,都要为Standby节点“抢占”一定的资源;第四,一般而言,软件故障的概率远远大于硬件故障,而 Spark ThriftServer 软件故障一般是由于客户端高并发请求或者查询结果集过大导致的,这种模式下,一旦发生切换,客户端重试机制同时触发,新的 Standby 节点只会面对更大的宕机风险。为了解决 Spark ThriftServer Driver 单点问题,更加合适的做法是负载均衡模式,这样当客户端的请求量上来,我们只要水平的扩充 Spark ThriftServer 即可。但这种模式也有一定的限制,每个 Spark ThriftServer 都是有状态的,两个服务之间并不能共享,比如一些全局临时视图、UDF 等,这表示客户端每次连接如果要复用都必须重新创建它们。
包括前面谈到的 UDF 在高可用下的复用问题,对单个 Spark ThriftServer 来说,还面临这 Jar 包冲突及无法更新和删除的问题。另外,由于 UDF 是直接加载到 Spark ThriftServer 服务端的,但 UDF 中包含一些无意或者恶意的逻辑,如直接调用 System.exit(-1), 或者类似 Kerberos 认证等影响服务全局的一些操作,可能直接将服务杀掉。
这里我们主要对网易数帆开源的 Kyuubi 项目与 Spark Thrift Server进行功能上的比较,对 Kyuubi 实现架构感兴趣的可参考 - Kyuubi: 网易数帆开源的企业级数据湖探索平台(架构篇)。此外,这里也引入HiveServer2 进行更加全面的分析。大致维度的对比,请见下表。
从接口和协议上来说,Kyuubi、Spark Thrift Server以及HiveServer2是完全一致的。因此,从用户的角度来讲,总体使用方式是不变的。与HiveServer2相比,前两者带给用户最大的不同应该就是性能上的飞跃。用户可以选择市面上既有的 BI 工具, 如网易易数、 DBeaver,及Hadoop生态圈的Hue / Hive Beeline 等工具完整适配三个不同的服务。
从 SQL 语法兼容的角度,Kyuubi 和 Spark Thrift Server 一样完全委托 Spark SQL Catalyst 层完成,所以和 Spark SQL 全兼容。而 Spark SQL 也基本上完全支持 Hive QL集合,只有少量可枚举的 SQL 行为及语法上的差异。
维基百科: 多租户技术(英语:multi-tenancy technology)或称多重租赁技术,是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共享相同的系统或程序组件,并且仍可确保各用户间资料的隔离性。
Kyuubi、Spark ThriftServer 和 HiveServer2 的使用场景是一个典型的多租户架构。
在资源层面,我们要考虑的是在资源隔离的基础上 1) 如何在逻辑上更加安全和高效的利用这些计算资源 2) 如何赋予用户对属于自己的资源“足够”的控制能力。
HiveServer2 在这方面应该是做的最暴力最灵活的一个,每条 SQL 都被编排成若干 Spark 独立程序进行执行,在执行前都可以设置队列、内存等参数。但这种做法一个方面是导致了极高 Spark 启动时延,另一个方面也无法高效地利用计算资源。
Spark ThriftServer 的方向则完全相反,因为 Spark 程序只有一个且已经预启动,从用户的接口这边已无法进行队列信息、内存参数等资源调整。内部可以通过Fair Scheduler Pools进行“隔离”和“共享”,只能对不同的 Pool 设置权重,然后将 SQL 任务抛进不同的 Pool 而得到公平调度的能力,离真正的意义上的资源隔离还差的很远。
Kyuubi 在这个方面在其他两个系统的实现上,做了“中和”。围绕 Kyuubi Engine 这个概念实现多租户特性,一个 Engine 即为一个 Spark 程序。
在数据层面,我们需要考虑就是元数据和数据访问的安全性,不同的用户只能访问其有权限访问的数据。如文件级别的ACL权限或者是元数据层面的基于 SQL 标准实现的诸如 SELECT/CREATE/ATTER/UPDATE/DROP 等类型和 DATABASE/TABLE/COLUMN 等级别的细粒度权限控制。这一切的实现都依赖一个基本的概念多租户,Spark ThriftServer 单用户显然是不可能适配这套模型的,而 Kyuubi 由于实现了多租户,天然就支持了文件级别的 ACL 权限控制,另外 通过适配 spark-authorizer 插件,也有能力实现 SQL 标准的权限控制。
Spark ThriftServer 的高可用问题前文已经涉及,这里不再赘述。在 Kyuubi 中,我们以负载均衡模式提供高可用, Kyuubi 服务本身不是集群资源的消耗大户,水平扩展不会有过多的负担。
无论是 HiveServer2 还是 Spark ThriftServer 的 SQL 的编译优化都在 Server端完成,这个阶段需要单点地完成元数据的获取,对于大型分区表扫描存在一定的瓶颈。另外 Spark ThriftServer 同时兼具这 Spark 计算侧的 Driver 角色,负责调度服务,原则上 Executor 数量越多,处理的数据量越大,Server 端的压力也就越大。在 Kyuubi 中,用户提交的 SQL 编译优化都在 Engine 端完成,SQL Analyzer 阶段对于元数据的访问获取可以从 Server 端释放出来,同时所有的计算过程也都在 Engine 端完成,极大降低了 Kyuubi Server 端的工作负载。
撇开高可用功能不讲,单个 Spark ThriftServer 由于客户端响应逻辑和 Spark 计算强耦合,一方面提高客户端并发能力则会分走大量的 Driver 端线程资源,另一方面 Driver 端在高计算负载下面临繁重的 GC 问题,丧失一定的客户端响应能力。当部分查询返回较大的结果集时,也很容易造成 OOM 的风险。 Kyuubi 由于 Server 和 Engine 分离的设计,在这方面完全不存在问题。对于 UDF 使用的问题,因为是在 Engine 内部进行加载和使用的,如果用户“行车不规范”,最多也就“自己两行泪”,不会对服务的稳定性造成任何影响。
Kyuubi 在统一接口基础上,拓展了 Spark ThriftServer 在多租户模式下的使用场景,并依托多租户概念获得了完善的资源隔离共享能力和数据安全隔离的能力。而得益于 Kyuubi Server 和 Engine 松耦合的架构极大提升了服务自身的并发能力和服务稳定性。另外,由于篇幅有限,Kyuubi 对数据湖的友好支持将在以后的分享中进行介绍。
作者简介:燕青,目前就职于网易数帆-易数事业部,专注于开源大数据领域。网易数帆开源 Kyuubi 数据湖探索平台作者。Apache Spark Committer / Apache Submarine Committer.