导入 spark 和 spark-streaming 依赖包
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>2.4.5</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.4.5</version>
</dependency>
从本机的7777端口源源不断地收到以换行符分隔的文本数据流,并计算单词个数
package cn.kgc.kb09.Spark
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.ReceiverInputDStream
object SparkStreamDemo1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("SparkStreamDemo1").setMaster("local[*]")
// 采集周期,指定的3秒为每次采集的时间间隔
val streamingContext = new StreamingContext(conf,Seconds(3))
// 指定采集的方法
val socketLineStream: ReceiverInputDStream[String] =
streamingContext.socketTextStream("192.168.247.201",7777)
// 将采集来的信息进行处理,统计数据(wordcount)
val wordStream = socketLineStream.flatMap(line => line.split("\\s+"))
val mapStream = wordStream.map(x => (x,1))
val wordcountStream = mapStream.reduceByKey(_+_)
// 打印
wordcountStream.print()
// 启动采集器
streamingContext.start()
streamingContext.awaitTermination()
}
}
这时候在Linux中输入的内容会在控制台打印wordcount单词统计:
根据指定的采集周期,每次采集的时间间隔3秒。spark streaming的本质是微批处理。
Spark Streaming拥有两类数据源
(1)基本源(Basic sources):这些源在StreamingContext API中直接可用。例如文件系统、套接字连接、Akka的actor等。
(2)高级源(Advanced sources):这些源包括Kafka,Flume,Kinesis,Twitter等等。
基本数据源输入源码:
SparkStream 对于外部的数据输入源,一共有下面几种:
(1)用户自定义的数据源:receiverStream
(2)根据TCP协议的数据源: socketTextStream、socketStream
(3)网络数据源:rawSocketStream
(4)hadoop文件系统输入源:fileStream、textFileStream、binaryRecordsStream
(5)其他输入源(队列形式的RDD):queueStream
package cn.kgc.kb09.Spark;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaPairDStream;
import org.apache.spark.streaming.api.java.JavaReceiverInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import scala.Tuple2;
import java.util.Arrays;
import java.util.Iterator;
/**
* @Qianchun
* @Date 2020/12/18
* @Description
*/
public class SparkStreamJavaDemo1 {
public static void main(String[] args) throws InterruptedException {
// 第一步:配置SparkConf
SparkConf conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreamJavaDemo1");
// 第二步:创建SparkStreamingContext
JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(3));
/**
* 第三步:创建Spark Streaming 输入数据来源 input Stream
* 1、数据输入来源可以基于 File,HDFS,Flume,Kafka,Socket等
* 2.在这里我们制定数据来源于网络 Socket端口,Spark Streaming链接上改端口并在运行的时候一直监听该端口的数据(当然该端口服务首先必须存在),
* 并且在后续会根据业务需要不断的有数据产生(当然对于Spark Streaming 引用程序的运行而言,有无数据其处理流程都是一样的)
* 3.如果经常在每隔 5 秒钟没有数据的话不断的启动空的 Job 其实是会造成调度资源的浪费,因为彬没有数据需要发生计算;
* 真实的企业级生产环境的代码在具体提交 Job 前会判断是否有数据,如果没有的话,不再提交 Job;
*/
JavaReceiverInputDStream<String> lines = jsc.socketTextStream("192.168.247.201", 7777);
/**第四步:接下来就是对于 Rdd编程一样基于 DStream进行编程
* 原因是DStream是RDD产生的模板(或者说类), 在 Saprk Stream发生计算前,其实质是把每个 Batch的DStream的操作翻译成为 Rdd 的操作!!!
*/
JavaDStream<String> flatmap = lines.flatMap(new FlatMapFunction<String, String>() {
@Override
public Iterator<String> call(String s) throws Exception {
String[] split = s.split("\\s+");
return Arrays.asList(split).iterator();
}
});
JavaPairDStream<String, Integer> mapToPair = flatmap.mapToPair(new PairFunction<String, String, Integer>() {
@Override
public Tuple2<String, Integer> call(String s) throws Exception {
return new Tuple2<String, Integer>(s, 1);
}
});
JavaPairDStream<String, Integer> reduceByKey = mapToPair.reduceByKey(new Function2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer v1, Integer v2) throws Exception {
return v1 + v2;
}
});
/**
* 此处print并不会直接触发 job 的执行,因为现在的一切都是在 Spark Streaming 框架的控制之下的,对于 Spark Streaming 而言具体是否触发真正的 job 运行
* 是基设置的 Duration 时间间隔触发
* 一定要注意的是 Spark Streaming应用程序要想执行具体的Job,对DStream就必须有 output Stream操作
* output Stream有很多类型的函数触发,类print,saveAsTextFile,saveAsHadoopFile等,最为重要的一个方法是 foreachRDD,因为Spark Streaming处理的结果一般都会放在 Redis,DB,
* DashBoard等上面,foreachRDD主要就是用来完成这些功能的,而且可以随意的自定义具体数据到底放在那里
*/
reduceByKey.print();
/**
* Spark Streaming 执行引擎也就是Driver开始运行,Driver启动的时候是位于一条新的线程中的,当然其内部有消息接收应用程序本身或者 Executor 中的消息;
*/
jsc.start();
jsc.awaitTermination();
}
}
运行代码,在虚拟机上输入nc -lk 7777
代表向7777号端口输入数据,来进行测试,会计算出每三秒中每个词出现的次数
还是以wordcount为例,自定义Receiver实现一下
声明一个receiver类,通常需要继承原有的基类,在这里需要继承自Receiver,该基类有两个方法需要重写分别是:
1、 onstart() 接收器开始运行时触发方法,在该方法内需要启动一个线程,用来接收数据。
2、onstop() 接收器结束运行时触发的方法,在该方法内需要确保停止接收数据。
当然在接收数据流过程中也可能会发生终止接收数据的情况,这时候onstart内可以通过isStoped()来判断 ,是否应该停止接收数据
数据存储:
一旦接收完数据,则必须要进行数据的存储,并交由SparkStreaming 来处理,Spark以store(data)方法来支持此流程。由于数据格式的不同,当然store方法必须要支持各种类型的数据存储。store方法是以一次存储一条记录或者一次性收集全部的序列化对象。
代码实现:
采集端口内输入内容,接收到“end”停止
package cn.kgc.kb09.Spark
import java.io.{BufferedReader, InputStreamReader}
import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.receiver.Receiver
import org.apache.spark.streaming.{Seconds, StreamingContext}
// 自定义采集器
// 1)继承Receiver
// 2)重写方法 onStart(), onStop()
class MyReceiver(host:String, port:Int) extends Receiver [String](StorageLevel.MEMORY_ONLY) {
// 接收socket数据
var socket: java.net.Socket = null
def receive: Unit = {
socket = new java.net.Socket(host, port)
// 通过BufferedReader ,将输入流转换为字符串
val reader = new BufferedReader(
new InputStreamReader(socket.getInputStream, "UTF-8")
)
var line: String = null
// 将采集的数存储到采集器的内部进行转换
while ((line=reader.readLine()) != null) {
if (line.equals("end")) {
return
} else {
this.store(line)
}
}
}
override def onStart(): Unit = {
new Thread(new Runnable {
override def run(): Unit = {
receive
}
}).start()
}
override def onStop(): Unit = {
if (socket != null) {
socket.close()
socket = null
}
}
}
object MyReceiverStream {
def main(args: Array[String]): Unit = {
// spark的配置对象
val conf = new SparkConf().setMaster("local[*]").setAppName("myReceiverStream")
// 实时分析的环境对象
// 采集周期:以指定的时间为周期采集实时数据
val streamingContext = new StreamingContext(conf, Seconds(5))
// 在这里转换成自定义的采集器
val receiverStream =
streamingContext.receiverStream(new MyReceiver("192.168.247.201", 7777))
// 将采集的数据进行分割
val lineStream = receiverStream.flatMap(line => line.split("\\s+"))
// 将数据进行结构的转变进行统计分析
val mapStream = lineStream.map((_, 1))
// 将转换结构后的数据进行聚合处理
val sumStream = mapStream.reduceByKey(_ + _)
// 将结果打印
sumStream.print()
// 启动采集器
streamingContext.start()
// Driver等待采集器的执行
streamingContext.awaitTermination()
}
}
运行代码,启动nc
nc-lk 7777
textFileStream路径如果是hdfs的路径 你直接hdfs dfs -put
到你的监测路径就可以
如果是本地目录如E:\\qianchun\\Kafka\\kafkaStream\\in\\test
,你不能直接在目录里创建文件或移动文件到这个目录,必须用流的形式写入到这个目录形成文件才能被监测到。可在其它地方创建一个文件然后另存到此本地目录下可以完成此项测试。
package cn.kgc.kb09.Spark
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object SparkStreamFileDataSourceDemo2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[2]").setAppName("FileDataSource")
// 创建StreamingContext对象
val streamingContext = new StreamingContext(conf,Seconds(5))
// 文件为HDFS文件
// val fileDStream: DStream[String] = streamingContext.textFileStream("hdfs://hadoopwei:9000/kb09file")
// 文件为本地Windows文件
val inputDir = "E:\\qianchun\\Kafka\\kafkaStream\\in\\test"
// 对StreamingContext对象调用 .textFileStream()方法生成一个文件流类型的InputStream
val fileDStream = streamingContext.textFileStream(inputDir)
// 编写流计算过程
val wordStream = fileDStream.flatMap(line => line.split("\\s+"))
val mapStream = wordStream.map((_,1))
val sumStream = mapStream.reduceByKey(_+_)
// 打印结果
sumStream.print()
// 启动流计算
streamingContext.start()
streamingContext.awaitTermination()
}
}
(1) 版本选型
ReceiverAPI:需要一个专门的 Executor 去接收数据,然后发送给其他的 Executor 做计算。存在的问题,接收数据的 Executor 和计算的 Executor 速度会有所不同,特别在接收数据的 Executor速度大于计算的 Executor 速度,会导致计算数据的节点内存溢出。早期版本中提供此方式,当前版本不适用。
DirectAPI:是由计算的 Executor 来主动消费 Kafka 的数据,速度由自身控制。
(2)Kafka 0-10 Direct模式
需求:通过 SparkStreaming 从 Kafka 读取数据,并将读取过来的数据做简单计算,最终打印到控制台。
导入spark-streaming-kafka依赖包
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
<version>2.4.5</version>
</dependency>
出现报错,因为有一个依赖版本过高:
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Incompatible Jackson version: 2.9.6
需要添加依赖:
<!-- 版本降维 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.6</version>
</dependency>
代码部分
package cn.kgc.kb09.Spark
import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object SparkStreamKafkaSource {
def main(args: Array[String]): Unit = {
// 创建SparkConf
val conf = new SparkConf().setMaster("local[2]").setAppName("KafkaDemo")
// 创建StreamingContext
val streamingContext = new StreamingContext(conf,Seconds(5))
// 设置checkpoint目录
streamingContext.checkpoint("checkpoint")
// 配置Kafka相关参数
val kafkaParams: Map[String, String] = Map(
// kafka集群有几台机器就写几台
(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "192.168.247.201:9092"),
// 因为是消费topic,所以需要K-V反序列化
(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer"),
(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer"),
// 定义消费者组别
(ConsumerConfig.GROUP_ID_CONFIG, "kafkaGroup1")
)
// 通过 KafkaUtils.createDirectStream接受kafka数据,这里采用是kafka低级api偏移量不受zk管理
val kafkaStream: InputDStream[ConsumerRecord[String, String]] =
KafkaUtils.createDirectStream(
streamingContext, //不再是直接从streamingContext点出来的基本源,而是作为参数生成InputDStream
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe(Set("sparkKafkaDemo"), kafkaParams) //sparkKafkaDemo是生产者的topic
)
// 对获取的数据进行处理
val wordStream = kafkaStream.flatMap(v => v.value().toString.split("\\s+"))
val mapStream = wordStream.map((_,1))
// 无状态
// val sumStream = mapStream.reduceByKey(_+_)
// sumStream.print()
// 有状态 hello,2 再输入hello,则返回(2,1)
// 前提条件:需要设置checkpoint
val stateSumStream: DStream[(String, Int)] = mapStream.updateStateByKey {
case (seq, buffer) => {
println(seq, seq.sum, buffer.getOrElse(0))
val sum = buffer.getOrElse(0) + seq.sum
Option(sum)
}
}
// 打印结果
stateSumStream.print()
// 启动流计算
streamingContext.start()
streamingContext.awaitTermination()
}
}
通过 KafkaUtils.createDirectStream接受kafka数据,这里采用是kafka低级api偏移量不受zk管理
LocationStrategies
:本地策略。为提升性能,可指定Kafka Topic Partition的消费者所在的Executor。
LocationStrategies.PreferConsistent
:一致性策略。一般情况下用这个策略就OK。将分区尽可能分配给所有可用Executor。
LocationStrategies.PreferBrokers
:特殊情况,如果Executor和Kafka Broker在同一主机,则可使用此策略。
LocationStrategies.PreferFixed
:特殊情况,当Kafka Topic Partition负荷倾斜,可用此策略,手动指定Executor来消费特定的Partition.
ConsumerStrategies
:消费策略。
ConsumerStrategies.Subscribe/SubscribePattern
:可订阅一类Topic,且当新Topic加入时,会自动订阅。一般情况下,用这个就OK。
ConsumerStrategies.Assign
:可指定要消费的Topic-Partition,以及从指定Offset开始消费。
特点:
1、不需要使用单独的Receiver线程从Kafka获取数据
2、使用Kafka简单消费者API,不需要ZooKeeper参与,直接从Kafka Broker获取数据。
3、执行过程:Spark Streaming Batch Job触发时,Driver端确定要读取的Topic-Partition的OffsetRange,然后由Executor并行从Kafka各Partition读取数据并计算。
4、为保证整个应用EOS, Offset管理一般需要借助外部存储实现。如Mysql、HBase等。
5、由于不需要WAL,且Spark Streaming会创建和Kafka Topic Partition一样多的RDD Partition,且一一对应,这样,就可以并行读取,大大提高了性能。
6、Spark Streaming应用启动后,自己通过内部currentOffsets变量跟踪Offset,避免了基于Receiver的方式中Spark Streaming和Zookeeper中的Offset不一致问题。
参考文献:
https://www.cnblogs.com/redhat0019/p/10817597.html
https://blog.csdn.net/timicai/article/details/111485113
https://blog.csdn.net/wangpei1949/article/details/89419691
https://www.cnblogs.com/upupfeng/p/12325201.html