いわゆる手動オフセット管理では、メッセージが実際に処理されるタイミングをユーザーが定義し、通常、オフセットを送信する前にオフセットを使用して他の操作を実行します。利点は、メッセージのみが実際に処理されることをユーザーが保証できることです。オフセットを再度送信してください。したがって、コードロジックでリアルタイムオフセットを取得し、<<タスク処理の完了後にオフセットを送信>>のタイミングを確保する必要があります。
Kafkaオフセットの手動管理には、次の利点があります。
a)通常の状況では、データが失われたり、繰り返し消費されたりしないことを確認してください
b)オフセット情報を簡単に表示できます
操作APIはOffsetRangeクラスであり、untilOffset()メソッドがあり、データのバッチ内の操作のパーティションに対応する終了オフセットを取得できます。開始オフセットに対応するfromOffset()メソッドもあります。
一般的な操作の考え方は次のとおりです
- 在Kafka DirectStream初始化时,取得当前所有partition的中数据的offset
- 读取offset数据,处理并存储结果。
- 提交offset,并将其持久化在可靠的外部存储中。
同時に、0.10前後の現在のKafkaが一般的に使用されていることに注意してください。Kafka0.10以降のバージョンでは、オフセットのデフォルトストレージがZooKeeperからKafkaの自己完結型トピックに移動されました。 SparkStreamingも特殊化されていますオフセットを送信するためのcommitAsyncAPIを提供します
提出されたコアコードは次のとおりです
//stream是streaming数据集
stream.foreachRDD {
rdd =>
//把rdd转化
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
//中间是所需代码业务,此处省略
// 在计算已经完成就提交偏移量
stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
}
使用されるコードの完全なセットは次のとおりです
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.{
SparkConf, TaskContext}
import org.apache.spark.streaming.kafka010.{
CanCommitOffsets, HasOffsetRanges, KafkaUtils}
import org.apache.spark.streaming.{
Seconds, StreamingContext}
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
object StreamFromKafka {
def main(args: Array[String]): Unit = {
//前面的可以不用看,就是正常的流计算
val conf = new SparkConf().setAppName("StreamWordCount").setMaster("local[2]")
val sc = new StreamingContext(conf,Seconds(10))
sc.sparkContext.setLogLevel("ERROR")
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "192.168.182.149:9092,192.168.182.147:9092,192.168.182.148:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "group1",
"enable.auto.commit" -> (true: java.lang.Boolean) //将提交设置成手动提交false,默认true,自动提交到kafka
)
/**
* LocationStrategies.PreferBrokers() 仅仅在你 spark 的 executor 在相同的节点上,优先分配到存在 kafka broker 的机器上;
* LocationStrategies.PreferConsistent(); 大多数情况下使用,一致性的方式分配分区所有 executor 上。(主要是为了分布均匀)
* 新的Kafka使用者API将预先获取消息到缓冲区。因此,出于性能原因,Spark集成将缓存的消费者保留在执行程序上(而不是为每个批处理重新创建它们),并且更喜欢在具有适当使用者的主机位置上安排分区,这一点很重要。
*在大多数情况下,您应该使用LocationStrategies.PreferConsistent,如上所示。这将在可用执行程序之间均匀分配分区。如果您的执行程序与Kafka代理在同一主机上,请使用PreferBrokers,它更愿意为该分区安排Kafka领导者的分区。
*/
val topics = Array("test")
val stream = KafkaUtils.createDirectStream[String, String](
sc,
PreferConsistent,
Subscribe[String, String](topics, kafkaParams)
)
val kafkaStream = stream.map(record => (record.key, record.value))
stream.foreachRDD(rdd =>{
//此处获取当前rdd中分区的offset值
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd.foreachPartition(partitions =>{
//此处将的遍历分区,通过TaskContext类得到每个分区的标识从而获取fromOffset和untilOffset
val o = offsetRanges(TaskContext.get().partitionId())
//打印到控制台可以明了的查看offset值
println(o.fromOffset+"- - - - - - - - - - "+o.untilOffset)
//这里就是业务中怎么处理offset了,也可以处理数据
partitions.foreach(line =>{
//将分区中数据的key,value值打印到控制台
println("key"+line.key()+"...........value"+line.value())
})
})
//手动提交处理后的offset值到kafka
stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
})
val words = kafkaStream.flatMap(_._2.split(" "))
val pairs = words.map {
x => (x,1) }
val wordCounts = pairs.reduceByKey(_+_)
wordCounts.print()
sc.start()
sc.awaitTermination()
}
}
また、0.10より前のKafkaを使用している場合は、kafkaの代わりにzookeeperに送信する必要があります。操作コードは、次のとおりです。
object KafkaDirectWordCount_zookeeper {
def main(args: Array[String]): Unit = {
//消费者id
val group = "g001"
//主题
val topic = "wordcount"
val topics = Array(topic)
//创建SparkConf
val conf = new SparkConf().setAppName("KafkaDirectWordCount_zookeeper").setMaster("local[2]")
val streamingContext = new StreamingContext(conf, Seconds(5));
//指定kafka的broker地址
val brokerList = "node01:9092,node02:9092,node03:9092"
//指定zk的地址,为了后面处理偏移量准备
val zkQuorum = "node01:2181,node02:2181,node03:2181"
//创建一个 ZKGroupTopicDirs 对象,这个对象对应的是一个zkClient中可以查看的一个路径
//该路径标识着对应消费者组的所有信息
val topicDirs = new ZKGroupTopicDirs(group, topic)
//获取这个消费者组在zookeeper中消费wordcount的偏移量存储路径
//按照上面的配置获取到的路径应该是/g001/offsets/wordcount/
val zkTopicPath = s"${topicDirs.consumerOffsetDir}"
//配置kafka的参数
val kafkaParams = Map[String, Object](
//指定broker所在位置
"bootstrap.servers" -> brokerList,
//指定写入数据和读取数据的编码方式
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> group,
"auto.offset.reset" -> "earliest", // 从0偏移量开始读取数据,默认lastest从最后也就是最新数据,最终用那个都是看业务需求,这里是为了测试代码要多次运行才每次重头开始读取
//关闭自动提交偏移量
"enable.auto.commit" -> (false: java.lang.Boolean)
)
//创建一个zookeeper的客户端,维护偏移量
val zkClient = new ZkClient(zkQuorum)
//我们要判断一下这个路径下是否有子路径,如果是空的,就表示该组没有过
val children = zkClient.countChildren(zkTopicPath)
var kafkaStream: InputDStream[ConsumerRecord[String, String]] = null
//如果有子节点,那么就表示之前消费过且保存了offset,我们要对其做处理
if (children>0){
//如果 zookeeper 中有保存 offset,我们就用这个 offset 作为 kafkaStream 的起始位置
var offsets: collection.mutable.Map[TopicPartition, Long] = collection.mutable.Map[TopicPartition, Long]()
//手动维护过偏移量
//1.先将已有的对应每个topic分区的偏移量读取出来与kafka的分区对应
for (i <- 0 until children){
// 获取zookeeper中保存的分区的offset,如/g001/offsets/wordcount/0 代表0分区
val partitionOffset = zkClient.readData[Long](s"$zkTopicPath/${i}")
// 建立一个分区的标识,对应topic的分区
val tp =new TopicPartition(topic, i)
//将获取到的漂移量和topic里面的分区一一对应在Map中
offsets.put(tp,partitionOffset.toLong)
}
//通过KafkaUtils创建直连的DStream(fromOffsets参数的作用是:按照前面计算好了的偏移量继续消费数据)
kafkaStream = KafkaUtils.createDirectStream[String,String](streamingContext,LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String,String](topics,kafkaParams,offsets))
}else{
//如果未保存,那就表示之前没有被消费offset
kafkaStream = KafkaUtils.createDirectStream[String,String](streamingContext,LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String,String](topics,kafkaParams))
}
//读取完数据,我们先维护最新偏移量,和0.10之后的差不多的操作
kafkaStream.foreachRDD(rdd=>{
val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
//业务处理
val lines: RDD[String] = rdd.map(_.value())
//对RDD进行操作,触发Action
lines.foreachPartition(partition =>
partition.foreach(x => {
println(x)
})
)
//更新偏移量
for(osr <- ranges) {
// /g001/offsets/wordcount/0
val zkPath = s"${topicDirs.consumerOffsetDir}/${osr.partition}"
//将该 partition 的 offset 保存到 zookeeper
//如果目录不存在先创建
//println(zkPath)
if (!zkClient.exists(zkPath)) {
zkClient.createPersistent(zkPath, true)
}
//写入数据
zkClient.writeData(zkPath, osr.untilOffset)
}
})
streamingContext.start()
streamingContext.awaitTermination()
}
}
動物園の飼育係のようなメンテナンス方法は、Kafkaがオフセット自体を維持するため、実際にはmysqlなどの単一の動物園の飼育係ではなく、外部のメンテナンスオフセットの開発部門になっています。方法を紹介します。 redisでオフセットを維持する
import java.{
lang, util}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.log4j.{
Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010._
import org.apache.spark.streaming.{
Seconds, StreamingContext}
import redis.clients.jedis.Jedis
object WCKafkaRedisApp {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("xx")
//每秒钟每个分区kafka拉取消息的速率
.set("spark.streaming.kafka.maxRatePerPartition", "100")
// 序列化
.set("spark.serilizer", "org.apache.spark.serializer.KryoSerializer")
// 建议开启rdd的压缩
.set("spark.rdd.compress", "true")
val ssc = new StreamingContext(conf, Seconds(2))
//启动一参数设置
val groupId = "test002"
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "hdp01:9092,hdp02:9092,hdp03:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> groupId,
"auto.offset.reset" -> "earliest",
"enable.auto.commit" -> (false: lang.Boolean)
)
val topics = Array("test")
//启动二参数设置 JedisOffset是自己分装了一个代码作用和原先的差不多,都是判断是不是有偏移量的,如果有分装Map
var formdbOffset: Map[TopicPartition, Long] = JedisOffset(groupId)
//拉取kafka数据
val stream: InputDStream[ConsumerRecord[String, String]] = if (formdbOffset.size == 0) {
KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams)
)
} else {
KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Assign[String, String](formdbOffset.keys, kafkaParams, formdbOffset)
)
}
//数据偏移量处理。
stream.foreachRDD({
rdd =>
//获得偏移量对象数组
val offsetRange: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
//逻辑处理
rdd.flatMap(_.value().split(" ")).map((_, 1)).reduceByKey(_ + _).foreachPartition({
it =>
val jedis = RedisUtils.getJedis
it.foreach({
v =>
jedis.hincrBy("wordcount", v._1, v._2.toLong)
})
jedis.close()
})
//偏移量存入redis
val jedis: Jedis = RedisUtils.getJedis
for (or <- offsetRange) {
jedis.hset(groupId, or.topic + "-" + or.partition, or.untilOffset.toString)
}
jedis.close()
})
ssc.start()
ssc.awaitTermination()
}
}
import java.util
import org.apache.kafka.common.TopicPartition
object JedisOffset {
def apply(groupId: String) = {
var formdbOffset = Map[TopicPartition, Long]()
val jedis1 = RedisUtils.getJedis
val topicPartitionOffset: util.Map[String, String] = jedis1.hgetAll(groupId)
import scala.collection.JavaConversions._
val topicPartitionOffsetlist: List[(String, String)] = topicPartitionOffset.toList
for (topicPL <- topicPartitionOffsetlist) {
val split: Array[String] = topicPL._1.split("[-]")
formdbOffset += (new TopicPartition(split(0), split(1).toInt) -> topicPL._2.toLong)
}
formdbOffset
}
}
比較して見つけるのは難しいことではありません。実際、オフセットが格納されている場所とその使用方法を除いて、書き込み方法は同じです。
チェックポイントが使用されない理由を考える人もいます。実際、Spark Streamingのチェックポイントメカニズムは間違いなく最も使いやすいですが、チェックポイントデータはHDFSに保存されます。ストリーミングアプリケーションがダウンした場合、すぐに復元できます。
ただし、ストリーミングプログラムのコードが変更された場合、再パッケージ化して実行すると、逆シリアル化の例外が発生します。これは、チェックポイントが初めて永続化されるときにjarパッケージ全体がシリアル化され、再起動時に復元できるためです。再パッケージ化後、新旧のコードのロジックが異なる場合、エラーが報告されるか、古いコードが引き続き実行されます。
この問題を解決するには、HDFS上のチェックポイントファイルのみを削除できますが、これにより、Kafkaのオフセット情報も同時に削除されるため、意味がありません。