每天早上,运营小姐姐都会在群里催:"销售大屏数据怎么还没出来?"你看着终端里那个跑了一整晚的awk命令,光标还在可怜巴巴地闪烁。没错,你遇到了所有后端工程师迟早会撞上的南墙------单机处理能力到头了。
一个悲伤的故事:电商平台的每日销售报表
你的电商平台每天产生5TB新订单,历史数据已经堆到PB级。老板要求每天凌晨2点前看到昨天的销售大屏,内容包括:各商品销售额排行、各品类销售额、各时段订单趋势。你用Python写了脚本,在一台128GB内存的机器上跑,结果内存不够,磁盘I/O爆了,第二天早上8点还没出数。这不是你的代码写得烂,而是单机批处理(Single-node Batch Processing)的物理极限到了。
这时候,你需要请出分布式批处理系统 (Distributed Batch Processing System)。它做的事情本质上和你那台单机一样:读数据、算结果、写输出。区别在于,它能把一台机器干不完的活,分给几百台机器一起干。这就是所谓分布式系统(Distributed System)的朴素哲学------人多力量大。
数据先得放得下:分布式存储
第一步,你需要一个能装下PB级数据的仓库。单机硬盘不够,那就把文件切成块,分散到多台机器的硬盘上。这就是分布式文件系统 (Distributed File System, DFS)。比如HDFS,默认把文件切成128MB的块 (Block),每个块复制三份放到不同的数据节点 (Data Node)上。一个叫元数据服务(Metadata Service)的东西负责记录哪个块在哪个节点上。
你可以选择另一种方案:对象存储(Object Storage),比如S3。它更像一个巨大的键值仓库,每个对象有一个唯一的key。但它没有真正的目录树,也不能原子重命名。
对于我们的报表场景,传统观点认为使用DFS有个好处:数据本地性 (Data Locality)------把计算任务直接调度到存放数据块的节点上,省去网络搬数据的成本。不过在2026年的今天,万兆乃至十万兆网络已经让网络传输不再那么肉疼。现代大数据架构越来越多地采用存算分离(Storage-Compute Separation),比如用对象存储搭配Alluxio这样的全量缓存层,照样跑得飞起。
作业编排:谁先谁后别乱套
有了存储,接下来要组织计算。报表生成不是一个单一任务,而是一个工作流 (Workflow),通常画出来是一个有向无环图(Directed Acyclic Graph, DAG)。我们的DAG长这样:
- Stage 1:清洗订单,过滤退款 ------ 输出"有效订单"
- Stage 2:把有效订单和商品表按商品ID做连接(Join),得到带品类的订单宽表
- Stage 3:并行计算三个指标(销售额排行、品类聚合、时段趋势)
- Stage 4:合并三份结果,写入大屏数据库
这里的关键是:Stage 2必须等Stage 1完成;Stage 3的三个子任务可以同时跑,但必须等Stage 2完成;Stage 4等Stage 3全部完成。你不能让Stage 3提前开始,否则它读不到数据。
做Join或者后续做聚合时,不同机器上的相关数据需要通过网络重新分发、洗牌。这个过程在分布式里叫混洗(Shuffle)。它是整个链路中最吃网络带宽、最容易产生瓶颈的地方。你可以把它想象成让几百个人同时交换手里的扑克牌------牌越多,场面越混乱。
依赖关系靠工作流调度器(Workflow Scheduler)来保证,比如Airflow、Dagster。它只负责"什么时候启动什么任务",而真正跑代码的资源调度归YARN或Kubernetes管。这就好比项目经理(工作流调度器)定计划,而工头(资源调度器)分配工人和机器。
资源分配:僧多粥少调度器
你的集群总共有200个CPU核心。夜间高峰期,三个并行任务同时要求资源:销售额排行要150核,品类聚合要80核,时段趋势要60核。加一起远超200。调度器(Scheduler)必须做出取舍。
最简单的策略是FIFO (First In First Out):谁先来谁先占满资源。但这样后到的任务可能饿死。另一种是公平调度 (Fair Scheduling):按比例分配,比如销售排行得100核,另外两个各得50核。但销售排行是老板最关心的,必须在2点前跑完。那就可以给销售排行高优先级 ,必要时抢占(Preempt)低优先级任务的资源。
被抢的低优先级任务怎么办?我们可以把它丢到云厂商的Spot实例(竞价实例)上去跑------这种实例极度便宜,代价是随时可能被回收。但没关系,配合后面要讲的容错机制,断了也能重试。省下来的钱够给团队天天点奶茶。
容错:机器挂了,数据不能挂
运行到一半,存放订单表某个块的数据节点突然宕机了。正在那个节点上跑的Stage 1任务直接失败。如果没有容错,整个报表就要重新跑,2点前肯定交不了差。
分布式批处理框架都支持任务粒度重试(Task-level Retry)。调度器发现任务失败后,会查元数据服务,找到同一个块的另一个副本所在节点,然后在那台机器上重新启动失败的任务。用户完全感知不到中间出了岔子。
但这里有一个坑:重试会不会导致数据重复写入?比如Stage 4写大屏数据库,如果第一次写了一半失败了,重试时又把成功的那半再写一遍,销售额就翻倍了。运营小姐姐会拿着刀来找你的。这就要求我们的任务------尤其是写入逻辑------必须支持幂等性(Idempotence):同一个输入重跑多少次,输出结果都一样。
不同框架处理中间结果的方式也不一样。老的MapReduce会把每个阶段的输出写到DFS,安全但慢。Spark使用血缘 (Lineage)追踪每个弹性分布式数据集(Resilient Distributed Dataset, RDD)是怎么计算出来的,如果数据丢了,就根据血缘重新算,而不是全部落盘。这就像你丢了草稿纸上的中间结果,但记得推导过程,可以再算一遍,而不必把整本书重抄一次。
小结
从单机脚本跑不动,到用分布式存储装下海量数据,用DAG工作流组织任务,用调度器在资源稀缺时做出艰难抉择,再用任务级重试和幂等性撑住机器的日常崩溃------这一套组合拳,就是分布式批处理的工程骨架。