一、引言
在 Spark、MapReduce、Tez 等分布式计算任务中,Shuffle 往往是最容易放大性能瓶颈和稳定性问题的环节。在传统 Spark Shuffle 中,Map 端会把中间数据落在 Executor 所在节点的本地磁盘上,Reduce 端再跨节点拉取这些数据。这个模式在中小规模任务中足够直接,但在大规模集群、动态资源调度、云原生环境和超大 Shuffle 作业中,会暴露几个典型问题。
- Shuffle 数据和计算资源强绑定。Executor 退出、节点下线、磁盘损坏或本地目录被清理,都可能影响后续 Reduce 端读取。
- 大量 Reduce 端并发拉取 Map 输出时,会带来连接数膨胀和随机 I/O 压力。
- 在云原生和弹性资源场景中,计算 Pod 或 Executor 的生命周期更短,Shuffle 数据如果仍然绑在计算节点上,动态伸缩会受到限制。
Apache Uniffle 的核心思路是把 Shuffle 数据从计算节点本地磁盘中解耦出来,交给独立的远程 Shuffle 服务管理,从而减少连接数、降低随机 I/O、缓解大规模任务中的内存或磁盘空间失败,并提升资源编排弹性。

远程 Shuffle 的关键变化不是"把本地磁盘换成 HDFS"这么简单,而是引入一层专门的 Shuffle 服务,把写入、缓存、合并、索引、容错和存储选择从计算引擎中剥离出来。
二、Uniffle 是什么
Apache Uniffle 是一个 Remote Shuffle Service,它为 Apache Spark 应用提供把 Shuffle 数据存储到远程服务器的能力。 同时,Uniffle 也被描述为面向多个计算框架的统一 Shuffle 引擎,已经提供 Spark 和 MapReduce 的可插拔客户端插件来启用远程 Shuffle。
从用户视角看,Uniffle 不是替代 Spark、MapReduce 或 Tez 的计算引擎,而是替代或增强这些引擎的 Shuffle 层。应用仍然由 Spark Driver、Executor、Map Task、Reduce Task 执行;变化在于 Shuffle 数据不再主要依赖 Executor 本地目录,而是通过 Uniffle 客户端写入 Shuffle Server,并按配置落到内存、本地磁盘、HDFS 等存储介质。
Uniffle 的定位概括为:高性能、通用的远程 Shuffle 服务,面向分布式计算引擎。 这意味着它的目标不是解决所有计算性能问题,而是聚焦 Shuffle 这个高 I/O、高网络、高状态管理复杂度的环节。
三、架构组成
Uniffle 的核心架构包含三类组件:Coordinator 集群、Shuffle Server 集群,以及必要时使用的远程存储,例如 HDFS。Coordinator 是控制面,Shuffle Server 是数据面。Coordinator 负责收集 Shuffle Server 状态,并为作业分配 Shuffle Server;Shuffle Server 负责接收 Shuffle 数据、合并数据并写入存储。

四、Shuffle 读写流程
Uniffle 的 Shuffle 写入流程可以分成客户端缓冲、发送、服务端缓存、刷盘、索引记录和读取几个阶段。
Spark Driver 会先向 Coordinator 请求 Shuffle Server;Spark Task 写 Shuffle 数据时,会先把 KV 数据写入 Buffer,在 Buffer 满或 Buffer Manager 满时刷入队列,再由线程池处理发送。Shuffle Server 会先把数据缓存到内存,在 Buffer Manager 满时刷入队列,最终把数据以 index file 和 data file 的形式写入存储;写完后,Task 会向 Shuffle Server 上报所有 blockId,用于后续数据校验。
arduino
Uniffle Shuffle 写入链路
Spark Driver
|
| 1. 向 Coordinator 请求 Shuffle Server 分配
v
Coordinator
|
| 2. 返回可用 Shuffle Server
v
Spark Map Task
|
| 3. KV 写入客户端 Buffer
v
Client Buffer / Queue
|
| 4. 请求 Shuffle Server 内存并发送数据
v
Shuffle Server Memory
|
| 5. 内存缓存,达到条件后进入 flush queue
v
Flush Thread Pool
|
| 6. 写入 Local Disk / HDFS / Hybrid Storage
v
Data File + Index File
|
| 7. 上报 blockId,用于后续校验
v
Shuffle Metadata
读取时,Spark Task 会根据不同存储类型,从 Shuffle Server、远程存储或二者同时读取 Shuffle 数据。 这也是 Uniffle 与传统本地 Shuffle 的重要区别:读取路径由服务端和存储模式共同决定,而不是固定从 Executor 本地磁盘拉取。
Uniffle 的 Shuffle 数据以 data file 和 index file 存储:data file 保存特定 partition 的所有 blocks,index file 保存每个 block 的元数据。 这种文件组织方式有助于读端按 partition 和 block 元信息定位数据。
sql
Shuffle 文件格式示意
Partition N
data file
+---------+---------+---------+---------+
| Block 1 | Block 2 | Block 3 | Block 4 |
+---------+---------+---------+---------+
index file
+---------+--------+--------+----------+
| BlockId | Offset | Length | Checksum |
+---------+--------+--------+----------+
| B1 | 0 | 128K | CRC |
| B2 | 128K | 256K | CRC |
| B3 | 384K | 64K | CRC |
+---------+--------+--------+----------+
五、存储模式
Uniffle 支持多种存储模式:Memory & Local、Memory & Remote Storage、Memory & Local & Remote Storage,生产环境推荐 Memory & Local & Remote Storage。
在配置中,对应的rss.storage.type支持MEMORY_LOCALFILE、MEMORY_HDFS、MEMORY_LOCALFILE_HDFS。
| 存储模式 | 典型配置 | 适合场景 | 主要取舍 |
|---|---|---|---|
| Memory + Local File | MEMORY_LOCALFILE | 本地磁盘充足、追求较低延迟 | 依赖本地磁盘容量和健康状态 |
| Memory + HDFS | MEMORY_HDFS | 本地磁盘受限、希望借助远程存储 | 网络与 HDFS 写入性能更关键 |
| Memory + Local File + HDFS | MEMORY_LOCALFILE_HDFS | 生产环境、大小 Shuffle 混合负载 | 配置和调优复杂度更高 |
Uniffle 的一个设计原则是同时处理小 Shuffle 和大 Shuffle,因此 Shuffle Server 会把小 block 写到本地磁盘,把大 block 写到 HDFS,阈值由 rss.server.flush.cold.storage.threshold.size 配置;建议根据 workload 和存储系统情况调节该参数,使远程写和本地写比例适配实际负载。
这个阈值很容易被误解,rss.server.flush.cold.storage.threshold.size 默认值是 64M;如果在 MEMORY_LOCALFILE_HDFS 模式下把它设置成 100g,那么即使启用了 HDFS,也不会有数据写入 DFS,因此使用 DFS 时需要设置合适值,例如 64m 或 128m。

六、功能特性
Uniffle 的优势可以从性能、可靠性、弹性和多引擎支持四个角度理解。主要包含 Fast、Reliable、Elastic 三个核心卖点:Fast 是减少 Shuffle 中的连接数和随机 I/O;Reliable 是减少大作业的 OOM 或磁盘空间失败;Elastic 是支持编排并提升资源利用率。在计算引擎支持方面,支持 Spark、MapReduce/Tez 和 Kubernetes Operator ,Uniffle 为 Spark 和 MapReduce 提供可插拔客户端插件。
| 能力 | 说明 | 工程价值 |
|---|---|---|
| 远程 Shuffle | Spark 应用可把 Shuffle 数据存到远程服务器 | 解耦计算节点与 Shuffle 数据 |
| Coordinator 分配 | Coordinator 收集 Server 状态并为作业分配 Server | 支持集中调度和负载均衡 |
| 多存储模式 | 支持 MEMORY_LOCALFILE、MEMORY_HDFS、MEMORY_LOCALFILE_HDFS | 可按成本、性能、可靠性选择存储 |
| 动态客户端配置 | Coordinator 可启用 dynamic client conf | 降低客户端逐作业配置成本 |
| Client Quorum | 客户端可把 block 写入多个 Server,读端从其中一个读取 | 提高 Shuffle Server 故障容忍度 |
| Adaptive Remote Shuffle | Spark 可通过 DelegationRssShuffleManager 智能选择内置 Shuffle 或远程 Shuffle | 支持灰度和按条件启用 |
| Kubernetes Operator | 扩展 Kubernetes API 管理 Uniffle 实例 | 适配云原生部署 |
| Spark Uniffle UI | 0.10.0 引入 Spark Uniffle UI | 改善观测和问题定位 |
Client Quorum 是一个值得关注的可靠性特性,该机制是客户端行为,Shuffle writer 会把每个 block 发送到多个 Server,Shuffle reader 可从其中一个 Server 获取 block 数据;由于多副本会降低 Shuffle 性能并增加资源消耗,因此它被设计为可选能力。
Adaptive Remote Shuffle 则更适合渐进式落地,Uniffle 支持 adaptive enabling,客户端使用 DelegationRssShuffleManager 并提供唯一 access_id,Coordinator 据此判断是否启用远程 Shuffle;当前该特性仅支持 Spark。
七、适用场景
Uniffle 最适合 Shuffle 成本高、稳定性要求高、计算资源生命周期不稳定或本地磁盘压力明显的场景。比如:大规模 Spark SQL、宽依赖聚合、Join、排序、窗口计算、超大 Shuffle ETL 作业,以及需要在 Kubernetes 或弹性 YARN 环境中频繁伸缩 Executor 的集群。
Uniffle 使用内存降低写阶段随机 I/O,如果 partition 太多,会把小数据刷到磁盘并影响性能,因此建议保证每个 partition 的内存大于 3MB,例如最繁忙时有 1k partitions,就应为它们提供 3GB 内存。Uniffle 偏 network-bound,使用 10Gbps 或更高网络是让这些应用更快的最佳方式。

不太适合优先引入 Uniffle 的场景也很明确:作业 Shuffle 很小、瓶颈在 CPU 或源端存储、集群网络较弱、团队暂时缺少独立运维 Shuffle 服务的能力。Uniffle 能降低 Shuffle 层的局部复杂度,但它本身也引入了 Coordinator、Shuffle Server、存储路径、指标监控、容量规划和版本兼容性管理。
八、部署使用
Uniffle 的部署通常分为三步:部署 Coordinator、部署 Shuffle Server、接入计算引擎客户端。
Coordinator 流程包括解压包到 RSS_HOME、配置 rss-env.sh、配置 coordinator.conf、配置 dynamic_client.conf和 exclude_nodes,最后通过 start-coordnator.sh 启动 Coordinator。
Shuffle Server 流程类似:解压包,配置 rss-env.sh,配置 server.conf,再通过 start-shuffle-server.sh 启动服务。Server 侧常见配置包括 RPC 端口、HTTP 端口、Coordinator quorum、本地存储路径、服务端 Buffer 容量、读取 Buffer 容量、心跳间隔、flush 线程池大小和存储类型。
bash
最小部署路径
1. 部署 Coordinator
RSS_HOME
├── bin/rss-env.sh
├── conf/coordinator.conf
├── conf/dynamic_client.conf
└── conf/exclude_nodes
2. 部署 Shuffle Server
RSS_HOME
├── bin/rss-env.sh
└── conf/server.conf
3. 接入 Spark
SPARK_HOME/jars/
└── rss-client-spark*-shaded.jar
4. 修改 Spark 配置
spark.shuffle.manager
spark.rss.coordinator.quorum
Spark 接入时,要把客户端 jar 加入 Spark classpath,例如SPARK_HOME/jars/;Spark2 jar 位于<RSS_HOME>/jars/client/spark2/,Spark3 jar 位于<RSS_HOME>/jars/client/spark3/。随后在 Spark 配置中启用 Uniffle,例如设置spark.shuffle.manager org.apache.spark.shuffle.RssShuffleManager和spark.rss.coordinator.quorum <coordinatorIp1>:19999,<coordinatorIp2>:19999。
bash
# Spark 接入 Uniffle 的基础配置示例
spark.shuffle.manager org.apache.spark.shuffle.RssShuffleManager
spark.rss.coordinator.quorum <coordinatorIp1>:19999,<coordinatorIp2>:19999
# Spark2 注意事项
# Spark2 不支持 AQE,因此应关闭:
spark.sql.adaptive.enabled false
如果要使用 Spark Dynamic Allocation,需要对 Spark 代码应用补丁,应用补丁并重新构建 Spark 后,需要设置 spark.shuffle.service.enabled false 和 spark.dynamicAllocation.enabled true;对于 Spark 3.5 或以上,还需要增加 spark.shuffle.sort.io.plugin.class org.apache.spark.shuffle.RssShuffleDataIo。
MapReduce 接入时,需要把 MR 客户端 jar 加入每个 NodeManager 的 classpath,并配置 mapreduce.rss.coordinator.quorum、RssMRAppMaster、RssMapOutputCollector 和 RssShuffle 等参数。另外,RssMRAppMaster 会自动关闭 slow start 和 job recovery。
九、配置与调优
Uniffle 调优首先看磁盘、内存、网络。HDD 或 SSD 都可以用于 Uniffle,但要提供足够数量的磁盘;通常 HDD 提供 100MB/s 写入速度,如果集群应用最繁忙时写入 1GB/s,就应该提供 10 块 HDD。
建议使用多个磁盘分摊 I/O 并提升总吞吐,RAID10 可获得较低延迟、高吞吐和较好容错,但会消耗双倍空间。 由于 Shuffle 数据通常是临时数据,如果成本敏感,可以优先从多块 HDD 加合理冗余开始,而不是直接堆高规格 SSD。
内存方面,Server 端的 rss.server.buffer.capacity 用于限制 Shuffle 数据 Buffer 占用的内存,但元数据内存不受该参数控制;建议在 Shuffle Server JVM 进程的 XMX_SIZE 中,在 rss.server.buffer.capacity + rss.server.read.buffer.capacity 之外预留一定冗余,例如 20%。
lua
Shuffle Server 内存预算示意
XMX_SIZE
+------------------------------------------------------+
| rss.server.buffer.capacity |
| 写入侧 Shuffle Buffer |
+------------------------------------------------------+
| rss.server.read.buffer.capacity |
| 读取侧 Buffer |
+------------------------------------------------------+
| Metadata / JVM / 线程 / 其他对象 |
| 建议预留冗余,例如 20% |
+------------------------------------------------------+
客户端写入方面,Spark Client 会为每个 partition 分配 Buffer;当总内存超过spark.rss.writer.buffer.spill.size时,客户端会把所有 Buffer 数据发送到 Shuffle Server,因此对于巨大 Shuffle 应用,建议增大该阈值以避免小网络 I/O。
十、小结
Apache Uniffle 的价值可以概括为一句话:把 Shuffle 从计算节点的本地状态中解耦出来,交给独立服务统一管理。它更适合大 Shuffle、多租户、弹性伸缩、云原生部署和本地磁盘压力明显的集群,而不是所有作业都必须引入的一层基础设施。