Spark Streaming + Kafka

Spark Streaming基于kafka低阶api的Direct访问方式(No Receivers)

我的原文地址https://hywelzhang.github.io/2017/04/01/Spark-Streaming-kafka.html

关于使用Direct Approach (No Receivers)方式来接收Kafka数据的好处我就不多讲了。长话短说:
1. 防止数据丢失。基于Receiver的方式,会启用一个接数线程将接收的数据暂时保存在excutor,另起线程处理数据。如果程序中途失败,在excutor中未来得及处理的数据将会丢失。所以基于Receiver的方式需要启用WAL机制来防止数据丢失。这样就会造成数据一次写两份,效率不够高效。
2. 与Receiver方式相比更加高效(原因如1中所讲)
3. kafka分区与接收过来的RDD分区一一对应,更符合逻辑,在不用重新分区时,能够提升效率。但是也有例外情况,当kafka分区比较少时,directDStream分区也相应比较少,这样并行度不够。repartition又会引发shuffle操作。所以需要自己权衡一下分区策略。

初步实现

先申明本篇Blog使用的版本,注意适用范围(不说版本,上来就讲的都是耍流氓 –Spark1.6.1 –kafka-0.8)
Direct方式,会将每批读取数据的offset保存在流里边,所以如果不需要将offset写会基于zookeeper的监控工具中,实现起来超级简单
首先,导包是必不可少的,我默认大家使用的是maven构建的项目,在pom.xml中添加依赖

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka_2.10</artifactId>
    <version>1.6.1</version>
</dependency>

示例:

//需要导入kafka包
import org.apache.spark.streaming.kafka_
...

object directTest{
  def main(args: Array[String]) {
    val conf = new SparkConf().setAppName("kafka direct test")
    val sc = new SparkContext(conf)
    val ssc = new StreamingContext(sc,Seconds(10))
    /**
    *设置kafka的参数
    *metadata.broker.list 设置你的kafka brokers列表,一般是"IP:端口,IP:端口..."
    *
    *auto.offset.reset 这里没设置,默认为kafka读数从最新数据开始。
    *还有一个可选设置值smallest,会从kafka上最小的offset值开始读取
    */
    val kafkaParams = Map("metadata.broker.list" -> yourBrokers)
    val topic = "testTopic"
    directKafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, Set(topic))
    directKafkaStream.foreachRDD{
    }
  }
}

OK,到这里,你就已经把数给取到了,接下来用directKafkaStream.foreachRDD进行操作了,是不是超级简单?

但是问题来了,试想一下,生产环境上肯定都会有一个kafka监控工具,用direct的方式,你如果不把offset推回去,监控程序怎么能知道你数据消费没有?

进阶实现

你拿到了directDstream,官方文档只是简单的介绍了一下你可以通过offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges去获得该批数据的offset,但是没讲怎么推回去。虽然对大神来讲,so easy的问题。但是我这对zk(zookeeper简写,偷懒ing)编程又不熟的一小白开始搞的时候也是遇到很多问题。
下面我就用代码+注释的方式详细讲讲我是怎么实现的:
当然,需要和zk协作,必须先加上zk的依赖

 <dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.3</version>
</dependency>

代码及讲解:

object directTest{
  def main(args: Array[String]) {
    val conf = new SparkConf().setAppName("kafka direct test")
    val sc = new SparkContext(conf)
    val ssc = new StreamingContext(sc,Seconds(10))

    //kafka基本参数,yourBrokers你的brokers集群
    val kafkaParams = Map("metadata.broker.list" -> yourBrokers)
    val topic = "testTopic"
    val customGroup = "testGroup"

    //新建一个zkClient,zk是你的zk集群,和broker一样,也是"IP:端口,IP端口..."
    /**
    *如果你使用val zkClient = new ZKClient(zk)新建zk客户端,
    *在后边读取分区信息的文件数据时可能会出现错误
    *org.I0Itec.zkclient.exception.ZkMarshallingError: 
    *  java.io.StreamCorruptedException: invalid stream header: 7B226A6D at org.I0Itec.zkclient.serialize.SerializableSerializer.deserialize(SerializableSerializer.java:37) at org.I0Itec.zkclient.ZkClient.derializable(ZkClient.java:740) ..
    *那么使用我的这个新建方法就可以了,指定读取数据时的序列化方式
    **/
    val zkClient = new ZkClient(zk, Integer.MAX_VALUE, 10000,ZKStringSerializer)
    //获取zk下该消费者的offset存储路径,一般该路径是/consumers/test_spark_streaming_group/offsets/topic_name   
    val topicDirs = new ZKGroupTopicDirs(fvpGroup, fvpTopic)
    val children = zkClient.countChildren(s"${topicDirs.consumerOffsetDir}")

    //设置第一批数据读取的起始位置
    var fromOffsets: Map[TopicAndPartition, Long] = Map()
    var directKafkaStream : InputDStream[(String,String)] = null

    //如果zk下有该消费者的offset信息,则从zk下保存的offset位置开始读取,否则从最新的数据开始读取(受auto.offset.reset设置影响,此处默认)
    if (children > 0) {
      //将zk下保存的该主题该消费者的每个分区的offset值添加到fromOffsets中
      for (i <- 0 until children) {
        val partitionOffset = zkClient.readData[String](s"${topicDirs.consumerOffsetDir}/$i")
        val tp = TopicAndPartition(fvpTopic, i)
        //将不同 partition 对应的 offset 增加到 fromOffsets 中
        fromOffsets += (tp -> partitionOffset.toLong)
        println("@@@@@@ topic[" + fvpTopic + "] partition[" + i + "] offset[" + partitionOffset + "] @@@@@@")
        val messageHandler = (mmd: MessageAndMetadata[String, String]) =>  (mmd.topic,mmd.message())
        directKafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, (String,String)](ssc, kafkaParams, fromOffsets, messageHandler)
      }
    }else{
      directKafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, Set(fvpTopic))
    }

    /**
    *上边已经实现从zk上保存的值开始读取数据
    *下边就是数据处理后,再讲offset值写会到zk上
    */
    //用于保存当前offset范围
    var offsetRanges = Array.empty[OffsetRange]
    val directKafkaStream1 = directKafkaStream.transform { rdd =>
      //取出该批数据的offset值
      offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
      rdd
    }.map(_._2).foreachRDD(rdd=>{
      //数据处理
      ...

      //数据处理完毕后,将offset值更新到zk集群
      for (o <- offsetRanges) {
        val zkPath = s"${topicDirs.consumerOffsetDir}/${o.partition}"
        ZkUtils.updatePersistentPath(zkClient, zkPath, o.fromOffset.toString)
      } 
    })
  }
}

好的,基本操作已经完成,按照上边操作,已经能够实现direct方式读取kafka,并实现zk来控制offset。
更多的细节优化,下次再更。。。
更多请关注我的博客:https://hywelzhang.github.io/

spark streaming 是基于 spark 引擎的实时数据处理框架,可以通过集成 kafka 来进行数据流的处理。然而,在使用 spark streaming 进行 kafka 数据流处理时,可能会遇到一些坑。 首先,要注意 spark streamingkafka 版本的兼容性。不同版本的 spark streamingkafka 可能存在一些不兼容的问题,所以在选择版本时要特别留意。建议使用相同版本的 spark streamingkafka,以避免兼容性问题。 其次,要注意 spark streaming 的并行度设置。默认情况下,spark streaming 的并行度是根据 kafka 分区数来决定的,可以通过设置 spark streaming 的参数来调整并行度。如果并行度设置得过高,可能会导致任务处理过慢,甚至出现 OOM 的情况;而设置得过低,则可能无法充分利用集群资源。因此,需要根据实际情况进行合理的并行度设置。 另外,要注意 spark streamingkafka 的性能调优。可以通过调整 spark streaming 缓冲区的大小、批处理时间间隔、kafka 的参数等来提高性能。同时,还可以使用 spark streaming 的 checkpoint 机制来保证数据的一致性和容错性。但是,使用 checkpoint 机制可能会对性能产生一定的影响,所以需要权衡利弊。 最后,要注意处理 kafka 的消息丢失和重复消费的问题。由于网络或其他原因,可能会导致 kafka 的消息丢失;而 spark streaming 在处理数据时可能会出现重试导致消息重复消费的情况。可以通过配置合适的参数来解决这些问题,例如设置 KafkaUtils.createDirectStream 方法的参数 enable.auto.commit,并设置适当的自动提交间隔。 总之,在使用 spark streaming 进行 kafka 数据流处理时,需要留意版本兼容性、并行度设置、性能调优和消息丢失重复消费等问题,以免踩坑。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值