我们这边离线集群跑Spark任务,单stage shuffle量过PB是常有的事。以前用Spark原生shuffle,一到这种超大规模任务就各种翻车:Fetch failed重试、小文件打爆NameNode、临时目录撑满磁盘、GC把作业拖到超时。后来我们决定把Shuffle从计算节点挪出去,调研了一圈,最终落地了Apache Celeborn做远程Shuffle服务。从切完到现在,稳定性和作业耗时都有质的改善。这篇就我个人的实操和踩坑经验,聊聊Celeborn在PB级Shuffle场景下的优化思路、部署参数和故障排查,给同样被Shuffle折磨的Spark运维、开发同学一个参考。
1. 为什么PB级任务最先扛不住的是Shuffle
1.1 Spark本地Shuffle的四个老大难问题
先说说我们原来为什么不得不换方案。Spark原生Shuffle在数据量到了PB这个量级后,问题会被放大得特别明显。
第一个是Fetch Failed的指数级放大。本地Shuffle模式下,每个Map Task都会给每个Reduce Partition物化一份data和index文件。PB级作业的Map Task数量能到几万甚至几十万,Reduce也有几千,你算一下这个文件对的规模:几万乘几千,几千万个文件。任何一个data或index文件因为磁盘坏道、节点重启、临时文件清理被误删而缺失,对应的Fetch请求就会失败。Reduce端一拉不到数据就重试,重试还失败就抛FetchFailedException,然后整个Map Stage被调度器推倒重算一部分。数据量越大,文件越多,出故障的概率几乎是线性往上升的。
第二个是小文件风暴。几千万个shuffle小文件意味着NameNode要处理海量getBlockLocations请求,RPC队列直接积压,HDFS全局受影响。那时候我们的NameNode经常因为某个超大作业出现毛刺,其他所有任务的HDFS读写都被拖慢。事后看监控,瓶颈全在shuffle小文件的元数据查询上。
第三个是数据倾斜。PB级作业里倾斜几乎无法避免,比如按某个商家或用户维度做聚合,头部key能占到全量的百分之二三十。单个Reducer要拉走几百GB的数据,内存缓冲根本兜不住,GC时间飙升,Fetch反复重试,最后只能靠人工调spark.sql.shuffle.partitions碰运气。
第四个是计算和存储互相踩踏。shuffle数据占的是计算节点的本地磁盘,大任务一跑,临时目录能打满一两块盘,直接影响同节点其他任务的shuffle读写。我们碰到过几次因为一个任务把磁盘写满,导致同节点其他任务的JVM进程莫名其妙卡死的状况。
这些问题不是调一两个Spark参数能解决的。本地Shuffle的设计假设是“数据放在计算节点附近”,但到了PB级,文件数量、故障概率、IO争抢已经完全超过了这个假设的cover范围。
1.2 远程Shuffle服务的取舍与选型
既然本地Shuffle扛不住,思路就是把中间数据拿到一个独立的Shuffle集群去存。Map端的中间结果不落本机磁盘,而是推送到独立的远程Shuffle Service,Reduce端再从这个服务拉取。这样计算节点只负责算,Shuffle节点只负责中转和存储。
主流的开源方案那时候主要对比了Celeborn(当时还在Apache孵化器,叫RSS)和Unshuffle,也聊了自研方案。Unshuffle的思路是做线程模型优化,减少shuffle读写时的线程切换开销,但生态相对窄,社区迭代速度一般。自研RSS可控性最强,但人力投入太大,要维护的模块从Master、Worker到客户端SDK全套,不是我们短期能接住的。
最后选了Celeborn。核心原因是它把“文件合并”和“流式读写”这些关键设计做得很扎实,同时兼容Spark和Flink,一套集群两套引擎都能用。而且社区活跃度很高,遇到问题能在社区找到不少案例,我们当时判断这个方向的长期可维护性最好。
1.3 为什么是Celeborn而不是其他方案
简单说下选择Celeborn的几个具体原因。
一是它解决了Fetch失败和小文件问题的本质:数据在Worker端按Partition合并成大的Partition文件,而不是保留Map Task维度的小文件。Fetch的时候按
