作者: vivo 互联网大数据团队-Wang Zhiwen、Cai Zuguang
vivo大数据平台通过引入RSS服务来满足混部集群中间结果(shuffle 数据)临时落盘需求,在综合对比后选择了Celeborn组件,并在后续的应用实践过程中不断优化完善,本文将分享vivo在Celeborn实际应用过程中对遇到问题的分析和解决方案,用于帮助读者对相似问题进行参考。
1分钟看图掌握核心观点👇
一、背景
近年来,随着vivo大数据平台的数据量和任务量持续快速增长,新增的计算机资源已无法满足不断扩大的存储和计算需求。同时,我们观察到互联网和算法等在线业务在白天流量高峰,而在夜间流量显著下降,导致部分服务器在低峰时段资源利用不充分。与此相对,离线业务的高峰期正好落在在线业务的低峰期间。
在此背景下,我们于2023年起与容器团队合作,启动了在离线混部项目,旨在探索将海量的大数据离线任务运行到K8S在线集群中。通过实现在线服务、大数据离线任务、与机器学习离线任务的混合部署,我们期望缓解计算资源的潮汐现象,实现资源的弹性伸缩和共享,增强大数据平台的资源供应,同时提升公司整体服务器的利用率并达到降本增效的目的。为实现上述混部效果我们对当前主流的方案YARN on K8S和Spark on K8S做了大量的调研,方案对比情况如下:
基于多方面因素综合考虑,我们团队决定采用Spark on K8S方案去支持在离线混部项目,为此我们急需一套成熟的remote shuffle service服务支持项目的推进。
二、RSS 初体验
2.1 百花齐放,只取一支
在Spark作业中,高效的Shuffle服务对性能至关重要。我们对主流Shuffle方案(Celeborn、Uber RemoteShuffleService、Firestorm)进行了深度调研。以下是我们vivo团队对主流shuffle服务的初步调研结论。
基于全面评估,我们团队初步选择Celeborn作为核心Shuffle解决方案。以下是我们选择Celeborn的关键决策因素:
-
生态兼容性:Celeborn对Spark 3.x(包括最新Spark版本)的Dynamic Allocation原生支持,优于Uber RSS的有限兼容(仅Spark2.4/3.0)和Firestorm的不支持Spark 3.2。符合vivo当前使用的spark版本,避免未来Spark兼容带来的风险。
-
可观测性与运维友好性:Celeborn的完整指标系统(集成Prometheus)提供了端到端监控能力,可在运维过程中实时识别瓶颈。而Uber RSS的监控缺失和Firestorm无指标问题,将显著增加生产环境故障排查难度。
-
性能与稳定性保障:Celeborn使用Off-Heap内存机制大幅减少JVM垃圾回收的STW停顿,提升吞吐量。同时采用Slot并发控制避免Worker过载,预防资源争抢导致的作业失败。Firestorm虽优化文件格式,但不支持最新Spark版本;Uber RSS缺乏并发管理能力。
-
劣势的可行性规避:Celeborn不支持DFS的情况,在当前架构下可接受,未来开源社区正推进DFS支持,技术债可控。
基于上述的全面评估,Celeborn在兼容性、可靠性、可观测性三方面脱颖而出,是平衡长期演进与短期落地的最优解。
2.2 精心培养,绽放异彩
2.2.1 硬件适配:机型选择与性能调优
我们选取了三类线上常见且服务时间较长的服务器进行测试,充分利用现有硬件资源,验证Celeborn在不同配置下的性能表现:
通过对比发现,机型B表现出最佳的综合性能,而机型C虽然单盘性能出色(SSD达1.6GB/s),但受限于RAID5配置和较小容量,在持续写入场景反而出现数据挤压,这提示我们Celeborn对磁盘数量比单盘性能更敏感。基于测试结果,我们总结出Celeborn的硬件选择原则:
-
优先考虑磁盘数量:多块HDD的聚合带宽往往优于少量高性能SSD
-
避免过度RAID化:shuffle数据一般为临时数据,即使有少量丢数对离线业务影响也不大,无需做raid
-
内存适度配置:Celeborn对内存需求并非线性增长,250G左右已能满足PB级Shuffle需求
-
利用过保或服务较久的老旧机型:Celeborn对硬件要求相对宽容,是消化老旧服务器的理想场景
这些结论帮助我们优化了硬件选择策略,将Celeborn集群部署在性价比最优的机型B配置上,实现了资源利用的最大化。
2.2.2 服务健壮性:高可用与运维验证
作为关键基础设施,Celeborn的服务稳定性和运维友好性是我们评估的重点。我们设计了多场景的故障模拟测试,验证其生产级可靠性。
Master高可用测试
Celeborn采用Raft协议实现Master高可用,其高可用的能力是我们验证的重中之重,为此我们模拟线上的日常操作对master进行测试验证得出以下结论:
-
无脏数据情况下,Master节点轮流重启不影响运行中任务
-
整个过程中Shuffle服务零中断
-
三Master架构下,即使两个节点同时故障,只要一个存活就不影响任务运行
在整个测试中我们发现在k8s环境下Worker重启会带来IP的变动,若有频繁的重启会在Master产生脏数据,当Master触发重启操作时恢复会非常慢。为此我们在k8s环境上都固定了Worker的IP,让Worker的变动不影响master。
服务热更新验证
版本变更是生产环境常态,我们测试了不同副本配置下的更新影响:
测试表明,双副本模式是生产环境的必选项,虽然节点下线需要更长时间等待数据传输完成,但保证了服务连续性。
2.2.3 性能测试
在性能方面我们对RSS的要求是不明显低于ESS,为此我们在3master+5slave的集群下做了对比验证,测试数据如下:
结果呈现三个显著趋势:
-
全面加速:在小规模数据量(5.9TB)场景,Celeborn耗时仅为ESS的47%,优势明显
-
稳定优势:随着数据量增大,Celeborn始终快于ESS,28.3TB时仍有20%提升
-
可靠性差异:ESS在79.3T及以上Shuffle量时完全失败,而Celeborn能稳定完成所有测试案例
另外在数据验证中我们发现使用Celeborn会偶发存在丢数情况,对此我们反馈给社区开发人员一起联调测试,发现Celeborn确实存在有丢数的情况,问题详细记录在CELEBORN-383。对此非常感谢社区的大力支持,帮助我们快速修复问题得以项目能准时上线。
三、与Celeborn共渡800天后的经验分享
在大数据计算领域,Shuffle作为连接Map阶段和Reduce阶段的关键环节,其性能与稳定性直接影响着整个作业的执行效率。大数据平台运维团队目前运营着多个Celeborn集群,用上百个节点规模去支撑日均PB级Shuffle数据量、几十万应用,我们在过去800天的生产实践中,针对Celeborn这一新兴的Remote Shuffle Service进行了深度优化与调优。接下来的篇章中将系统性地分享我们在性能提升和稳定性保障两方面的实战经验,涵盖问题定位、优化思路、实现方案以及最终效果,为同行提供可落地的参考方案。
3.1 性能优化:从瓶颈突破到全面提升
在超大规模集群环境下,Celeborn作为Shuffle数据的"交通枢纽",面临着诸多性能挑战。我们通过全链路监控和细粒度分析,识别出三个关键性能瓶颈点,并实施了针对性的优化措施。
3.1.1 异步处理OpenStream请求:解决读阻塞难题
问题现象与影响
在日常运维中,我们频繁观察到Shuffle Read耗时异常波动的情况:读取耗时从正常的几十毫秒突然增加到秒级甚至分钟级,同时读取数据量呈现断崖式下降。这种现象在业务高峰期尤为明显,直接导致作业执行时间延长30%以上。
根因分析
通过分析Celeborn Worker的线程堆栈和IO等待情况,我们发现当Worker节点在进行大规模文件排序操作时,读取排序文件的reduce任务会同时发起对排序文件读取的openstream请求到Worker,这些请求会长期占用线程直到排序结束,从而导致Worker的读线程数被占满,后续的读请求无法被处理形成恶性循环。
优化方案
我们通过修改客户端发起异步openstream请求方式去减少服务端线程被长时间占用的问题,从而降低对fetch请求的影响,相应的pr为:Github | apache | celeborn
3.1.2 小文件缓存:攻克高负载下的IO瓶颈
问题现象
在Celeborn集群负载超过70%时,我们注意到一个反常现象:部分本应快速完成的KB级小文件读取操作,耗时却异常攀升至几十分钟级别。这类"小任务大延迟"问题导致简单作业的SLA难以保障。
根因定位
通过服务和机器负载的持续分析,我们发现高负载场景下小文件的读取的耗时主要用在io等待上,造成这一情况的主要原因是操作系统对于io的调度是遵循FIFO实现的,即按请求到达的顺序处理,在请求较多或较随机时,即使很小的文件也可能出现长时间等待的问题。
优化实现
在Celeborn中读写框架大概可以分为三个阶段:shuffle文件写入阶段、shuffle文件commit阶段、shuffle文件读取阶段。shuffle文件写入阶段主要是spark程序主动推送数据到Celeborn服务端,Celeborn通过pushHandler将数据保留到服务端,在将数据写入磁盘对应的文件前会先将数据写入FlushBuffer,当buffer被写满的时候才会生产FlushTask将数据做落盘处理,也以此来降低磁盘的iops。而shuffle文件commit阶段则是对之前写入的文件做一个确认,服务端同时将在FlushBuffer中的数据做最后一次落盘处理。shuffle文件读取阶段则通过服务端的FetchHandler处理spark stage的shuffle read请求,返回相应的shuffle数据。为解决上述的小文件读取瓶颈我们基于原有的读写框架设计了文件缓存体系来优化小文件访问,整体实现框架如下:
小文件缓存主要是通过增加一个FileMemCacheManager并作用在shuffle文件commit阶段实现,当服务端收到某个文件的commit请求时,会判断该文件是否之前有发生过刷盘操作,若没有且文件大小符合缓存策略,则会将文件缓存到FileMemCacheManager中去避免落盘。在Shuffle读取阶段,也会先校验文件是否被缓存,若缓存在内存中则从FileMemCacheManager中获取相应的文件数据,否则走原逻辑从磁盘中获取。
优化效果
通过灰度发布验证,优化后效果显著:
-
**最差情况:**Shuffle Read最大耗时从4分钟降至2分钟以内
-
**平均延迟:**从200ms+降至60ms以下
优化前压测服务指标
优化后压测服务指标
3.1.3 磁盘级线程控制:实现多盘负载均衡
问题现象
在多磁盘(12-24块)配置的Worker节点上,我们经常观察到磁盘利用率不均衡现象:1-2块磁盘的IO利用率持续保持在100%,而其他磁盘却处于空闲状态。这种不均衡导致整体吞吐量无法随磁盘数量线性增长。
技术分析
Celeborn原有的线程分配策略采用全局共享线程池,所有磁盘的读请求竞争同一组线程资源。当某块磁盘因数据倾斜或硬件故障导致IO延迟升高时,它会独占大部分线程,造成资源分配失衡。
具体表现为:
-
单盘异常可导致节点吞吐量下降50%+
-
线程竞争引发大量上下文切换开销
-
无差别的重试机制加剧磁盘拥塞
优化方案
针对上述场景我们设计了磁盘级线程均衡策略去严格控制每个磁盘能使用的线程上限,以此来避免单盘异常导致所有读线程被占用的情况。
整体设计思路如下:
在设计方案上引入两个新的变量FetchTask和DiskReader,FetchTask主要用于把fetch请求的参数封装保留给到DiskReader。而DiskReader则控制着单个磁盘读写并行度,每个磁盘对应一个DiskReader,其不负责具体的read实现逻辑,read逻辑还是在ChunkStreamManager中实现。
FetchTask的数据解构如下:
class FetchChunkTask(client: TransportClient, request: ChunkFetchRequest, fetchBeginTime: Long){
private val cli = client
private val req = request
private val beginTime = fetchBeginTime
def getClient: TransportClient = cli
def getRequest: ChunkFetchRequest = req
def getBeginTime: Long = beginTime
}
DiskReader控制磁盘读写并行度的逻辑大概如下:
-
尝试申请对当前磁盘的读操作
-
申请成功则占用处理锁并从FetchTask队列中获取待处理的Task
-
处理Task中的fetch逻辑
-
释放处理锁
实施效果
优化后集群表现出更好的数据吞吐:
-
机器磁盘io利用率无明显差异
-
单盘异常对节点影响范围缩小80%
优化后压测各磁盘表现情况
3.2 稳定性保障:从被动应对到主动防御
在保证性能优化的同时,集群的长期稳定运行同样至关重要。我们针对Celeborn在超大规模集群环境下暴露出的稳定性问题,构建了多层次保障体系。
3.2.1 动态负载感知的Slot分配策略
问题背景
Celeborn提供RoundRobin和LoadWare两种slot分配策略,其中RoundRobin分配策略相对简单,其主要逻辑是获取当前可用磁盘并对这些磁盘做轮询分配。而LoadWare分配策略则需要先按照每个磁盘的读写压力进行排序分组,按照磁盘的压力等级逐级降低各个组的slot分配数量。在我们线上采用后者分配方式将slot分配到各个Worker尽量避免worker持续处于高负载的情况。在实际运营中我们发现当worker有磁盘出现shuffle压力时会很难恢复,有一部分原因可能是按照LoadWare分配策略集群仍可能往上面分配新的写任务从而恶化情况,虽然我们可以通过调整celeborn.slots.assign.loadAware.num-
DiskGroups和celeborn.slots.assign.loadAware
.diskGroupGradient参数来让部分磁盘不分配slot,但这个参数相对比较难合理的评估出来,而且出现的节点往往只是个别的机器磁盘,通过原来分组的方式可能会影响其他同组正常的worker读写数据,为此我们决定保留原有的配置上实现了剔除高负载磁盘分配的策略,具体实现如下:
在上述流程中计算最大可容忍的高负载磁盘个数我们通过设置的celeborn.slots.assign.loadAware
.discardDisk.maxWeight(默认配置0.3)参数计算得来,其计算公式为集群磁盘总数 * celeborn.slots.assign.loadAware.discardDisk.maxWeight,例如我们总共有500块磁盘,按上述公式我们最多可能容忍150块磁盘不参与slot分配。对于高负载磁盘的判定,我们参考线上实际的平均读、写耗时阈情况将阈值设置为200ms。通过引入上述策略,在凌晨高峰期时能及时剔除部分负载特别高的磁盘,防止worker持续恶化让服务性能更加稳定。
3.2.2 智能流量调度与权限管控
挑战分析
在管理800+节点的Celeborn集群时,我们面临如下问题:
-
接入数量不可控:Master地址暴漏后无法控制接入人群
-
任务流量不可控:异常大shuffle任务会冲击整个集群稳定性
-
故障隔离差:单集群问题影响全站业务
-
运维复杂度:多集群协同困难
客户端用户鉴权与任务切流
为了解决上述问题,我们在Celeborn 客户端侧做了一些改造,用户接入Celeborn不再依赖Mater URL配置(spark.celeborn.master.endpoints),在客户端侧仅需要配置一个Celeborn集群标识,Celeborn客户端会基于集群标识和用户账号向VIVO配置中心发起请求,通过配置中心获取真实的Master URL
以上改造一个是解决了用户任务接入不受限制的情况,另一个是在客户端和集群中间多了一个配置层,一旦单个集群出现故障,可以通过在配置层修改Master URL进行热迁移。
服务端异常shuffle流量识别
Celeborn开源版本提供一个CongestionControl功能,可以针对user粒度进行push过程的流量控制,在CongestionControl这套功能架构下有流量统计的模块代码(基于滑动窗口原理实现),可以基于该模块做app粒度的流量监控,识别push流量明显异常的application
Celeborn开源版user粒度Push异常流量识别流程图:
功能扩展:Shuffle Write阶段App Push异常流量识别流程图
相较于社区版user粒度的异常流量识别,app粒度的异常流量识别有更严格的判断,首先该app本身的流量要大于设定的阈值(我们当前设定的是200MB/s),其次该app本身的流量要大于所有app平均流量/raito,raito相当于一个权重比例,通过以上两种方式提升异常流量app的识别准确性。
服务端识别出异常app以后,会返回给客户端一个状态码,至于客户端如何处理,Celeborn有提供不同的PushStrategy给用户选择(通过celeborn.client.push.limit.strategy配置),也可以自定义开发,常见的策略例如,客户端先暂停推送,间隔一段时间以后再恢复推动。
四、未来规划与展望
作为支撑日均6PB级Shuffle数据、13万+应用的核心基础设施,我们的Celeborn集群已进入稳定运营期,刷盘耗时控制在5ms以内(平均1.5ms),文件读取耗时低于500ms(平均50ms)。这一成绩的取得来之不易,但技术演进永无止境。后续我们会持续在运维平台化和社区跟进两大方向投入人力去持续优化现有集群,并进一步展望Celeborn在云原生、智能化等前沿领域的可能性。
4.1 运维平台化:从黑屏操作到统一管理
当前Celeborn集群的部署模式经历了从Kubernetes独立部署到物理机混合部署的演进,但运维操作仍以手工命令行为主,面临三大核心痛点:
-
操作风险高:扩缩容、配置变更等关键操作依赖人工执行,易出错且难以追溯
-
效率瓶颈:集群规模突破800+节点后,人工运维响应速度跟不上业务需求
-
配置维护混乱:通过黑屏操作Celeborn服务很难统一集群配置
目前我们所有大数据组件都是通过ambari做统一管理,未来我们也计划将Celeborn服务加入ambari去管理,通过ambari去实现节点快速扩缩容、配置统一下发等替代人工黑屏操作的工作。
4.2 社区跟进:从版本滞后到行业对齐
当前集群仍运行Celeborn 0.3.0版本,落后社区最新版本多个重要迭代,错失了包括列式Shuffle、向量化加速等关键特性,版本升级不仅是功能更新,更是技术债偿还的过程。Celeborn作为下一代Shuffle基础设施,还有更广阔的创新空间值得我们探索,后续我们团队也会持续投入人力跟紧同行的步伐一起探索Celeborn的更多可能性。