0. SparkStreaming
- 流式计算简介
- SparkStreaming实时处理入门案例
- SparkStreaming和HDFS整合
- SparkStreaming与Kafka整合
- SparkStreaming常见transformation算子
- SparkStreaming高可用及其优化建议
1. 流式计算简介
1.1. 流式计算
如何去理解流式计算,最形象的例子,就是小明的往水池中放(入)水又放(出)水的案例。流式计算就像水流一样,数据连绵不断的产生,并被快速处理,所以流式计算拥有如下一些特点:
-
数据是无界的(unbounded)
-
数据是动态的
-
计算速度是非常快的
-
计算不止一次
-
计算不能终止
反过来看看一下离线计算有哪些特点:
-
数据是有界的(Bounded)
-
数据静态的
-
计算速度通常较慢
-
计算只执行一次
-
计算终会终止
1.2. 常见的离线和流式计算框架
-
常见的离线计算框架
-
mapreduce
-
spark-core
-
flink-dataset
-
-
常见的流式计算框架
-
storm(jstorm)
第一代的流式处理框架,每生成一条记录,提交一次作业。实时流处理,延迟低。
-
spark-streaming
第二代的流式处理框架,短时间内生成mirco-batch,提交一次作业。准实时,延迟略高,秒级或者亚秒级延迟。
-
flink-datastream(blink)
第三代的流式处理框架,每生成一条记录,提交一次作业。实时,延迟低。
-
1.3. SparkStreaming简介
SparkStreaming,和SparkSQL一样,也是Spark生态栈中非常重要的一个模块,主要是用来进行流式计算的框架。流式计算框架,从计算的延迟上面,又可以分为纯实时流式计算和准实时流式计算,SparkStreaming是属于的准实时计算框架。
所谓纯实时的计算,指的是来一条记录(event事件),启动一次计算的作业;离线计算,指的是每次计算一个非常大的一批(比如几百G,好几个T)数据;准实时呢,介于纯实时和离线计算之间的一种计算方式。显然不是每一条记录就计算一次,显然比起离线计算数据量小的多,怎么表示?Micro-batch(微小的批次)。
SparkStreaming是SparkCore的api的一种扩展,使用DStream(discretized stream or DStream)作为数据模型,基于内存处理连续的数据流,本质上还是RDD的基于内存的计算。
DStream,本质上是RDD的序列。SparkStreaming的处理流程可以归纳为下图:
1.4. SparkStreaming基本工作原理
接收实时输入数据流,然后将数据拆分成多个batch,比如每收集1秒的数据封装为一个batch,然后将每个batch交给Spark的计算引擎进行处理,最后会生产出一个结果数据流,其中的数据,也是由一个一个的batch所组成的。
Spark Streaming提供了一种高级的抽象,叫做DStream,英文全称为Discretized Stream,中文翻译为“离散流”,它代表了一个持续不断的数据流。DStream可以通过输入数据源来创建,比如Kafka、Flume、ZMQ和Kinesis;也可以通过对其他DStream应用高阶函数来创建,比如map、reduce、join、window。
DStream的内部,其实一系列持续不断产生的RDD。RDD是Spark Core的核心抽象,即,分布式式弹性数据集。DStream中的每个RDD都包含了一个时间段内的数据。
对DStream应用的算子,比如map,其实在底层会被翻译为对DStream中每个RDD的操作。比如对一个DStream执行一个map操作,会产生一个新的DStream。但是,在底层,其实其原理为,对输入DStream中每个时间段的RDD,都应用一遍map操作,然后生成的新的RDD,即作为新的DStream中的那个时间段的一个RDD。底层的RDD的transformation操作。
还是由Spark Core的计算引擎来实现的。Spark Streaming对Spark Core进行了一层封装,隐藏了细节,然后对开发人员提供了方便易用的高层次的API。
1.5. Storm V.S. SparkStreaming V.S. Flink
- 三者对比
-
storm和flink简介
storm: storm.apache.org
flink: flink.apache.org
1.6. 如何选择一款合适的流式处理框架
-
对于Storm来说:
1、建议在需要纯实时,不能忍受1秒以上延迟的场景下使用,比如实时计算系统,要求纯实时进行交易和分析时。
2、在实时计算的功能中,要求可靠的事务机制和可靠性机制,即数据的处理完全精准,一条也不能多,一条也不能少,也可以考虑使用Storm,但是Spark Streaming也可以保证数据的不丢失。
3、如果我们需要考虑针对高峰低峰时间段,动态调整实时计算程序的并行度,以最大限度利用集群资源(通常是在小型公司,集群资源紧张的情况),我们也可以考虑用Storm -
对于Spark Streaming来说:
1、不满足上述3点要求的话,我们可以考虑使用Spark Streaming来进行实时计算。
2、考虑使用Spark Streaming最主要的一个因素,应该是针对整个项目进行宏观的考虑,即,如果一个项目除了实时计算之外,还包括了离线批处理、交互式查询、图计算和MLIB机器学习等业务功能,而且实时计算中,可能还会牵扯到高延迟批处理、交互式查询等功能,那么就应该首选Spark生态,用Spark Core开发离线批处理,用Spark SQL开发交互式查询,用Spark Streaming开发实时计算,三者可以无缝整合,给系统提供非常高的可扩展性。 -
对于Flink来说:
支持高吞吐、低延迟、高性能的流处理
支持带有事件时间的窗口(Window)操作
支持有状态计算的Exactly-once语义
支持高度灵活的窗口(Window)操作,支持基于time、count、session,以及data-driven的窗口操作
支持具有Backpressure功能的持续流模型
支持基于轻量级分布式快照(Snapshot)实现的容错
一个运行时同时支持Batch on Streaming处理和Streaming处理
Flink在JVM内部实现了自己的内存管理
支持迭代计算
支持程序自动优化:避免特定情况下Shuffle、排序等昂贵操作,中间结果有必要进行缓存
2. SparkStreaming实时处理入门案例
2.1. 创建项目模块
指定maven左表
执行存储位置
导入maven依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.2.2</version>
</dependency>
完整的pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spark-parent-1903</artifactId>
<groupId>com.desheng.parent</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.desheng.bigdata</groupId>
<artifactId>spark-streaming</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
</dependency>
</dependencies>
</project>
2.2. 编码
2.2.1. 入口类StreamingContext
SparkStreaming中的入口类,称之为StreamingContext,但是底层还是得需要依赖SparkContext。
object _01SparkStreamingWordCountOps {
def main(args: Array[String]): Unit = {
/*
StreamingContext的初始化,需要至少两个参数,SparkConf和BatchDuration
SparkConf不用多说
batchDuration:提交两次作业之间的时间间隔,每次会提交一个DStream,将数据转化batch--->RDD
所以说:sparkStreaming的计算,就是每隔多长时间计算一次数据
*/
val conf = new SparkConf()
.setAppName("SparkStreamingWordCount")
.setMaster("local[*]")
val duration = Seconds(2)
val ssc = new StreamingContext(conf, duration)
//业务
//为了执行的流式计算,必须要调用start来启动
ssc.start()
//为了不至于start启动程序结束,必须要调用awaitTermination方法等待程序业务完成之后调用stop方法结束程序,或者异常
ssc.awaitTermination()
}
}
2.2.2. 业务编写
object _01SparkStreamingWordCountOps {
def main(args: Array[String]): Unit = {
if(args == null || args.length < 2) {
println(
"""
|Usage: <hostname> <port>
""".stripMargin)
System.exit(-1)
}
val Array(hostname, port) = args
/*
StreamingContext的初始化,需要至少两个参数,SparkConf和BatchDuration
SparkConf不用多说
batchDuration:提交两次作业之间的时间间隔,每次会提交一个DStream,将数据转化batch--->RDD
所以说:sparkStreaming的计算,就是每隔多长时间计算一次数据
*/
val conf = new SparkConf()
.setAppName("SparkStreamingWordCount")
.setMaster("local[*]")
val duration = Seconds(2)
val ssc = new StreamingContext(conf, duration)
//接入数据
val lines:ReceiverInputDStream[String] = ssc.socketTextStream(hostname, port.toInt)
// lines.print()
val retDStream:DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_, 1)).reduceByKey(_+_)
retDStream.print()
//为了执行的流式计算,必须要调用start来启动
ssc.start()
//为了不至于start启动程序结束,必须要调用awaitTermination方法等待程序业务完成之后调用stop方法结束程序,或者异常
ssc.awaitTermination()
}
}
2.2.3. 使用netcat进行测试
2.3. StreamingContext和Receiver说明
2.3.1. StreamingContext
StreamingContext是程序的入口类,用于创建DStream,维护SparkStreaming程序的声明周期。
-
关于local说明
当我们将上述程序中的master由local[*],修改为local的时候,程序业务不变,发生只能接收数据,无法处理数据。
local[*]和local的区别,后者只为当前程序提供一个线程来处理,前者提供可用的所有的cpu的core来处理,当前情况下为2或者4。
所以我们推测,当前程序无法处理数据的原因,只能是拥有cpu-core或者线程个数造成的。
同时还可以推到出来的是,SparkStreaming在当下案例中,优先使用线程资源来接收数据,其次才是对数据的处理,接收数据的对象就是Receiver。
所以,以后注意,如果读取数据的时候有receiver,程序的线程个数至少为2。
-
start
start方法是用来启动当前sparkStreaming应用的,所以,是不能在ssc.start()之后再添加任何业务逻辑,否则,凉凉!
- awaitTermination
2.3.2. Receiver
Receiver,顾名思义,就是数据的接收者,这里把资源分成了两部分,一部分用来接收数据,一部分用来处理数据。Receiver接收到的数据,说白了就是一个个的batch数据,是RDD,存储在Executor内存。Receiver就是Executor内存中的一部分。
不是所有的streaming作业都需要有Receiver。
通过下图,来阐述基于Receiver的程序执行的流程
3. SparkStreaming和HDFS整合
3.1. 说明
SparkStreaming监听hdfs的某一个目录,目录下的新增文件,做实时处理。这种方式在特定情况下还是挺多的。需要使用的api为:ssc.fileStream()。
监听的文件,必须要从另一个相匹配的目录移动到其它目录。
-
监听本地
无法读取手动拷贝,或者剪切到指定目录下的文件,只能读取通过流写入的文件。
-
监听hdfs
有的操作系统和监听本地是一样。
正常情况下,我们可以读取到通过put上传的文件,还可以读取通过cp拷贝的文件,但是读取不了mv移动的文件。
读取文件的这种方式,没有额外的Receiver消耗线程资源,所以可以指定master为local
3.2. 编码
object _01SparkStreamingHDFS {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.spark_project").setLevel(Level.WARN)
val conf = new SparkConf()
.setAppName("SparkStreamingHDFS")
.setMaster("local")
val duration = Seconds(2)
val ssc = new StreamingContext(conf, duration)
//读取local中数据 --->需要通过流的方式写入
// val lines = ssc.textFileStream("file:///E:/data/monitored")
//hdfs
val lines = ssc.textFileStream("hdfs://bigdata01:9000/data/spark")
lines.print()
ssc.start()
ssc.awaitTermination()
}
}
4. SparkStreaming与Kafka整合(*)
4.1. 整合简述
kafka是做消息的缓存,数据和业务隔离操作的消息队列,而sparkstreaming是一款准实时流式计算框架,所以二者的整合,是大势所趋。
二者的整合,有主要的两大版本。
在spark-stremaing-kafka-0-8的版本中又分为了两种方式:receiver的方式和direct的方式来读取kafka中的数据,主要区别就是是否依赖zookeeper来管理offset信息,以及是否拥有receiver。
4.2. spark-stremaing-kafka-0-8
api地址:
http://spark.apache.org/docs/2.2.2/streaming-kafka-0-8-integration.html
导入依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
<version>2.2.2</version>
</dependency>
入口类便是KafkaUtils
4.2.1. Receiver的方式
-
编程
/** * 使用kafka的receiver-api读取数据 */ object _02SparkStreamingWithKafkaReceiverOps { def main(args: Array[String]): Unit = { val conf = new SparkConf() .setAppName("SparkStreamingWithKafkaReceiver") .setMaster("local[*]") val duration = Seconds(2) val ssc = new StreamingContext(conf, duration) val kafkaParams = Map[String, String]( "zookeeper.connect" -> "bigdata01:2181,bigdata02:2181,bigdata03:2181/kafka", "group.id" -> "g_1903_1", "zookeeper.connection.timeout.ms" -> "10000" ) val topics = Map[String, Int]( "spark" -> 3 ) val messages:ReceiverInputDStream[(String, String)] = KafkaUtils .createStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topics,StorageLevel.MEMORY_AND_DISK_SER_2) messages.print() ssc.start() ssc.awaitTermination() } }
-
说明
这种方式使用Receiver来获取数据。Receiver是使用Kafka的高层次Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存中的,然后Spark Streaming启动的job会去处理那些数据。 然而,在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制(Write Ahead Log,WAL)。该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。
-
需要注意的地方
-
Kafka的topic分区和Spark Streaming中生成的RDD分区没有关系。 在KafkaUtils.createStream中增加分区数量只会增加单个receiver的线程数,不会增加Spark的并行度
-
可以创建多个的Kafka的输入DStream, 使用不同的group和topic, 使用多个receiver并行接收数据。
-
如果启用了HDFS等有容错的存储系统,并且启用了写入日志,则接收到的数据已经被复制到日志中。因此,输入流的存储级别设置StorageLevel.MEMORY_AND_DISK_SER(即使用KafkaUtils.createStream(…,StorageLevel.MEMORY_AND_DISK_SER))的存储级别。
-
-
数据会丢失原因
4.2.2. Direct的方式
-
编码
//基于direct方式整合kafka object _03SparkStreamingWithKafkaDirectOps { def main(args: Array[String]): Unit = { Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN) Logger.getLogger("org.apache.spark").setLevel(Level.WARN) Logger.getLogger("org.spark_project").setLevel(Level.WARN) val conf = new SparkConf() .setAppName("SparkStreamingWithKafkaDirect") .setMaster("local[*]") val duration = Seconds(2) val ssc = new StreamingContext(conf, duration) val kafkaParams = Map[String, String]( "bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092", "group.id" -> "g_1903_2", "auto.offset.reset" -> "largest" ) val topics = "spark".split(",").toSet val messages: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topics) messages.foreachRDD((rdd, bTime) => { if(!rdd.isEmpty()) { val offsetRDD = rdd.asInstanceOf[HasOffsetRanges] val offsetRanges = offsetRDD.offsetRanges for(offsetRange <- offsetRanges) { val topic = offsetRange.topic val partition = offsetRange.partition val fromOffset = offsetRange.fromOffset val untilOffset = offsetRange.untilOffset println(s"topic:${topic}\tpartition:${partition}\tstart:${fromOffset}\tend:${untilOffset}") } rdd.count() } }) ssc.start() ssc.awaitTermination() } }
-
说明
-
简化的并行性:不需要创建多个输入Kafka流并将其合并。 使用directStream,Spark Streaming将创建与使用Kafka分区一样多的RDD分区,这些分区将全部从Kafka并行读取数据。 所以在Kafka和RDD分区之间有一对一的映射关系。
-
效率:在第一种方法中实现零数据丢失需要将数据存储在预写日志中,这会进一步复制数据。这实际
上是效率低下的,因为数据被有效地复制了两次:一次是Kafka,另一次是由预先写入日志(Write
Ahead Log)复制。这个第二种方法消除了这个问题,因为没有接收器,因此不需要预先写入日志。
只要Kafka数据保留时间足够长。 -
正好一次(Exactly-once)的语义:第一种方法使用Kafka的高级API来在Zookeeper中存储消耗的偏移量。传统上这是从Kafka消费数据的方式。虽然这种方法(结合提前写入日志)可以确保零数据丢失(即至少一次语义),但是在某些失败情况下,有一些记录可能会消费两次。发生这种情况是因为Spark Streaming可靠接收到的数据与Zookeeper跟踪的偏移之间的不一致。因此,在第二种方法中,我们使用不使用Zookeeper的简单Kafka API。在其检查点内,Spark Streaming跟踪偏移量。这消除了Spark Streaming和Zookeeper/Kafka之间的不一致,因此Spark Streaming每次记录都会在发生故障的情况下有效地收到一次。为了实现输出结果的一次语义,将数据保存到外部数据存储区的输出操作必须是幂等的,或者是保存结果和偏移量的原子事务。
幂等说明:多次操作结果都一样,把这种操作称之为幂等操作,比如数据库的delete操作,或者:
INSERT INTO USER (id, NAME, age)
-