news 2026/10/13 4:17:35

Databricks性能优化实战:从架构到Delta Lake的全面指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Databricks性能优化实战:从架构到Delta Lake的全面指南

如果你做过数据平台、数仓,或者只是把 Spark 当作日常作业引擎用过一阵子,那“Databricks”这个名字大概率绕不开。我接触这个平台也是从一次事故开始的:线上数仓作业莫名其妙从十分钟涨到三小时,老板盯着看板,我盯着一堆看 VNC 都懒得开的 Spark Executor,最后顺着那张表的管理日志,被一步步带到了 Delta Lake 和 Databricks 的文档里。那晚之后我突然想明白了一件事:很多自以为是的优化,其实只是在给错误的架构缺陷打补丁。

后来我把团队里不少设计迁移到 Databricks 上,从最初只会点点 Notebook 跑 SQL,到慢慢啃透它的全过程架构、底层执行引擎逻辑,再到真正掌握文件、分区、Join 这些优化命脉,中间踩坑无数。这篇文章就是把我这段经历里最有价值的东西整理出来,从架构设计到引擎原理,再到一套能直接套用的优化策略,分五部分讲清楚。如果你正在评估这个平台、刚上手不久,或者被 Spark 作业性能折腾得头大,这篇应该能帮你省下不少翻文档、搜报错的时间。

1. 为什么我劝你先别急着抄配置:先看懂 Databricks 在解决什么问题

1.1 自建 Spark 集群的“三座大山”

网上关于 Spark 的教程多如牛毛,教你写 DataFrame、调参数、配 Executor 的都有,但真正把一个 Spark 平台用出生产力的人,都会逐渐发现自己被三件事卡住。

第一是资源管理和运维。自己搭集群意味着你得管理机器、处理安全组、操心网络拓扑,还要自己在 Kubernetes 或者 YARN 上设计伸缩策略。半夜三点告警说 OOM,爬起来看了一通日志,最后发现只是某个节点的临时磁盘满了。这一整套事情,跟数据处理本身完全无关。

第二是数据湖的事务能力。传统湖上你直接读写 Parquet,做同步任务的时候,哪怕一个分区文件有人覆盖到一半,就会导致下游读出来脏数据。没有事务、没有版本回滚,数据质量问题永远是悬在头上的刀。

第三是数据做的速度跟管理它的成本越来越不成正比。你调了一天的 Executor 内存,作业快了 10%,但算上调试时间,整体产出其实是亏的。

Databricks 作为统一数据分析平台,恰恰是把这三座大山用一套工程方案帮你移开。它不是一个 SQL 工具,也不是简单的“云上 Spark”,而是一个把计算、存储、管线、治理、协作全包掉的工作平台。最核心的定位,是让你专注于业务的查询和建模逻辑,而不是花时间去分辨到底是哪个 Worker 把数据搞坏了。

1.2 一个平台替你吃掉了哪些复杂度

用大白话讲,Databricks 试图帮你做到三件事。

第一件事是计算与存储分离的极致实践。Storage 放在你的云账号下的对象存储里,Compute 按作业需求随时拉起、用完释放。你不再需要维护一堆常年空转的虚拟机,而是按查询量花钱。这个概念现在听着不稀奇,但真正落地到“伸缩在秒级完成、集群元数据全部托管”的细节里,做得干净舒服的不多。

第二件事是让数据具备数据库般的事务能力。靠的是它旁边的 Delta Lake 存储层。一张表可以在湖上像数据库表一样 ACID 操作,支持时间旅行,读旧版本,写失败自动回滚。这对数据工程师来说是质变:以前做数仓任务最怕跑了一半失败需要手工清理垃圾文件,现在一个 MERGE 搞定,失败重跑也是一条命令的事。

第三件事是平台级的协作环境。Notebook、SQL Editor、Dashboard、Workflow 编排、权限管理,全在一个工作区里。数据工程师写好增量任务,分析师直接在同一个 Catalog 上写查询,权限到行级别列级别,靠 Unity Catalog 统一治理。大家操作的是同一份数据、同一套元数据,而不是反复邮件传 CSV。

一句话总结:Databricks 本质上是把 Spark、数仓、数据工程和 AI 配套的复杂工程能力打包成 PaaS,让你从“伺候平台”回到“使用平台”。

1.3 什么人最适合读这篇文章

我写这篇的预设读者有三类。第一类是把 Databricks 当成黑盒在用的人,会用 Notebook 跑 SQL、开集群,但说不清楚 Control Plane 和 Data Plane 是什么,性能出了问题不知道从哪排查。第二类是刚从自建 Spark 迁过来的人,还在用以前那套思维——比如强行调 Executor 内存、到处 repartition,结果发现平台机制变了,很多旧经验不但没用,还会坏事。第三类是正在做技术选型的人,想理解平台的能力边界和成本结构,避免上了之后发现跟预期不符。

不管你是哪类,我从架构讲起,再到引擎原理,最后落到优化实操。前面两章像地基,决定了你对后面优化策略的理解深度,建议不要跳过。

2. 架构拆解:先记住“控制面”和“数据面”两条线

2.1 Control Plane 与 Data Plane:一图记住 Databricks 的骨架

很多初学者第一次接触 Databricks 架构图,容易被花花绿绿的组件搞晕。其实记住两个词就够了:Control Plane(控制面)和 Data Plane(数据面)。

控制面可以理解为平台的管理大脑。你在浏览器里打开的工作区环境、Notebook 界面、作业编排配置、集群配置页面、权限控制、元数据管理,都跑在这里。它由 Databricks 自身账号体系托管,用户不需要也不能进去看机器细节。你在 Studio 上点击“创建集群”时,请求先到控制面,由控制面帮你规划任务、下发指令。

数据面则是真正算数据的地方。这里又分两层:一个是你云账号里的计算集群,可以是虚拟机集群,也可以是 Serverless 托管算力;另一个是你的数据文件所存放的对象存储和网络环境。数据面跟控制面最大的区别,是它完全活在客户自己的云账号里。所以你的数据从物理上讲始终留在你自己的云环境中,Databricks 只负责下发代码、拉取日志回去。

这个分离设计带来的好处很实际:平台升级不会影响你的的数据链路,你删掉集群、释放全部计算资源,数据还安静躺在存储桶里;做等保、网络策略、存储加密,所有的合规动作都在你自己的账号体系内可控。

2.2 经典工作区与 Serverless 模式,怎么选

既然分成了控制面和数据面,接下来一个绕不开的问题是:计算到底跑在哪儿。Databricks 目前主流有经典数据面模式和 Serverless 数据面模式,名称在不同版本里略有出入,但逻辑是一致的。

经典模式,也叫虚拟机网络模式,计算集群会真实创建在你自己云账号下的一个 VPC/VNet 里面。你可以在自己的云控制台上看到一台台机器、磁盘、安全组,甚至可以直接登上节点看环境细节。这个模式的好处是网络隔离完全由你控制,适合有严格安全合规要求的客户,也适合需要装载自定义库、自定义挂载点、联合查询本地数据的场景。

Serverless 模式则完全相反,计算集群创建在 Databricks 托管的计算平面里,你不需要管理任何云资源,不用关心实例规格、弹性策略,后台自动调度,冷启动也要快得多。代价是:数据的网络通路是由 Databricks 通过安全方式接入你的对象存储和元数据服务。它能做到的是按实际查询时间计费,闲置零成本。

我的经验是:日常 SQL 分析、小规模 ETL、需要快速起停的探索性任务,优先用 Serverless。而需要固定资源池、运行超稳定调度的生产作业,用经典模式在独立 VPC 里建好固定集群,配合池(Pool)机制预留资源,冷启动也能压到秒级。

有几个明显的坑得提醒一下。第一,Serverless 模式下你不能安装需要 root 权限的第三方包;第二,你的对象存储访问必须通过它托管的网络路径打通,如果你们安全团队管控很严,前期光打通权限可能就要两三天;第三,Serverless 是按时计价,跑一个重查询如果写得烂、扫描了全表几 TB,账单会很酸爽。 所以要不要上 Serverless,得先把自己的查询质量管好。

2.3 控制面里容易被忽视的组件:Catalog、Cluster、Job

再梳理一遍你日常会接触的控制面核心组件。

Catalog 在最新版本里已经统一到 Unity Catalog 体系之下。它把数据目录、权限、审计、血缘全部集中化。以前的 Hive Metastore 你可能要在数据面里自扛一堆元数据库,现在控制面统一管理,跨工作区共享同一个 Catalog 是标准做法。权限不再是“你在这个 Notebook 里能读”,而是“你对这张表的 SELECT 权限是 Catalog 级别授予的”。

Cluster 管理上,Databricks 集群分为交互式集群和作业集群。交互式集群适合人挂在里面跑实验;作业集群是每次作业任务启动时自动拉起、跑完自动释放。很多团队图省事把交互式集群挂七天,让作业往里提交,成本高且容易互相踩内存。正确姿势是作业集群按需拉起,优先级高再配 Pool。

Job 调度功能也值得单独提一提。你可以把 Notebook、Python 脚本、SQL 查询、Delta Live Tables 管线全部编排成一个工作流,支持依赖关系、重试策略、报警。过去跑批任务要自己搭 Airflow、要设计重启恢复点,现在原生编排完成了八九成需求。对中小团队来说,省下的运维精力非常可观。

3. 引擎层深挖:Spark 只是“基座”,真正拉开差距的都在外围

3.1 Catalyst、Tungsten 与 AQE:Spark 性能的“老底”

聊 Databricks 就绕不开 Spark,但可能很多人没真正搞明白 Spark 到底快在哪儿。这里必须提到两个执行层机制:Catalyst 优化器和 Tungsten 执行引擎。

Catalyst 做的事简单说就是“把你自己写的乱七八糟的代码翻译成高效的物理执行计划”。你在 DataFrame 里写过滤、Join、聚合,本想起码三个阶段跑,但 Catalyst 会帮你做列裁剪,把没用的列在扫描阶段就扔掉;做谓词下推,把过滤条件下推到数据扫描层;还做常量折叠、逻辑重排。很多 SQL 看着一样的写法,执行效率千差万别,正是 Catalyst 帮你在背后抹平了一部分差异。

Tungsten 则是内存和计算层的优化。它把 JVM 对象转换成紧凑的二进制格式,减少内存占用和 GC 压力;并引入了代码生成技术,把某些算子直接编译成高效 JVM 字节码。JVM 里频繁创建对象的开销有多夸张,写过 Java 的人都懂,Tungsten 省掉的正是这部分成本。

另一个在最新版里彻底改变实践的是 Adaptive Query Execution(AQE)。以前 Spark 的优化是“静态推断”,执行计划定死了。AQE 在运行时根据中间结果的实际大小动态调整:Join 策略可以在 SortMergeJoin 和 BroadcastHashJoin 之间智能切换;Shuffle 分区数可以自动合并;遇到数据倾斜,它还能自动把大分区分裂重试。这就是后来很多 Databricks 跑作业“无脑也能快”的底层原因之一。

但注意,AQE 毕竟是启发式的,它只能修正一部分明显次优的方案。更进一步,如果你想自己干预 join,还是可以显式加.hint("skew")或者.hint("shuffle_key"),这样在运行时优化器会额外考虑你的建议。特别是对几十 GB 级别的倾斜大表,手动加 hint 往往比依赖自动探测要来得更稳。

3.2 Delta Lake 的事务魔法:ACID、时间旅行、Schema 保护

如果只选一个理由向别人推荐 Databricks,我会选 Delta Lake。它不是一种独立数据库,而是一个基于 Parquet 文件之上的存储层,给湖上文件加上了“事务日志”。

每次写入、更新、删除,Delta Lake 都会在_delta_log目录下追加一个 JSON 格式的原子操作记录。这个记录里描述了这次操作新增、删除了哪些文件。读取时不是直接从目录列表去猜,而是从日志重建表版本快照。这就是 ACID 事务的来源。如果任务写到一半跑挂,日志里没有提交记录,旧版本数据依然完整。这个机制极其朴素,但极其好用。

时间旅行是它另一个让我觉得钱花得值的功能。表里每次事务都会递增版本号,你可以用TIMESTAMP AS OF或者VERSION AS OF直接查询历史某个时间点的数据状态。这个功能解决的最大痛点是“报表数据被任务覆盖了,上游改坏了,根本找不回来”。现在只需要一条 SQL 就能读到昨天甚至上周的数据,及时口径对账,不用找 binlog,不用跑备份恢复。

Schema Enforcement 和 Schema Evolution 也省了很多事。写入的新数据如果字段类型对不上,默认会直接报错,而不是悄悄塞进去;确认需要加列,用ALTER TABLE ... ADD COLUMNS或者mergeSchema选项即可。以前在纯 Parquet 湖上加列,简直是灾难级的操作,现在只是几分钟的问题。

不过 Delta 也不是没有陷阱。它的事务日志是单写者设计的,多个并发写同一张表,冲突会导致任务重试或失败。生产中尤其要避免两个任务同时频繁 MERGE 同一张事实表。减少这种冲突的办法,通常是调整分区粒度或者干脆对写入任务做调度锁,这也是我实际报错ConcurrentAppendException才体会到的。

3.3 Photon:用向量化执行引擎重新定义“快”

Databricks 对 Spark 引擎最大的“魔改”,要数 Photon。这是它用 C++ 从头实现的一套向量化执行引擎,兼容 Spark API,但是数据以更连续的列式格式批量处理,配合 CPU 缓存友好的向量化原语,把原来基于 JVM 的一行一行处理方式替换成批量操作。

初看之下,Photon 不是玄学,它的提速主要体现在三个方面。第一,原本由 JVM 生成的执行代码改为原生码,不再受 GC 和动态分派拖累。第二,算子间数据以列式批处理,SIMD 指令能发挥更大威力。第三,它针对常用的过滤、聚合、Join、窗口函数做了深度编译优化。我自己用 TPC-DS 风格查询测过,在纯 SQL 负载下,开启 Photon 后整体跑批提速普遍在 1.5 至 3 倍之间,部分聚合密集的查询能到 4 倍以上。

但 Photon 有适用边界。它主要覆盖 SQL、DataFrame 和 ETL 的通用算子;如果作业里的核心逻辑是纯 Python UDF,或者大量自定义 Java/Scala 代码,Photon 几乎是帮不上什么忙的,因为自定义代码绕不开 JVM 和序列化开销。另外一个实际问题是,开启 Photon 后账单也会高一些。所以建议按作业粒度开启,而不是全局默认开。很多团队全开之后发现成本暴涨,又不得不一个个关掉,反而折腾。

3.4 引擎周边的两个重要组件:Delta Live Tables 与 Unity Catalog

Delta Live Tables(DLT)是引擎层最值得一提的“管线上层建筑”。它用声明式的方式定义数据表,比如定义一张表依赖上游哪几张表、按什么规则清洗、目标粒度是什么。平台解释这份声明,自动编排执行顺序、自动维护表依赖、自动管理一致性检查。

我一开始觉得 DLT 不过是套了一层 YAML 的外壳,后来在团队里用它重写了一批 SCD(渐变维度)任务才发现厉害之处。原来我要自己写启动顺序、失败恢复、依赖判断,DLT 直接把这些全部内置了。你要做的只是定义声明规则,然后让平台去跑。它的失败跳过、数据质量检查(Expectations)功能,能在一张表写入前卡住异常数据。生产环境数据异常时的排查成本能降到原来的三分之一。

Unity Catalog 在前面提过,这里再强调它的一个特性:跨工作区跨引擎的权限管理。你可以给一个表设置列级别权限,明细字段只有特定角色可见,而不会影响其他查询。审计日志也是默认开启的,谁在什么时间查了哪张表,全都有记录。这在给分析师开权限时尤其有用,不用再“我给你一坨脱敏数据”这么粗暴,而是直接共享同一份基础表,权限精细控制。

4. 优化策略实战:文件、分区、排序、Join 四条主线

4.1 文件大小是第一生产力:先治小文件病

在 Databricks 上做性能优化,我个人的第一原则永远是“先看文件”。十次慢查询里,有七次是小文件太多,而不是什么深奥的执行计划问题。

小文件导致慢的原理不难理解:Spark 每个文件扫描都要拉一次文件系统元数据、打开文件流、处理 footer 信息。文件数量到几十万级别时,光规划任务(Planning)阶段就能卡你几分钟。加上对象存储的弱一致性(部分云厂商)问题,元数据重试更是雪上加霜。

常规做法有三个层次。第一层:写数据时控制目标大小。使用 Spark 的maxRecordsPerFile选项(写在 DataFrame 写入配置里)或spark.sql.files.maxPartitionBytes,让写入产生大概 256 MB 或 512 MB 的大文件。经验值设定在 256-512 MB 之间比较合理,太大了不利于并行扫描,太小了元数据不堪重负。

第二层:用OPTIMIZE命令合并。OPTIMIZE my_table;会做一次 Bin-Packing 压缩,把一堆小文件合并成理想大小。这个操作是增量的,只重写必要文件,不是全表重写。建议在生产上把它挂成每天或每周的维护任务。OPTIMIZE跑完,表的 Delta 日志会有一次新版本,原来的文件会标记删除,但不会立即物理删除,会留给VACUUM去清理。

第三层:开启 Auto Optimize。在表属性上设置delta.autoOptimize.optimizeWrite = true和delta.autoOptimize.autoCompact = true,写入时会自动在分区级别执行合并。最大的价值是它免除了写后手动维护任务。代价是写入吞吐会有轻微耗损。我的选择是给高频写入的明细表开 autoOptimize,给批量 ETL 输出表跑定时 OPTIMIZE,两种方式看负载来。

顺带说一个排查技巧:判断是不是小文件问题,最简单的方法是看 Spark UI 里 SQL 计划的 Scan 阶段,如果发现“numFiles”达到数万且“fileSize”分布极不均匀,几乎可以肯定是小文件病。另外,直接跑一下DESCRIBE DETAIL my_table;查看numFiles和sizeInBytes,也可以快速确认。

4.2 分区策略的“两个不要”和一个必杀技

分区是湖上表最常见的管理手段,但实际用崩的情况也比比皆是。我总结出的两个“不要”。

第一个不要,是不要用高基数列做分区。比如按user_id分区、按order_id分区,一张表动辄几百万个分区。Spark 规划任务时要翻遍所有分区元数据,光 Planning 就会卡死。而且每个小分区里可能也没几个文件,直接退化为另一种“小文件”。第二个不要,是不要过度嵌套分区。比如按年/月/日/小时四层分区,前端分区路径过长,扫描性能并不会因为多套了一层而更快;相反,元数据路径膨胀、目录列举成本飙升,很多时候得不偿失。

我的默认建议是:用你查询中过滤频率最高、基数适中(通常几百到几千)的列做分区键,一般按日期分区到今天/月级别就够。如果某张表需要按业务维度频繁聚合,再额外用 Z-Order 或者 Bucketing 来优化,而不是堆分区。

再讲一个必杀技:动态分区裁剪(Dynamic Partition Pruning,简称 DPP)。这是 Spark 3.0 引入的特性,在 Databricks 上默认开启。它的作用是:Join 两边表时,如果一边是事实表、一边是维表,执行引擎会先算维表的过滤结果,再用结果去裁剪事实表要扫描的分区。这样一来,事实表不必全量扫描。效果有多明显?我曾经优化过一条 10 亿行的订单明细表 Join 用户的 SQL,开启 DPP 后直接从 7 分钟降到 40 秒。所以排查慢查询时,第一件事不是改代码,而是打开执行计划,看有没有该裁剪却没裁剪的扫描。

要确保 DPP 生效,有三个先决条件:第一,被裁剪的表一定要有合适分区键;第二,维表过滤条件要足够“早”被算出来,不要依赖子查询里嵌套了重计算;第三,需要用 Spark 3.0 以上版本(Databricks Runtime 基本都满足)。如果条件全满足而 DPP 没生效,多半是统计信息缺失,跑一下ANALYZE TABLE ... COMPUTE STATISTICS准没错。

4.3 Z-Order 与 Bucketing:多维过滤和 Join 加速的取舍

分区只能管住一个维度。比如按日期分区,那按地域维度的查询就只能全分区扫描了。这时 Z-Order 排序就有用武之地。Z-Order 是一种把多维数据映射到一维空间的排序方式,让“空间上离得近的数据在文件里也放得近”。对经常做多维过滤的列执行 Z-ORDER 后,查询可以跳过大量不相关的数据块,效果接近对多个维度同时做裁剪。

实操中,它跟分区的定位很清晰:分区管“粗粒度”裁剪,适合基数低但绝对高频的列;Z-Order 管“细粒度”修剪,适合基数高、无法作为分区的多个过滤列。比如一张用户行为表,按日期分区,用户经常按channel_id+region过滤,那么就把这两个列做 Z-ORDER。跑OPTIMIZE my_table Z-ORDER BY (channel_id, region);即可,非常简单。

不过 Z-Order 也有明显代价:OPTIMIZE 过程会全量重写文件,时间可能很长;而且过度 Z-Order 会让某些原本顺序读的场景反而变慢。所以不要把所有列都 Z-Order,一般挑两到四个经常组合过滤的列即可。另外,Z-Order 对低基数枚举列效果有限,因为本身压缩率就高、裁剪效果不明显。

再说 Bucketing(分桶)。它按指定列的哈希值把数据分配到固定数量的桶文件里。好处是相同键的数据物理上放在一起,在做 Join 或聚合时,如果两边表桶数相同或成倍数,Spark 可以直接用 SortMergeJoin 甚至 BucketJoin,跳过昂贵的 Shuffle。代价是写数据时必须严格按桶数写,错配会导致任务失败或退化。我的建议是:只有当两张表高频按相同键 Join,且这个键本身数据分布均匀时,才用 Bucketing。否则优先 Z-Order。

4.4 Join 与 Shuffle 的优化节奏:先看 Plan,再下药

优化任何一条慢查询,我推荐一个标准流程,而不是一上来就堆资源。第一步,打开 Spark UI 看这条查询的 DAG,定位哪个 Stage 耗时最长。第二步,点进那个 Stage,看它的 Shuffle Read/Write 大小和分区数。第三步,把 SQL 的物理计划拉出来看 Join 类型,确认是不是应该用 Broadcast 却用了 SortMergeJoin。

如果你的查询里有一张小维表(比如几万行的分类表)Join 大事实表,Spark 默认没指定 broadcast 的话,它可能会用 SortMergeJoin。SortMergeJoin 两边都必须按 Join Key 做全量 Shuffle,成本比 BroadcastHashJoin(把维表复制到每个 Executor 的内存里)高一个数量级。修复方式很简单:在 SQL 里给维表加一个/*+ BROADCAST(dim) */提示,或者用 DataFrame 的.hint("broadcast")。

另一个常见问题是 Shuffle 分区数不合适。如果spark.sql.shuffle.partitions默认是 200,但你处理的中间结果只有几 GB,那每个分区只有十几 MB,任务调度开销远大于计算开销。反过来,如果你在跑一个 10 TB 的大聚合,200 个分区又不够,每个 Executor 上要拖几 GB 的数据,OOM 风险暴涨。我的建议是不要全局统一调,而是对单个作业在运行时配置(Runtime Configuration)里调整。默认推荐值先用spark.sql.shuffle.partitions=500来跑,然后根据 DAG 里每个 Stage 的 Shuffle Read 大小,把目标分区大小压在 128 MB 左右,反过来推算出分区数。比如一个 Shuffle Read 是 200 GB,那就设置成 1600 左右,保证每个分区 128 MB 上下,性能最稳。

数据倾斜是 Join 优化里最让人头大的问题。典型症状是:某个 Stage 有 199 个任务秒完,最后一个任务跑半小时还不结束。原因无非是某个 Key 的值特别大,比如城市维度里“上海”占了 80% 数据。解决方案有三板斧:一是用 AQE 的自动倾斜处理,开启后平台会把大分区自动拆成多个小任务并行处理;二是手动加盐,给大 Key 拼接随机后缀,把它拆散到多个分区去 Join,完事再聚合;三是改业务逻辑,能先过滤就先过滤,别让无效的大 Key 混进来。我最常用的是第二招加盐,因为稳定可控。具体做法是:复制一份维表,给每个维表 Key 扩展 10 个加盐后缀;事实表按 Key 加随机 0-9 后缀后 Join,最后按原始 Key 去重聚合。这样 Shuffle 就均匀了。

最后补充一点关于“不要过早优化”的体会。很多时候跑得慢不是算子问题,而是资源没喂饱。比如 Executor 内存配太小,GC 频繁导致整条链路慢半拍。我的顺序固定是:先查文件数量、分区策略、Join 类型,这几类问题占了绝大多数;再查 Shuffle 大小和倾斜;最后才考虑加资源或换 Photon。绝大多数情况下,改完前两类,查询性能已经够用了。

5. 常见问题排查与避坑实录

5.1 作业越跑越慢?八成是小文件在作祟

症状:同一个 SQL 任务,昨天跑 20 分钟,今天跑了 1 小时。查看 Spark UI,Shuffle Read 没有明显变大,但 Stage 数量暴涨,Executor 大量时间花在“列出目录”“获取文件状态”。

排查步骤:先DESCRIBE DETAIL your_table;看 numFiles。如果一张表只有几十 GB 数据、文件数却有十几万,恭喜你找到原因。再用OPTIMIZE your_table;合并文件,观察时间是否回落。如果这是周期性现象,说明写入任务产生了大量小文件,需要检查写入前是否做了不必要的repartition(1000),或者上游数据源天生文件细碎。我通常会顺手在写入端设置spark.sql.files.maxPartitionBytes为 256 MB,再开maxRecordsPerFile兜底,双管齐下。

这里有个容易被忽略的操作坑:不要频繁用VACUUM来“维护表”。VACUUM只负责清理旧版本文件,对当前查询性能几乎没帮助,而且如果清理得过于激进,可能连时间旅行需要的旧版本都被删光。正确的维护顺序永远是:先 OPTIMIZE,后 VACUUM,且 VACUUM 的保留期设置要跟你的时间旅行需求匹配。

5.2 明明加了资源还是 OOM,可能根本不是内存问题

症状:作业反复报ExecutorLostFailure或java.lang.OutOfMemoryError,你给 Executor 加了内存、加了 Driver 内存,还是照旧。

我在排查这类问题时发现,真正的元凶往往是三个:第一是数据倾斜,前面说过,少数大 Key 把一个 Executor 的堆挤爆;第二是排序或聚合用到大量状态,比如groupBy后collect_list这种,本质是内存爆炸型算子;第三是对象存储 IO 超时被误判为 OOM——某次大规模读取文件时,大量并发连接导致某个节点无响应,Spark 会把这个节点杀掉,里层的错误其实是Connection timed out。

对策不能盲目加spark.executor.memoryOverhead。正确做法是:先看 DAG 里 OOM 出现在哪个算子。如果出现在聚合节点,优化聚合键分布,或改用两阶段聚合;如果出现在 Join 节点,大概率是倾斜,用加盐;如果出现在扫描节点,优先检查存储侧的访问配置,比如对象存储的并发上限和超时设置。加内存虽然偶尔救急,但治标不治本。而我个人更推荐一个排查习惯:先切到 SQL 计划,看 OOM 算子的子节点是不是一个Scan,是的话先确认 Scan 的文件数量。文件太多的时候,每个 Executor 上缓存的元数据结构也是不小的内存开销,这时候光加大内存,不如先合并文件见效快。

5.3 Delta 事务日志狂涨,读越来越慢为哪般

症状:表本身不大,但查询起来越来越慢,跑DESCRIBE HISTORY能看到几百上千条记录。这个问题的本质:Delta 的_delta_log目录下堆积了大量 JSON 事务日志,而查询在读取表状态时,需要按时间顺序演算这些日志文件才能拿到当前快照。事务越多、日志越碎,这个演算过程就越慢。

触发这个问题的典型场景是高频 MERGE。某些任务每隔五分钟就跑一次 MERGE,每次哪怕只更新几行也会新增一段日志和若干文件。解决办法有两个维度:一是把表分区粒度加大或调整调度策略,减少写入频率;二是定期跑OPTIMIZE且用VACUUM清理旧日志。给个参考:VACUUM your_table RETAIN 168 HOURS;是保留 7 天版本,既兼顾时间旅行的窗口,也能把陈年日志清干净。

还有个我们踩过最深的坑:不要在大事务量场景下为 Delta 表设置过于激进的事务隔离级别。你确实需要 ACID,但不一定每秒都需要最高的隔离级别。如果业务允许最终一致,适度拉长批处理间隔,不仅能降低并发冲突报错,也能显著减少元数据负担。要记得,事务能力不是免费的,你为 ACID 支付的,除了每次写入多写的日志,还有读取时做快照演算的 CPU。

5.4 Photon 开了没提速,先查是不是 IO 瓶颈

症状:集群开了 Photon,跑了一遍基准 SQL,结果提速不明显,甚至有些查询还慢了。查了配置确认 Photon 已生效,但 Spark UI 上算子的耗时没有明显下降。

按照我的经验,这个时候刻意盯着 CPU 是没用的,真正的瓶颈在 IO。Photon 只是加速了“计算”部分。如果你的查询瓶颈在扫描对象存储大文件、或者网络拉取 Shuffle 数据,那无论引擎多快,数据来不及搬进来一切都是空谈。具体判断标准:看 Spark UI 的某个 Stage,“Task Time” 中的 GC 时间和序列化时间占比低、而“Input”读取时间占比高,那瓶颈就在 IO。

对策主要有四条:第一,检查文件是否“Z-Order 优化过”,没用过的话扫描量偏大很正常;第二,关掉无关的列扫描,尤其是SELECT *这种情况,白白给 IO 增加负担;第三,确认是否真的需要全量读,能用过滤条件裁剪的坚决裁剪;第四,对高并发查询,开启 Delta 的本地缓存功能,让热数据落到 SSD 上,能显著降低远端对象存储的压力。当然,如果你全表读取本身就几十 TB,那 Photon 也不是救世主,优化查询逻辑、做预聚合才是正路。

5.5 常见问题速查表

症状可能原因优先处理动作
作业越跑越慢,Stage 特别多小文件过多OPTIMIZE,重设写入文件大小上限
单个任务 OOM,其他任务很快数据倾斜加盐 Join、开启 AQE 倾斜处理
表不大但读取很慢Delta 日志过多调低 MERGE 频率,VACUUM 旧日志
Join 两个表,Shuffle 巨大缺 broadcast显式加BROADCASThint
Shuffle Read 大,分区数量不合适分区数不匹配数据量按目标分区大小反推shuffle.partitions
Photon 开了没提速IO 瓶颈优化扫描列、裁剪分区、开本地缓存
并发写同一张 Delta 表总报冲突单写者事务限制调整调度周期,减少写频率

这张表不复杂,但每一行都是真实报错和排查的血泪汇总。我建议刚上手 Databricks 的团队,把这几个问题打印出来贴墙上。遇到性能问题先照表排查一轮,大概率比直接去搜索引擎翻报错要快得多。

优化这件事做久了,我最大的体会是:真正高级的优化,不是把一个慢引擎调到极致的快,而是从一开始就别写会变慢的东西。文件大小选得合理、分区键想得清楚、Join 类型心里有数,后面就不需要一次次在深夜对着 Spark UI 发呆。Databricks 能做的是把 Spark 的复杂度收敛到一个更高的抽象层,但它不会替你思考数据分布和查询模式。你依然需要回到第一性原理,去看数据是怎么放的、作业到底做了什么。平台的工程水平决定了你的起点,而你自己的基本功决定了最终能走到哪一步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 4:17:06

EPLAN电气设计超实用指南:项目图纸结构、操作流程与故障排查

搞电气设计这些年,最不后悔的一件事,就是早点把EPLAN用了起来。你可能也遇到过这种场景:用纯CAD画电气原理图,画到几十页之后,改一个点位编号要全局搜索替换,核对端子排和电缆清册要对着图纸一条条数&#…

作者头像 李华
网站建设 2026/10/13 4:16:43

RPGMZ插件列表实战:从插件冲突到参数配置与插件开发

简介:面向RPG Maker MZ开发者的插件集合,围绕游戏系统扩展打包了技能树、炼金合成、虚拟按键、任务追踪、商店界面、装备状态、队伍编制、战斗中断等常用功能,可直接用于增强项目的玩法深度与操作体验。压缩包共29个文件,其中27个…

作者头像 李华
网站建设 2026/10/13 4:16:42

多标签文本分类三大实战方案:图神经网络、Seq2Seq与层级分类

1. 这不是文献综述,是三年前那几篇让我反复调试模型的“硬核论文”实操笔记2019年那会儿,多标签文本分类还没被大模型带火,但实际业务里早就在啃硬骨头了——电商商品要打“保暖加厚纯棉韩版”四五个标签,医疗问诊记录得同时归类到…

作者头像 李华
网站建设 2026/10/13 4:16:38

Python图像增强工具实战:从OpenCV算法到PySide6 GUI开发全解析

处理图像这档子事,干得多了总觉得差点意思。市面上现成的看图工具、修图软件一大堆,可真到批量处理、算法验证或者做原型演示的时候,还是得自己动手写一套趁手的家伙。这个项目的标题很直白:用Python做一套图像增强工具&#xff0…

作者头像 李华
网站建设 2026/10/13 4:16:31

Claude Code 客户端源码深度解析:IDE 智能补全架构与实战避坑

简介:本资源是面向中高级AI工程开发者与终端智能体研究者的深度技术专著《深入理解Claude Code源码》,聚焦Anthropic推出的命令行AI编程助手Claude Code(v2.1.x)的全栈实现原理。书籍系统剖析其作为终端原生AI Agent运行时的核心架…

作者头像 李华
网站建设 2026/10/13 4:16:26

卷积与系统性质:信号处理的底层操作协议

1. 这不是数学考试,是信号处理的“操作系统”在启动你翻开《信号与系统》教材,看到“卷积”两个字,第一反应是不是头皮发紧?公式长得像天书,积分上下限来回跳,δ函数又像幽灵又像开关,更别提后面…

作者头像 李华