ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kyuubi+Spark on YARN资源利用率问题排查:隐形上限与调优实践

Kyuubi+Spark on YARN资源利用率问题排查:隐形上限与调优实践 先说结论上个月接到一个有点“反直觉”的工单业务方把截图拍在群里——通过 Kyuubi 以 Spark 3.4.1 提交的单用户任务跑了二十多分钟还没结束YARN 队列却只用了不到一半的 vCore。最气人的是任务没有报错、没有 OOM、没有 shuffle 剧烈溢写一切看起来“正常”但就是慢。这类“k yuu bi spark yarn”组合下的资源利用率问题我在近几年的集群运维里见过太多次根因往往不是单一参数而是一整条链路上多个隐形上限叠出来的。这篇文章就完整复盘一次从现象到根因的排查过程把这套排查思路和数据化调参方法分享出来希望帮你少走弯路。Kyuubi 做多租户 SQL 网关、Spark 3.4.1 作为底层引擎、YARN 承载调度资源这套组合在大数据平台里很典型。本文适合负责数据平台底座、Kyuubi 日常运维的工程师也适合被“队列明明有资源但任务就是吃不满”折磨过的 Spark 开发同学。我会从现场现象、单用户场景的特殊性、逐层排查路径、最终参数方案四个维度展开最后一节汇总常见问题和避坑经验。内容全部来自真实一线排障记录不是教科书式的参数罗列。1. 先复盘现场任务在跑队列却在“摸鱼”1.1 第一现场从 YARN 界面能读到什么当时 YARN ResourceManager 页面上的数据是这样的目标队列配置上限 200 vCore当前已使用 88 vCore大约 22 个 ContainerSpark UI 里显示 22 个 Executor每个 Executor 4 个 vCore数值上能对上。但问题也在这里——队列明明还有 112 个 vCore 的空余可 Spark 任务就是不再申请更多 Executor也不加速。业务方第一反应是“运维没把队列资源给我加满”实际上队列配额是够的瓶颈在整个资源申请链路的中间某一环。我先说一个很容易被忽略的前提Spark on YARN 的提交机只是一个客户端它只负责把 ApplicationMaster 提交进集群真正的 Driver、Executor 全部跑在 YARN Container 里。所以遇到执行慢、资源用不满的问题先别怀疑提交机的 CPU 配置重点要看集群内部 Application 的资源和任务并行度。这也是很多热词里反复出现“Spark on YARN 提交是不是只需要客户端”的原因——提交端确实只需要一个能跑的 Spark 客户端但资源利用率的答案全部在集群侧。1.2 我记录下的环境快照与初始配置先把现场环境亮出来后面所有分析都基于这套配置集群 12 个节点每台 64 vCore / 256G 内存YARN 使用 Capacity SchedulerKyuubi 1.8 Spark 3.4.1队列 capacity 配置为 200 vCore 上限。Kyuubi 侧走的是 JDBC 接入业务统一用一个“报表用户”登录所有查询最终以该用户身份提交看起来就是典型的单用户场景。初始 Spark 配置才是重点盘。当时 spark-defaults.conf 里开了动态分配spark.dynamicAllocation.enabledtrue但spark.executor.cores没显式设置spark.dynamicAllocation.maxExecutors配的 30spark.sql.shuffle.partitions用的默认 200也没有单独配置 External Shuffle Service 相关项。这些配置单看都没有致命错误可组合起来就变成了“资源天花板”的三层嵌套Executor 并发度不足、动态分配扩不动、任务并行度撑不满。后面我逐个拆解。2. 为什么“单用户提交”会成为资源不满的高发场景2.1 Kyuubi 的引擎复用会“锁死”你的资源上限Kyuubi 是个多租户 SQL 网关它把 JDBC 请求转换成 Spark Job但底层并不是每个 SQL 都拉起一个 Spark Application而是通过 Engine 复用来降低启动开销。这个 Engine 本质上就是一个常驻的 Spark ApplicationKyuubi 的 share level 默认是 USER意思是同一个用户的所有连接共享同一个 Engine。对单用户场景来说这就带来一个直接后果不管你开多少个 JDBC 连接、并发提交多少条 SQL底层可能只有一个 Spark Application在服务。这一个 Spark Application 的资源上限等于“单个 Executor 的核心数 × 动态分配的最大 Executor 数”。回到现场队列上限 200 vCorespark.executor.cores默认是 1 时就算动态分配能扩到 30 个 Executor也最多用 30 个 vCore即便 Executor 核心配置成了 4如果maxExecutors30同样最多只有 120 vCore 可用利用率天然到不了 60%。这就是单用户场景的第一个坑Kyuubi 的引擎复用机制把多个任务的资源诉求压缩到了一个 Application 里而这个 Application 的资源上限又由动态分配参数硬性限定队列再空也没用。2.2 YARN 调度器的“单用户限额”其实是隐藏闸门除了 Kyuubi 这一层YARN 调度器本身对“单用户”也有隐性限制很多排查线程走到这就断了。Capacity Scheduler 里有个参数yarn.scheduler.capacity.queue.user-limit-factor默认值是 1它在计算单个用户最大可用资源时会把队列容量按活跃用户数均分。虽然当前活跃用户只有 1 个时理论上可以把整个队列资源都给这个用户但如果这个参数被运维人为调低或者队列同时配置了maximum-capacity小于 100%单用户就会在调度器层面被卡住。这里特别要提醒一点Kyuubi 是否开启了 doAs以真实登录用户身份提交会直接影响调度器对“用户数”的统计。如果没开 doAs所有任务都会以 Kyuubi 启动用户身份提交那就相当于很多业务代码在调度器眼里是同一个用户如果开了 doAs 但大家都用同一个账号登录比如现场这个“报表用户”那么在 YARN 看来仍然是单用户。Fair Scheduler 场景下还有maxRunningApps、用户权重等限制思路是类似的单用户在 YARN 调度器那里往往不是“自由人”而是被一堆默认参数绑着手脚。所以排查资源不满时调度器配置一定要单独过一遍。3. 排查“资源不满”的完整路径3.1 第一站任务自身的并行度是否撑得起资源我当时没有一上来就调动态分配参数而是先打开 Spark UI 的 Stages 页看 Task 数和 Pending 情况。这一步很关键因为动态分配触发扩容的一个硬条件是“存在尚未调度的 Pending Task”。如果任务本身并行度不够Executor 再大也没有意义。看现场这个报表任务输入是一张约 200G 的分区表spark.sql.files.maxPartitionBytes默认 128MB算下来输入分区数大概在 1600 左右中间有一个大 JoinShuffle 后的分区数由spark.sql.shuffle.partitions决定默认 200。问题就出在最后阶段只生成了 200 个 Task而当时已经有 22 个 Executor、每个 4 核总并行能力是 88 个并发槽位——往多了算也就同时跑 88 个 Task200 个 Task 跑两批多一点就完了。但更麻烦的是AQE 开启后会进一步把 200 个分区合并成更少的分区Task 数少到一定程度队列资源自然就空闲了。判断任务并行度是否够有个实用经验让目标 Executor 总核心数乘以 2 到 3作为 Shuffle 分区数的下限。比如目标是让 40 个 Executor × 4 核跑满那 160 到 240 个 Shuffle 分区算是及格线如果最终阶段只有 30 个 Task给再多 Executor 都会闲置。所以第一站就要确认是资源给不够还是任务根本不需要那么多资源。3.2 第二站Spark 动态分配到底为什么扩不上去确认任务并行度没大问题后我紧接着查动态分配的实际伸缩日志。Spark 3.4.1 的动态分配已经不是“开启就算完”的功能它依赖两个前置条件之一集群配了 External Shuffle ServiceESS或者开启spark.dynamicAllocation.shuffleTracking.enabled。没有这两个前提Executor 回收时会担心 shuffle 文件丢失动态分配就表现为只增不减甚至原地不动。现场环境其实没有配置 ESSspark.shuffle.service.enabled没开shuffleTracking 也没开等于说动态分配处于“半瘫痪”状态。我后来在 Spark 相关日志里看到Executor 在空闲后因为无法安全删除 shuffle 数据会被保留这直接锁死了多个资源槽位。而且spark.dynamicAllocation.initialExecutors没有设置Spark 3.x 在某些场景下会从很小的初值开始爬升爬升速度又受任务调度频率影响导致明明队列有资源Executor 却像挤牙膏一样慢慢增长。动态分配的扩容决策依据只有一条是否存在未被调度的 Pending Task且当前 Executor 数小于maxExecutors。反过来如果任务本身并行度不够、Task 一下子就调度完就不会产生长时间 PendingExecutor 自然也就不会扩到队列允许的上限。所以动态分配和并行度必须放在一起看单独调任何一个都会踩跷跷板。3.3 第三站YARN 队列与调度器限额核查任务侧和 Spark 侧看完之后我开始翻 YARN Capacity Scheduler 的配置文件重点是三个参数yarn.scheduler.capacity.queue.maximum-capacity、yarn.scheduler.capacity.queue.user-limit-factor、yarn.scheduler.capacity.maximum-am-resource-percent。第一个决定队列资源天花板第二个决定单用户能分到的比例第三个限制所有 ApplicationMaster 占用的资源百分比。我当时用了两个很实用的检查命令yarn queue -status 队列名能看到当前队列的容量、最大容量、已用资源yarn top能实时观察 Application 级别对 CPU 和内存的占用。另外一个更隐蔽的问题在内存维度YARN 调度器同时管理 vCore 和内存两个维度DRF 策略下只要其中一个维度达到上限就不会再分配 Container。现场spark.executor.memory配到了 24G 加 6G overhead单个 Executor 占 30G 内存40 个 Executor 就要 1.2T队列内存配额早早就被打满CPU 侧却还空着一大截。这种“CPU 没满、内存先满”的现象极其常见排查时一定要同时看两个维度别只盯 vCore。3.4 第四站Kyuubi 会话与引擎生命周期四个排查站里Kyuubi 这一层最容易被漏掉。因为 share level 默认是 USER我们看到的 22 个 Executor 全部属于同一个 Kyuubi EngineSpark Application 的资源上限就是整个用户的上限。如果业务方同时提交了五个任务这五个任务在这个 Engine 里是串行执行的完全不会触发第二个 Application 的资源申请所以队列剩下的资源没人用。现场的 Kyuubi 引擎参数里kyuubi.engine.session.timeout当时配的 24 小时引擎空闲也长时间不回收导致资源被“挂住”。如果想用多引擎分摊压力可以考虑把 Kyuubi 的 share level 调成 CONNECTION让不同连接各自拉起独立 Spark Application但要注意每个 Engine 都是一套 Driver AM会额外占用内存和 AM 资源YARN 的maximum-am-resource-percent必须同步评估。Kyuubi 各版本参数名略有差异建议以你部署版本的官方配置页为准思路比参数名更重要单用户场景下引擎复用是资源利用率的“总闸门”。4. 参数调优方案与验证过程4.1 可直接抄作业的参数清单四个站排查完我把问题收敛到三处Executor 核心数没显式配置、动态分配配套缺失且 maxExecutors 过小、Executor 内存配置导致内存维度提前打满。优化后的关键参数如下Kyuubi 场景下建议直接配置在 Engine 启动默认的 spark-defaults.conf 里spark.executor.cores4让单个 Executor 真正占满 4 个 vCore。这个参数默认值是 1不设它就意味着每个 Executor 都是一个“单核工人”资源利用率天然低下。spark.executor.memory8Gspark.executor.memoryOverhead2G单个 Executor 总内存降到 10G避免内存维度提前触顶。这不是死值要结合单个 Executor 处理的数据量估算但要保证 Executor 数量 × 单 Container 内存不超过队列内存上限。spark.dynamicAllocation.enabledtrue保留。spark.dynamicAllocation.initialExecutors10spark.dynamicAllocation.minExecutors1spark.dynamicAllocation.maxExecutors50上限从 30 提到 50等于给单用户 Engine 开到了 200 vCore 的理论峰值。动态分配配套二选一集群没有 ESS 就开启spark.dynamicAllocation.shuffleTracking.enabledtrue集群有 ESS 就spark.shuffle.service.enabledtrue。现场没有 ESS我选了 shuffleTracking这里要特别注意不要两个同时开。spark.sql.adaptive.enabledtrue保持开启但spark.sql.adaptive.coalescePartitions.enabledfalse暂时关掉分区合并避免 AQE 在 Shuffle 后把分区数压得太低等任务并行度稳定后再评估是否重新开启。spark.sql.shuffle.partitions200暂时保留以目标并发 200 个 vCore 计算200 个分区能保证每个 Task 大约对应 1 个核心足够撑起调度压力。这些参数之间是有依赖关系的不是孤立调优。比如动态分配 maxExecutors 调到 50 之后如果 Executor 内存还保持原来的 30G内存维度会再次成为瓶颈如果 Executor 核心数不设 450 个 Executor 也只占 50 个 vCore。所以调整时要拉着计算器一起算总 vCore、总内存、单 Executor 资源要同时演算一遍。4.2 验证效果与观察指标参数下发后我没有直接重启全部接入方而是让业务用同一份报表 SQL 分批验证。第一次跑起来我盯着 YARN 页面和 Spark UI 看四个指标Executor 数量是否增长、Pending Task 是否持续存在、vCore 使用率是否上升、Container 是否因内存不足被拒绝。结果比较理想Executor 从 22 个逐步扩到 44 个左右vCore 使用率稳定在 176 到 184 之间接近 90% 的队列阈值同一份 SQL 的耗时从 21 分钟降到 8 分钟不到。还要看一个容易被忽略的指标GC 时间和 shuffle 溢写。如果 Executor 内存调低后出现频繁 Full GC 或磁盘溢写说明 8G 内存对单 Executor 不够需要微调而不是硬扛。这个案例的负载属于中等偏重的聚合场景8G 配合 200 个分区够用跑了几轮都没有明显溢写。验证期我建议至少跑一天覆盖白天高峰和夜间批量两类负载。动态分配调大后队列资源会被单用户任务快速吃满如果同一个队列还有别的业务线可能出现“一个任务吃饱、其他任务等资源”的新矛盾。现场队列是独立给这个报表用户的所以问题不大如果是混合队列最好在队列层面把 capacity、maximum-capacity、user-limit-factor 一起重新规划再用 Fair Scheduler 的权重或 Capacity Scheduler 的分队策略做隔离。5. 常见问题速查与避坑记录5.1 问题速查表我把这次排查里涉及的高频现象、原因、检查方式和修复思路整理成一个速查表后续再遇到“YARN 队列不满”类工单可以按表逐项对照。现象可能原因检查点修复思路队列 vCore 只用一半就停了队列 maximum-capacity 或 user-limit-factor 限流capacity-scheduler.xml、yarn queue -status调大 maximum-capacity 到 100、user-limit-factor 适当调大动态分配不扩 Executor没配 ESS 且没开 shuffleTracking或没有 Pending TaskSpark UI 的 Pending 数、RM 日志补开 shuffleTracking 或 ESS先保证任务并行度每个 Executor 只有 1 个 vCorespark.executor.cores 未配置默认 1Executor 页面看 core 数显式配置 spark.executor.coresCPU 还有余量但 Container 申请不上内存维度先满或超过 maximum-allocationYARN 页面看内存指标、队列内存配额降低单 Executor 内存与 overhead压低总内存多个 JDBC 连接还是同一个引擎Kyuubi share levelUSER引擎复用Kyuubi UI 看 Engine 数量改 CONNECTION 或调整引擎数量参数AQE 开启后并行度下降coalescePartitions 合并且分区太激进Spark UI 各 Stage Task 数变化调小合并目标或先关闭 coalesce这个表基本覆盖了“Kyuubi Spark on YARN 单用户任务资源用不满”的主流成因。实际工单里经常是表中两到三行叠加所以排查时建议按任务并行度、动态分配、调度器限额、Kyuubi 会话的顺序逐层过不要只盯着某一个参数猜。5.2 我踩过的几个坑第一个坑是 shuffleTracking 和 ESS 同时配置。验证阶段有人建议“两个都开着更保险”我在一个测试环境试了一次日志里直接出现 shuffle 目录归属混乱的告警Executor 对 shuffle 数据的清理行为变得不可预期。动态分配到了 3.4 版本已经比较成熟ESS 和 shuffleTracking 属于两条不同的实现路径选一条适合当前集群的就行别贪多。第二个坑是只调大 maxExecutors 不管 AM 资源百分比。第一次我把 maxExecutors 提到了 80想着利用率一定拉满结果任务在 ResourceManager 里长期处于 SUBMITTED 状态后来发现maximum-am-resource-percent默认只有 0.1AM 资源不够导致多个 Application 无法正常启动。这里提醒一句Kyuubi 开 CONNECTION 模式后会产生多个 Engine也就是多个 AMAM 资源这条隐藏线索很容易被忽略。第三个坑和 AQE 有关。开启 AQE 之后spark.sql.adaptive.coalescePartitions会自动把 200 个 Shuffle 分区合并得更少当时我觉得分区越少 I/O 压力越小结果一个大 Join 出现明显数据倾斜少数 Task 拖慢整体进度。后来我把 coalesce 关掉再根据实际数据分布单独调spark.sql.adaptive.advisoryPartitionSizeInBytes才稳定下来。AQE 是很强但它会改变分区数量和 Task 分布配合动态分配调资源时千万别忘了看最终 Stage 的实际 Task 数。说到底YARN 队列使用不满尤其是 Kyuubi 单用户场景下的不满从来不是“队列配额不够”而是应用能够触达的资源天花板太低。Kyuubi 的引擎复用、Spark 的动态分配、YARN 调度器的限额、任务自身的并行度这四个环节里只要有任意一环塌陷队列就会显示出一片绿色空余任务的运行速度却一点都提不起来。我在实际处理这套工单时最深的体会是先把 Spark UI 上每个 Stage 的 Task 数看清楚再决定要不要动集群参数方向对了调参就是顺势而为。以后再遇到类似的“队列空着但作业跑不动”的怪象不妨也按这条链路重新走一遍大概率能找到那个悄悄卡住资源的隐形上限。
返回列表