版本动态
本周 Spark 未发布小版本或预览版。
技术研讨
Catalyst 树转换中剪枝状态失效机制确认
现象与痛点
- 项目背景:开发者 Asif Shahid 针对 Spark Catalyst 引擎中
transformUpWithPruningAPI 的内部状态维护机制发起技术咨询。 - 核心痛点:在 Catalyst 优化器的规则执行过程中,剪枝状态(Pruning State)通常作为局部变量维护在节点中。讨论的核心在于:当一个 Batch 内存在多个 Rule 交互时,前序 Rule 对树结构的修改(产生新节点)是否会导致后续 Rule 的剪枝信息丢失,从而造成不必要的重复遍历。
预期与目标
- 发起人初衷:明确
TreeNode或Expression在被新节点替换后,原本用于追踪 Rule 是否有效的状态位是否会随之消失。 - 架构底线:PMC 成员明确了架构设计的保守性原则:在无法保证新旧节点剪枝行为完全一致的情况下,必须优先保证转换的正确性,而非盲目继承剪枝状态。
各方见解
- Asif Shahid(开发者)
- 逻辑推演:提出了一种典型的干扰场景——假设 Batch 中存在 Rule A 和 Rule B,若 Rule B 触发了节点重建,即便 Rule A 本该在该分支被剪枝,由于新节点没有携带旧对象的局部变量,Rule A 的剪枝效果也将失效。
- 结论建议:在泛化剪枝行为的框架下,理解并接受这种由于节点重构导致的信息丢失。
- Wenchen Fan(Spark PMC)
- 核心论点:确认该现象符合设计预期。其逻辑链为:新节点不等于旧节点。由于系统无法自动推断新生成的节点是否具有与旧节点相同的剪枝属性,因此重置状态是唯一的安全做法。
- 技术溯源:引导开发者参考了该功能的初始设计提交,强调了在性能优化与逻辑正确性之间的权衡。
总结复盘
- 核心共识:
transformUpWithPruning的剪枝标识是基于实例(Instance-based)的。一旦规则导致节点替换,剪枝效果在当前转换路径上会局部失效。 - 后续行动:开发者在编写 Rule Batch 时需意识到此行为。对于高频变动树结构的场景,应评估剪枝优化的实际收益。
- 遗留风险:在极复杂的规则链中,频繁的节点重建可能导致 TreePattern 剪枝加速器的实际命中率低于理论预期,在大规模 Plan 优化时可能存在性能损耗。
方案思辨
Spark Connect 交互延迟攻坚:元数据异步解析与客户端缓存机制
核心动机
- 项目背景:Spark Connect 通过解耦客户端与服务端(JVM),实现了真正的远程执行模型。但在交互式数据探索场景中,用户习惯频繁调用 df.columns 或 df.schema 来检查数据结构。
- 核心痛点:
- '千次 RPC 之死':目前 Spark Connect 采用'Eager Analysis'模式。每一次元数据访问都会触发一个同步的、阻塞式的 AnalyzePlan gRPC 调用。
- 性能回退:在本地 Spark 中亚毫秒级的操作,在 Connect 模式下因网络延迟变成了 40-200 毫秒。这种累积延迟使得 Spark Connect 在交互式 Notebook 中显得迟滞。
- 开发体验割裂:开发者为了规避性能损耗,被迫编写非直观的代码来绕过元数据检查,这违背了 Spark Connect 的设计初衷。
- 问题核心:如何在不改变 Catalyst 核心优化规则的前提下,消除高频元数据操作带来的 O(N) 网络延迟开销。
关键设计
该提案引入了一个客户端元数据'跳过层',其架构包含三大支柱:
- Plan Piggybacking(计划搭载):利用现有的执行流,SparkConnectService 会在 ExecutePlanResponse 中自动附带中间关系的已解析 Schema。这意味着当用户执行某个操作时,相关 DataFrame 的元数据会被'顺风车'带回,无需额外的 RPC 调用即可填充本地缓存。
- Session-Scoped Cache(会话级缓存):在 SparkSession 中维护一个托管的客户端缓存,用于存储已解析的 Schema。缓存失效策略基于 DDL 事件或可配置的 TTL(生存时间),以平衡一致性与性能。
- Batch Resolve API(批量解析接口):扩展 AnalyzePlan 协议,允许客户端在单个 gRPC 调用中提交多个未解析 DataFrame 的元数据请求。这对于通过循环批量处理 DataFrame 的场景尤为有效,能大幅减少网络握手次数。
影响价值
- 性能提升:对于元数据密集型的分析工作负载,端到端分析时间预计可减少 30%–50%。目标是在高延迟的网络环境中,实现与'Spark Classic'(本地 JVM)同等的交互响应速度。
- 开发体验:恢复了远程环境下的操作流畅度。开发者无需再为了性能而牺牲代码的可读性,原本需要手动优化的元数据获取逻辑转变为系统内置能力。
- 架构意义:此方案专注于 gRPC 通信层的优化,不侵入 Spark 执行器(Executor)的物理执行逻辑,保持了内核的稳定性。
社区探讨
- 赞成观点(收益)
- 解决刚需:几乎所有使用 PySpark 或远程 Spark Connect 的开发者都会受益,这是一个修补 4.x 版本交互体验短板的关键特性。
- 低侵入性:方案被严格限制在协议层和客户端,不会增加服务端复杂的锁机制或影响计算吞吐量。
- 反对/质疑观点(担忧)
- 元数据陈旧:最核心的风险在于一致性。如果表结构在另一个会话中被修改,本地缓存可能返回过期的 Schema。对此,提案建议通过 TTL 和 DDL 失效逻辑来缓解。
- 客户端内存压力:在复杂的长运行作业中,缓存成千上万个临时 DataFrame 的 Schema 可能会增加客户端的内存开销。
- 实施周期:完整的协议变更和缓存实现预计需要约 8 周时间,这对于紧凑的发布计划是一个挑战。
打破语言边界:通过 AST 转译让 Python UDF 获得原生 Catalyst 性能
核心动机
- 项目背景:随着 Pandas on Spark 的普及,越来越多的数据科学家和工程师倾向于使用 Python 原生语法或 Pandas API 进行数据处理。然而,距离上一次社区深入探讨 UDF(用户自定义函数)的'转译(Transpilation)'技术已过去数年,当前的 Python UDF 执行机制成为了显著的性能瓶颈。
- 核心痛点:目前 Spark 在执行 Python UDF 时,需要跨越 JVM 与 Python 进程的边界,涉及繁重的序列化/反序列化(SerDe)开销以及进程间通信成本。这就导致了一个尴尬的现状:用户为了追求代码的可读性和习惯(使用 Python/Pandas 语法),不得不牺牲大量的执行性能。实际上,许多 Python UDF 内部逻辑仅仅是简单的数学运算或逻辑判断,这些逻辑如果用 SQL 或 Scala 编写,本可以直接利用 Spark Catalyst 优化器的高效执行路径。
- 问题核心:如何能在不改变用户编写 Python 代码习惯的前提下,消除简单的 Python UDF 带来的性能损耗?Holden Karau 提出的 SPIP 旨在解决这一问题:通过将 Python UDF 的代码逻辑自动转译为 Catalyst 表达式,从而绕过低效的 Python 执行路径。
关键设计
该方案的核心在于引入一个基于'转译'的优化规则,其工作流并非生成 Java 字节码,而是直接映射到 Catalyst 的逻辑计划(Logical Plan)。
- 核心机制:
- Python 侧转译:在客户端(Python 端)解析 UDF 的源代码,生成抽象语法树(AST)。
- AST 映射:将 Python 的 AST 节点映射为 Spark SQL 内部的 Catalyst 表达式(Expressions)。例如,将 Python 的 a + b 节点直接转换为 Catalyst 的 Add 表达式。
- 计划替换:如果转译成功,优化器将原本需要通过 BatchEvalPython 等昂贵算子执行的节点,替换为纯粹的 Catalyst 表达式树。
- 设计原则与约束:
- 覆盖范围:初期聚焦于'简单'的 UDF,主要指那些可以被直接翻译为现有 SQL 表达式的逻辑(如基本数学运算、逻辑判断)。
- 静默回退(Graceful Fallback):这被设计为一条优化器规则(Optimizer Rule)。如果 UDF 过于复杂无法转译,或者转译过程中出现不兼容,系统将自动回退到传统的 Python UDF 执行模式,不会抛出异常中断作业。
- 语义一致性:尽管错误信息可能无法做到 100% 对齐,但核心行为(如 NULL 处理、类型提升 Type Promotion)必须通过基于属性的测试来确保与原 Python 逻辑一致。
影响价值
- 性能收益:
- 零 SerDe 开销:对于成功转译的 UDF,完全消除了 JVM 与 Python 之间的进程间通信和数据序列化成本。
- 全速执行:转译后的逻辑变为原生 Catalyst 表达式,这意味着它可以直接享受 Spark 的全阶段代码生成(Whole-Stage Codegen)、向量化执行(Vectorized Execution)以及其他优化器红利(如常量折叠)。
社区探讨
本次讨论非常深入,主要围绕'覆盖边界'与'可观测性'展开。
- 核心争论点:
- 关于'覆盖范围'的质疑(Jungtaek Lim & Wenchen Fan):
- 质疑:是否只能覆盖那些'用户本可以用 SQL 写但没写'的场景?如果是这样,价值是否有限?特别是 Pandas/Arrow UDF 中涉及向量化操作,Spark 原生可能缺乏对应的向量化算子。
- 回应:确实初期主要覆盖可映射为 SQL 的场景。但这对 Pandas on Spark 至关重要,因为用户不应被强迫重写代码。对于向量化 UDF,只要是简单的数学表达,同样可以转译。
- 关于'行为一致性'的担忧(Wenchen Fan):
- 担忧:Python 和 Spark SQL 在溢出(Overflow)、NULL 处理和错误抛出机制上存在差异。模拟完全一致的行为极难。
- 共识:追求 100% 的错误信息一致是不现实的(Out of scope),但必须保证数据计算结果(包括类型处理和 NULL 行为)的一致性。
- 关于'显式 vs 隐式'的设计哲学(Serge Rielau):
- 建议:Serge 建议引入显式语法,当用户期望转译但失败时报错,通过这种'契约'让用户明确性能预期,避免莫名其妙的性能回退。
- 最终决策:Holden 坚持采用类似'过滤器下推(Filter Pushdown)'的隐式优化策略。即:尽最大努力优化,失败则静默回退。理由是不能因为优化失败而破坏作业运行,这符合 Spark 优化器的一贯哲学。
- 折衷方案:为了解决可观测性问题,将在 EXPLAIN 查询计划或日志中明确标记哪些 UDF 被成功转译,方便用户调试性能回退问题。
- 关于'覆盖范围'的质疑(Jungtaek Lim & Wenchen Fan):
Spark On K8s 内存'超卖'机制:Pinterest 六百万美元降本背后的算法博弈与安全边界之争
核心动机
- 项目背景:随着 Spark 在 Kubernetes(K8s)上的广泛应用,内存资源管理的效率问题日益凸显。目前,Spark 用户在 K8s 上通常为每个 Executor 指定一个固定的 memoryOverhead 值。
- 核心痛点:当前的资源分配方式过于僵化。
- 资源绑定僵化:在 K8s 中,memoryOverhead 被同时用于设置容器的内存请求(Request)和内存限制(Limit)。
- '超卖'与'OOM'的两难困境:由于 memoryOverhead 的使用通常具有突发性(Bursty),用户为了避免被 OOM Kill(内存溢出查杀),往往按照峰值进行配置。这导致了严重的资源浪费(大部分时间内存闲置)。
- 集群利用率低:当成千上万的 Pod 都预留了峰值内存时,集群整体利用率显著下降,且容易造成内存碎片化,导致调度困难。
- 问题定义:如何在不修改 Spark 内存管理内核且不牺牲稳定性的前提下,利用 K8s 原生的 Request/Limit 机制,实现 memoryOverhead 的弹性分配,从而大幅降低成本?
关键设计
该方案的核心思想是将 memoryOverhead(记为 OOO)拆分为两部分:保证部分(Guaranteed, GGG)和共享/突发部分(Shared/Burst, SSS)。
- 核心算法:引入控制参数 spark.executor.memoryOverheadBurstyFactor(记为 BBB),根据用户申请的堆内内存(HHH)与堆外开销(OOO)的比例动态计算:
- 保证部分 GGG:G = O - min{(H+O) × (B-1), O}。该公式设计巧妙地捕捉了 HHH 与 OOO 的关系。当 OOO 相对于 HHH 较大时(强依赖堆外内存的任务),公式会保留更多的 GGG 以确保安全;反之则将更多内存划入共享池。
- 共享部分 SSS:S = O - G。
- K8s 映射机制:
- Executor Request(调度请求):H + G。这意味着 Pod 更容易被调度,且占用的'保证资源'更少。
- Executor Limit(硬性限制):H + G + S = H + O。这保证了在突发情况下,Executor 仍然可以使用到用户原始配置的峰值上限。
影响价值
- 显著的成本节约:通过降低 Pod 的内存 Request 值,K8s 可以在同一节点上堆叠更多的 Pod。Pinterest 的生产实践表明,该功能实现了年均 600 万美元的成本节省。
- 提升集群调度效率:减少了因内存碎片导致的调度失败,提高了集群整体吞吐量。
- 兼容性与低风险:方案完全基于 K8s 原生的 Request/Limit 语义,未修改 Spark 内部内存管理逻辑,且为可选功能(Opt-in),对现有业务行为无侵入。
社区探讨
本次 SPIP 在社区引发了关于'生产收益'与'理论安全性'的激烈辩论,核心焦点在于是否为了极致的成本优化而牺牲了部分系统保障。
- 赞成观点(以 Pinterest、ByteDance 为代表):
- 实战验证:方案源自 VLDB 2024 论文并在 Pinterest 大规模生产环境验证,收益巨大。
- 设计哲学:保持设计简洁,避免引入复杂的配置项。通过'保守的 Bursty Factor'和'节点级系统预留'来规避风险,而不是在代码层面增加防御逻辑。
- 机制自洽:通过解耦 Request 和 Limit,利用 K8s 的弹性能力是行业通用做法,且 Cgroup OOMKiller 通常比 K8s 驱逐(Eviction)反应更快,因此过分纠结 QoS 等级(Quality of Service)意义有限。
- 反对/担忧观点(以 Vaquar Khan 为代表):
- QoS 降级风险:Request < Limit 会导致 Pod 的 QoS 等级降为 Burstable。在节点压力大时,Kubelet 会优先驱逐此类 Pod,失去了 Guaranteed 等级的保护伞。
- '零保证'边缘情况:对于大堆内存(High Heap)但小 Overhead 的任务,公式计算可能导致 G=0。Vaquar 认为这在 JVM 层面是不安全的(无法分配基础线程栈等),存在'静默失败'的风险。
- 防御性配置缺失:强烈建议引入 minGuaranteedRatio(最小保证比例)和 priorityClassName 配置作为'安全底座(Safety Floor)',以防止极端情况下的系统崩溃。
- 争议解决与走向:
- 作者回应:Nan Zhu 指出,在生产环境中,G=0 并不意味着立即崩溃(节点通常有系统预留内存)。他反对'配置膨胀(Config Bloat)',认为如果不确定任务是否安全,运维人员应在部署层面控制,而非在代码中增加复杂性。
- 社区干预:Sean Owen 介入指出了讨论中存在的 AI 生成内容痕迹,提醒保持沟通效率。
- 最终态势:虽然 Vaquar 列举了 Kubelet Bug #131169 等技术细节佐证风险,但核心作者团队坚持维持方案的简洁性,倾向于通过文档预警而非修改代码逻辑来处理边缘风险。
周边生态
本周周边生态无版本发布。

