kafka因为版本的不同可能会导致一下接口的差异还有功能等的区别,我用的是0.10版本的kafka,0.8版本的kafka将topic及其分区等元数据是默认保存在zookeeper中,新版本的kafka有一个自带的topic__consumer_offset以存储offset代替zookeeper的相应功能,即:offset会存到kafka中,我们处理kafka中的数据时,kafka有个参数enable.commit.auto,默认为true即kafka会根据默认的时间(好像是5秒)刷新一次offset值,即5秒自动提交一次。这种方式会因为程序等因无法抗拒的因素挂掉导致数据出现问题。一般都选择手动提交,在数据处理逻辑后提交更新后的offset。
可以直接将offset提交到kafka中,此种方式代码简单易实现,
还可以将offset存到zookeeper中,此种方式代码量相对较多。
还可以将offset存到外部数据库中,比如mysql、sqlserver等数据库。
除了这几种方式还可以使用checkpoint,设置检查点的方式去管理offset值。
本次我们就使用zookeeper管理offset值。有相对代码量较少的kafka管理offset,感兴趣的朋友可以去我博客里面看。
废话不多说,上我的代码。
package zookeeper_offset
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.{HasOffsetRanges, KafkaUtils}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* spark-streaming读取kafka
* 适用本版:spark-streaming-kafka-0-10
* (0.10和0.8的API有较大的区别)
*/
object DirectKafkaManagerMeterData {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[4]").setAppName("DirectKafkaMeterData")
val ssc = new StreamingContext(conf, Seconds(5))//流数据分批处理时间间隔
//kafka节点
val BROKER_LIST = "10.204.118.101:9092,10.204.118.102:9092,10.204.118.103:9092"
val BOOTSTRAP_SERVER = "10.204.118.101:9092,10.204.118.102:9092,10.204.118.103:9092"
val ZK_SERVERS = "10.204.118.101:2181,10.204.118.102:2181,10.204.118.103:2181"
val GROUP_ID = "test_consumer" //消费者组
val topics = Array("kafka010_test") //待消费topic
val kafkaParams = Map[String,Object](
"bootstrap.servers" -> BOOTSTRAP_SERVER,
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> GROUP_ID,
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> "false"
)
//采用zookeeper手动维护偏移量
val zkManager = new KafkaOffsetZKManager(ZK_SERVERS)
val fromOffsets = zkManager.getFromOffset(topics,GROUP_ID)
//创建数据流
var stream:InputDStream[ConsumerRecord[String, String]] = null
if (fromOffsets.size > 0){
stream = KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent,
Subscribe[String, String](topics, kafkaParams, fromOffsets)
)
}else{
stream = KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent,
Subscribe[String, String](topics, kafkaParams)
)
println("第一次消费 Topic:" + topics)
}
//处理流数据
stream.foreachRDD { rdd =>
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
val rs = rdd.map(record => (record.offset(), record.partition(), record.value())).collect()
for(item <- rs) println(item)
rdd.foreachPartition(partitions =>{
partitions.foreach(line =>{
//处理数据逻辑
//等等逻辑
})
})
// 处理数据存储到HDFS或Hbase等
// 存储代码(略)
// 处理完数据保存/更新偏移量
zkManager.storeOffsets(offsetRanges,GROUP_ID)
}
ssc.start()
ssc.awaitTermination()
}
}
然后另创一个类用来维护zookeeper中的offset
package zookeeper_offset
import org.apache.curator.framework.CuratorFrameworkFactory
import org.apache.curator.retry.ExponentialBackoffRetry
import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange
/**
* kafka偏移量zookeeper维护类
* 适用本版:spark-streaming-kafka-0-10
*
* @param zkServers zookeeper server
*/
class KafkaOffsetZKManager(zkServers : String) {
//创建zookeeper连接客户端
val zkClient = {
val client = CuratorFrameworkFactory
.builder
.connectString(zkServers)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
// .namespace("kafka")//创建包含隔离命名空间的会话
.build()
client.start()
client
}
val _base_path_of_kafka_offset = "/kafka/offsets" //offset 路径起始位置
/**
* 获取消费者组topic已消费偏移量(即本次起始偏移量)
* @param topics topic集合
* @param groupName 消费者组
* @return
*/
def getFromOffset(topics: Array[String], groupName:String):Map[TopicPartition, Long] = {
// Kafka 0.8和0.10的版本差别:0.10->TopicPartition ,0.8->TopicAndPartition
var fromOffset: Map[TopicPartition, Long] = Map()
for(topic <- topics){
val topic = topics(0).toString
println(topic+"'''''''''''''''''''''''''''''''''''")
// 读取ZK中保存的Offset,作为Dstrem的起始位置。如果没有则创建该路径,并从 0 开始Dstream
val zkTopicPath = s"${_base_path_of_kafka_offset}/${groupName}/${topic}"
// 检查路径是否存在
checkZKPathExists(zkTopicPath)
// 获取topic的子节点,即 分区
val childrens = zkClient.getChildren().forPath(zkTopicPath)
// 遍历分区
import scala.collection.JavaConversions._
for (p <- childrens){
// 遍历读取子节点中的数据:即 offset
val offsetData = zkClient.getData().forPath(s"${zkTopicPath}/${p}")
println(offsetData.toString+"----------------------------------")
// 将offset转为Long
val offset = java.lang.Long.valueOf(new String(offsetData)).toLong
fromOffset += (new TopicPartition(topic, Integer.parseInt(p)) -> offset)
}
}
println(fromOffset+"+++++++++++++++++++++++++++++++++++++")
fromOffset
}
/**
* 检查ZK中路径存在,不存在则创建该路径
* @param path
* @return
*/
def checkZKPathExists(path: String)={
if (zkClient.checkExists().forPath(path) == null) {
zkClient.create().creatingParentsIfNeeded().forPath(path)
}
}
/**
* 保存或更新偏移量
* @param offsetRange
* @param groupName
*/
def storeOffsets(offsetRange: Array[OffsetRange], groupName:String) = {
for (o <- offsetRange){
val zkPath = s"${_base_path_of_kafka_offset}/${groupName}/${o.topic}/${o.partition}"
// 检查路径是否存在
checkZKPathExists(zkPath)
// 向对应分区第一次写入或者更新Offset 信息
println("---Offset写入ZK------\nTopic:" + o.topic +", Partition:" + o.partition + ", Offset:" + o.untilOffset)
zkClient.setData().forPath(zkPath, o.untilOffset.toString.getBytes())
}
}
}
在启动程序处理数据的时候,会先从zookeeper得到对应的offset值
本人也是刚入坑的大白,大佬们不喜勿喷,有要一块学习的大佬们可以留下联系方式咱们一块努力,有问题可以互相探讨探讨。。。