Spark streaming与kafka相结合

版本:Spark streaming 2.11   Kafka:0.9.0.0  scala:2.11.8

Spark streaming消费kafka主要有两种方式:receiver方式和直连方式。

一、receiver方式:

1、利用kafka高阶API,offset由zookeeper维护。

2、方式:

KafkaUtils.createStream(streamingContext, [ZK quorum], [consumer group id], [per-topic number of Kafka partitions to consume])

3、主要实现逻辑:

Receiver方式接收数据的方式如下图:

 

Receiver源源不断的从数据源kafka接收数据,然后发送数据到spark内存,最后更新zookeeper中kafka offset的内存。

这样就存在这样一个问题:

如果receiver接收kafka消息发送到spark内存交由spark处理,更新完offset,这个时候如果driver程序挂掉,那么spark内存中正在处理中的数据就会丢失;driver重启后,因为kafka中的offset已经更新为最新,那么会继续从最新offset处处理数据,就会造成上一次处理数据的丢失。

Receiver方式为了保证数据零丢失,引入了checkpoint和wal机制。

(1)、checkpoint:spark提供的一种容错机制,主要有两种:

①元信息checkpoint:用来保存DStreamGraph以及相关配置信息,

以便在Driver崩溃重启的时候能够接着之前进度继续进行处理;

②消费数据的checkpoint:保存生成的RDD到一个可靠的存储系统中,常用的HDFS,通常有状态的数据横跨多个batch流的时候,需要做checkpoint.

(2)、WAL预写日志:receiver接收数据后,先将数据持久化到日志中(HDFS),如果driver重启,将会从日志文件中恢复数据。

启用checkpoint和wal机制的receiver方式如下图:

 

Receiver从kafka获取数据,将数据通过WAL持久化到HDFS上面,然后发送给spark内存进行处理,处理过程中将元数据信息checkpoint到hdfs上面,最后更新kafka的offset。

如果在处理过程中driver挂掉,恢复成功后会从checkpoint目录中寻找未执行的任务元信息,然后从wal日志中进行恢复,避免了数据丢失。

但是采用这种方式会存在重复消费的问题,如果最后一步更新kafka offset失败的话,那么spark下一次batch会重新从上一次的offset处重新拿去数据,造成另一次处理。

综上,receiver有如下特点:

1、至少处理一次

2、WAL减少了接收器的吞吐量,因为接受到的数据必须保存到可靠的分布式文件系统中;而且kafka和HDFS中存在两份数据,造成了资源的浪费。

为了解决recevier这些问题,spark streaming1.3引入了kafka直连的方式,而在实际生产环境中,大多数都使用的是直连方式。

二、直连方式:

1、利用kafka低阶API,offset由spark checkpoint维护。

2、方式:

createDirectStream(StreamingContext ssc, scala.collection.immutable.Map<String,String> kafkaParams, scala.collection.immutable.Set<String> topics, scala.reflect.ClassTag<K> evidence$19, scala.reflect.ClassTag<V> evidence$20, scala.reflect.ClassTag<KD> evidence$21, scala.reflect.ClassTag<VD> evidence$22)

3、主要优点:

(1)、不采用wal机制,减少了数据冗余存储。

(2)、创建的DStream的rdd的partition做到了和Kafka中topic的partition一一对应。

(3)、基于direct的方式,使用kafka的简单api,Spark Streaming自己就负责追踪消费的offset,并保存在checkpoint中。Spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。

但是直连方式也有一个特点:因为kafka的offset由checkpoint维护,这就导致了zookeeper无法知道此时kafka的offset信息,会导致一些常用的工具如kafkaOffsetMoniter等无法使用,如果想继续使用这些监控offset的工具,可以在spark处理完数据后手动更新zookeeper的offset。

三、自定义维护kafka offset

在消费kafka过程中,checkpoint起到至关重要的作用,但是checkpoint有个弊端,每次流式程序升级的时候会报错误,因为checkpoint第一次持久化的时候会把整个相关的jar给序列化成一个二进制文件,每次重启都会从里面恢复,但是当你新的程序打包之后序列化加载的仍然是旧的序列化文件,这就会导致报错或者依旧执行旧代码,为了解决checkpoint这个问题,我们可以废弃checkpoint,自己维护offset的变化,具体思路如下:

1、首次启动,先从第三方存储介质(可以使redis或者zk)中找是否有上次存储的偏移量,如果没有就从最新的消费,然后保存偏移量至第三方存储介质中

2、如果从第三方存储介质中找到了偏移量,那么就从指定的偏移量处开始消费处理,每个批处理处理完毕后,都会更新新的offset到第三方存储介质中, 这样以来无论是程序故障,还是宕机,再次启动后都会从上次的消费的偏移量处继续开始消费,而且程序的升级或功能改动新版本的发布都能正常运行 并做到了消息不丢。

 下面看一个具体例子:

val calculateReulstInputDstream = KafkaIO.createCustomDirectKafkaStream(ssc, kafkaParams, calculateResultTopics, dmpRealtimeSequenceUrl,Constants.CROWD_CALCULATE_KAFKA_SEQUENCE_ID)

KafkaIO为定义的工具类,createCustomDirectKafkaStream为定义的获取kafka信息的方法,它的主要功能为每次从redis中获取最新的kafka offset,如果获取到了,那么根据offset去kafka集群获取消息然后更新最新的offset到redis中;如果获取不到,则从最新offset处开始获取数据,并更新offset到redis,具体代码如下:

def createCustomDirectKafkaStream(ssc:  StreamingContext, kafkaParams: Map[String, String], topics: Set[String], key:  String): InputDStream[(String, String)] = {
    val kafkaStream =  readOffsetFromJimDB(dmpRealtimeSequenceUrl, key) match {
        case None =>
            KafkaUtils.createDirectStream[String, String,  StringDecoder, StringDecoder] . 
            (ssc, kafkaParams, topics)
        case Some(offset) => 
            val msgHandler = (mmd:  MessageAndMetadata[String, String]) => 
            (mmd.key, mmd.message)
    KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder,  (String, 
        String)](ssc, kafkaParams, offset, msgHandler)}
    kafkaStream.foreachRDD(rdd => saveOffsetToJimDB(rdd,  dmpRealtimeSequenceUrl, key))
    kafkaStream
}
     
def saveOffsetToJimDB(rdd: RDD[_],key: String): Unit =  {
    var offsets =  rdd.asInstanceOf[HasOffsetRanges].offsetRanges
    var value = offsets.map(f=>s"${f.topic}:${f.partition}:${f.fromOffset}").mkString(",")
    writeOffset(value, key)
}
        
def readOffsetFromJimDB(dmpRealtimeSequenceUrl: String,  key: String): Option[Map[TopicAndPartition, Long]] = {
    var value = readOffset(dmpRealtimeSequenceUrl, key)
    value.map {
        v =>  v.split(",").map(_.split(":")).map {
            case  Array(topic, partition, offset) => TopicAndPartition(topic,  
          partition.toInt) -> offset.toLong
     }.toMap
    }
}
 
def writeOffset(offset: String, key: String): Unit = {
    // 存储offset信息到db
}
 
def readOffset(key: String): Option[String] = {
    var offset: String = null
    //  从db中获取存储的offset信息
    Option(offset)
}

PS: kafka从0.9版本之后,offset默认不保存在zk中,而是保存在broker服务器上一个名为__consumer_offsets 的Topic中,感兴趣的可以了解一下

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值