Spark:spark-streaming踩坑

看这个程序:

import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.kafka010._
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.Seconds
import org.apache.spark.sql._
import java.util.Calendar

object KafkaTest {
  val group = "KafkaTest1" // kafka消费者组
  val bootstrap_servers = "10.18.1.2:9092"

  def main(args: Array[String]) {
    val spark = SparkSession
      .builder()
      .appName("KafkaTest")
      //.config("spark.streaming.stopGracefullyOnShutdown", true)
      .enableHiveSupport()
      .getOrCreate()
    import spark.implicits._
    val sc = spark.sparkContext
    val ssc = new StreamingContext(sc, Seconds(3))

    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> bootstrap_servers,
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> group,
      "auto.offset.reset" -> "earliest",
      "enable.auto.commit" -> (false: java.lang.Boolean))

    val topic = "ss"
    val stream = KafkaUtils.createDirectStream[String, String](
      ssc,
      PreferConsistent,
      Subscribe[String, String](Array(topic), kafkaParams))

    var offsetRanges = Array[OffsetRange]()
    var start_time = 0L
    var round_idx = 0
    stream
      .transform { raw_rdd =>
        start_time = System.currentTimeMillis()
        offsetRanges = raw_rdd.asInstanceOf[HasOffsetRanges].offsetRanges

        round_idx += 1
        val cal = Calendar.getInstance()
        cal.setTimeInMillis(start_time)
        val time_now = cal.get(Calendar.HOUR_OF_DAY) +
          ":" + cal.get(Calendar.MINUTE) +
          ":" + cal.get(Calendar.SECOND)
        println(s">>>>>>>>>>>>>>>>主题${topic}第${round_idx}轮..." +
          s"开始 ${time_now}")

        println(offsetRanges.mkString(", "))

        println(s"<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n")
        raw_rdd
      }
      .map(_.value())
      .foreachRDD { rdd =>
        Thread.sleep(1000 * 10)
        
        println("###############################################")
        println(rdd.collect().mkString("[", ", ", "]"))
        println(offsetRanges.mkString(", "))
        stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
        println("###############################################\n")
        
      }

    ssc.start()
    ssc.awaitTermination()
  }

}

这里面对Dstream有三个操作,transform+map+foreachRDD,前两个是转换操作,后一个是可以出发job的操作。

我们忽略map操作,仅看另外两个操作——

注意spark-streaming程序定的接收间隔是3秒,而job处理时间最少会用10秒。会出现这样的情况:

  • transform会每3秒执行一次,而foreachRDD中的调用会至少隔10秒执行一次。
  • 当foreachRDD第一次执行到打印信息的时候,transform已经执行了多次了,而transform每次执行都会修改offsetRanges变量的值。
  • 也就是说,当foreachRDD第一次执行获取到的offsetRanges的值,已经是transform多次修改后的值了。
  • 但是spark会保证foreachRDD第n次执行拿到的rdd是第n个接受周期汇总得到的rdd。

这样问题就来了:

  • 第1次执行foreachRDD拿到的rdd是第一个周期接受的数据汇总得到的rdd
  • 而offsetRanges却不是第1个周期获取到的offsetRanges(因为这时候offsetRanges已经被transform操作多次修改)
  • 所以foreachRDD执行最后提交的offset比处理到的数据是超前的(就是说处理的数据延后于提交的offset)。
  • 如果这时程序由于某种原因终结了,下次程序启动会从新offset开始接受数据,这样上次未处理完的数据就丢失了。

解决思路一:

增加config("spark.streaming.stopGracefullyOnShutdown", true)配置。
开启了这个选项之后,spark程序在收到终止命令后,会先停止接收数据,也就是transform就不再执行了。
而且会一直等待所有未执行的foreachRDD执行,直到处理完所有已经接受到的数据后,才会真正退出程序。
一定程度上可以防止数据丢失。

但是同样存在问题,比如由于断电或kill -9等原因程序被终止,即使加了这个配置还是会造成数据丢失。

而且在eclipse下直接终止程序这个配置好像也无效,程序会直接被终止。

另外设置了这个参数后还要看这个参数:spark.streaming.gracefulStopTimeout(单位ms),默认是生成批次间隔时间的10倍。当超过这个时间还没处理完,就强制停止。

而且经过测试还发现这个机制一个问题:终止程序命令的job处理时,println会失效,也就是会不打印输出。

解决思路二(推荐):

更好的解决方法是,将整个逻辑(主要是获取和提交offset的代码)都放到foreachRDD中执行,在执行完上一个批次的数据处理前,下一批次的数据处理程序就不会被执行。
这样就避免了rdd的批次和offsetRanges的提交不一致。

如果将offsetRanges的提交放到rdd数据处理代码后,就算由于意外原因程序终止,可能会造成数据重复,但是不会丢失。

数据处理部分的代码改成如下:

stream
  .foreachRDD { raw_rdd =>
    start_time = System.currentTimeMillis()
    offsetRanges = raw_rdd.asInstanceOf[HasOffsetRanges].offsetRanges

    round_idx += 1
    val cal = Calendar.getInstance()
    cal.setTimeInMillis(start_time)
    val time_now = cal.get(Calendar.HOUR_OF_DAY) +
      ":" + cal.get(Calendar.MINUTE) +
      ":" + cal.get(Calendar.SECOND)
    println(s">>>>>>>>>>>>>>>>主题${topic}第${round_idx}轮..." +
      s"开始 ${time_now}")
    println(offsetRanges.mkString(", "))
    println(s"<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n")

    val rdd = raw_rdd.map(_.value())
    Thread.sleep(1000 * 10)
    
    println("###############################################")
    println(rdd.collect().mkString("[", ", ", "]"))
    println(offsetRanges.mkString(", "))
    stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
    println("###############################################\n")

  }

如果采用这种思路,可以将spark.streaming.stopGracefullyOnShutdown配置为false,原因下面分析。

以上第二种解决思路详解:

看这句代码:

val ssc = new StreamingContext(sc, Seconds(3))

这个3秒的真正含义是,接收器每接受3秒的数据看作一个批次,汇总成一个rdd,然后处理。
虽然foreachRDD的处理时间是10秒,但是这对数据批次的生成却不会产生影响,总会是3秒生成一个批次……

接收器会每3秒将一个批次的数据放到内存中,然后多个批次排队等待foreachRDD来处理,而foreachRDD每次执行只会取一个批次的数据。

所以上面第二种解决思路的程序也会造成数据在内存中积压,若开启了config("spark.streaming.stopGracefullyOnShutdown", true),则在终止程序时也会等待半天才真正结束程序。但是完全可以将这个配置为false,并不会造成数据丢失。因为数据再没有处理前(即使数据已经被接收并生成了批次),它的offset不会被提交,下一次启动程序,会从之前的offset开始重新接受数据。
也就是说上次结束程序前那些加载到内存但没有处理完的数据会重新接受并处理,所以数据不会丢失。

但若是将spark.streaming.stopGracefullyOnShutdown配置为false,如果程序在处理了一部分或全部数据后,还未提交offset前,程序终止。下次再启动程序,程序上一次处理的最后一个批次的数据由于未提交offset会重新被接收处理,就会造成数据重复。

posted @ 2019-01-04 17:25  xuejianbest  阅读(597)  评论(0编辑  收藏  举报