spark面试题

Spark面试题

1. sparksql执行过程中发生数据倾斜导致任务卡顿该怎么解决???

分析: 数据倾斜一般都发生在shuffle过程中,部分key存在占用比例过大导致大量数据分发到同一个task中导致任务执行缓慢甚至导致OOM异常。

数据倾斜的现象: 多数task执行速度较快,少数执行时间非常长,或者等待很长时间提示内存不足,执行失败。

原因: 1.key本身分部不均衡,key设计不合理。2.shuffle并行度不足。

解决方案:

  1. **聚合源数据:**针对hive表中数据,对key进行重新设计对key打上随机数前缀将key打散,这样就不会造成shuffle操作,也就不存在热点数据的情况。之后再进行自定义的逻辑处理。 —此时数据已经比较均匀的分散在各个task上,再在task上做局部聚合,之后去除key的前缀信息,再做全局聚合。 【局部聚合+全局聚合】

    1. sql中实现需要使用双重groupBy改写SQL,两次groupBy。一先将key拼接随机的前缀[rand(10)],进行groupby,第二次将拼接的key去除拼接前缀再groupBy。
  2. 查找造成数据倾斜的key,使用sql的where条件过滤掉导致数据倾斜的key,即可避免shuffle操作导致的数据倾斜。针对热点key可以采用方案[1] 做处理。

  3. 提高shuffle并行度:groupByKey(1000),spark.sql.shuffle.partitions(默认是200); 也就是增加reduceTask的数量,修改之后会创建指定数量的reduce task从一定程度上可以是sparksql跑的更快。

  4. reduce join转换为map joinspark.sql.autoBroadcastJoinThreshold(默认是10485760);可以自己将表做成RDD,自己手动去实现map join;SparkSQL内置的map join,默认如果有一个10M以内的小表,会将该表进行broadcast,然后执行map join;调节这个阈值,比如调节到20M、50M、甚至1G

  5. 采样倾斜key并单独进行join:纯Spark Core的一种方式,sample、filter等算子。如果发现有一个或几个key对应的数据量特别大。此时只能将数据量多的key拉取出来,然后进行一个优化操作-> 针对热点key添加随机前缀,在执行shuffle操作将数据打散,再进行处理。

    如果你发现整个RDD中有多个key对应的数据量都特别多,此时,只能将数据量多的key拉取出来,然后进行一个优化操作。从另外一个要join的表中,也过滤出来一份数据,比如某个key可能就只有一条数据。
      然后我们再对那个只有一条数据的RDD,进行flatMap操作,打上100个随机数,作为前缀,返回100条数据。
      然后再将刚刚拉取出来的key对应的数据量特别多的RDD,给每一条数据,都打上一个100以内的随机数,作为前缀。然后就可以进行join操作了,join完以后,执行map操作将之前打上的随机数给去掉,然后再和另外一个普通RDD join以后的结果再进行union操作。

  6. 使用随机数以及扩容表进行join

    ​ 这个方案是没办法彻底解决数据倾斜的,只是一种对数据倾斜的缓解。
      1、选择一个RDD,要用flatMap,进行扩容,将每条数据,映射为多条数据,每个映射出来的数据,都带了一个n以内的随机数,通常来说会选择10。
      2、将另外一个RDD,做普通的map映射操作,每条数据都打上一个10以内的随机数。
      3、最后将两个处理后的RDD进行join操作。
      4、因为两个RDD都很大,所以你没有办法去将某一个RDD扩的特别大,一般就是10倍。且需要在最后清楚重复的9倍数据。
      5、如果就是10倍的话,那么数据倾斜问题的确是只能说是缓解和减轻,不能说彻底解决。

2. spark并行度为200,输入文件block为100个/300个,实际任务的并行度分别是多少?

​ **信息介绍:**Spark会根据文件的大小自动设置要在每个文件上运行的“映射”任务的数量。默认的分区数 = 输入文件的大小 / splitsize; 分区数及对应spark的启动task的数量。

​ splitsize是mapreduce中每个输入内容的大小。 一般splitsize = dfs.block.size [1]从2.7.3版本开始,官方关于Data Blocks 的说明中,block size由64 MB变成了128 MB的。

hdfs文件数据读取时的并行度: 文件所占block数(一个block对应一个task)。

解答: 默认spark并行度与输入文件数保持一致。 block为100时并发度为100,block为300时并发度为200(按照分配的资源最大值)

3. Spark checkpoint & Accumulators & Broadcast Variables 作用及应用场景。

### checkpoint
metadata checkpointing
>  主要出现在driver failures 的情况下用于恢复作业。
  1. configuration

    用于创建流式计算应用的配置信息

  2. DStream operations

    用于定义流式计算应用的 dstream的集合。

  3. Incomplete baches

    在队列中等待执行但还未执行完成的任务批次。

    [1] 针对无状态转换的作业,如果作业恢复过程中 metadatacheckpointing也是适用的,但是会存在部分接收到但是未处理的数据可能会丢失。

data checkpointing

适用于有状态转换(stateful transformation)操作的作业中。

​ 例如:updateStateByKey,reduceByKeyAndWindow等

​ 将生成的RDDs保存到可靠的存储中。在一些跨多个批次合并数据的有状态转换中,创建checkpoint是必须的。计算链路的增长会导致依赖链路的增长也会导致任务在失败是恢复的时间随之增长(与依赖链成正比)。有状态转换的中间RDDs定期被检查点到可靠存储(如HDFS),以切断依赖链。

job失败时主动从checkpoint中重启
/** checkpoint使用方法
*/
//创建一个新的streamcontext的方法,
// 内部包含了业务处理的所有逻辑
def functionToCreateContext(): StreamingContext = {
  ...
  //任务停止是否删除checkpoint,默认不删除。也可以在job进行逻辑调整之后从checkpoint中恢复。--问题checkpoint存储算子信息可能导致job恢复失败。‘
  sparkConf.set("spark.cleaner.referenceTracking.cleanCheckpoints","false")
  val ssc = new StreamingContext(...)   // new context
  val lines = ssc.socketTextStream(...) // create DStreams
  ...
  ssc.checkpoint(checkpointDirectory)   // set checkpoint directory
  ssc
}
//创建流式计算的上下文环境,并指定任务失败时重启加载的metadatacheckpointing的路径 及新创建流式计算上下文的方法
/**	1.【创建】 如果checkpointDirectory目录不存在则按照functionToCreateContext中的逻辑创建streamingcontext
		2.【恢复】如果checkpointDirectory存在则从检查点目录中构建出streamcontext内容,不执行functionToCreateContext中的创建逻辑*/
val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)


// Start the context
context.start()
context.awaitTermination()

[1] 由于检查点的设置会导致一定的性能消耗,并增加批处理的时间。 因此,需要仔细设置检查点的间隔。 在小批量(例如1秒)时,每批检查点可能会大大降低操作吞吐量。 相反,检查点太不频繁会导致沿袭和任务规模增加,这可能会产生不利影响。对于需要RDD检查点的有状态转换,默认间隔为批处理间隔的倍数,至少应为10秒。 可以使用dstream.checkpoint(checkpointInterval)进行设置。 通常,DStream的5-10个滑动间隔的检查点间隔是一个不错的尝试。

[2]spark.streaming.receiver.writeAheadLog.enable =true 开启write ahead logs功能,通过接收器接收的所有输入数据都将保存到预写日志中,以便在驱动程序发生故障后将其恢复。预写日志保存在checkpoint目录中。

共享变量——Accumulators, Broadcast Variables

​ accumulators和boradcast 变量都可以从流处理的checkpoint中恢复。必须为Accumulators和Broadcast变量创建延迟(lazy)实例化的单例实例,以便在驱动程序因故障而重启后可以重新实例化它们。 在下面的示例中显示。

//自定义广播变量
//spark默认会将下一阶段需要用到的数据通过广播变量的方式发送到对应的节点上
//针对跨多个阶段的任务需要相同的数据时或者以反序列化形式缓存数据非常重要时,显示创建广播变量才是有用的。
object WordBlacklist {

  @volatile private var instance: Broadcast[Seq[String]] = null

  def getInstance(sc: SparkContext): Broadcast[Seq[String]] = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          val wordBlacklist = Seq("a", "b", "c")
          instance = sc.broadcast(wordBlacklist)
        }
      }
    }
    instance
  }
}

//自定义accumulator
object DroppedWordsCounter {

  @volatile private var instance: LongAccumulator = null

  def getInstance(sc: SparkContext): LongAccumulator = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          instance = sc.longAccumulator("WordsInBlacklistCounter")
        }
      }
    }
    instance
  }
}

wordCounts.foreachRDD { (rdd: RDD[(String, Int)], time: Time) =>
  // Get or register the blacklist Broadcast
  val blacklist = WordBlacklist.getInstance(rdd.sparkContext)
  // Get or register the droppedWordsCounter Accumulator
  val droppedWordsCounter = DroppedWordsCounter.getInstance(rdd.sparkContext)
  // Use blacklist to drop words and use droppedWordsCounter to count them
  val counts = rdd.filter { case (word, count) =>
    if (blacklist.value.contains(word)) {
      droppedWordsCounter.add(count)
      false
    } else {
      true
    }
  }.collect().mkString("[", ", ", "]")
  val output = "Counts at time " + time + " " + counts
})

accumulator 针对action类型的操作可以保证只执行一次计数,即使任务重启也只会计数一次。但在transformation操作中,不能保证只更新一次。spark lazy模式在有action动作时才会真正执行,对应的累加器也只会在action操作触发式进行计数。

4. 什么时候spark才会使用Map-side Join?

Map side join1是针对以下场景进行的优化:两个待连接表中,有一个表非常大,而另一个表非常小,以至于小表可以直接存放到内存中。这样,我们可以将小表复制多份[广播],让每个task内存中存在一份(比如存放到hash table中),然后只扫描大表:对于大表中的每一条记录key/value,在hash table中查找是否有相同的key的记录,如果有,则连接后输出即可。

只有当要进行join的表的大小小于spark.sql.autoBroadcastJoinThreshold(默认是10M)的时候,才会进行mapjoin。

-- 手动添加 mapsidejoin注释 达到优化目的
select /*+ BROADCAST (b) */ * from a where id not in (select id from b)
//广播表b再与表a进行join操作
private val sqlcontext: SQLContext = session.sqlContext
sparksession.sparkContext.broadcast(sqlcontext.table("b").join(sqlcontext.table("a")))

[2] 在使用map reduce处理数据的时候,join操作有两种选择:一种选择是在map端执行join操作,即所谓的Map-side Join(Broadcast join);另一种选择是在reduce端执行join操作,即所谓的Reduce-side Join(shuffle join)。在map端执行join操作,适合在有一个表比较小的情况下,能把整个表放到内存,发送到各个节点进行join操作。

5.spark中如何划分stage

  1. spark application中可以因为不同的action触发众多job。每个job是由一个或者多个stage构成的,stage之间构成前后依赖关系。
  2. stage划分的依据是宽依赖,即产生shuffle操作的算子都会产生宽依赖。例:groupbykey,reducebykey等。
  3. 由action导致sparkcontext.runjob的执行,最终导致了DAGScheduler中submitJob的执行,其核心是通过发送一个case class JobSubmitted对象给eventProcessLoop.eventProcessLoop是DAGSchedulerEventProcessLoop的具体实例,而DAGSchedulerEventProcessLoop是eventLoop的子类,具体实现EventLoop的onReceive方法,onReceive方法转过来回调doOnReceive。
  4. 在doOnReceive中通过模式匹配的方法把执行路由到相应的dagScheduler.handle*处理阶段。
  5. 对于新提交的任务匹配到:JobSubmitted会执行handleJobSubmitted处理创建finalStage,在内部的createResultStage中调用getOrCreateParentStages查找父依赖的stage并做根据是否shuffle操作切分stage。

[1] 源码中每个action算子中都执行了runjob方法触发任务的执行。跟踪runjob就可以追踪到stage切分以及父依赖的获取。

6. spark 如何防止内存溢出

driver端的内存溢出

可以增大driver的内存参数:spark.driver.memory (default 1g)
这个参数用来设置Driver的内存。在Spark程序中,SparkContext,DAGScheduler都是运行在Driver端的。对应rdd的Stage切分也是在Driver端运行,如果用户自己写的程序有过多的步骤,切分出过多的Stage,这部分信息消耗的是Driver的内存,这个时候就需要调大Driver的内存。

map过程产生大量对象导致内存溢出

这种溢出的原因是在单个map中产生了大量的对象导致的,例如:rdd.map(x=>for(i <- 1 to 10000) yield i.toString),这个操作在rdd中,每个对象都产生了10000个对象,这肯定很容易产生内存溢出的问题。针对这种问题,在不增加内存的情况下,可以通过减少每个Task的大小,以便达到每个Task即使产生大量的对象Executor的内存也能够装得下。具体做法可以在会产生大量对象的map操作之前调repartition方法,分区成更小的块传入map。例如:rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)。
面对这种问题注意,不能使用rdd.coalesce方法,这个方法只能减少分区,不能增加分区,不会有shuffle的过程。

数据不平衡导致内存溢出

数据不平衡除了有可能导致内存溢出外,也有可能导致性能的问题,解决方法和上面说的类似,就是调用repartition重新分区。这里就不再累赘了。

shuffle后内存溢出

shuffle内存溢出的情况可以说都是shuffle后,单个文件过大导致的。在Spark中,join,reduceByKey这一类型的过程,都会有shuffle的过程,在shuffle的使用,需要传入一个partitioner,大部分Spark中的shuffle操作,默认的partitioner都是HashPatitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions) , spark.default.parallelism参数只对HashPartitioner有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量。

standalone模式下资源分配不均匀导致内存溢出

在standalone的模式下如果配置了–total-executor-cores 和 –executor-memory 这两个参数,但是没有配置–executor-cores这个参数的话,就有可能导致,每个Executor的memory是一样的,但是cores的数量不同,那么在cores数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。这种情况的解决方法就是同时配置–executor-cores或者spark.executor.cores参数,确保Executor资源分配均匀。

使用rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)代替rdd.cache()

rdd.cache()和rdd.persist(Storage.MEMORY_ONLY)是等价的,在内存不足的时候rdd.cache()的数据会丢失,再次使用的时候会重算,而rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)在内存不足的时候会存储在磁盘,避免重算,只是消耗点IO时间。

7.简要描述Spark分布式集群搭建的步骤

1.进入spark_home/conf 打开spark-env.sh

# 配置JDK安装位置 
JAVA_HOME=/usr/java/jdk1.8.0_201 
# 配置hadoop配置文件的位置 
HADOOP_CONF_DIR=/usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop 
# 配置zookeeper地址 
SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=hadoop001:2181,hadoop002:2181,hadoop003:2181 -Dspark.deploy.zookeeper.dir=/spark"
  1. mv slaves.template slaves 打开slaves文件添加worker节点的host_name。

  2. 将单节点的spark配置分发到集群中的所有节点。

  3. 启动zookeeper zkServer.sh start

  4. 启动hadoop集群 # 启动dfs服务 start-dfs.sh # 启动yarn服务 start-yarn.sh

  5. 启动spark集群 进入到主节点的机器 执行 start-all.sh

  6. 启动master的standby节点 进入需要启动的节点执行start-master.sh

  7. 验证:查看 Spark 的 Web-UI 页面,端口为 8080。master节点状态为ACTIVE。其他两个备用master节点为STANDBY

  8. 验证高可用: 在master节点使用kill杀死master进程。此时备用的master会重新选择一个作为master节点。并获取全部存活的worker。

  9. 作业提交:

    spark-submit \ 
    --class org.apache.spark.examples.SparkPi \ 
    --master yarn \ 
    --deploy-mode client \ 
    --executor-memory 1G \
     --num-executors 10 \ 
    /usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \
     100
    

8. kafka整合sparkStreaming问题

spark+kafka官网

	### 1).	如何实现sparkstreaming读取kafka中的数据

​ 两种方式:在kafka0.10版本之前有二种方式与sparkStreaming整合receiver和direct。0.10之后只有direct方式了。

**receiver:**是采用了kafka高级api,利用receiver接收器来接受kafka topic中的数据,从kafka接收来的数据会存储在spark的executor中,之后spark streaming提交的job会处理这些数据,kafka中topic的偏移量是保存在zk中的

​ 基本使用:

val topics = Array("click_events").toIterable

    val kafkaParams = Map[String, Object](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "localhost:9092",
      ConsumerConfig.GROUP_ID_CONFIG -> "spark_stream_test",
      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer],
      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer]
    )
val kafkaStream = KafkaUtils.createDirectStream[String, String](
      stream,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](topics, kafkaParams)
    )

还有几个需要注意的点:

  • 在Receiver的方式中,Spark中的partition和kafka中的partition并不是相关的,所以如果我们加大每个topic的partition数量,仅仅是增加线程来处理由单一Receiver消费的主题。但是这并没有增加Spark在处理数据上的并行度.
  • 对于不同的Group和topic我们可以使用多个Receiver创建不同的Dstream来并行接收数据,之后可以利用union来统一成一个Dstream。
  • 在默认配置下,这种方式可能会因为底层的失败而丢失数据. 因为receiver一直在接收数据,在其已经通知zookeeper数据接收完成但是还没有处理的时候,executor突然挂掉(或是driver挂掉通知executor关闭),缓存在其中的数据就会丢失. 如果希望做到高可靠, 让数据零丢失,如果我们启用了**Write Ahead Logs(spark.streaming.receiver.writeAheadLog.enable=true)**该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中. 所以, 即使底层节点出现了失败, 也可以使用预写日志中的数据进行恢复. 复制到文件系统如HDFS,那么storage level需要设置成 StorageLevel.MEMORY_AND_DISK_SER,也就是KafkaUtils.createStream(…, StorageLevel.MEMORY_AND_DISK_SER)

direct: 在spark1.3之后,引入了Direct方式。不同于Receiver的方式,Direct方式没有receiver这一层,其会周期性的获取Kafka中每个topic的每个partition中的最新offsets,之后根据设定的maxRatePerPartition来处理每个batch。(设置spark.streaming.kafka.maxRatePerPartition=10000。限制每秒钟从topic的每个partition最多消费的消息条数)。

//设定对目标topic每个partition每秒钟拉取的数据条数。总量为 1000*kafkapartitions*每个批次的时间间隔
sparkconf.set("spark.streaming.kafka.maxRatePerPartition","1000")

2) 对比这2中方式的优缺点:

  • 采用receiver方式:这种方式可以保证数据不丢失,但是无法保证数据只被处理一次,WAL实现的是At-least-once语义(至少被处理一次),如果在写入到外部存储的数据还没有将offset更新到zookeeper就挂掉,这些数据将会被反复消费. 同时,降低了程序的吞吐量。

  • 采用direct方式:相比Receiver模式而言能够确保机制更加健壮. 区别于使用Receiver来被动接收数据, Direct模式会周期性地主动查询Kafka, 来获得每个topic+partition的最新的offset, 从而定义每个batch的offset的范围. 当处理数据的job启动时, 就会使用Kafka的简单consumer api来获取Kafka指定offset范围的数据。

    • 优点:

      • 1、简化并行读取

        • 如果要读取多个partition, 不需要创建多个输入DStream然后对它们进行union操作. Spark会创建跟Kafka partition一样多的RDD partition, 并且会并行从Kafka中读取数据. 所以在Kafka partition和RDD partition之间, 有一个一对一的映射关系.
      • 2、高性能

        • 如果要保证零数据丢失, 在基于receiver的方式中, 需要开启WAL机制. 这种方式其实效率低下, 因为数据实际上被复制了两份, Kafka自己本身就有高可靠的机制, 会对数据复制一份, 而这里又会复制一份到WAL中. 而基于direct的方式, 不依赖Receiver, 不需要开启WAL机制, 只要Kafka中作了数据的复制, 那么就可以通过Kafka的副本进行恢复.
      • 3、一次且仅一次的事务机制

        • 基于receiver的方式, 是使用Kafka的高阶API来在ZooKeeper中保存消费过的offset的. 这是消费Kafka数据的传统方式. 这种方式配合着WAL机制可以保证数据零丢失的高可靠性, 但是却无法保证数据被处理一次且仅一次, 可能会处理两次. 因为Spark和ZooKeeper之间可能是不同步的. 基于direct的方式, 使用kafka的简单api, Spark Streaming自己就负责追踪消费的offset, 并保存在checkpoint中. Spark自己一定是同步的, 因此可以保证数据是消费一次且仅消费一次。不过需要自己完成将offset写入zk的过程,在官方文档中都有相应介绍.
          *简单代码实例:

          messages.foreachRDD(rdd=>{
          val message = rdd.map(_._2)//对数据进行一些操作
          message.map(method)//更新zk上的offset (自己实现)
          updateZKOffsets(rdd)
          })
          

          * sparkStreaming程序自己消费完成后,自己主动去更新zk上面的偏移量。也可以将zk中的偏移量保存在mysql或者redis数据库中,下次重启的时候,直接读取mysql或者redis中的偏移量,获取到上次消费的偏移量,接着读取数据。

9.如何保证数据不重复/不丢失

1.设置checkpoint

​ 1.检查点的开启可以保证作业在失败时可以充checkpoint中获取最新的数据状态恢复作业,从而保证job在失败前后的联系性。

​ 2.checkpoint中还会保存kafka的offset信息,从而保证不重复消费数据。

2.手动更新kafkaoffset

​ 1.Kafka 0.10 之后使用的 direct API 为kafka底层API offset的跟踪交给spark,spark使用checkpoint保存offset。用户代码可以手动在逻辑处理完毕之后将offset信息更新到kafka中,或者保存到外部存储中。

​ [1] job重启是怎么指定从checkpoint 答案-job失败时主动从checkpoint中重启 --文章内连接 : command+鼠标点击

3.保证数据处理的唯一性,使用mysql/redis保存已消费的数据,做唯一性校验。

10. spark任务暂停,修改计算逻辑再重启,checkpoint保存状态数据是否可用?? checkpoint中都保存了什么数据???

​ 1.checkpoint保存的数据不可再用,逻辑的调整会导致job内部的依赖关系发生变化,job的graph发生变化即无法再找到对应关系无法恢复。

​ 2. 分两部分 [元数据chechpoint和数据checkpoint 键](#3. Spark checkpoint & Accumulators & Broadcast Variables 作用及应用场景。)

20. 参考文章

spark相关面试题

基于zookeeper搭建spark高可用集群


  1. ↩︎
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ink__Bamboo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值