1. 为什么要动Shuffle:PB级场景下的几个硬伤
做离线数仓和大数据跑批的同学,对Shuffle应该都不陌生。Map阶段处理完数据,要按照Key重新分区交给Reduce端,这个过程中间数据的搬运和落盘,就是Shuffle。很多Spark作业跑得慢、动不动OOM、任务失败后恢复要半天,根子往往不在计算逻辑本身,而是Shuffle环节先撑不住了。
我们这次要聊的,是vivo在PB级Shuffle场景下做的一项典型优化实践——引入Celeborn作为Remote Shuffle Service,把Shuffle从“本地磁盘反复读写”的模式,改造成“远端服务统一聚合”的模式。对于单次作业Shuffle数据量达到PB级的平台来说,这不仅仅是提速,更是让任务能稳定跑完的关键手段。
先说清楚原生Shuffle在PB级场景下到底有哪些问题,否则你很难理解为什么非要引入一套独立服务。
1.1 Map端落盘、Reduce端拉取的老链路
原生Spark的Shuffle链路大致是这样:每个Map Task处理完数据后,按照分区器把数据写到本地磁盘,生成Shuffle中间文件。Reduce Task启动后,从各个Map Task所在的节点拉取属于自己的那部分数据,边拉边聚合、边做Reduce计算。
这套设计本身没毛病,小规模任务完全够用。但当数据量大到PB级,问题就来了。
Map端每个Task都会产生一份Shuffle文件,Task数量一上来,文件数量呈爆炸式增长。一个几千个Map Task的作业,光Shuffle小文件就是几万、几十万个。这些文件在节点之间分散存储,Reduce端要跨节点随机拉取,随机IO的比例非常高,整个集群的磁盘和网络压力都很大。
1.2 PB级场景下原生方案的四道坎
第一道坎是磁盘空间浪费。很多平台为了保障Shuffle数据不丢,底层存储做了三副本。Shuffle中间数据本身是临时数据,用完就删,却要和正式数据一样占三倍空间,这个成本对大集群来说非常肉疼。
第二道坎是任务失败恢复慢。Spark的Shuffle文件是跟随Executor生命周期的。如果一个节点宕机,或者一个Executor被Kill,上面所有的Shuffle数据就没了。Spark只能把上游所有Map Task重新跑一遍,才能重新生成这些数据。在PB级作业里,重算上游意味着几十分钟甚至几个小时的额外开销,稳定性很难保证。
第三道坎是IO毛刺和列队阻塞。Task并发一高,本地磁盘频繁读写,再加上其他计算任务的干扰,磁盘IO很容易出现毛刺。某个节点磁盘慢了,拉取Shuffle的对端任务就会跟着等,形成连锁阻塞,最终表现为整个作业变慢。
第四道坎是资源利用率不均。原生Shuffle下,数据落在哪个节点,Reduce任务就得到哪个节点去读。节点之间数据量天然不均衡,加上集群里同时跑很多作业,热点节点的磁盘和网络会被打满,而其他节点可能很闲。
1.3 Shuffle外部化的思路为什么能成立
既然Shuffle中间数据放在本地有这么一堆问题,业界很自然的想法就是:把Shuffle数据统一收口到一个独立服务里,由这个服务负责存储和供给。Map端把数据推上去,Reduce端从服务里拉,本地磁盘不再承担Shuffle中间数据的压力。这就是Remote Shuffle Service,简称RSS。
Celeborn就是这种思路的一个开源实现,它前身是腾讯的Apache Incubator项目,现在已经在很多公司的大数据平台落地。我们的实践场景和它非常契合:PB级Shuffle、大集群、对稳定性要求高、存储成本敏感。所以当初选型,思路很快就确定下来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Celeborn整体设计与工作流程拆解
Celeborn这个项目,名字叫"喜树",寓意"聚木成林"。它的核心设计可以概括为:用一个 Master 集群管理元数据,用一组 Worker 节点承担Shuffle数据的存储和读取,再用一个内嵌在Spark应用里的Client完成数据的上推和拉取。
2.1 Master、Worker、Client三方角色
Master的角色类似Spark的Driver,也可以理解成
