Spark Streaming使用MySQL存储offset保证Exactly Once语义

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条!!!

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值