Spark修改几行源码,解决Kafka数据积压

导致Kafka数据积压的几种情况

  • 第一种情况,SparkStreaming 通过receivers(或者Direct方式)以生产者生产数据的速率接收数据。当Batch procecing time > batch interval 的时候,也就是每个批次数据处理的时间要比SparkStreaming批处理间隔时间长;越来越多的数据被接收,但是数据的处理速度没有跟上,导致系统开会出现数据堆积,可能进一步导致Excutor端OOM问题而出现失败的情况。

  • 第二种情况,就是你的流计算程序没有自动拉起脚本,或者是自动拉起脚本设计不合理,导致服务较长时间处于停止状态。

  • 第三种情况,是由于kafka内数据自定义Key导致Kafka分区内数据分布不均匀。

Spark Streaming消费Kafka的方式

  1. 基于Receiver的方式

这种方式使用Receiver来获取数据。Receiver是使用Kafka的高级Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存中的(如果突然数据暴增,大量batch堆积,很容易出现内存溢出的问题),然后Spark Streaming启动的job会去处理那些数据。
然而,在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制(Write Ahead Log,WAL)。该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。

在这里插入图片描述2. 基于Direct的方式

这种新的直接方式,是在Spark 1.3中引入的,从而能够确保更加健壮的机制。替代掉使用Receiver来接收数据后,这种方式会周期性地查询Kafka,来获得每个topic+partition的最新的offset,从而定义每个batch的offset的范围。当处理数据的job启动时,就会使用Kafka的简单Consumer api来获取Kafka指定offset范围的数据。

在这里插入图片描述

常规解决上述三种场景的方式

针对第一种由于数据量较大分区较小的情况产生数据积压

  1. 最常见的就是开启Spark Streaming的反压制机制。
  2. 再有就是通过shuffle,在Spark消费阶段进行repartition
  3. 最根本的方法就是增加Kafka分区数

针对第二种程序宕机导致的消费滞后

  1. 最常见的方式任务启动从最新的消费,历史数据采用离线修补。
  2. 还可以再次启动时加大资源,提升消费速度

针对第三种Kafka数据分布不均匀的情况

  1. 最常见的就是给Key增加随机后缀,尽可能让数据散列均匀避免数据倾斜

通过修改源码来解决数据积压

如果按常规的模式来解决这些问题,就太平凡了,根本不够骚。

Spark如何确定分区数

Spark Streaming生产KafkaRDD-Rdd的分区数,完全可以是大于kakfa分区数的!

其实,经常阅读源码应该了解,RDD的分区数,是由RDD的getPartitions函数决定。比如KafkaRDD的getPartitions方法实现如下:

val offsetRanges: Array[OffsetRange]

override def getPartitions: Array[Partition] = {
    offsetRanges.zipWithIndex.map { case (o, i) =>
        new KafkaRDDPartition(i, o.topic, o.partition, o.fromOffset, o.untilOffset)
    }.toArray
  }

具体位置:
KafkaRDD --> getPartitions

OffsetRange存储一个kafka分区元数据及其offset范围,然后进行map操作,转化为KafkaRDDPartition。实际上,我们可以在这里下手,将map改为flatmap,然后对offsetrange的范围进行拆分,但是这个会引发一个问题

修改分区规则

其实,我们可以在offsetRange生成的时候做下转换。位置是DirectKafkaInputDstream的compute方法。具体实现:
在这里插入图片描述

// 是否开启自动重分区分区
sparkConf.set("enable.auto.repartition","true")

// 避免不必要的重分区操作,增加个阈值,只有该批次要消费的kafka的分区内数据大于该阈值才进行拆分
sparkConf.set("per.partition.offsetrange.threshold","300")

// 拆分后,每个kafkardd 的分区数据量。
sparkConf.set("per.partition.after.partition.size","100")

然后,在DirectKafkaInputDstream里获取着三个配置

val repartitionStep = _ssc.conf.getInt("per.partition.offsetrange.size",1000)
val repartitionThreshold = _ssc.conf.getLong("per.partition.offsetrange.threshold",1000)
val enableRepartition = _ssc.conf.getBoolean("enable.auto.repartition",false)

对offsetRanges生成的过程进行改造,只需要增加几行源码即可

val offsetRanges = untilOffsets.flatMap{ case (tp, uo) =>
  val fo = currentOffsets(tp)
  val delta = uo -fo
  if(enableRepartition&&(repartitionThreshold < delta)){
    val offsets = fo to uo by repartitionStep
    offsets.map(each =>{
      val tmpOffset = each + repartitionStep
      OffsetRange(tp.topic, tp.partition, each, Math.min(tmpOffset,uo))
    }).toList
  }else{
    Array(OffsetRange(tp.topic, tp.partition, fo, uo))
  }
}

修改后:
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章