1.准备
1.1 本次测试的运行环境如下
- <spark.version>2.3.0</spark.version>
- <kafka.version>0.10.0-kafka-2.1.1</kafka.version>
- <scala.version>2.11.8</scala.version>
- <hadoop.version>2.6.0-cdh5.10.0</hadoop.version
- <spark.streaming.kafka.version>2.3.0</spark.streaming.kafka.version>
- <mysql.jdbc.version>6.0.6</mysql.jdbc.version>
1.2 代码
package com.onlinelog.www.test.kafka_offset_manage
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.internal.Logging
import org.apache.spark.{SparkConf, TaskContext}
import org.apache.spark.streaming.kafka010.{HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.ConsumerStrategies.{Assign, Subscribe}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scalikejdbc._
import scalikejdbc.config._
object KafkaOffsetJDBCManageApp extends Logging {
def main(args: Array[String]): Unit = {
//创建SparkStreaming入口
val conf = new SparkConf()
.setMaster("local[4]")
.setAppName("KafkaOffsetJDBCManageApp")
val ssc = new StreamingContext(conf, Seconds(5))
//kafka消费主题
val topics = "offset_manage".split(",").toSet
val cloudHost = "hadoop004"
val brokers = s"$cloudHost:9092"
//kafka参数
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> brokers,
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "offset_manage_group",
"auto.offset.reset" -> "latest", //earliest latest
"enable.auto.commit" -> (false: java.lang.Boolean)
)
DBs.setup()
val fromOffset = DB.readOnly(implicit session => {
SQL("select * from online_analysis_kafka_offset").map(rs => {
new TopicPartition(rs.string("topic"), rs.int("kafka_partition")) -> rs.long("end_offset")
}).list().apply()
}).toMap
//如果MySQL表中没有offset信息,就从0开始消费;如果有,就从已经存在的offset开始消费
val messages = if (fromOffset.isEmpty) {
logInfo("------ 从头开始消费... ------")
KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent,
Subscribe[String, String](topics, kafkaParams)
)
} else {
logInfo("------ 从已存在记录开始消费... ------")
KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent,
Assign[String, String](fromOffset.keys.toList, kafkaParams, fromOffset)
)
}
messages.foreachRDD(rdd => {
if (!rdd.isEmpty()) {
logInfo("------ 数据统计记录为:" + rdd.count() + " ------")
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd.foreachPartition { _ =>
val o: OffsetRange = offsetRanges(TaskContext.get.partitionId)
logInfo(s"------ ${o.topic}, ${o.partition}, ${o.fromOffset}, ${o.untilOffset} ------")
// begin your transaction
DB.localTx(implicit session => {
// update results
// update offsets where the end of existing offsets matches the beginning of this batch of offsets
SQL("replace into online_analysis_kafka_offset(topic, group_id, kafka_partition, begin_offset, end_offset) values (?,?,?,?,?)")
.bind(o.topic, "offset_manage_group", o.partition.toInt, o.fromOffset.toLong, o.untilOffset.toLong)
.update().apply()
// assert that offsets were updated correctly
})
// end your transaction
}
}
})
ssc.start()
ssc.awaitTermination()
}
}
2. MySQL创建表
CREATE TABLE `online_analysis_kafka_offset` (
`topic` varchar(64) NOT NULL,
`group_id` varchar(64) NOT NULL,
`kafka_partition` int(11) NOT NULL,
`begin_offset` bigint NOT NULL,
`end_offset` bigint NOT NULL,
PRIMARY KEY (`topic`, `group_id`, `partition`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3. Kafka相关设置
3.1 创建topic
bin/kafka-topics.sh \
--create \
--zookeeper hadoop004:2181/kafka \
--replication-factor 1 \
--partitions 3 \
--topic offset_manage
3.2 查看offset_manage这个topic是否创建成功
bin/kafka-topics.sh \
--list \
--zookeeper hadoop004:2181/kafka
3.3 先启动一个消费者
bin/kafka-console-consumer.sh \
--zookeeper hadoop004:2181/kafka \
--topic offset_manage \
-from-beginning
4. 启动SparkStreaming程序
4.1 查看控制台输出
4.2 查看MySQL中online_analysis_kafka_offset表的数据
4.3 演示Spark Streaming程序挂了
再生产100条数据,途中,程序突然挂了,这里只要把replace into 改为insert into 就能实现这种效果
然后再重启Spark Streaming
查看MySQL
从控制台以及MySQL都可以看到,当Spark Streaming程序挂掉之后重启,它会先读取MySQL中的第三个分区的offset,然后从该offset开始读取新的数据。但是从上面的结果也能看出,只有一个分区有数据,而且此时Kafka中应该有200条数据,但是第一次只消费了33条,第二次消费了33条。下面逐步解决。
5. 解决数据不一致
首先,这里设置topic的分区数为1,先解决Spark Streaming消费的数据跟Kafka里的数据不一致的问题,不然看到的结果只有一个分区有数据,并且数据还是少的。
在这里,我又重新创建了一个名为offset_manage_partition_1的topic,该topic只有一个分区,并且创建了名为online_analysis_kafka_offset_1的表,修改代码,然后运行Spark Streaming。第一次Kafka生产者发送了100条数据,第二次10条
从上图可以看到,当topic的分区数为1,Spark Streaming是能够精确的消费数据的
但是生产上Kafka的topic可不能只有一个分区啊,从上面的演示来看,当topic的分区大于1的时候,Spark Streaming在消费的时候只消费了三个分区中的一个。此时,我们再切换到开篇的程序,topic的分区数是3,这时,我们先看看,当Kafka生产者发送了那么多数据到broker,broker有没有接收到这些数据
bin/kafka-run-class.sh kafka.tools.GetOffsetShell \
--topic offset_manage \
--time -1 \
--broker-list 103.210.23.154:9092 \
--partitions 1
从上图可以看到,broker里面是有数据的,并且数据之和等于Kafka生产者发送到broker的数据量
这时候检查了一下代码,断点调试了一番,代码逻辑是没有问题的。于是Google了一番,答案是kafka-clients的版本问题。下面贴上几个相关的issue
https://github.com/apache/spark/pull/16278
https://issues.apache.org/jira/browse/KAFKA-4547
https://issues.apache.org/jira/browse/KAFKA-4845
好了,知道了解决方案,接下来只要把kafka-clients替换成0.10.2.1以上就好了,我这里是直接替换成0.10.2.1
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.10.2.1</version>
</dependency>
再次运行Spark Streaming,然后Kafka生产者再次发送100条数据
可以看到其他分区也已经不为0了,再去MySQL看看
好了,Spark Streaming只消费一个分区的问题解决了,我们再来看一看,数据是否一致
此时,Kafka生产者在发送100条数据到broker,稍等片刻,Spark Streaming消费完毕,查看MySQL
刚好100条!!!