Apache Kyuubi实战:多租户高并发SQL网关架构与部署指南
2026/9/20 3:28:39 网站建设 项目流程

时间拉回到2019年,我第一次在内部集群上把Spark SQL通过HiveServer2的方式暴露给业务方。当时最痛苦的一件事就是:一组大数据工程师写的报表任务,只是并发十几个过来,HiveServer2就开始假死,日志里全是排队和内存溢出。再往后,多个部门共用一套HiveServer2实例,权限隔离基本靠自觉,谁跑一个重量级查询,整条链路上的其他任务全部遭殃。也就是那时候,我开始认真关注Apache Kyuubi——一个真正能解决“多租户、高并发、资源隔离”问题的SQL网关。

这篇内容很适合正在做数据平台、数仓工具链建设,或者给业务侧提供即席查询服务的读者。Kyuubi这些年从网易内部孵化到Apache顶级项目,已经不是单纯的服务端代理,而是围绕着Spark SQL生态形成了一整套“应用层网关+引擎池化”的最佳实践。下面我会把它的背景、架构、部署、选型对比以及实际踩坑经历完整拆开讲,尽量讲人话,不给官方文档念经。

1. 为什么要单独搞一个SQL网关:Kyuubi的诞生背景

1.1 HiveServer2的并发瓶颈与体验痛点

理解Kyuubi之前,得先搞明白它要替代的旧方案到底哪里难受。传统HiveServer2是Hive自带的服务端组件,负责接收JDBC请求、解析SQL、提交到YARN上执行。在MapReduce时代,HiveServer2的设计还算够用,因为底层MapReduce任务本身就重,秒级并发请求量不大;但换成Spark作为计算引擎后,SQL任务一个比一个轻快,业务方一旦开始大量使用BI工具、自助分析平台,HiveServer2就成了集群生态里最先扛不住的环节。

具体问题我总结成三个:

第一是连接数瓶颈。HiveServer2对每个会话都会创建独立的Session和相关的Hive Metastore连接,并发连接一多,服务端线程资源直接被打满,大量请求处于排队状态,最终表现为“连接超时”或“加载目录卡死”。

第二是引擎无法共享。老方案里每个用户连上HiveServer2,提交Spark任务时都会拉起一个独立的Spark Application,提交到YARN上。十几个用户同时跑查询,意味着同时有十几个Spark App在抢资源,每个App都要抢占driver内存、executor资源,集群很快被吃空,实际上这些任务可能只跑几秒钟。

第三是资源隔离形同虚设。多个业务方共用一个服务,要么把所有Spark任务丢进同一个default队列,要么通过队列名硬编码区分,运维人员根本没法做到按用户、按部门去做细粒度的资源配额。谁跑一个full scan,其他人陪葬,业务方互相抱怨,平台组就得背锅。

1.2 Kyuubi的设计目标:把“会话”和“引擎”解耦

Kyuubi的出现,本质上是要重构“客户端-执行引擎”的交互模式。它明确提出了一个核心思路:建立服务端的Session管理,并且让多个Session共享一个引擎实例(Engine)

我这里用生活化的方式描述一下区别。传统模式像在餐厅里每个顾客点菜后,后厨立即单独开一个灶台,菜一多灶台就满了;Kyuubi的做法则是后厨维护一个“灶台池”,同一个厨师团队(Engine)可以同时出多桌客人的菜,灶台不够了才扩容,客人走了也不急着关灶台——再等等,说不定下一位客人马上就来点菜。

这个设计带来的直接收益特别大:

  • 连接复用:服务端会维护一个引擎实例,多个客户端Session通过Kyuubi Server接入后,转发到对应的Engine上执行,而不是每个会话独立拉起Spark App。
  • 启动延迟显著降低:HiveServer2直提交Spark时,每次都要等SparkContext初始化,冷启动经常要半分钟甚至更久;Kyuubi的引擎提前拉起并保持常驻,SQL提交后基本只花几秒完成解析和调度。
  • 多租户隔离成为可能:Kyuubi支持按用户、按分组共享或分离引擎,不同组的用户可以用不同的Engine跑在各自的YARN队列里,互不干扰。

这也是为什么Kyuubi很快在业界流行起来——它不是把HiveServer2本地改几个参数,而是从架构层面改变了“网关”与“计算引擎”的定位关系。后面我会着重拆解这套架构在工程上是怎么落地的。

2. 核心架构拆解:Kyuubi到底在管什么

2.1 三层层级:Server、Engine、Session

Kyuubi的架构可以从上到下分成三层:

  • Kyuubi Server(服务端):一个常驻进程,负责接收客户端(beeline、JDBC、Thrift客户端)的连接请求,管理Session的创建、绑定、释放,还负责将请求负载均衡到不同的Engine上。
  • Engine(执行引擎实例):本质上是嵌入了一段Kyuubi服务的Spark Application,运行在YARN、Kubernetes或者本地模式上。Engine内部包含了SparkContext、SQLSession,并对外提供Thrift接口。
  • Session与Operation(会话与操作):Session是客户端连接在Server侧的逻辑会话,Operation则对应用户执行的SQL查询、获取元数据、设置参数这些具体操作。

整个链路的大致流程是:

  1. 客户端通过JDBC连接Kyuubi Server;
  2. Server根据连接中的用户信息、配置标签,查找是否已有可用的Engine;
  3. 若存在,则直接复用,将Session转发到该Engine上;
  4. 若不存在,则调用Spark Submission接口启动一个新的Engine,并注册到引擎列表中;
  5. Session的SQL提交后,在Engine内部创建Operation去执行,最后把结果拉回客户端。

2.2 引擎共享与隔离:为什么能一处复用、处处隔离

这里有一个重要的逻辑:引擎并不是全局共享一个,而是按“用户/分组/配置”维度划分多个共享池

默认情况下,同一个用户提交的多个Session会共享同一个Engine;不同用户之间默认使用不同的Engine。这样做的理由很直接:

  • 同一个人、同一类查询需求,共享引擎能最大化复用SparkContext和缓存,执行效率最高;
  • 不同用户共用同一引擎会带来权限越权和资源争用的问题,所以必须隔离。

但如果真的为每个用户单独起一个Engine,又有可能造成资源碎片化。生产环境里通常会设置“分组引擎(engine group)”策略:比如给数据分析部门配置一个标签batch=ad-hoc,让该部门的所有用户路由到同一个Engine实例中,部门内部共享,部门之间隔离。

Kyuubi还支持通过**标签(tag)**进一步分化。例如可以对重量级ETL和轻量级即席查询分别指定不同Engine,避免大查询把即席小查询拖住。这在实际集群运维里非常重要。

2.3 引擎生命周期管理:空闲释放与自动拉起

Engine如果一直常驻,空闲时还会占用YARN上的driver和executor资源。所以Kyuubi提供了引擎生命周期管理机制,核心参数包括:

  • kyuubi.session.engine.idle.timeout:Engine空闲多久后被关闭,默认大约30分钟。
  • kyuubi.engine.start.timeout:等待Engine启动的最大时间。
  • kyuubi.engine.exec.max.size:同一分组下共享Engine的最大Session数。

我在生产环境里常用这样一组配置:

# Kyuubi Server侧 kyuubi.session.engine.idle.timeout=PT30M kyuubi.engine.start.attempts=3
# Spark侧(通过Kyuubi引擎提交参数传入) spark.dynamicAllocation.enabled=true spark.dynamicAllocation.executorIdleTimeout=120s spark.dynamicAllocation.cachedExecutorIdleTimeout=360s

简单说:Spark的executor动态伸缩负责释放计算资源,Kyuubi的engine idle timeout负责释放driver资源。两者配合,能在“查询性能”和“资源占用”之间取得一个不错的平衡。

3. 从零部署:Kyuubi的安装与环境准备

3.1 前置依赖与版本匹配

Kyuubi的安装其实不算难,但前置依赖需要小心。部署Kyuubi之前,需要准备好:

  • JDK 8或11;
  • 已安装的Apache Spark发行版(建议用官方预编译版本或自编译版本);
  • 可访问的Hadoop集群(如果跑在YARN上);
  • Hive Metastore服务,Spark SQL访问已有数仓表时需要。

版本匹配是个重要的坑。早期Kyuubi对Spark版本很敏感,不同的Kyuubi版本可能对应不同版本的Spark集成模块。拿Kyuubi 1.6及以上版本来说,基本支持Spark 2.4和Spark 3.x系列,但如果你想在Spark 3.3上使用某个Kyuubi老版本,可能会碰上Spark内部API不兼容导致的ClassNotFound问题。现在官方提供kyuubi-spark-...对应的jar包,建议直接下载与Spark版本匹配的release包。

我们的做法是:用Kyuubi官方编译好的二进制包,内部自带的Spark统一采用Spark 3.3.x,通过环境变量SPARK_HOME来指定。

3.2 配置文件里的关键参数

解压Kyuubi后,目录下有conf/kyuubi-defaults.conf.template,复制成kyuubi-defaults.conf后开始改配置。核心参数如下:

# Kyuubi Server的绑定地址和端口 kyuubi.frontend.bind.host=0.0.0.0 kyuubi.frontend.port=10009 # 使用ZooKeeper进行服务发现 kyuubi.ha.enabled=true kyuubi.ha.zookeeper.quorum=zk1:2181,zk2:2181,zk3:2181 kyuubi.ha.zookeeper.namespace=kyuubi-server # 认证方式 kyuubi.authentication=LDAP kyuubi.authentication.ldap.url=ldap://ldap.example.com:389 kyuubi.authentication.ldap.base.dn=ou=users,dc=example,dc=com # 引擎(Spark Application)提交方式 kyuubi.engine.engine.share.level=USER

这些配置的含义分别是:

  • frontend.bind.hostport决定Kyuubi监听地址,客户端直接通过这个端口来连;
  • ha.enabled开启高可用,多个Kyuubi实例挂在同一个ZooKeeper路径下,客户端通过ZooKeeper获取可用实例;
  • engine.share.levelUSERGROUPSERVER三个级别,生产环境多数用USERGROUP

3.3 启动服务端并用beeline验证

配置完毕,执行启动脚本:

# 设置Spark路径 export SPARK_HOME=/usr/local/spark # 启动Kyuubi $KYUUBI_HOME/bin/kyuubi start

日志默认输出到logs/kyuubi-xxx.out,看到类似“Starting ky uubi server”和“INFO KyuubiServer: Start the KyuubiServer”的日志,说明服务起来了。

验证流程也很直观,使用beeline连接:

$SPARK_HOME/bin/beeline -u "jdbc:hive2://node1:10009/default;principal=kyuubi/node1@EXAMPLE.COM"

输入账号密码后,执行一条简单的SQL:

show databases;

第一次执行时会看到Kyuubi开始申请Spark引擎,日志里会有“Starting Spark engine”字样。等引擎启动完成后,后续的查询会明显变快,不会再出现每次提交都拉起Spark Applicaiton的情况。

4. 多租户与高可用:生产级配置要点

4.1 认证与权限控制

生产环境中Kyuubi至少要做好认证(Authentication)和授权(Authorization)两层。

认证方面,Kyuubi支持NONELDAPKERBEROSCUSTOM等几种方式。大数据集群内部普遍已经接了Kerberos,Kyuubi可以开启Kerberos认证,客户端连接时需要提供票据;如果企业内部已有统一LDAP/AD体系,用LDAP方式会更方便,账号密码直接对接。

授权方面,这里要区分“外部权限”和“引擎侧权限”:

  • 外部权限:Kyuubi在接收Session请求时,可以将用户信息以代理用户方式提交给Spark引擎。开启kyuubi.engine.proxy.enabled=true后,Spark作业的提交身份会转换为真实用户,YARN上看到的就是这个人自己的作业,这样就能匹配已有的队列配额和ACL策略。
  • 引擎侧权限:SparkSQL本身并不具备太细粒度的表级鉴权,通常依赖Ranger或Hive Metastore的Authorization组件来实现。Kyuubi会把当前用户透传给底层引擎,Ranger就能对它做库表列级别的权限控制。

4.2 基于ZooKeeper的高可用部署

单点Kyuubi在实验环境跑一跑没有问题,但一旦成为平台入口,宕机意味着所有BI报表直接挂掉。Kyuubi的官方高可用方案就是基于ZooKeeper的服务发现:

  1. 部署多个Kyuubi Server节点;
  2. 所有节点向同一个ZooKeeper路径注册临时节点;
  3. 客户端使用jdbc:hive2://zk1:2181,zk2:2181/;serviceDiscoveryMode=zooKeeper;zooKeeperNamespace=kyuubi-server连接;
  4. ZooKeeper临时节点断开后,客户端自动感知并切换到可用节点。

这个方案的实现比传统VIP+Keepalived要优雅,节点可以横向扩容,不需要关心谁是主谁是备。

4.3 资源队列映射与动态分配

Kyuubi能成为多租户网关的关键在于每个Engine指定运行在哪个YARN队列里

常见做法是通过kyuubi.engine.engine.share.level=GROUP配合分组标签来实现:在服务端配置中为引擎指定spark.yarn.queue,打个比方:

# 给数据分析组的引擎配置 kyuubi.engine.<analysis>.spark.yarn.queue=root.adhoc kyuubi.engine.<analysis>.spark.executor.memory=8g

客户端连接时可以带上配置:

beeline -u "jdbc:hive2://node1:10009/default;#kyuubi.engine.share.level=GROUP;kyuubi.engine.tags=analysis"

这样该连接对应的所有Session都会进入预定义的引擎池,Spark作业跑在指定的root.adhoc队列中。如果业务方之间的资源配额差异很大,这种组合能让队列层面的资源隔离落地得非常干净。

5. 选型对比:Kyuubi与其他SQL访问方案的差异

5.1 一张表看懂Kyuubi、Livy、HiveServer2、Trino

很多刚开始做数据平台的人会纠结:到底是选Kyuubi,还是把Livy直接暴露给BI,或者干脆上Trino?我做了个对比表格,方便你对照选型:

维度KyuubiApache LivyHiveServer2Trino/Presto
引擎类型共享Spark Engine每个Session一个Spark AppHive/Spark任务自研分布式查询引擎
多租户支持强(按用户/分组共享与隔离)弱(以Session为单位)弱(服务端全局)中等(按目录和资源组)
连接复用高,常驻引擎低,每次提交Spark App低,连接即Session高,常驻Worker
SQL覆盖Spark SQL语法Spark SQL语法Hive SQL为主ANSI SQL为主,很多函数与Spark不同
权限集成可透传用户,配合Ranger用户模拟有限最成熟有独立权限体系
适合场景服务端多团队共享SQL服务程序化提交Spark作业传统Hive数仓交互式查询、联邦查询

从表里能看出来,Kyuubi最强的定位是“给Spark生态做统一SQL服务门户”,特别是你已经大量使用Spark SQL做数仓加工,又不想每个业务团队各自连接集群内部接口的时候。

5.2 在真实架构中Kyuubi的位置

以我接触过的中型规模数据团队为例,比较合理的组合通常是:

  • 离线ETL仍由调度平台直接触发Spark任务,这些无需走Kyuubi;
  • 即席查询和自助分析统一接入Kyuubi,业务人员通过前端工具连接;
  • 超大规模数据集上的秒级交互,额外部署一套Trino/Presto作为补充,Trino跑不了的复杂UDF和Spark原生逻辑,回退到Kyuubi节点。

两套SQL入口并存是常见状态,因为它们的优势方向并不重合。Kyuubi依赖的是Spark生态的弹性和丰富算子,Trino则更适合跨数据源联邦查询和低并发高吞吐场景。

6. 生产中的坑与排查实录

6.1 “Could not locate engine”类问题

典型现象:客户端连接Kyuubi后,提交SQL出现类似“No engine is available”或“Could not locate engine”的报错。

排查路径:第一反应是先查看Kyuubi Server日志,重点确认Engine启动有没有成功注册到ZooKeeper。我遇到过几次都是因为kyuubi.engine.engine.share.level设置成了SERVER,但Spark引擎实际启动用户与Server进程启动用户不一致,导致Engine无法完成注册信息上报。解决方式:检查启动用户、检查KYUUBI_HOMESPARK_HOME环境变量是否传递到各个节点,尤其要注意主机名解析,Engine进程里解析的localhost和Server的hostname不一致,注册就会失败。

6.2 空闲引擎未释放导致资源被占

现象:集群总结了很多YARN应用,查看后全是Kyuubi引擎,占用资源迟迟不退。

原因kyuubi.session.engine.idle.timeout默认值看起来是30分钟,但如果客户端侧一直有连接保持打开,比如某个BI工具建立了连接池且不断发心跳,Session并不算真正空闲,Engine自然不释放。

解决经验:一方面在Kyuubi侧设置一个合理的idle.timeout,另一方面要在BI工具侧配置连接池的空闲回收时间,双向配合才能让引擎按时缩容。同时建议给引擎配置Spark Dynamic Allocation,只保留一个最小executor数量,空闲期不让executor继续占着大批资源。

6.3 Spark动态资源与Kyuubi的连接数冲突

现象:设置了动态资源分配后,Kyuubi并发大查询一多,executor扩容很慢,查询排队严重。

原因:Spark的动态扩容需要估算“挂起任务数”,但Kyuubi的多Session共享引擎场景里,不同任务提交到同一个SparkContext中,动态资源分配的判定逻辑有时不够及时。

对策:可以在提交引擎时设置更激进的动态分配参数,比如缩短spark.dynamicAllocation.schedulerBacklogTimeoutspark.dynamicAllocation.sustainedSchedulerBacklogTimeout,让Spark更快判断是否需要扩executor。另外,并发高的场景下可以给不同业务方分配不同的Engine,而不是所有人都挤在一个引擎里等扩容。

6.4 一些Kyuubi特有的Set参数坑

Kyuubi支持客户端用set key=value动态修改部分Spark参数,但并非所有参数都能实时生效。比如spark.executor.memory在Executor启动后改不了,Kyuubi却不会显式报错,只会悄悄忽略,容易给人“设置成功”的错觉。解决方案是,涉及资源类的参数,写在引擎组的配置文件里,或者通过kyuubi.engine.<group>.spark.xxx和服务端联动,确保引擎启动时就用上正确配置,而不是依赖session级set。

在实际跑Kyuubi的过程中,我最深的一个感受是,它把“连接复用”和“多租户”这两件事从工程层面规范化了。以前我们靠各种壳和脚本硬撑多团队共享Spark服务,能跑但总是不太体面;Kyuubi以一种相当完整的姿态把这个场景做得明明白白。如果你想在现有Spark生态上搭一套稳定的SQL服务入口,它应当是第一梯队里的首选。哪怕只做一个几十人的内部数据分析平台,Kyuubi也能很快让你告别“一跑大查询就全组瘫痪”的尴尬局面。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询