SparkCore之PartitionNum和ShuffleManager生产调优

RDD算子中分区数(并行度)的调整

生产+面试。
下面①②③:
①是textFile和reduceByKey的分区(并行度)都是默认分区数;
②textFile分区数修改成1,reduceByKey不变
③textFile分区数修改成1,reduceByKey修改成4
以上这些在生产上调优很重要。

spark.sparkContext.textFile("file:///home/hadoop/data/wordcount.txt")
.flatMap(_.split("\t"))
.map((_,1))
.reduceByKey(_+_)
.collect

在这里插入图片描述

//源码
//可以看出来textFile中有两个参数,第一个参数是路径,第二个参数是分区数,不写就是默认分区数
  def textFile(
      path: String,
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
`....................

//默认最小分区数是(默认最小平行度和2)的最小值
//默认最小并行度是totalCoreCount
  def defaultMinPartitions: Int = math.min(defaultParallelism, 2)


  def defaultParallelism: Int = {
    assertNotStopped()
    taskScheduler.defaultParallelism
  }
//默认最小并行度是Core的总数量和2的最大值
  override def defaultParallelism(): Int = {
    conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
  }

//spark-shell --master local[1]  这样task数是1
//spark-shell --master local[2]  这样task数是2
//spark-shell --master local[4]  大于2的,这样task数还是2

scala>     spark.sparkContext.textFile("file:///home/hadoop/data/wordcount.txt",1)
.flatMap(_.split("\t"))
.map((_,1))
.reduceByKey(_+_)
.collect
res5: Array[(String, Int)] = Array((us,1), (room,2), (word,1), (china,3), (computer,1))

在这里插入图片描述

scala>     spark.sparkContext.textFile("file:///home/hadoop/data/wordcount.txt",1)
.flatMap(_.split("\t"))
.map((_,1))
.reduceByKey(_+_,4)
.collect
res6: Array[(String, Int)] = Array((us,1), (word,1), (room,2), (china,3), (computer,1))

在这里插入图片描述
shuffle前,textFile把并行度变成了1,reduceByKey并行度变成了4。

总结:
RDD操作过程中,遇到shuffle就会产生stage,以上例子有一个shuffle,划分为两个stage,shuffle后面的stage叫做ResultStage,或者FinalStage。前面的stage叫做ShuffleMapStage。这两个stage的并行度是可以自己去调整的,这个在生产上调优分厂重要的。

  • map端,输入文件,hdfs上有几个block块,就有几个partition,就有几个task并行度。
    你要map读文件读的快,你可以把map的并行度提高,
  • reduce端,如果你觉得跑的太慢,可以在reduce端加并行度,比如:reduceByKey(fun,4),4个并行度。
  • ResultStage(也就是reduce端)若未指定Patition数,则默认和ShuffleMapStage(也就是map端)最后RDD的分区数(并行度)保持一致。如果指定了分区数,就以指定的为准。
ShuffleManager

生产+面试。

源码

可将spark的代码调至1.6版本(pom.xml文件修改一下版本),查看SparkEnv.class文件,搜索一下shuffleManager。
看一下Spark1.6版本的源码:

    // Let the user specify short names for shuffle managers
    val shortShuffleMgrNames = Map(
      "hash" -> "org.apache.spark.shuffle.hash.HashShuffleManager",
      "sort" -> "org.apache.spark.shuffle.sort.SortShuffleManager",
      "tungsten-sort" -> "org.apache.spark.shuffle.sort.SortShuffleManager")
    val shuffleMgrName = conf.get("spark.shuffle.manager", "sort")  //这个默认是sort(1.2版本之前是hash)
    val shuffleMgrClass = shortShuffleMgrNames.getOrElse(shuffleMgrName.toLowerCase, shuffleMgrName)
    val shuffleManager = instantiateClass[ShuffleManager](shuffleMgrClass)
//instantiateClass这个是什么???
//包名加类名去查找,底层走的是Class.forName

看一下Spark2.4.0版本的源码:

    // Let the user specify short names for shuffle managers
    val shortShuffleMgrNames = Map(
      "sort" -> classOf[org.apache.spark.shuffle.sort.SortShuffleManager].getName,
      "tungsten-sort" -> classOf[org.apache.spark.shuffle.sort.SortShuffleManager].getName)
    val shuffleMgrName = conf.get("spark.shuffle.manager", "sort")  //这个默认是sort
    val shuffleMgrClass =
      shortShuffleMgrNames.getOrElse(shuffleMgrName.toLowerCase(Locale.ROOT), shuffleMgrName)
    val shuffleManager = instantiateClass[ShuffleManager](shuffleMgrClass)

Spark2.4.0版本里已经没有"hash",已经被移除。但是HashShuffleManager虽然被移除,面试的时候还是要被问的,比如HashShuffleManager什么原理。

Spark1.2之前默认使用的是HashShuffleManager、1.2和1.2版本之后默认使用SortShuffleManager,2.0之后HashShuffleManager被废除。

HashShuffleManager架构解析

maptask理解为:ShuffleMapStage
reducetask理解为:ResultStage,或者FinalStage

前提条件:现在有一台机器,2个core,那么并行度就是2。如果现在maptask有4个,reducetask有2个。
在这里插入图片描述

分析:

  • 4个maptask,2个core,要跑两轮,才能把任务跑完。

  • shuffle:把相同的key的数据分发到一个地方。

  • 每个bucket桶的大小默认为 :spark.shuffle.file.buffer:32k
    这个参数在生产太小,肯定要进行调整。 在map task处理的数据量比较大的情况下,而你的task的内存缓冲默认是比较小的,32kb。可能会造成多次的map端往磁盘文件的spill溢写操作,发生大量的磁盘IO,从而降低性能。
    在这里插入图片描述

  • 每个maptask都会生成n个reducetask的文件。**如果maptask的数量为m,reducetask的数量为r,那么它就会生成 m*r 个文件。**生成的这些文件,先写到内存中(一个个小的bucket),然后再写到本地磁盘(默认路径:spark.local.dir=/tmp)。生产上,挂多个磁盘,多个目录之间以逗号分隔,这样可以写的更快,而且不至于写一个磁盘导致系统挂掉。
    启动的bucket桶的数量为:core的数量 * r
    (如上图,要跑两轮,第一轮跑的时候,第二轮还没开始,第二轮的bucket还没有)
    假如现在有8个core,1000个task,那么需要为bucket开辟的内存为:8*1000*32k=256M
    加入现在有100个executor,每个executor2个core,总共有1000个maptask,1000个reducetask。那么要输出的文件数为:1000*1000=100万个临时文件。这个在生产上是很致命的,肯定不行。

  • 然后各个reducetask再去上面各个文件中去拉数据,拉来的数据也要先放在一个缓存buffer里去接收它。这个缓存buffer大小就是spark.reducer.maxSizeInFlight。
    每个reduce任务获取的map输出的最大大小spark.reducer.maxSizeInFlight:48M
    (这个参数一般可以调为2倍,96M)
    在这里插入图片描述

总结三个问题:(最主要是文件数太多这个问题)
①一个是maptask产生的文件数太多;一、将临时文件合并:spark.shuffle.consolidateFiles=true,默认是false,此时生成的文件数是 Core*R,但是当R很大时依旧会崩溃。二、写到本地磁盘(默认路径:spark.local.dir=/tmp)。生产上,挂多个磁盘,多个目录之间以逗号分隔,这样可以写的更快,而且不至于写一个磁盘导致系统挂掉。
②缓冲区的buffer太小,需要频繁的写磁盘,spark.shuffle.file.buffer这个参数需要调整,这个要调大很多。
③reducetask从map拉来的数据先放在一个缓存buffer里去接收它,这个buffer大小spark.reducer.maxSizeInFlight:48M,一般可以调整为2倍。

在这里插入图片描述

可参考:https://blog.csdn.net/tian_qing_lei/article/details/78002395

首次适应算法是一种内存分配算法,它从空闲分区链的起始位置开始查找,找到第一个能够满足作业需要的空闲分区进行分配。下面是一个C语言实现的示例代码: ```c #include <stdio.h> #include <stdlib.h> #define MAX_SIZE 100 // 内存分区结构体 typedef struct { int start; // 起始地址 int size; // 大小 int status; // 状态,0表示空闲,1表示已分配 } Partition; Partition memory[MAX_SIZE]; // 内存分区数组 int partitionNum = 0; // 内存分区数量 // 初始化内存分区 void initMemory() { memory[0].start = 0; memory[0].size = MAX_SIZE; memory[0].status = 0; partitionNum = 1; } // 显示内存分配情况 void showMemory() { printf("Memory allocation status:\n"); for (int i = 0; i < partitionNum; i++) { printf("Partition %d: start=%d, size=%d, status=%d\n", i, memory[i].start, memory[i].size, memory[i].status); } } // 分配内存 void allocateMemory(int jobSize) { int i; for (i = 0; i < partitionNum; i++) { if (memory[i].status == 0 && memory[i].size >= jobSize) { // 找到第一个空闲分区 break; } } if (i == partitionNum) { // 没有找到合适的分区 printf("No available partition for job with size %d\n", jobSize); return; } if (memory[i].size == jobSize) { // 分区大小刚好等于作业大小 memory[i].status = 1; } else { // 分区大小大于作业大小,需要拆分分区 Partition newPartition; newPartition.start = memory[i].start; newPartition.size = jobSize; newPartition.status = 1; memory[i].start += jobSize; memory[i].size -= jobSize; for (int j = partitionNum; j > i; j--) { // 插入新分区 memory[j] = memory[j - 1]; } memory[i + 1] = newPartition; partitionNum++; } printf("Job with size %d allocated to partition %d\n", jobSize, i); } // 回收内存 void freeMemory(int partitionIndex) { if (memory[partitionIndex].status == 0) { // 分区未分配 printf("Partition %d is not allocated\n", partitionIndex); return; } memory[partitionIndex].status = 0; // 合并相邻的空闲分区 if (partitionIndex > 0 && memory[partitionIndex - 1].status == 0) { memory[partitionIndex - 1].size += memory[partitionIndex].size; for (int i = partitionIndex; i < partitionNum - 1; i++) { memory[i] = memory[i + 1]; } partitionNum--; partitionIndex--; } if (partitionIndex < partitionNum - 1 && memory[partitionIndex + 1].status == 0) { memory[partitionIndex].size += memory[partitionIndex + 1].size; for (int i = partitionIndex + 1; i < partitionNum - 1; i++) { memory[i] = memory[i + 1]; } partitionNum--; } printf("Partition %d freed\n", partitionIndex); } int main() { initMemory(); showMemory(); allocateMemory(20); showMemory(); allocateMemory(30); showMemory(); allocateMemory(10); showMemory(); freeMemory(1); showMemory(); freeMemory(0); showMemory(); return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值