Spark Streaming整合kafka-★★★★★
- Kafka概念回顾
- Kafka命令回顾
- 整合方式说明
- Receiver模式--仅仅为了面试
- Direct模式--开发用这个
- 结论
- 整合API说明
- 代码实现-自动提交偏移量
- 代码实现-扩展-手动维护偏移量到默认主题
- 代码实现-扩展-手动维护偏移量到MySQL
- 准备建表语句
- 准备操作MySQL数据库的工具类
- 手动维护偏移量到MySQL
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有变化(更加强大)
- 以前官网上有下面的表格:
<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()
}
}