Spark Streaming整合kafka-★★★★★

  • Kafka概念回顾
  • Kafka命令回顾
  • 整合方式说明
  • Receiver模式--仅仅为了面试
  • Direct模式--开发用这个
  • 结论
  • 整合API说明
  • 代码实现-自动提交偏移量
  • 代码实现-扩展-手动维护偏移量到默认主题
  • 代码实现-扩展-手动维护偏移量到MySQL
  • 准备建表语句
  • 准备操作MySQL数据库的工具类
  • 手动维护偏移量到MySQL


Kafka概念回顾

spark 消费redis_kafka

  • Broker: 安装Kafka服务的机器就是一个broker
  • Producer :消息的生产者,负责将数据写入到broker中(push推)
  • Consumer:消息的消费者,负责从kafka中拉取数据(pull拉),老版本的消费者需要依赖zk,新版本的不需要
  • Topic: 主题,相当于是数据的一个分类,不同topic存放不同业务的数据 –主题:区分业务
  • Replication:副本,数据保存多少份(保证数据不丢失) –副本:数据安全
  • Partition:分区,是一个物理的分区,一个分区就是一个文件,一个Topic可以有1~n个分区,每个分区都有自己的副本 –分区:并发读写
  • Consumer Group:消费者组,一个topic可以有多个消费者/组同时消费,多个消费者如果在一个消费者组中,那么他们不能重复消费数据 --消费者组:提高消费者消费速度、方便统一管理
  • 注意:一个Topic可以被多个消费者或者组订阅,一个消费者/组也可以订阅多个主题
  • 注意:读数据只能从Leader读, 写数据也只能往Leader写,Follower会从Leader那里同步数据过来做副本!!!
  • 如果要从副本读数据,要等到数据同步之后,效率反而低了
  • 所以Kafka的副本只用来容错,并发读写,吞吐量…由分区来保证/提高

Kafka命令回顾

  • 启动
/export/servers/kafka/bin/kafka-server-start.sh -daemon /export/servers/kafka/config/server.properties
  • 查看主题
/export/servers/kafka/bin/kafka-topics.sh --list --zookeeper node01:2181
  • 创建主题
/export/servers/kafka/bin/kafka-topics.sh --create --zookeeper node01:2181 --replication-factor 2 --partitions 3 --topic spark_kafka
  • 启动控制台生产者
/export/servers/kafka/bin/kafka-console-producer.sh --broker-list node01:9092 --topic spark_kafka
  • 启动控制台消费者
/export/servers/kafka/bin/kafka-console-consumer.sh --bootstrap-server node01:9092 --topic spark_kafka --from-beginning

整合方式说明

Receiver模式–仅仅为了面试

  • Receiver模式下有一个Receiver接收器作为一个常驻进程的Task,运行在Executor中,一直等待Kafka数据的到来
  • 1.一个Receiver接收器效率很低, 那么可以使用多个,但是多个Receiver接收器接受完数据还得手动合并,很麻烦
  • 2.并且如果Receiver接收器挂了, 数据就丢了, 也可以开启WAL预写日志保证数据安全,但是效率要低一点
  • 3.并且Receiver模式下,是使用ZK来连接Kafka的(新版本的官方已经不推荐了)
  • 4.并且Receiver模式是使用Kafka的低版本的高阶API(封装之后的)来维护offset的,提交到ZK中(新版本的官方已经不推荐了),和SparkStreaming维护在Checkpoint中的可能有冲突
  • 全是缺点…
  • 所以不管从何种角度去说, Receiver模式早就已经淘汰了! 学习和开发的时候都不会用, 面试的时候能够说上几点就可以了

Direct模式–开发用这个

  • Direct模式下, 是由SparkStreaming直接连接到Kafka的各个分区,并拉取数据,提高了数据读取的并发能力
  • Direct模式可以使用新版本的消费者API(封装了旧的低阶和高阶API)(官方推荐的API), 可以自动提交偏移量到默认主题/Checkpoint中, 还可以手动维护偏移量到MySQL/Redis等其他地方
  • 注意:offset不要存在ZK中了, 因为官方自己都不把offset存在ZK了,ZK不适合频繁修改数据!
  • Direct模式 + 手动维护偏移量 + MySQL事务 可以保证数据仅仅被处理一次,也就是Exactly-Once

实现方式

消息语义

存在的问题

Receiver

at most once最多被处理一次

会丢失数据

Receiver+WAL

at least once最少被处理一次

不会丢失数据,但可能会重复消费,且效率低

Direct+手动操作

exactly once只被处理一次/精准一次

不会丢失数据,也不会重复消费,且效率高

结论

  • 学习和开发中都直接使用Direct模式即可
  • Receiver模式面试时能说上几点就行
  • 开发中谁用Receiver模式谁SB! 缺点太多了! 早就淘汰了!

整合API说明

  • 开发中SparkStreaming和kafka集成有两个版本:0.8及0.10+
  • 0.8版本支持Receiver和Direct模式(但是0.8版本生产环境问题较多,在Spark2.3之后不支持0.8版本了)
  • 0.10以后只保留了Direct模式(因为Reveiver模式不适合生产环境,官方不认用了,所以只保留了Direct模式),并且0.10版本API有变化(更加强大)
  • spark 消费redis_kafka_02

  • 以前官网上有下面的表格:
  • spark 消费redis_kafka_03

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
    <version>${spark.version}</version>
</dependency>

代码实现-自动提交偏移量

package cn.hanjiaxiaozhi.stream

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
 * Author hanjiaxiaozhi
 * Date 2020/7/26 11:30
 * Desc SparkStreaming整合Kafka消费数据并处理
 */
object SparkStreaming_Kafka {
  def main(args: Array[String]): Unit = {
    //1.准备环境
    val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))

    //2.准备Kafka的连接参数
    val kafkaParams: Map[String, Object] = Map[String, Object](
      "bootstrap.servers" -> "node01:9092",//Kafka集群地址
      "key.deserializer" -> classOf[StringDeserializer],//key的反序列化类型
      "value.deserializer" -> classOf[StringDeserializer],//value的反序列化类型
      "group.id" -> "spark",//消费者组id
      //earliest :当各分区下有已提交的 Offset 时,从提交的 Offset开始消费;无提交的Offset 时,从头开始消费;
      //latest : 当各分区下有已提交的 Offset 时,从提交的 Offset 开始消费;无提交的 Offset时,消费新产生的该分区下的数据
      //none : Topic 各分区都存在已提交的 Offset 时,从 Offset 后开始消费;只要有一个分区不存在已提交的 Offset,则抛出异常。
      "auto.offset.reset" -> "latest",//偏移量自动重置位置
      "enable.auto.commit" -> (true: java.lang.Boolean),//自动提交偏移量到Kafka的默认主题__consumer_offsets中,如还开启了Checkpoint那么也会存在Checkpoint中一份(后续会演示手动)
      "auto.commit.interval.ms" -> "1000"//自动提交的时间间隔
    )
    val topics = Array("spark_kafka")//要订阅的主题


    //3.使用spark-streaming-kafka-0-10_2.11依赖中提供的KafkaUtils的createDirectStream方法
    //使用直连方式/Direct模式直接对接Kafka的各个分区拉取分区中的数据返回DStream
    /*
      ssc: StreamingContext,上面已经创建好了
      locationStrategy: LocationStrategy,
        位置策略:在大多数情况下直接使用源码中推荐的LocationStrategies.PreferConsistent
        表示将SparkStreaming的分区和Kafka的分区均匀一致的对应
      consumerStrategy: ConsumerStrategy[K, V]
        消费策略:在大多数情况下直接使用源码中推荐的ConsumerStrategies.Subscribe
        表示根据参数订阅Kafka主题中的消息
     */
    //kafkaDStream: InputDStream[ConsumerRecord[String, String]]
    //表示使用指定的策略从kafka中消费到的消息组成的DStream
    val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](topics, kafkaParams)
    )

    //4.处理从Kafka中消费的消息,也就是要取出value
    //kafkaDStream.map(record=>record.value())
    val dataDStream: DStream[String] = kafkaDStream.map(_.value())
    //拿到数据之后可以做任意想做的事情,如WordCount

    //5.直接输出结果
    dataDStream.print()

    //6.开启并等待停止
    ssc.start()
    ssc.awaitTermination()
  }
}

代码实现-扩展-手动维护偏移量到默认主题

package cn.hanjiaxiaozhi.stream

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{CanCommitOffsets, ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies, OffsetRange}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

/**
 * Author hanjiaxiaozhi
 * Date 2020/7/26 11:30
 * Desc SparkStreaming整合Kafka消费数据并处理-手动维护偏移量
 */
object SparkStreaming_Kafka2 {
  def main(args: Array[String]): Unit = {
    //1.准备环境
    val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))

    //2.准备Kafka的连接参数
    val kafkaParams: Map[String, Object] = Map[String, Object](
      "bootstrap.servers" -> "node01:9092",//Kafka集群地址
      "key.deserializer" -> classOf[StringDeserializer],//key的反序列化类型
      "value.deserializer" -> classOf[StringDeserializer],//value的反序列化类型
      "group.id" -> "spark",//消费者组id
      "auto.offset.reset" -> "latest",//偏移量自动重置位置
      "enable.auto.commit" -> (false: java.lang.Boolean)//flase表示不自动提交偏移量,那么就是手动提交,提交到默认主题__consumer_offsets中,如还开启了Checkpoint那么也会存在Checkpoint中一份
    )
    val topics = Array("spark_kafka")//要订阅的主题


    //3.使用spark-streaming-kafka-0-10_2.11依赖中提供的KafkaUtils的createDirectStream方法
    //使用直连方式/Direct模式直接对接Kafka的各个分区拉取分区中的数据返回DStream
    val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](topics, kafkaParams)
    )

    //4.消费数据并提交偏移量
    //我们需要手动提交偏移量,那么也就意味着,消费了一小批数据就应该提交一次偏移量
    //而在SparkStreaming中,一小批数据其实就对应与DStream中的一个RDD
    //也就是说我们接下来应该对DStream中的RDD进行处理/消费,每处理/消费一个RDD(每消费一小批数据)就应该提交一次偏移量
    //所以应该使用foreachRDD对Dstream里面的每个RDD进行操作
    kafkaDStream.foreachRDD(rdd=>{
      //如果rdd中有数据再消费并提交偏移量
      if(rdd.count() > 0){
        rdd.foreach(record=>println("消费到的Kafka的消息为:"+record))
        //消费到的Kafka的消息为:ConsumerRecord(topic = spark_kafka, partition = 1, offset = 3, CreateTime = 1595745414588, checksum = 778953969, serialized key size = -1, serialized value size = 31, key = null, value = hadoop hadoop spark spark spark)
        //代码走到这里说明该批次数据/该RDD的数据已经消费了,就应该提交偏移量了而偏移量如何提交?---参考官网,先获取再提交
        //获取偏移量
        //rdd中的数据就是从Kafka中消费的消息,里面应该包括了该消息的所有内容,包括key/value/topic/partition/offset
        //所以从rdd中获取偏移量,而spark-streaming-kafka-0-10_2.11依赖包中提供了便捷的API可以直接转换
        val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges//获取rdd中的偏移信息
        //如果想看提交的是啥玩意,可以将offsetRanges输出一下
        offsetRanges.foreach(o=>{
          println(s"topic=${o.topic},partition=${o.partition},from=${o.fromOffset},until=${o.untilOffset}")
          //topic=spark_kafka,partition=2,from=4,until=5
          //topic=spark_kafka,partition=1,from=3,until=4
          //topic=spark_kafka,partition=0,from=3,until=4
        })
        //提交偏移量
        kafkaDStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
      }
    })

    //5.开启并等待停止
    ssc.start()
    ssc.awaitTermination()
  }
}

代码实现-扩展-手动维护偏移量到MySQL

准备建表语句

CREATE TABLE `t_offset` (
  `topic` varchar(255) NOT NULL,
  `partition` int(11) NOT NULL,
  `groupid` varchar(255) NOT NULL,
  `offset` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`topic`,`partition`,`groupid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

准备操作MySQL数据库的工具类

package cn.hanjiaxiaozhi.stream

import java.sql.{DriverManager, PreparedStatement, ResultSet}

import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange

import scala.collection.mutable

object OffsetUtil {
    //从数据库读取偏移量
    def getOffsetMap(groupid: String, topic: String) = {
      val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "root", "root")
      val ps: PreparedStatement = connection.prepareStatement("select * from t_offset where groupid=? and topic=?")
      ps.setString(1, groupid)
      ps.setString(2, topic)
      val rs: ResultSet = ps.executeQuery()
      val offsetMap = mutable.Map[TopicPartition, Long]()
      while (rs.next()) {
        //offsetMap += new TopicPartition(rs.getString("topic"), rs.getInt("partition")) -> rs.getLong("offset")
        offsetMap.put(new TopicPartition(rs.getString("topic"), rs.getInt("partition")),rs.getLong("offset"))
      }
      rs.close()
      ps.close()
      connection.close()
      offsetMap
    }

    //将偏移量保存到数据库
    def saveOffsetRanges(groupid: String, offsetRange: Array[OffsetRange]) = {
      val connection =DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "root", "root")
      //replace into表示之前有就替换,没有就插入
      val ps: PreparedStatement = connection.prepareStatement("replace into t_offset (`topic`, `partition`, `groupid`, `offset`) values(?,?,?,?)")
      for (o <- offsetRange) {
        ps.setString(1, o.topic)
        ps.setInt(2, o.partition)
        ps.setString(3, groupid)
        ps.setLong(4, o.untilOffset)//消费到哪个偏移量了,下次应该接着这里消费
        ps.executeUpdate()
      }
      ps.close()
      connection.close()
    }
}

手动维护偏移量到MySQL

package cn.hanjiaxiaozhi.stream

import java.sql.DriverManager

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010._
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable

/**
 * Author hanjiaxiaozhi
 * Date 2020/7/26 11:30
 * Desc SparkStreaming整合Kafka消费数据并处理-手动维护偏移量到MySQL
 */
object SparkStreaming_Kafka3 {
  def main(args: Array[String]): Unit = {
    //1.准备环境
    val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))

    //2.准备Kafka的连接参数
    val kafkaParams: Map[String, Object] = Map[String, Object](
      "bootstrap.servers" -> "node01:9092",//Kafka集群地址
      "key.deserializer" -> classOf[StringDeserializer],//key的反序列化类型
      "value.deserializer" -> classOf[StringDeserializer],//value的反序列化类型
      "group.id" -> "spark",//消费者组id
      "auto.offset.reset" -> "latest",//偏移量自动重置位置
      "enable.auto.commit" -> (false: java.lang.Boolean)//flase表示不自动提交偏移量,那么就是手动提交,提交到默认主题__consumer_offsets中,如还开启了Checkpoint那么也会存在Checkpoint中一份
    )
    val topics = Array("spark_kafka")//要订阅的主题


    //3.使用spark-streaming-kafka-0-10_2.11依赖中提供的KafkaUtils的createDirectStream方法
    //使用spark-streamin连接kafka之前得去查看一下之前MySQL有没有记录offset信息,如果有则应该从记录的位置开始消费,如果没有从latest开始消费
    //offsetMap: mutable.Map[主题分区组成的TopicPartition对象, 下次应该要从哪个offset开始消费]
    val offsetMap: mutable.Map[TopicPartition, Long] = OffsetUtil.getOffsetMap("spark","spark_kafka")
    val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = if (offsetMap.size > 0){//map中有数据,表示MySQL中有offset记录,所以应该从该offset开始消费
      println("MySQL中有offset记录,所以应该从该offset记录开始消费")
      KafkaUtils.createDirectStream[String, String](
        ssc,
        LocationStrategies.PreferConsistent,
        ConsumerStrategies.Subscribe[String, String](topics, kafkaParams,offsetMap)
      )
    }else{//map中没有数据,表示MySQL中没有offset记录,所以应从latest开始消费
      println("MySQL中没有offset记录,所以从latest开始消费")
      //使用直连方式/Direct模式直接对接Kafka的各个分区拉取分区中的数据返回DStream
      KafkaUtils.createDirectStream[String, String](
        ssc,
        LocationStrategies.PreferConsistent,
        ConsumerStrategies.Subscribe[String, String](topics, kafkaParams)
      )
    }

    //4.消费数据并提交偏移量
    //我们需要手动提交偏移量,那么也就意味着,消费了一小批数据就应该提交一次偏移量
    //而在SparkStreaming中,一小批数据其实就对应与DStream中的一个RDD
    //也就是说我们接下来应该对DStream中的RDD进行处理/消费,每处理/消费一个RDD(每消费一小批数据)就应该提交一次偏移量
    //所以应该使用foreachRDD对Dstream里面的每个RDD进行操作
    kafkaDStream.foreachRDD(rdd=>{
      //如果rdd中有数据再消费并提交偏移量
      if(rdd.count() > 0){
        rdd.foreach(record=>println("消费到的Kafka的消息为:"+record))
        //消费到的Kafka的消息为:ConsumerRecord(topic = spark_kafka, partition = 1, offset = 3, CreateTime = 1595745414588, checksum = 778953969, serialized key size = -1, serialized value size = 31, key = null, value = hadoop hadoop spark spark spark)
        //代码走到这里说明该批次数据/该RDD的数据已经消费了,就应该提交偏移量了而偏移量如何提交?---参考官网,先获取再提交
        //获取偏移量
        //rdd中的数据就是从Kafka中消费的消息,里面应该包括了该消息的所有内容,包括key/value/topic/partition/offset
        //所以从rdd中获取偏移量,而spark-streaming-kafka-0-10_2.11依赖包中提供了便捷的API可以直接转换
        val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges//获取rdd中的便宜信息
        //提交偏移量
        //提交到默认主题
        //kafkaDStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
        //提交到MySQL
        OffsetUtil.saveOffsetRanges("spark",offsetRanges)
      }
    })

    //5.开启并等待停止
    ssc.start()
    ssc.awaitTermination()
  }
}