做大数据的人对shuffle这个词都不会陌生。我在vivo数据平台这边主要负责计算引擎优化,这几年业务规模一路往PB级走,Shuffle的性能问题就成了最让人头疼的事。经常是一张跑几个小时的大表任务,其中一半甚至更多时间耗在Shuffle上;一旦集群抖动,磁盘或网络先扛不住,整个任务直接失败,重跑又要再交一次时间成本。去年我们花了大半年时间,把Spark的Shuffle机制从原生模式替换成Apache Celeborn这套远程Shuffle服务,陆续接入了上百个核心批处理任务,取得了比较明显的收益。这篇内容就是这次PB级Shuffle优化处理的完整复盘,覆盖机制分析、部署接入、参数调优和线上排障,希望能给正为Shuffle发愁的同行一些参考。
1. 为什么PB级数据下原生Shuffle撑不住了
1.1 先看一组典型的线上表现
以一条核心的用户标签计算链路为例,每天早晨批处理阶段,单条作业的Shuffle数据量能到几十TB,整条链路一天下来Shuffle总量经常突破PB级。任务运行的时间分布大致是:Source表读取和解析占两成,计算转换占两成,Shuffle相关占近五成,剩下是结果写出。这个比例在大数据量场景下并不罕见,但真正的问题是它不稳定。大促或者业务上量的时候,Shuffle耗时波动特别大,一个原本跑40分钟的任务突然变成2小时,平台侧就只能干着急。
你可以把Shuffle理解成一次大规模的“数据搬家”:上游Map任务把中间结果整理好,下游Reduce任务再来取。数据量小的时候,这个搬家过程没什么感觉,但数据量到了PB级,搬家的方式、路线和存储就非常关键。原生Spark把中间结果直接写在每个Executor的本地磁盘上,Reduce端再来远程“取件”,这个机制在数据量上来之后会暴露几个致命弱点。
1.2 小文件风暴
第一个问题是Shuffle中间文件的数量和规模完全失控。Spark Map端每个Task会为下游每个Partition生成一份数据文件,虽然执行时会做一定程度的合并,但遇到Reducer数量多、Map Task多的情况,中间文件数量轻松突破百万级。我们有一个两百多个Reduce的作业,一次运行之后统计到的Shuffle临时文件超过一百二十万份,把节点的磁盘Inode直接占满,其他进程开始报“No space left on device”,而实际上磁盘容量还有很多。
这是一个非常反直觉的问题:容量没满,但文件太多,文件系统先扛不住了。这个问题的根源是原生Shuffle的设计目标偏向中小规模批处理场景,它没有把“超大规模文件管理”当成一等公民。当文件数到了百万级别,光是文件系统元数据的维护开销就已经很大,更别说后续清理阶段还需要遍历删除这些文件,又会抢走一批CPU和磁盘I/O。
1.3 网络连接数爆炸
第二个问题出现在Reduce端的Pull阶段。下游每个Task需要到上游所有Executor上去拉取数据,假设上游有500个Executor、下游有500个Reduce Task,一次Shuffle就要建立几十万次网络连接。这些连接大多还是短连接,频繁创建和销毁,把NodeManager和Executor的线程池占满,反过来把任务压垮。
我在排查一次线上事故时看过监控,任务运行到Shuffle拉取阶段时,单台机器同时打开的TCP连接超过三万个,网络层已经出现SYN重传,应用层的拉取速度掉到几百KB每秒,整个任务卡在一个“不失败也跑不动”的状态。这种问题单靠调大连接数参数很难根治,因为瓶颈往往不在某一个参数上,而在整个网络栈的承载能力。
1.4 稳定性与GC问题
第三个问题来自内存和GC。原生Shuffle在Reduce端拉数据时,需要把大量数据块放到内存缓冲里做聚合,当单个Partition的数据特别大时,很容易触发Full GC,甚至直接OOM。特别是遇到数据倾斜,一个Task拉到的数据量是其他Task的几十倍,节点内存直接被拉满,紧接着就是一连串的任务失败和Executor失联。
这些问题单看都不是特别难解决,但它们交织在一起,就变成了一套结构性瓶颈。沿着“加机器、加参数”的思路去补,今天修好磁盘,明天网络又爆;今天调大内存,明天GC又把CPU吃满。一个很自
