做数仓的同学大多都有过这种体验:白天业务高峰期,集群上永远排着一长串作业,前面跑不完后面就等着,报表产出时间一拖再拖;到了晚上和凌晨,集群资源又大面积闲置,CPU 利用率惨不忍睹,可机器和机房的成本一分不少。这种“高峰不够用、低峰用不完”的资源矛盾,在迅雷这类业务波峰波谷明显的公司里体现得尤其突出。
今天想聊的,就是迅雷把核心离线数仓任务逐步迁移到阿里云 EMR Serverless Spark 之后,资源效率和业务交付速度的明显变化。这不是一篇简单的产品介绍,而是把选型逻辑、迁移方法、参数调优、成本核算、踩坑记录都串起来的完整复盘。如果你正在自建 Hadoop 集群和 Serverless 方案之间纠结,或者已经在用 Spark 但被资源管理折腾得够呛,这篇文章应该能给你提供一条可以直接参考的路径。
先交代一下背景。迅雷业务线多,下载、影音、云盘等产品每天产生的日志量和业务数据量都不小,数仓团队承接的是全公司的离线分析、报表产出、用户画像、推荐特征等任务。过去这些任务跑在自建 Hadoop 集群上,YARN 调度,队列按业务线划分,高峰期大家抢资源,低峰期资源空转,系统的物理资源上限决定了并发上限,想临时加资源根本来不及。后来我们把目光转向了阿里云 EMR Serverless Spark,核心诉求就一个:把资源和业务解耦,让团队从“运维集群”回归到“加工数据”。
1. 从自建集群的痛点说起
1.1 资源的高峰与低谷,远比想象中夸张
先别急着聊 Serverless 多好,得先把自建集群的真实处境说清楚。迅雷的数据任务有明显的“潮汐效应”:每天上午 10 点到下午 6 点是业务查数和报表需求最密集的时段,各种临时任务、看板刷新、数据订正任务全挤在一起;晚上 10 点后进入离线批处理高峰,ETL 任务一波接一波;到了凌晨 2 点到早上 8 点,大部分任务跑完,集群基本处于半空闲状态。
我见过不少团队的资源利用率数据,自建集群能做到 40% 平均利用率已经算不错了。但问题的关键还不是平均利用率,而是“高峰期的排队时长”。业务方不会关心你集群平均利用率是多少,他们只关心上午 11 点提的数,下午 3 点能不能出来。一旦集群资源被打满,新提交的作业就只能进队列等着,一等就是半小时甚至两小时。为了缓解排队,我们做过很多努力:调队列权重、限制大任务并发、把不重要的任务错峰调度,但说到底集群的物理规模就摆在那里,再怎么调度也是拆东墙补西墙。
另一个隐性成本是运维人力。自建集群的版本升级、组件兼容、节点故障处理、磁盘满告警、NameNode 元数据维护,这些事看着不大,但每一件都会打断数据开发的正事。我们团队不算大,运维占用的精力比例一旦超过两成,留给数仓建模、数据治理、业务需求支持的时间就会被严重压缩。这也是后来推动迁移的一个重要原因——不是 Spark 跑不动了,而是团队精力分配出了问题。
1.2 为什么最终选择了 EMR Serverless Spark
市面上能选的弹性计算方案不少,有自建 Kubernetes 加 Spark Operator,有普通 EMR 集群按需扩容,也有各类 Serverless 产品。我们当时重点评估过三条路。
第一条路是继续用自建集群,但引入 Kubernetes 做弹性伸缩。思路是保留 YARN 作业不动,用 K8s 动态拉起 NodeManager 来扩容。但实际评估下来,这套方案的复杂度很高:YARN 和 K8s 的联动要考虑节点注册、资源回收、镜像管理、网络打通、调度器配置,相当于自己把云厂商的活儿干了一遍。而且自建集群的硬件资源还是自己先买好或预留好,弹性只是“在已有资源池里腾挪”,并没有真正解决资源天花板的问题。
第二条路是使用阿里云 EMR 普通集群,也就是经典的开源 Hadoop/Spark 集群形态,按需创建、按量付费。这个方案的优点是环境熟悉,迁移成本相对低,但本质上还是要管理集群生命周期,只是在“自己买服务器”和“云上开集群”之间换了个地方。高峰期仍然要考虑扩容时间,节点拉起要等,作业提交要等,人还是得盯着集群状态。
第三条路就是 EMR Serverless Spark。它把 Spark 作业的“运行环境”完全托管了,我们只提交作业,不用关心 Executor 跑在哪台机器上、集群有多少节点、要不要扩容。作业来了,平台自动分配资源,作业跑完,资源自动释放。这个模式直击了“高峰不够用、低峰用不完”的痛点——资源不再是预先购买、固定存在的物理实体,而是按作业需要动态出现的计算能力。
最终选择它还有一个很现实的原因:阿里云 EMR Serverless Spark 兼容开源 Spark API 和生态,我们团队现有的 Spark 作业、SQL 脚本、开发习惯基本可以平移,不需要为了用 Serverless 而重写一套代码。对于一个已经有大量存量任务的数据团队来说,这一点比任何 fancy 的特性都重要。
2. 迁移前的架构梳理与作业盘点
2.1 迁移前的数仓架构长什么样
在动手迁移之前,我们先把现有数仓的底账摸了一遍。迅雷的离线数仓是典型的四层结构:ODS 层存原始日志和业务库同步数据,DWD 层做清洗和明细加工,DWS 层按主题汇总,ADS 层面向具体应用出报表和指标。调度上用的是 DolphinScheduler,任务之间靠依赖关系编排,核心任务有数据质量校验,跑挂了会自动告警重跑。
数据存储这一层当时还在 HDFS 上,Hive 表有几千张,其中热表、常用表集中在几十张核心表上。计算引擎以 Spark 为主,少量任务还在用 Hive MR 跑。整个架构从设计上讲没什么大问题,真正的瓶颈在计算资源层:Spark Executor 的调度受制于 YARN 队列配置,任务与任务之间相互挤占,核心任务和非核心任务难以做到真正的资源隔离。
这里要特别强调一下,迁移计算引擎不等于迁移存储。我们的做法是先把计算层迁到 EMR Serverless Spark,存储层暂时不动,仍然读写原 HDFS 或 OSS 上的数据文件。这样做的好处是风险可控,就算计算层出了问题,数据还在原地,随时可以切回原有计算方式。等到计算层跑稳定了,再考虑把部分存储迁移到 OSS 或阿里云数据湖构建服务。
2.2 作业分级:哪些先迁,哪些后迁
几千个作业不可能一次性全迁过去,这是迁移项目最容易犯的错误。我们按“重要性、稳定性、资源消耗”三个维度把作业分成了三类。
第一类是核心报表和线上数据依赖的任务,特点是每天必须准时产出,出问题影响面大。这类任务我们放在最后迁移,先用双跑的方式验证数据一致性,确认无误后再切换。第二类是常规 ETL 任务,特点是调度稳定、逻辑相对固定,这类任务适合作为迁移的主力,分批推进。第三类是临时查询和探索性分析任务,特点是可以容忍排队和失败重跑,这类任务是我们最早迁到 Serverless 的一批,因为即使出现问题,影响面也最小。
实际执行的时候,我们定了一个“先小后大、先跑通再优化”的节奏。第一批选了十几个作业,涵盖 Spark SQL、PySpark、Spark Scala 三种作业类型,先把提交方式、参数配置、日志查看、告警对接这些基础链路跑通。第二批扩大到几百个作业,覆盖主要业务线的常规 ETL。最后一批才是核心报表任务,逐个双跑验证,数据量、产出时间、结果明细都对比通过之后再切换。
2.3 调度系统和 Spark 作业怎么衔接
把作业迁到 Serverless Spark 之后,调度层不能乱。我们继续沿用 DolphinScheduler 作为总调度,只是把“提交到 YARN 集群”的环节替换成了“调用 EMR Serverless Spark 的 OpenAPI 提交作业”。
这里有一个设计上的关键点:调度平台只负责依赖编排和触发时机,不负责管理计算资源。每个被调度触发的作业,通过脚本调用 Serverless Spark 的提交接口,传入作业名称、SQL 文件或 Python 脚本地址、资源配置参数、依赖的 Jar 包位置等信息。作业在 Serverless 环境中运行,调度平台通过轮询接口获取作业状态,成功或失败都由调度平台统一记录和告警。
这个方案的好处是,业务方和调度系统几乎无感知,迁移前后看到的还是一个“按依赖关系运行的任务流”,只是底层执行引擎变了。对数据开发团队来说,学习成本也被压到了最低,平时怎么写 Spark 作业,现在还是怎么写,只是多了一层提交封装。
3. EMR Serverless Spark 的核心配置与调优
3.1 资源规格:Driver 和 Executor 的配比怎么定
我们刚开始用 EMR Serverless Spark 的时候,分配资源比较粗放,就是照着原先 YARN 作业的 Executor 数量直接换算。但用了一段时间后发现,Serverless 场景下资源是按作业维度申请和计费的,同样的资源配置,运行效果可能和 YARN 下差别很大,因为集群不再有“常驻资源池”的缓冲,每个作业的资源规划得更细致。
一个典型的 Spark SQL 作业,我们一般这样配:Driver 用 2 vCPU、4 GB 内存,Executor 用 2 vCPU、8 GB 内存,每个 Executor 挂 2 到 4 个 core,内存给 8 GB 到 16 GB。具体的配比取决于作业本身的 shuffle 量和数据规模。数据量小、逻辑简单的作业,不需要配太多 Executor,避免资源浪费;数据量大、join 和 group by 多的作业,要重点考虑 Executor 数量和 shuffle 分区数之间的匹配关系。
这里给一个比较实用的建议:看 Spark UI 的 Executor 运行记录,如果大多数 Executor 的 GC 时间占比偏高,说明内存不够或者单个 Executor 处理的数据量太大;如果 Executor 的 Shuffle Read 量分布非常不均匀,那大概率是数据倾斜,而不是资源不够。这两种情况对应的解决方式完全不同,前者加内存,后者要处理倾斜(后面会详细讲)。
3.2 Spark 作业参数的几个关键调整
迁移过程中,我们重点调过几个 Spark 参数,这些参数在自建集群上可能没那么敏感,但在 Serverless 模式下直接关系到成本和稳定性。
第一个是动态资源分配。开启动态分配后,Spark 会根据作业的 stage 执行情况动态调整 Executor 数量,空闲时自动释放,繁忙时自动追加。我们线上配置了spark.dynamicAllocation.enabled=true、spark.dynamicAllocation.minExecutors=1、spark.dynamicAllocation.maxExecutors=20这类参数。动态分配对批量小任务特别友好,不会出现小作业也占用一大堆 Executor 的情况。
第二个是 shuffle 分区数。spark.sql.shuffle.partitions默认值是 200,但在 Serverless 模式下,如果数据量不大,200 个分区反而会产生大量小 task,调度开销远大于计算收益。我们一般根据数据量手动设置,比如单表扫描量在几十 GB 级别的作业,分区数设到 100 左右就够了;几百 GB 的作业,再适当调高到 300 到 500。经验公式不复杂:目标分区大小控制在 200 MB 到 500 MB 之间比较合理,分区数 = 总 shuffle 数据量 / 目标分区大小。
第三个是任务重试。Serverless 平台上偶尔会遇到节点故障导致任务失败,合理的重试机制是必要的。我们配置了spark.task.maxFailures=4,允许 task 在遇到节点级故障时自动在其他计算资源上重试,而不是整个作业直接失败。这个参数看着不起眼,但在长任务上能省掉大量人工介入的时间。
3.3 成本控制:按量付费和预留资源的组合策略
说到 Serverless,大家最关心的往往不是性能而是钱。EMR Serverless Spark 的成本模型很简单:按实际消耗的 vCPU 和内存计费,作业跑多少算多少。这种计费模式对低峰期很友好,但高峰期如果大量固定作业同时跑,费用也会随之上涨。我们的应对策略是“按量 + 预留”混用。
对于每天晚上固定跑批的核心作业,我们评估了一下规律性:这些作业每天产生的资源消耗相对稳定,按照历史数据算出一个合理的预留资源量,在高峰期预留一部分资源池。对于临时查询、新任务验证、波动性较大的作业,走按量付费,跑完就释放,不为不确定的需求买单。
这里有一个成本对比的思路值得参考:不要把 Serverless 的费用和“原来集群的总成本”直接比,而要对比“有效计算成本”。原先自建集群虽然总成本固定,但其中有大量资源是闲置的,有效利用率可能只有 40% 到 50%。Serverless 模式下花费的钱基本都对应实际计算量,性价比的差异要看这个维度。我们迁移后的实际感受是,在同等业务量的前提下,计算资源总成本大约下降了 20% 到 30%,但这不是最关键的收益——最关键的是排队时间几乎消失了。
4. 资源效率提升的量化对比与分析
4.1 先定义指标,否则“效率提升”只是空话
迁都迁了,总要回答“到底提升了多少”这个问题。很多团队做完迁移后只会说“感觉快了很多”,这是不够的。我们把资源效率和业务提升拆成了几个可量化的指标。
核心指标包括:作业排队等待时长,即作业提交到开始执行的时间间隔;作业平均执行时长,即 Spark 作业从开始到结束的运行时间;集群资源平均利用率,迁移后虽然没有固定集群了,但可以通过各作业的资源申请量和使用量的比值来衡量;单位作业计算成本,把每个作业产生的费用汇总后除以作业数量或数据处理量;报表产出及时率,即关键报表在 SLA 时间内完成产出的比例。
这些指标在迁移前就要开始采集和记录。我们花了两周时间把存量作业的历史运行数据整理出来,包括调度平台上的开始时间、结束时间、运行日志中的耗时记录,再结合集群监控系统里的资源使用曲线,形成一份迁移前基线。没有这个基线,后面的对比分析就无从谈起,这一点希望所有做迁移项目的同学务必重视。
4.2 迁移前后核心指标对比
在业务量基本持平的前提下,我们对比了迁移前后一个月的数据,这里说说几个比较有代表性的变化。
排队等待时长方面,迁移前高峰期作业平均排队时间在 15 到 40 分钟之间,遇到大任务抢占资源时,个别作业排队超过 1 小时也不罕见。迁移后,由于资源是动态申请的,作业提交后基本是秒级启动,排队现象几乎消失,平均排队时间降到了 1 分钟内。
作业平均执行时长方面,迁移前单个 ETL 作业的平均执行时长大约 22 分钟,迁移后降到 15 分钟左右。缩短的原因一方面是资源不再被其他任务挤占,另一方面是我们在迁移过程中顺手做了一批参数调优,比如分区数合理化、小文件合并,这些优化在自建集群上也能做,但在 Serverless 模式下收益更明显。
资源利用率方面,如果只算有效计算资源消耗,自建集群的 CPU 平均利用率在 40% 上下,迁移后按作业实际申请资源与执行计算资源消耗的比值来算,利用率能稳定在 70% 以上。这说明同样一单位资源,真正花在计算上的比例大幅提高。
这些数字只是我们项目中的观察结果,不同团队的数据会不一样。但方向上我可以比较确定地说:资源效率的提升主要来自“资源不再被闲置和排队浪费”,而不是 Spark 程序本身变快了多少。
4.3 业务效率提升的间接收益
资源效率提升带来的最直接业务变化是报表产出时间提前。原先每天早上 8 点前必须产出的核心经营报表,迁移后普遍能提前 1 到 2 小时完成。这一点对业务方感知特别强,因为他们是按“几点能看到数据”来评判数据团队工作的,不会关心你底层用了什么技术。
另一个间接收益是需求响应速度变快了。以前业务方提一个数据需求,开发同学要先评估集群资源够不够,碰巧赶上高峰期,可能就只能排队等,需求排期动不动拖到下周。现在资源按需申请,开发验证环境随时能跑,从提需求到上线,周期从一两周压缩到三四天是常见的。老板看到这个变化,数据团队在公司里的存在感都会不一样。
此外,运维负担减轻后,团队开始有余力做过去一直想做但没时间做的事,比如数仓模型的规范化治理、指标口径的统一、历史数据归档。这些工作短期看不到直接的业务收益,但时间久了,会发现查询效率、开发效率、数据质量都在提升,属于典型的“磨刀不误砍柴工”。
5. 迁移过程中的踩坑与排查实录
5.1 冷启动和长尾任务的那些坑
Serverless 不是完美的,第一个要面对的就是冷启动问题。作业提交后,平台需要拉起 Driver、分配 Executor、加载依赖环境,这个过程需要一些时间。对于大部分批处理作业,几十秒的启动时间可以接受,但对那些耗时本身只有一两分钟的小任务,冷启动时间占比就非常明显,用户的感知是“我怎么感觉还没以前快”。
我们当时的处理办法是尽量合并小任务。原先在自建集群上,开发习惯是一个业务逻辑写一个作业,导致大量 1 到 3 分钟的小作业。迁移后,我们把同一数据源、相同调度频率的小作业合并成一个作业,通过多段 SQL 或 PySpark 脚本在内部串联。作业数量从几千个降到了几百个,不仅减少了冷启动的影响,还让调度链路更清爽、更容易排查问题。
长任务方面,我们也遇到过个别 Executor 因节点故障被回收导致 stage 反复重算的问题。排查后确认主要是数据倾斜引发的个别 task 长时间运行,超过了平台对单 task 的时长限制。解决思路是先把倾斜问题处理掉,而不是盲目调大超时时间,具体做法后面会讲。
5.2 数据倾斜:最容易被误判为“资源不够”的问题
数据倾斜在自建集群上就会出现,但迁移到 Serverless 后更容易暴露,因为每个作业的资源是动态分配的,一旦某个 task 数据量过大,其他 Executor 都在空等,作业整体跑得特别慢,还容易被误判成资源不足而盲目扩容。
我们线上遇到过一个典型案例:一张用户行为明细表按用户 ID 分组统计,少数头部用户的行为数据量是普通用户的几百倍,导致这几个 key 所在的 task 跑了几个小时,其他 task 十几分钟就结束了。后来在 SQL 里加了随机前缀做两阶段聚合,先把数据打散,再对热点 key 单独处理,执行时间从几个小时降到了二十分钟以内。
如果你不确定一个作业是不是数据倾斜,一个快速判断方法是用 Spark UI 看各个 task 的 Shuffle Read 和运行时长,如果某个 task 的 Shuffle Read 是其他 task 的几倍甚至几十倍,基本可以确定是倾斜。解决方案根据场景有多种,比如加盐两阶段聚合、广播小表、热点 key 单独 join,具体选哪种要看业务逻辑。这个排查思路在 Serverless 和自建集群上完全通用,是每个 Spark 开发人员都应该掌握的基本功。
5.3 网络打通与元数据服务的衔接细节
最后一个容易忽略的坑是网络和元数据配置。EMR Serverless Spark 的计算资源在云平台上,要读写我们原有数据存储,就必须先把网络打通。我们当时使用阿里云 VPC 环境,数据源和 Serverless 资源需要在同一个 VPC 或者通过安全组规则放通访问。
这里有个容易踩的细节:Hive 元数据服务如果还部署在自建集群上,Serverless 的 Spark 作业要访问它,同样要打通网络,并且注意访问权限和并发限制。我们一开始就是元数据服务连通性没处理好,导致作业频繁报 “Table not found” 或 “Connection refused”,排查了半天才发现是安全组没有放行对应端口。
后来我们把常用表的元数据逐步迁移到了阿里云的数据湖构建服务(DLF),对接过程中把 Hive Metastore 的地址、数据库名、表名做了清晰映射,作业访问的稳定性明显提升。如果你也打算迁到 Serverless 平台,建议提前梳理清楚元数据服务的连接方式,不要等到业务切换时才发现网络不通,那种临门一脚发现门锁了的感觉真的非常酸爽。
另外,存储路径的兼容性也值得注意。我们原始数据在 HDFS 上,迁移过程中逐步把部分热表的数据文件复制到 OSS,因为 HDFS 和 OSS 的访问协议、文件数量限制、命名空间管理都不太一样。OSS 上的文件,如果小文件特别多,也要做合并压缩处理,否则会产生大量的 list 操作和连接开销,直接影响 Spark 作业的读取效率。我们后来统一用 Parquet 格式加适当压缩存储,在数据读取速度和存储成本之间找到了一个平衡点。
整体上,这次迁移带给我们的收获,除了资源效率提升和成本下降,更重要的是让团队重新把精力放到了“数据”本身。以前有一半脑子要想着集群别出问题,现在只需要想清楚怎么加工数据、怎么提升模型质量。数仓的价值是靠数据质量和业务洞察体现的,不是靠能运维多少台机器体现的——想明白这一点,技术选型的方向其实就清晰了。