Spark UI中Shuffle dataSize 和shuffle bytes written 指标区别

文章分析了Spark3.1.1中ShuffleExchangeExec的dataSize与shufflebyteswritten指标,前者反映内存中UnsafeRow大小,用于AQE决策,后者涉及文件写入量,包括压缩影响。

背景

本文基于Spark 3.1.1
目前在做一些知识回顾的时候,发现了一些很有意思的事情,就是Spark UI中ShuffleExchangeExec 的dataSize和shuffle bytes written指标是不一样的,
那么在AQE阶段的时候,是以哪个指标来作为每个Task分区大小的参考呢

结论

先说结论 dataSzie指标是 是存在内存中的UnsafeRow 的大小的总和,AQE阶段(规则OptimizeSkewedJoin/CoalesceShufflePartitions)用到判断分区是否倾斜或者合并分区的依据是来自于这个值,
shuffle bytes written指的是写入文件的字节数,会区分压缩和非压缩,如果在开启了压缩(也就是spark.shuffle.compress true)和未开启压缩的情况下,该值的大小是不一样的。
开启压缩如下:
在这里插入图片描述
未开启压缩如下:
在这里插入图片描述

先说杂谈

这两个指标的值都在 ShuffleExchangeExec中:

case class ShuffleExchangeExec(
    override val outputPartitioning: Partitioning,
    child: SparkPlan,
    shuffleOrigin: ShuffleOrigin = ENSURE_REQUIREMENTS)
  extends ShuffleExchangeLike {

  private lazy val writeMetrics =
    SQLShuffleWriteMetricsReporter.createShuffleWriteMetrics(sparkContext)
  private[sql] lazy val readMetrics =
    SQLShuffleReadMetricsReporter.createShuffleReadMetrics(sparkContext)
  override lazy val metrics = Map(
    "dataSize" -> SQLMetrics.createSizeMetric(sparkContext, "data size")
  ) ++ readMetrics ++ writeMetrics

dataSize指标来自于哪里

涉及到datasize的数据流是怎么样的如下,一切还是得从ShuffleMapTask这个shuffle的起始操作讲起:

ShuffleMapTask
   ||
   \/
runTask
   ||
   \/
dep.shuffleWriterProcessor.write //这里的shuffleWriterProcessor是来自于 ShuffleExchangeExec中的createShuffleWriteProcessor
   ||
   \/
writer.write()  //这里是writer 是 UnsafeShuffleWriter类型的实例
   ||
   \/
insertRecordIntoSorter
   ||
   \/
UnsafeRowSerializerInstance.writeValue
   ||
   \/
dataSize.add(row.getSizeInBytes)

这里的 rowUnsafeRow的实例,这样就获取到了实际内存中的每个分区的大小,
而ShuffleMapTask runTask 方法最终返回的是MapStatus,而该MapStatus最终是在UnsafeShuffleWriter的closeAndWriteOutput方法中被赋值的:

void closeAndWriteOutput() throws IOException {
    assert(sorter != null);
    updatePeakMemoryUsed();
    serBuffer = null;
    serOutputStream = null;
    final SpillInfo[] spills = sorter.closeAndGetSpills();
    sorter = null;
    final long[] partitionLengths;
    try {
      partitionLengths = mergeSpills(spills);
    } finally {
      for (SpillInfo spill : spills) {
        if (spill.file.exists() && !spill.file.delete()) {
          logger.error("Error while deleting spill file {}", spill.file.getPath());
        }
      }
    }
    mapStatus = MapStatus$.MODULE$.apply(
      blockManager.shuffleServerId(), partitionLengths, mapId);
  }

shuffle bytes written指标来自哪里

基本流程和dataSize 一样,还是来自于ShuffleMapTask

ShuffleMapTask
   ||
   \/
runTask
   ||
   \/
dep.shuffleWriterProcessor.write //这里的shuffleWriterProcessor是来自于 ShuffleExchangeExec中的createShuffleWriteProcessor
   ||
   \/
writer.write()  //这里是writer 是 UnsafeShuffleWriter类型的实例
   ||
   \/
closeAndWriteOutput
   ||
   \/
sorter.closeAndGetSpills() ->  writeSortedFile -> writer.commitAndGet -> writeMetrics.incBytesWritten(committedPosition - reportedPosition) -> serializerManager.wrapStream(blockId, mcs) // 这里进行了压缩
   ||
   \/
mergeSpills
   ||
   \/
mergeSpillsUsingStandardWriter
   ||
   \/
mergeSpillsWithFileStream -> writeMetrics.incBytesWritten(numBytesWritten)
   ||
   \/
writeMetrics.decBytesWritten(spills[spills.length - 1].file.length())

<think> 嗯,用户再次询问SparkHadoop的Shuffle区别,看来对分布式计算的底层机制很感兴趣。之前已经给出过详细对比,但这次用户特别提供了新的参考资料,需要重点整合这些新信息。 用户提供的引用中提到几个关键点:SparkShuffle是hash-based而Hadoop是sort-based(引用[1]),Spark存在HashShuffleSortShuffle两种实现(引用[2]),以及Spark的bypass机制(引用[5])。这些正好补充了上次回答中关于Shuffle类型的细节。 用户身份可能是大数据开发工程师或学生,正在做性能调优研究。ta连续追问说明需要实操性强的技术细节,比如引用[5]提到的bypass机制触发条件(reduce task<200且无聚合算子)就很有工程价值。 需要特别注意三个新增技术点:第一,Spark从早期HashShuffle演进到SortShuffle的架构变化;第二,bypass机制作为特殊优化路径的存在;第三,Hadoop的强制性落盘与Spark内存优先的差异。这些在表格对比中要突出显示。 上次回答的“混合模式”表述需要修正,根据新资料应明确分为HashShuffleSortShuffle两个阶段。同时Reducer的内存缓冲区机制(引用[4])可以强化Spark内存计算的解释力。 考虑到用户可能正在处理实际性能问题,在优化策略部分增加bypass机制的适用场景说明会很有帮助。另外数据倾斜解决方案要区分Hive的建时分桶Spark的AQE动态调整。 最后生成的延伸问题要聚焦技术痛点,比如引用[5]提到的排序阈值调节就很实用。K8s环境的问题保留,因为云原生部署确实是趋势。 </think>以下是Spark Shuffle与Hadoop MapReduce Shuffle的深度对比分析,结合技术实现、性能特性优化策略: --- ### 一、核心区别总结 | **维度** | **Hadoop MapReduce Shuffle** | **Spark Shuffle** | |-------------------|------------------------------------------|-------------------------------------------| | **设计类型** | Sort-based(强制全局排序)[^1] | **混合模式**:<br>HashShuffle(早期)<br>SortShuffle(主流)[^2] | | **数据落盘** | 强制写磁盘(Map输出→HDFS)[^3] | **按需落盘**:<br>优先内存 → 溢出磁盘[^4] | | **内存使用** | 仅作缓冲区(固定大小) | **内存聚合**:<br>边Shuffle边执行combine[^4] | | **分区机制** | 固定HashPartitioner | **支持自定义分区器**(Partitioner API) | | **容错恢复** | 依赖磁盘数据重读 | 基于RDD血统重算(减少磁盘依赖) | --- ### 二、关键流程对比 #### 1. **Hadoop MapReduce Shuffle** ```mermaid graph LR A[Map Task] --> B[Partition+Sort in Memory] B --> C{内存缓冲区满?} C -->|是| D[Spill to Disk] C -->|否| B D --> E[Merge Sorted Files] E --> F[Reduce Fetch Sorted Data] F --> G[Reduce Task] ``` - **关键限制**:Map输出必须排序后落盘,Reduce需拉取**全量有序数据**[^3] #### 2. **Spark Shuffle(以SortShuffle为例)** ```mermaid graph LR A[Map Task] --> B[数据写入内存结构] B --> C{内存达到阈值?} C -->|是| D[按分区排序溢写磁盘] C -->|否| B D --> E[Merge索引&数据文件] E --> F[Reduce按索引精准读取] F --> G[Reduce Task] ``` - **优化点**: - **Bypass机制**:当Reduce任务数≤200且无聚合算子时,跳过排序直接Hash写入[^5] - **内存聚合**:`reduceByKey`等算子先在Map端局部聚合(Combiner) --- ### 三、性能瓶颈差异 | **场景** | Hadoop Shuffle | Spark Shuffle | |--------------------|-------------------------------------|-----------------------------------| | **小数据量** | 磁盘I/O开销占比高(固定成本) | 内存处理极快(无磁盘交互) | | **大数据量** | 稳定但速度慢(磁盘瓶颈) | 更快但可能OOM(需调优) | | **数据倾斜** | Reduce任务均匀变慢 | 个别Task成瓶颈(需手动处理) | | **网络开销** | 全量数据传输 | 可能减少传输量(Map端聚合) | > **案例**:处理1TB日志数据时 > - Hadoop:Shuffle阶段需写1TB+读1TB磁盘IO > - Spark:若内存充足,Shuffle数据仅需网络传输,耗时降低60%[^4] --- ### 四、内在联系 1. **阶段划分一致性** 两者均遵循 **Map → Shuffle → Reduce** 三阶段模型[^2] - Map阶段:生成`(Key, Value)`对 - Shuffle阶段:按Key分区分发 - Reduce阶段:跨分区聚合结果 2. **数据本地化优先** 均优先将Reduce任务调度到存有相关数据的节点(如HDFS Block位置感知) 3. **Shuffle服务的共用** 在YARN集群中,Spark可复用Hadoop的**Shuffle Service**(`spark.shuffle.service.enabled=true`) --- ### 五、优化策略对比 | **优化方向** | Hadoop 优化 | Spark 优化 | |--------------------|--------------------------------------|-------------------------------------| | **减少Shuffle** | 合并MR Job链 | 使用`broadcast join`替代Shuffle Join | | **分区优化** | 自定义Partitioner | 动态分区数(`spark.sql.shuffle.partitions`) | | **内存管理** | 调整`mapreduce.task.io.sort.mb` | 配置`spark.shuffle.memoryFraction` | | **压缩传输** | 启用`mapreduce.map.output.compress` | `spark.shuffle.compress=true` | > **Spark倾斜优化示例**: > ```scala > // 盐化倾斜Key解决数据倾斜 > val skewedRDD = rdd.map { > case (key, value) => > if (isSkewed(key)) (key + "_" + Random.nextInt(100), value) > else (key, value) > } > skewedRDD.reduceByKey(_ + _).map { > case (key, value) => (key.split("_")(0), value) > }.reduceByKey(_ + _) > ``` --- ### 总结 - **联系**:基础架构均遵循分布式Shuffle范式,依赖分区、排序、网络传输核心机制 - **区别的本质**: - Hadoop:**磁盘中心化**设计,牺牲性能保障稳定性 - Spark:**内存优先**理念,用血统容错换取性能飞跃 - **选择建议**: - 需强稳定性的批处理 → Hadoop - 迭代计算/低延迟场景 → Spark(配合内存调优)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值