一、 整合版本说明
这是一种流式数据处理中最常见的方式之一,使用SparkStreaming去从kafka中拉取数据有两大主要的版本。主要在spark2.0开始之后进行区分。
- SparkStremaing-kafka-0-8版本
在此版本中有两种方式来消费kafka中的数据,receiver的方式(已经被淘汰);最早出现的拉取kafka数据的方式,在1.2开始出现。direct的方式是1.3版本出现才出现的。 - SparkStremaing-kafka-0-10版本
只有一种处理方式——那就是direct的方式
二、以SparkStremaing-kafka-0-8版本来讲解两种整合方式
1、SparkStreaming基于Receiver的方式整合Kafka
这种方式使用Receiver来获取数据。Receiver是使用Kafka的高层次Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存中的,然后Spark Streaming启动的job会去处理那些数据。
然而,在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制(Write Ahead Log,WAL)。该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。
1.1代码整合实现
/**
* 使用Reciever的方式整合kafka
*
* 二者整合的入口类就是KafkaUtils
*
* kafka-topics.sh --create --topic spark --zookeeper bigdata01:2181/kafka --partitions 3 --replication-factor 3
*/
object SparkStreamingWithReceiver2KafkaOps {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("SparkStreamingWithReceiver2Kafka")
.setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(2))
// readFromKafka1(ssc)
val kafkaParams = Map[String, String](
"zookeeper.connect" -> "bigdata01:2181,bigdata02:2181,bigdata03:2181/kafka",
"group.id" -> "test-group-2",
"auto.offset.reset" -> "smallest"
)
val topics = Map[String, Int](
"spark" -> 3
)
val messages:ReceiverInputDStream[(String, String)] = KafkaUtils
.createStream[String, String, StringDecoder, StringDecoder](ssc,
kafkaParams, topics, StorageLevel.MEMORY_ONLY)
messages.print()
ssc.start()
ssc.awaitTermination()
}
private def readFromKafka1(ssc: StreamingContext) = {
//接收kafka中的数据
val zkQuorum = "bigdata01:2181,bigdata02:2181,bigdata03:2181/kafka"
val groupId = "test-group-1"
val topics = MapString, Int
/*
返回值的key:kafka中每一条record对应的key
返回值的value:kafka中每一条record对应的value
这种方式只能从offset最开始的位置消费数据
*/
val inputStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc, zkQuorum, groupId, topics)
inputStream.print()
}
1.2receiver原理说明
1.3receiver方式需要注意的点
- Kafka的topic的partition和RDD的partition没有任何关系,增加topic的partition数量,只是增加receiver中用于读取数据的线程个数。
- 如果我们要想提升消费topic数据的能力,我们可以多创建几个receiver,最后将多个receiver读取到得数据经union联合之后进行处理
- 如果开启了基于hdfs的wal机制,就不要在创建流的时候指定StorageLevel的时候备份了。
开启wal日志,只需要配置参数spark.streaming.receiver.writeAheadLog.enable=true即可。同时需要执行checkpoint目录,将数据保存起来。
ssc.checkpoint(hdfs-path)
2、SparkStreaming基于Direct的方式整合kafka
2.1代码整合实现
object SparkStreamingWithDirectOps {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("SparkStreamingWithDirect")
.setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(2))
//kafkautils
val kafkaParams = Map[String, String](
"bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
"auto.offset.reset" -> "largest",
"group.id" -> "test-group-3"
)
val topics = "spark".split(",").toSet
val messages:InputDStream[(String, String)] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topics)
messages.print()
ssc.start()
ssc.awaitTermination()
}
}
2.2direct方式说明
在receiver的方式中,框架会自动的将偏移量信息保存在zk中,在direct方式中并没有这样做,因为此时不需要zookeeper,是直接从kafka中拉取数据,此时的spark程序就是kafka的消费者,而且没有使用receiver,使用kafka底层的消费者api来实现的,自然就不会再这个目录下面创建相关的记录。
此时正常运行过程中的程序,是如果从正确的偏移量位置来拉取数据的?比如第一次拉取了10条数据,假如此时的偏移量为10,下一次拉取数据应该从11开始?
既然没有zk来管理offset,只有sparkstreaming来进行管理!偏移量信息都会保存在DStream中,此时DStream是一个有范围(offset)的RDD,把这个rdd用HasOffsetRange来表示。
2.2.1direct方式如何自己来管理offset
direct模式已经不用zk来管理offset,自己来管理,在选择从kafka什么位置读取数据的时候有两种方式:earliest、lastest。如果我们使用lastest的是没有办法读取到之前的数据;如果使用earliest,每一次都会从最开始的位置来进行消费,会有有会有重复的消费。所以直接使用这种方式,不管是lastest还是earliest都不行,还得需要我们人工来干预offset。
那么如何自己来管理offset呢?其实管理offset的方式非常多,比如,基于zk、checkpoint、hbase、redis、elasticsearch等等。
三、direct方式自己来管理offset的代码实现
1、zookeeper管理偏移量
1.1引入依赖
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.8.0</version>
</dependency>
</dependencies>
1.2代码实现
import kafka.common.TopicAndPartition
import kafka.message.MessageAndMetadata
import kafka.serializer.StringDecoder
import org.apache.curator.framework.CuratorFrameworkFactory
import org.apache.curator.retry.ExponentialBackoffRetry
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka.{HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.zookeeper.data.Stat
import scala.collection.mutable
/**
* 使用zk来管理direct模式下面的offset
* 操作的流程
* 和基于receiver一直,不同点在于offset是否能为我们掌控
*/
object StreamingWithDirectZKOps {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("StreamingWithDirectZK")
.setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(2))
//kafkautils
val kafkaParams = Map[String, String](
"bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
"auto.offset.reset" -> "smallest",
"group.id" -> "test-group-3"
)
val topics = "hadoop".split(",").toSet
val messages = createMessage(ssc, kafkaParams, topics)
//foreachRDD -->遍历dstream中的每一个rdd
messages.foreachRDD((rdd, bTime) => {
if(!rdd.isEmpty()) {
println("-------------------------------------------")
println(s"Time: $bTime")
println("#####################rdd's count: " + rdd.count())
println("-------------------------------------------")
//存储偏移量
storeOffsets(rdd.asInstanceOf[HasOffsetRanges].offsetRanges, kafkaParams("group.id"))
}
})
ssc.start()
ssc.awaitTermination()
}
/**
* 更新offset
* /kafka/consumers/offsets/${topic}/${group}/${partition}
*/
def storeOffsets(offsetRanges: Array[OffsetRange], group:String) = {
for (offsetRange <- offsetRanges) {
val topic = offsetRange.topic
val partition = offsetRange.partition
val offset = offsetRange.untilOffset
val path = s"/offset/${topic}/${group}/${partition}"
checkExist(path)
curator.setData().forPath(path, (offset + "").getBytes())
}
}
def createMessage(ssc: StreamingContext, kafkaParams: Map[String, String], topics:Set[String]): InputDStream[(String, String)] = {
//step 1 读取偏移量
val fromOffsets:Map[TopicAndPartition, Long] = getFromOffsets(topics, kafkaParams("group.id"))
/*
step 2 拉取kafka数据
有offset
从相关偏移量的位置拉取数据
无offset
从指定的offset.auto.reset对应的位置开始拉取数据
*/
var messages:InputDStream[(String, String)] = null
if(!fromOffsets.isEmpty) {
//有偏移量
val messageHandler = (mmd: MessageAndMetadata[String, String]) => (mmd.key, mmd.message)
messages = KafkaUtils.createDirectStream[String, String,
StringDecoder, StringDecoder,
(String, String)](ssc,
kafkaParams, fromOffsets,
messageHandler)
} else {
//无偏移量
messages = KafkaUtils.createDirectStream[String, String,
StringDecoder, StringDecoder](ssc, kafkaParams, topics)
}
messages
}
/*
获取topic和partition对应的偏移量
1、既然是基于zk的方式来管理offset,那么就需要在zk中的某个目录对应节点中存储offset信息,每次再更新
2、所以我们可以模仿基于receiver的方式来保存的offset
3、操作zk得需要zk的client
约定数据存储的目录
/kafka/consumers/bd-1901-group-1/offsets/spark/0 -->基于receiver的方式
模仿
/kafka/consumers/offsets/${topic}/${group}/${partition}
|data-offset
*/
def getFromOffsets(topics:Set[String], group:String):Map[TopicAndPartition, Long] = {
val fromOffset = mutable.Map[TopicAndPartition, Long]()
import scala.collection.JavaConversions._
for(topic <- topics) {
val basePath = s"/offset/${topic}/${group}"
//basepath可能存在,可能不存在
checkExist(basePath)
val partitions = curator.getChildren.forPath(basePath)//partitions是一个java的集合
for(partition <- partitions) {
val path = s"${basePath}/${partition}"
val offset = new String(curator.getData.forPath(path)).toLong
fromOffset.put(TopicAndPartition(topic, partition.toInt), offset)
}
}
fromOffset.toMap
}
def checkExist(path:String): Unit = {
val stat:Stat = curator.checkExists().forPath(path)
if(stat == null) {
//创建该目录
curator.create()
.creatingParentsIfNeeded()//如果目录中存在多级未创建的目录,需要指定递归创建目录的方式
.forPath(path)
}
}
val curator = {
val client = CuratorFrameworkFactory.builder()
.connectString("bigdata01:2181,bigdata02:2181,bigdata03:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.namespace("kafka/consumers")//需要协商确定存储的zk的路径
.build()
client.start()
client
}
}
2、redis管理偏移量
一般在公司中都只搭建一个zookeeper集群,来管理hadoop、kafka、flink、zookeeper、hbase等等,如果我们还用zk的方式来管理offset,对zk的负载是很高的,那么如果zk挂了,后果就会很严重,所以我们可以用redis等等来代替zk在这里的功能。
2.1引入依赖
在pom里添加上redis依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2.2代码实现
/**
* 使用redis来管理direct模式下面的offset
* 操作的流程
* 和基于receiver一直,不同点在于offset是否能为我们掌控
*/
object _02StreamingWithDirectRedisOps {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("StreamingWithDirectRedis")
.setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(2))
//kafkautils
val kafkaParams = Map[String, String](
"bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
"auto.offset.reset" -> "smallest",
"group.id" -> "test-group-3"
)
val topics = "hadoop".split(",").toSet
val messages = createMessage(ssc, kafkaParams, topics)
//foreachRDD -->遍历dstream中的每一个rdd
messages.foreachRDD((rdd, bTime) => {
if(!rdd.isEmpty()) {
println("-------------------------------------------")
println(s"Time: $bTime")
println("#####################rdd's count: " + rdd.count())
println("-------------------------------------------")
//存储偏移量
storeOffsets(rdd.asInstanceOf[HasOffsetRanges].offsetRanges, kafkaParams("group.id"))
}
})
ssc.start()
ssc.awaitTermination()
}
/**
* 更新offset
* /kafka/consumers/offsets/${topic}/${group}/${partition}
*/
def storeOffsets(offsetRanges: Array[OffsetRange], group:String) = {
for (offsetRange <- offsetRanges) {
val topic = offsetRange.topic
val partition = offsetRange.partition
val offset = offsetRange.untilOffset
val field = s"${group}|${partition}"
jedis.hset(topic, field, offset.toString)
}
}
def createMessage(ssc: StreamingContext, kafkaParams: Map[String, String], topics:Set[String]): InputDStream[(String, String)] = {
//step 1 读取偏移量
val fromOffsets:Map[TopicAndPartition, Long] = getFromOffsets(topics, kafkaParams("group.id"))
/*
step 2 拉取kafka数据
有offset
从相关偏移量的位置拉取数据
无offset
从指定的offset.auto.reset对应的位置开始拉取数据
*/
var messages:InputDStream[(String, String)] = null
if(!fromOffsets.isEmpty) {
//有偏移量
val messageHandler = (mmd: MessageAndMetadata[String, String]) => (mmd.key, mmd.message)
messages = KafkaUtils.createDirectStream[String, String,
StringDecoder, StringDecoder,
(String, String)](ssc,
kafkaParams, fromOffsets,
messageHandler)
} else {
//无偏移量
messages = KafkaUtils.createDirectStream[String, String,
StringDecoder, StringDecoder](ssc, kafkaParams, topics)
}
messages
}
def getFromOffsets(topics:Set[String], group:String):Map[TopicAndPartition, Long] = {
val fromOffset = mutable.Map[TopicAndPartition, Long]()
import scala.collection.JavaConversions._
for(topic <- topics) {
val map = jedis.hgetAll(topic)
for((field, value) <- map) {//field=group|partition
val partition = field.substring(field.indexOf("|") + 1).toInt
val offset = value.toLong
fromOffset.put(TopicAndPartition(topic, partition), offset)
}
}
fromOffset.toMap
}
/*
redis的java的api是jedis
jedis对象相当于redis的connection连接对象
*/
val jedis = {
val jedis = new Jedis("bigdata01", 6379)
jedis
}
}
对于上述的代码有可以优化的地方,就是使用类似数据库连接池的思想构建redis的连接池
封装redis的工具类
public class JedisUtil {
private JedisUtil(){}
private static JedisPool pool;
static {
Properties properties = new Properties();
try {
properties.load(JedisUtil.class.getClassLoader().getResourceAsStream("jedis.properties"));
String host = properties.getProperty(Constants.JEDIS_HOST);
int port = Integer.valueOf(properties.getProperty(Constants.JEDIS_PORT, "6379"));
JedisPoolConfig config = new JedisPoolConfig();
pool = new JedisPool(config, host, port);
} catch (IOException e) {
e.printStackTrace();
}
}
public static Jedis getJedis() {
return pool.getResource();
}
public static void returnJedis(Jedis jedis) {
// pool.returnResource(jedis);
jedis.close();
}
}
public interface Constants {
String JEDIS_HOST = "jedis.host";
String JEDIS_PORT = "jedis.port";
}
jedis.properties
jedis.host=hadoop01
jedis.port=6379
功能代码修正
import db.JedisUtil
import kafka.common.TopicAndPartition
import kafka.message.MessageAndMetadata
import kafka.serializer.StringDecoder
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka.{KafkaUtils, OffsetRange, HasOffsetRanges}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scala.collection.mutable
object _03StreamingWithDirectRedis {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("StreamingWithDirectRedis")
.setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(2))
//kafkautils
val kafkaParams = Map[String, String](
"bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
"auto.offset.reset" -> "smallest",
"group.id" -> "test-group-3"
)
val topics = "hadoop".split(",").toSet
val messages = createMessage(ssc, kafkaParams, topics)
//foreachRDD -->遍历dstream中的每一个rdd
messages.foreachRDD((rdd, bTime) => {
if(!rdd.isEmpty()) {
println("-------------------------------------------")
println(s"Time: $bTime")
println("#####################rdd's count: " + rdd.count())
println("-------------------------------------------")
//存储偏移量
storeOffsets(rdd.asInstanceOf[HasOffsetRanges].offsetRanges, kafkaParams("group.id"))
}
})
ssc.start()
ssc.awaitTermination()
}
/**
* 更新offset
* /kafka/consumers/offsets/${topic}/${group}/${partition}
*/
def storeOffsets(offsetRanges: Array[OffsetRange], group:String) = {
val jedis = JedisUtil.getJedis
for (offsetRange <- offsetRanges) {
val topic = offsetRange.topic
val partition = offsetRange.partition
val offset = offsetRange.untilOffset
val field = s"${group}|${partition}"
jedis.hset(topic, field, offset.toString)
}
JedisUtil.returnJedis(jedis)
}
def createMessage(ssc: StreamingContext, kafkaParams: Map[String, String], topics:Set[String]): InputDStream[(String, String)] = {
//读取偏移量
val fromOffsets:Map[TopicAndPartition, Long] = getFromOffsets(topics, kafkaParams("group.id"))
var messages:InputDStream[(String, String)] = null
if(!fromOffsets.isEmpty) {
//有偏移量
val messageHandler = (mmd: MessageAndMetadata[String, String]) => (mmd.key, mmd.message)
messages = KafkaUtils.createDirectStream[String, String,
StringDecoder, StringDecoder,
(String, String)](ssc,
kafkaParams, fromOffsets,
messageHandler)
} else {
//无偏移量
messages = KafkaUtils.createDirectStream[String, String,
StringDecoder, StringDecoder](ssc, kafkaParams, topics)
}
messages
}
def getFromOffsets(topics:Set[String], group:String):Map[TopicAndPartition, Long] = {
val fromOffset = mutable.Map[TopicAndPartition, Long]()
import scala.collection.JavaConversions._
val jedis = JedisUtil.getJedis
for(topic <- topics) {
val map = jedis.hgetAll(topic)
for((field, value) <- map) {//field=group|partition
val partition = field.substring(field.indexOf("|") + 1).toInt
val offset = value.toLong
fromOffset.put(TopicAndPartition(topic, partition), offset)
}
}
JedisUtil.returnJedis(jedis)
fromOffset.toMap
}
}
3、checkpoint
在设置ssc.checkpoint()即可完成offset的存储
但是这种方式一般情况下都不用,因为会生成大量的小文件存储hdfs中,同时读取文件的效率要远低于读取内存文件的效率。