SparkStreaming

Spark Streaming
1. SparkStreaming 简介
SparkStreaming 流式处理框架 ,是 Spark API RDD )的扩展,支持可扩展、高吞吐量、容错的
实时数据流处理
实时数据的来源可以是: Kafka, Flume, Twitter, ZeroMQ 或者 TCP sockets ,在接受数据同时可以
使用高级功能的 复杂算子来处理流数据
最终处理后的数据可以存放在文件系统,数据库等,方便实时展现。
2. SparkStreaming Storm 的区别
处理模型以及延迟
虽然两框架都提供了可扩展性 (scalability) 和可容错性 (fault tolerance) ,但是它们的处理模型
从根本上说是不一样的。 Storm 可以实现亚秒级时延的处理,而每次只处理一条 event ,而
Spark Streaming 可以在一个短暂的时间窗口里面处理多条 (batches)Event 。所以说 Storm
以实现亚秒级时延的处理,而 Spark Streaming 则有一定的时延。
容错和数据保证
然而两者的代价都是容错时候的数据保证, Spark Streaming 的容错为有状态的计算提供了更
好的支持。在 Storm 中,每条记录在系统的移动过程中都需要被标记跟踪,所以 Storm 只能保
证每条记录最少被处理一次,但是允许从错误状态恢复时被处理多次。这就意味着可变更的状
态可能被更新两次从而导致结果不正确。
Spark Streaming 仅仅需要在批处理级别对记录进行追踪,所以他能保证每个批处理记录仅仅
被处理一次,即使是 node 节点挂掉。虽然说 Storm Trident library 可以保证一条记录被处
理一次,但是它依赖于事务更新状态,而这个过程是很慢的,并且需要由用户去实现。
实现和编程 API
Storm 主要是由 Clojure 语言实现, Spark Streaming 是由 Scala 实现。如果你想看看这两个框
架是如何实现的或者你想自定义一些东西你就得记住这一点。 Storm 是由 BackType Twitter
开发,而 Spark Streaming 是在 UC Berkeley 开发的。
Storm 提供了 Java API ,同时也支持其他语言的 API Spark Streaming 支持 Scala Java 语言
( 其实也支持 Python)
批处理框架集成
Spark Streaming 的一个很棒的特性就是它是在 Spark 框架上运行的。这样你就可以想使用其
他批处理代码一样来写 Spark Streaming 程序,或者是在 Spark 中交互查询。这就减少了单独
编写流批量处理程序和历史数据处理程序。
生产支持
Storm 已经出现好多年了,而且自从 2011 年开始就在 Twitter 内部生产环境中使用,还有其他
一些公司。而 Spark Streaming 是一个新的项目,并且在 2013 年仅仅被 Sharethrough 使用 ( 作者了解 )
Storm Hortonworks Hadoop 数据平台中流处理的解决方案,而 Spark Streaming 出现在
MapR 的分布式平台和 Cloudera 的企业数据平台中。除此之外, Databricks 是为 Spark 提供技
术支持的公司,包括了 Spark Streaming
3. SparkStreaming 流式计算
3.1. 流式计算过程
计算流程
Spark Streaming 是将流式计算分解成一系列短小的批处理作业。
批处理引擎是 Spark Core ,也就是把 Spark Streaming 的输入数据按照 batch size (如 1 秒)分
成一段一段的数据( Discretized Stream ),
每一段数据都转换成 Spark 中的 RDD Resilient Distributed Dataset
然后将 Spark Streaming 中对 DStream Transformation 操作变为针对 Spark 中对 RDD
Transformation 操作
RDD 经过操作变成中间结果保存在内存中。整个流式计算根据业务的需求可以对中间的结
果进行叠加或者存储到外部设备。
Spark Streaming 在内部的处理机制:
接收实时流的数据,并根据一定的时间间隔拆分成一批批的数据,然后通过 Spark Engine
理这些批数据,最终得到处理后的一批批结果数据。
对应的批数据,在 Spark 内核对应一个 RDD 实例,因此,对应流数据的 DStream 可以看成是一
RDDs ,即 RDD 的一个序列。
通俗点理解的话,在流数据分成一批一批后,通过一个先进先出的队列,然后 Spark Engine
从该队列中依次取出一个个批数据,把批数据封装成一个 RDD ,然后进行处理,这是一个典
型的生产者消费者模型,对应的就有生产者消费者模型的问题,即如何协调生产速率和消费速
率。
receiver task 工作原理:
receiver task 7*24 小时一直在执行,一直接受数据,将一段时间内接收来的数据保
存到 batch 中。
假设 batchInterval 5s, 那么会将接收来的数据每隔 5 秒封装到一个 batch
batch 没有分布式计算特性,这一个 batch 的数据又被封装到一个 RDD 中最终封装到一个
DStream 中,然后 sparkStreaming 回启动一个 job 去计算
工作细节:
假设 batchInterval 5 秒,每隔 5 秒通过 SparkStreamin 将得到一个 DStream, 在第 6 秒的
时候计算这 5 秒的数据,假设执行任务的时间是 3 , 那么第 6~9 秒一边在接收数据,一边
在计算任务, 9~10 秒只是在接收数据。然后在第 11 秒的时候重复上面的操作。
问题:如果 job 执行的时间大于 batchInterval
如果接受过来的数据设置的级别是仅内存,接收来的数据会越堆积越多,最后可能会导
OOM
如果设置 StorageLevel 包含 disk, 则内存存放不下的数据会溢写至 disk, 加大延迟 。
3.2. 流式计算特性
容错性 首先我们要明确一下 Spark RDD 的容错机制。
每一个 RDD 都是一个不可变的分布式可重算的数据集,其记录着确定性的操作继承关系
lineage
只要输入数据是可容错的,那么任意一个 RDD 的分区( Partition )出错或不可用,都是可以
利用原始输入数据通过转换操作而重新算出的。
图中的每一个椭圆形表示一个 RDD ,椭圆形中的每个圆形代表一个 RDD 中的一个 Partition
图中的每一列的多个 RDD 表示一个 DStream (图中有三个 DStream ),而每一行最后一个
RDD 则表示每一个 Batch Size 所产生的中间结果 RDD
图中的每一个 RDD 都是通过 lineage 相连接的
实时性
Spark Streaming 将流式计算分解成多个 Spark Job ,对于每一段数据的处理都会经过 Spark
DAG 图分解以及 Spark 的任务集的调度过程。
对于目前版本的 Spark Streaming 而言,其最小的 Batch Size 的选取在 0.5~2 秒钟之间( Storm
目前最小的延迟是 100ms 左右)
所以 Spark Streaming 能够满足除对实时性要求非常高(如高频实时交易)之外的所有流式准
实时计算场景。
扩展性与吞吐量:
Spark 目前在 EC2 上已能够线性扩展到 100 个节点(每个节点 4Core ),可以以数秒的延迟处理
6GB/s 的数据量( 60M records/s ),其吞吐量也比流行的 Storm 2 5
3.3. 编程模型 DStream
DStream Discretized Stream )作为 Spark Streaming 的基础抽象,它代表持续性的数据流。 这些数据流既可以通过外部输入源赖获取,也可以通过现有的 Dstream transformation 操作来获
得。
在内部实现上, DStream 由一组时间序列上连续的 RDD 来表示。每个 RDD 都包含了自己特定时间间
隔内的数据流。
DStream RDD
Spark Streaming 计算还是基于 Spark Core 的, Spark Core 的核心又是 RDD. 所以 Spark
Streaming 肯定也要和 RDD 扯上关系。
Spark Streaming 并没有直接让用户使用 RDD 而是自己抽象了一套 DStream 的概念。
DStream RDD 是包含的关系,你可以理解为 Java 里的装饰模式,也就是 DStream 是对
RDD 的增强,但是行为表现和 RDD 是基本上差不多的。
生命周期
InputDStream 会将接受到的数据转化成 RDD, 比如 DirectKafkaInputStream 产生的就是
KafkaRDD
接着通过 MappedDStream 等进行数据转换,这个时候是直接调用 RDD 对应的 map 方法进行
转换的
在进行输出类操作时,才暴露出 RDD, 可以让用户执行相应的存储,其他计算等操作。
4. SparkStreaming 代码实现
4.1. 代码实现
pom.xml
启动 socket server 服务器(指定 9999 端口号):
Linux nc –lk 9999
Window nc –lp 9999
注意点
receiver 模式下接受数据, local 的模拟线程必须大于等于 2 ,一个线程用来 receiver 用来接受
数据,另一个线程用来执行 job
Durations 时间设置就是我们能接收的延迟度。这个需要根据集群的资源情况以及任务的执行
情况来调节。
StreamingContext.start() Streaming 框架启动后不能再次添加业务逻辑。
StreamingContext.stop() 无参的 stop 方法将 SparkContext 一同关闭, stop(false) ,不会关闭
SparkContext
StreamingContext.stop() 停止之后不能再调用 start
优雅的停止 sparkstreaming
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-streaming
-->
<dependency>
<groupId> org.apache.spark </groupId>
<artifactId> spark-streaming_2.12 </artifactId>
<version> 2.4.6 </version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-
streaming-kafka-0-10 -->
<dependency>
<groupId> org.apache.spark </groupId>
<artifactId> spark-streaming-kafka-0-10_2.12 </artifactId>
<version> 2.4.6 </version>
</dependency> 代码实现
4.2. DStream 转换操作
// spark.streaming.stopGracefullyOnShutdown # 设置成 true
sparkConf . set ( "spark.streaming.stopGracefullyOnShutdown" , "true" )
// 系统会发送一个 SIGTERM 的信号给对应的程序。这个信号是 termination 不同于 -9 的直接 kill ,它
同样会结束进程但是会给进程时间善后。
kill - 15 driverpid
package com . yjjxt
import org . apache . spark . SparkConf
import org . apache . spark . streaming . dstream .{ DStream ,
ReceiverInputDStream }
import org . apache . spark . streaming .{ Seconds , StreamingContext }
object HelloSparkStream {
def main ( args : Array [ String ]): Unit = {
val sparkConf : SparkConf = new
SparkConf (). setMaster ( "local[2]" ). setAppName ( "HelloSparkStream" )
val streamingContext = new StreamingContext ( sparkConf , Seconds ( 1 ))
val receiver : ReceiverInputDStream [ String ] =
streamingContext . socketTextStream ( "localhost" , 9999 )
val dStream : DStream [( String , Int )] = receiver . flatMap ( _ . split ( "
" )). map (( _ , 1 )). reduceByKey ( _ + _ )
dStream . print ()
streamingContext . start ()
streamingContext . awaitTermination ()
}
} 转换
描述
map (func)
DStream 的每个元素通过函数 func 返回一个新的
DStream
flatMap (func)
类似与 map 操作,不同的是每个输入元素可以被映射出 0
者更多的输出元素。
filter (func)
在源 DSTREAM 上选择 Func 函数返回仅为 true 的元素 , 最终返
回一个新的 DSTREAM
repartition (numPartitions)
通过输入的参数 numPartitions 的值来改变 DStream 的分区
大小。
union (otherStream)
返回一个包含源 DStream 与其他 DStream 的元素合并后的
DSTREAM
count ()
对源 DStream 内部的所含有的 RDD 的元素数量进行计数,返
回一个内部的 RDD 只包含一个元素的 DStreaam
reduce (func)
使用函数 func (有两个参数并返回一个结果)将源 DStream
中每个 RDD 的元素进行聚 合操作 , 返回一个内部所包含的
RDD 只有一个元素的新 DStream
countByValue ()
计算 DStream 中每个 RDD 内的元素出现的频次并返回新的
DStream[(K,Long)] ,其中 K RDD 中元素的类型, Long
元素出现的频次。
reduceByKey (func,
[numTasks])
当一个类型为(
K V )键值对的 DStream 被调用的时候 ,
回类型为类型为(
K V )键值对的新 DStream, 其中每个键
的值 V 都是使用聚合函数 func 汇总。注意:默认情况下,使
Spark 的默认并行度提交任务(本地模式下并行度为 2
集群模式下位 8 ),可以通过配置 numTasks 设置不同的并
行任务数。
join (otherStream,
[numTasks])
当被调用类型分别为(
K V )和(
K W )键值对的 2
DStream 时,返回类型为(
K ,(
V W ))键值对的一个新
DSTREAM
cogroup (otherStream,
[numTasks])
当被调用的两个 DStream 分别含有 (K, V) (K, W) 键值对时 ,
返回一个 (K, Seq[V], Seq[W]) 类型的新的 DStream
transform (func)
通过对源 DStream 的每 RDD 应用 RDD-to-RDD 函数返回一个
新的 DStream ,这可以用来在 DStream 做任意 RDD 操作。
updateStateByKey (func)
返回一个新状态的 DStream, 其中每个键的状态是根据键的
前一个状态和键的新值应用给定函数 func 后的更新。这个方
法可以被用来维持每个键的任何状态数据。
transform(func) 操作
transform 操作(转换操作)连同其其类似的 transformWith 操作允许 DStream 上应用任
RDD-to-RDD 函数。它可以被应用于未在 DStream API 中暴露任何的 RDD 操作。例如,在
每批次的数据流与另一数据集的连接功能不直接暴露在 DStream API 中,但可以轻松地使用
transform 操作来做到这一点,这使得 DStream 的功能非常强大。例如,你可以通过连接预先
计算的垃圾邮件信息的输入数据流(可能也有 Spark 生成的),然后基于此做实时数据清理的 筛选,如下面官方提供的伪代码所示。事实上,也可以在 transform 方法中使用机器学习和图
形计算的算法。
updateStateByKey 操作
updateStateByKey 操作可以让你保持任意状态,同时不断有新的信息进行更新。要使用
此功能,必须进行两个步骤 :
定义状态 - 状态可以是任意的数据类型。
定义状态更新函数 - 用一个函数指定如何使用先前的状态和从输入流中获取的新值 更新状态
使用到 updateStateByKey 要开启 checkpoint 机制和功能。
4.3. DStream 窗口操作
转换
描述
window (windowLength, slideInterval)
返回一个基于源 DStream 的窗口批次
计算后得到新的 DStream
countByWindow (windowLength,slideInterval)
返回基于滑动窗口的 DStream 中的元
素的数量。
reduceByWindow (func, windowLength,slideInterval)
基于滑动窗口对源 DStream 中的元素
进行聚合操作,得到一个新的
DStream
reduceByKeyAndWindow (func,windowLength,slideInterval,
[numTasks])
基于滑动窗口对(
K V )键值对类
型的 DStream 中的值按 K 使用聚合函
func 进行聚合操作,得到一个新的
DStream
reduceByKeyAndWindow (func,
invFunc,windowLength,slideInterval, [numTasks])
一个更高效的
reduceByKkeyAndWindow() 的实现
版本,先对滑动窗口中新的时间间隔
内数据增量聚合并移去最早的与新增
数据量的时间间隔内的数据统计量。
例如,计算 t+4 秒这个时刻过去 5 秒窗
口的 WordCount ,那么我们可以将
t+3 时刻过去 5 秒的统计量加上 [t+3
t+4] 的统计量,在减去 [t-2 t-1] 的统
计量,这种方法可以复用中间三秒的
统计量,提高统计的效率。
countByValueAndWindow (windowLength,slideInterval,
[numTasks])
基于滑动窗口计算源 DStream 中每个
RDD 内每个元素出现的频次并返回
DStream[(K,Long)] ,其中 K RDD
中元素的类型, Long 是元素频次。
countByValue 一样, reduce 任务
的数量可以通过一个可选参数进行配
置。
概念
Spark Streaming 中,数据处理是按批进行的,而数据采集是逐条进行的,因此在 Spark
Streaming 中会先设置好批处理间隔( batch duration ),当超过批处理间隔的时候就会把采
集到的数据汇总起来成为一批数据交给系统去处理。
对于窗口操作而言,在其窗口内部会有 N 个批处理数据,批处理数据的大小由窗口间隔
window duration )决定,而窗口间隔指的就是窗口的持续时间,在窗口操作中,只有窗口
的长度满足了才会触发批数据的处理。除了窗口的长度,窗口操作还有另一个重要的参数就是
滑动间隔( slide duration ),它指的是经过多长时间窗口滑动一次形成新的窗口,滑动窗口 默认情况下和批次间隔的相同,而窗口间隔一般设置的要比它们两个大。在这里必须注意的一
点是滑动间隔和窗口间隔的大小一定得设置为批处理间隔的整数倍。
释义
批处理间隔是 1 个时间单位,窗口间隔是 3 个时间单位,滑动间隔是 2 个时间单位。
窗口长度和滑动间隔必须是 batchInterval 整数倍 。如果不是整数倍会检测报错。
window (windowLength, slideInterval)
窗口总长度( window length ):你想计算多长时间的数据
滑动时间间隔( slide interval ):你每多长时间去更新一次
转换
描述
print ()
Driver 中打印出 DStream 中数据的前 10 个元素。
saveAsTextFiles (prefix,
[suffix])
DStream 中的内容以文本的形式保存为文本文件,其中每
次批处理间隔内产生的文件以 prefix-TIME_IN_MS[.suffix]
方式命名。
saveAsObjectFiles (prefix,
[suffix])
DStream 中的内容按对象序列化并且以 SequenceFile 的格
式保存。其中每次批处理间隔内产生的文件以 prefix
TIME_IN_MS[.suffix] 的方式命名。
saveAsHadoopFiles (prefix,
[suffix])
DStream 中的内容以文本的形式保存为 Hadoop 文件,其
中每次批处理间隔内产生的文件以 prefix
TIME_IN_MS[.suffix] 的方式命名。
foreachRDD (func)
最基本的输出操作,将 func 函数应用于 DStream 中的 RDD
上,这个操作会输出数据到外部系统,比如保存 RDD 到文件
或者网络数据库等。需要注意的是 func 函数是在运行该
streaming 应用的 Driver 进程里执行的。
dstream.foreachRDD 是一个非常强大的输出操作,它允将许数据输出到外部系统。但是 ,如何正
确高效地使用这个操作是很重要的,下面展示了如何去避免一些常见的错误。
通常将数据写入到外部系统需要创建一个连接对象(如 TCP 连接到远程服务器),并用它来发送数
据到远程系统。出于这个目的,开发者可能在不经意间在 Spark driver 端创建了连接对象,并尝试
使用它保存 RDD 中的记录到 Spark worker 上,如下面代码: 这是不正确的,这需要连接对象进行序列化并从 Driver 端发送到 Worker 上。连接对象很少在不同机
器间进行这种操作,此错误可能表现为序列化错误(连接对不可序列化),初始化错误(连接对象
在需要在 Worker 上进行需要初始化 ) 等等,正确的解决办法是在 worker 上创建的连接对象。
通常情况下,创建一个连接对象有时间和资源开销。因此,创建和销毁的每条记录的连接对象可能
招致不必要的资源开销,并显著降低系统整体的吞吐量 。一个更好的解决方案是使用
rdd.foreachPartition 方法创建一个单独的连接对象,然后使用该连接对象输出的所有 RDD 分区中
的数据到外部系统。
这缓解了创建多条记录连接的开销。最后,还可以进一步通过在多个 RDDs/ batches 上重用连接对
象进行优化。一个保持连接对象的静态池可以重用在多个批处理的 RDD 上将其输出到外部系统,从
而进一步降低了开销。
需要注意的是,在静态池中的连接应该按需延迟创建,这样可以更有效地把数据发送到外部系统。
另外需要要注意的是: DStreams 延迟执行的,就像 RDD 的操作是由 actions 触发一样。默认情况
下,输出操作会按照它们在 Streaming 应用程序中定义的顺序一个个执行。
5. SparkStreaming 检查点
5.1. 介绍
流应用程序必须保证 7*24 全天候运行,因此必须能够适应与程序逻辑无关的故障【例如:系统故
障、 JVM 崩溃等】。为了实现这一点, SparkStreaming 需要将足够的信息保存到容错存储系统中,
以便它可以从故障中恢复。
检查点有两种类型。
元数据检查点
将定义流式计算的信息保存到容错存储系统【如 HDFS 等】。这用于从运行流应用程序所
在的节点的故障中恢复。
元数据包括:
1. 配置 :用于创建流应用程序的配置。
2.DStream 操作:定义流应用程序的 DStream 操作集。
3. 不完整的批次:在任务队列中而尚未完成的批次。
数据检查点
将生成的 RDD 保存到可靠的存储系统。在一些跨多个批次组合数据的有状态转换中,这
是必须的。在这种转换中,生成的 RDD 依赖于先前批次的 RDD ,这导致依赖关系链的长
度随着时间而增加。
为了避免恢复时间的这种无限增加【与依赖链成正比】,有状态变换的中间 RDD 周期性
地检查以存储到可靠的存储系统中,以切断依赖链。
5.2. 需要检查点的情况
有状态转换的使用
如果在应用程序中使用了 updateStateByKey reduceByKeyAndWindow ,则必须提供检查
点以缓存之前批次的中间结果
从运行应用程序的节点故障中恢复,元数据检查点用于使用进度信息进行恢复。
5.3. 配置检查点
可以通过在容错,可靠的文件系统中设置目录来启用检查点,检查点信息将保存到该文件系统中。
使用: streamingContext.checkpoint(checkpointDirectory) 来设置的。
这将允许使用上述状态转换。此外,如果要使应用程序从节点故障中恢复,则应重写流应用程
序以使其具有以下行为。 当程序首次启动时,它将创建一个新的 StreamingContext ,设置所有流后调用 start()
当程序在失败后重新启动时,它将从检查点目录中的检查点数据重新创建
StreamingContext
6. SparkStreaming 数据源
6.1. 基础数据源
Spark Streaming 提供了 streamingContext.socketTextStream() 方法,可以通过 TCP 套接字连
接,从从文本数据中创建了一个 DStream
Spark Streaming 提供了 streamingContext.fileStream(dataDirectory) 方法可以从任何文件系统
( 如: HDFS S3 NFS 等)的文件中读取数据
然后创建一个 DStream Spark Streaming 监控 dataDirectory 目录和在该目录下任何文件被
创建处理 ( 不支持在嵌套目录下写文件 )
需要注意的是:读取的必须是具有相同的数据格式的文件;创建的文件必须在 dataDirectory
目录下,并通过自动移动或重命名成数据目录;文件一旦移动就不能被改变,如果文件被不断
追加 , 新的数据将不会被阅读。
Spark Streaming 提供了 streamingContext.textFileStream(dataDirectory) 来读取简单的文本文
件。
Spark Streaming 提供了 streamingContext.actorStream(actorProps, actor-name) 可以基于自定
Actors 的流创建 DStream
通过 Akka actors 接受数据流
Spark Streaming 提供了 streamingContext.queueStream(queueOfRDDs) 方法可以创建基于
RDD 队列的 DStream
每个 RDD 队列将被视为 DStream 中一块数据流进行加工处理。
6.2. 高级数据源
Twitter Spark Streaming TwitterUtils 工具类使用 Twitter4j Twitter4J 库支持通过任何方法提
供身份验证信息,你可以得到公众的流,或得到基于关键词过滤流。
Flume Spark Streaming 可以从 Flume 中接受数据。
Kafka Spark Streaming 可以从 Kafka 中接受数据。
Kinesis Spark Streaming 可以从 Kinesis 中接受数据。
6.3. Kafka 接受数据方式
Kafka Spark Streaming 集成时有两种方法:旧的基于 receiver 的方法,新的基于 direct stream
方法。
基于 receiver 的方法 基于 receiver 的方法采用 Kafka 的高级消费者 API ,每个 executor 进程都不断拉取消息,并同时
保存在 executor 内存与 HDFS 上的预写日志( write-ahead log/WAL )。当消息写入 WAL 后,
自动更新 ZooKeeper 中的 offset
它可以保证 at least once 语义,但无法保证 exactly once 语义。虽然引入了 WAL 来确保消息不
会丢失,但还有可能会出现消息已经写入 WAL ,但 offset 更新失败的情况, Kafka 就会按上一
次的 offset 重新发送消息。
receiver 的并行度是由 spark.streaming.blockInterval 来决定的,默认为 200ms, 假设
batchInterval 5s, 那么每隔 blockInterval 就会产生一个 block, 这里就对应每批次产生 RDD
partition, 这样 5 秒产生的这个 Dstream 中的这个 RDD partition 25 个,并行度就是 25 。如
果想提高并行度可以减少 blockInterval 的数值,但是最好不要低于 50ms
基于 direct stream 的方法
基于 direct stream 的方法采用 Kafka 的简单消费者 API ,它的流程大大简化了。 executor 不再
Kafka 中连续读取消息,也消除了 receiver WAL 。还有一个改进就是 Kafka 分区与 RDD
区是一一对应的,更可控。
driver 进程只需要每次从 Kafka 获得批次消息的 offset range ,然后 executor 进程根据 offset
range 去读取该批次对应的消息即可。由于 offset Kafka 中能唯一确定一条消息,且在外部只
能被 Streaming 程序本身感知到,因此消除了不一致性,达到了 exactly once
由于它采用了简单消费者 API ,我们就需要自己来管理 offset
Direct 模式的并行度是由读取的 kafka topic partition 数决定的。 6.4. Kafka0.10Higher
代码实现
package com . yjxxt
import org . apache . kafka . clients . consumer . ConsumerRecord
import org . apache . kafka . common . serialization . StringDeserializer
import org . apache . spark . SparkConf
import org . apache . spark . streaming . dstream .{ DStream , InputDStream }
import org . apache . spark . streaming . kafka010 . ConsumerStrategies . Subscribe
import org . apache . spark . streaming . kafka010 . KafkaUtils
import
org . apache . spark . streaming . kafka010 . LocationStrategies . PreferConsistent
import org . apache . spark . streaming .{ Seconds , StreamingContext }
object Hello07Kafka {
def main ( args : Array [ String ]): Unit = {
// 创建
val sparkConf = new
SparkConf (). setMaster ( "local[2]" ). setAppName ( "Hello07Kafka" )
val streamingContext = new StreamingContext ( sparkConf , Seconds ( 1 ))
// 配置信息
val kafkaParams = Map [ String , Object ](
"bootstrap.servers" -> "node01:9092,node02:9092,node03:9092" ,
"key.deserializer" -> classOf [ StringDeserializer ],
"value.deserializer" -> classOf [ StringDeserializer ],
"group.id" -> "yjx_bigdata" ,
"auto.offset.reset" -> "latest" ,
"enable.auto.commit" -> ( false : java . lang . Boolean )
)
val topics = Array ( "userlog" )
// 开始创建 Kafka
val kafkaDStream : InputDStream [ ConsumerRecord [ String , String ]] =
KafkaUtils createDirectStream ( streamingContext , PreferConsistent ,
Subscribe [ String , String ]( topics , kafkaParams ))
// 开始处理
val resultDStream : DStream [( String , Int )] =
kafkaDStream . map ( _ . value ()). flatMap ( _ . split ( " " )). map (( _ ,
1 )). reduceByKey ( _ + _ )
// 打印数据
resultDStream . print ()
streamingContext . start ()
streamingContext . awaitTermination ()
}
} 6.5. offset 操作
SparkStreaming Kafka 维护 offset 官网有三种实现方式
Checkpoints
Kafka itself
Your own data store
6.5.1. CheckPoint
spark streaming 里面管理偏移量的策略,默认的 spark streaming 它自带管理的 offset 的方式是通
checkpoint 来记录每个批次的状态持久化到 HDFS 中,如果机器发生故障,或者程序故障停止,
下次启动时候,仍然可以从 checkpoint 的目录中读取故障时候 rdd 的状态,便能接着上次处理的数
据继续处理,但 checkpoint 方式最大的弊端是如果代码升级,新版本的 jar 不能复用旧版本的序列化
状态,导致两个版本不能平滑过渡,结果就是要么丢数据,要么数据重复,所以官网搞的这个东
西,几乎没有人敢在生产环境运行非常重要的流式项目。
checkpoint 第一次持久化的时候会把整个相关的 jar 给序列化成一个二进制文件,每次重启都会从里
面恢复,但是当你新的
程序打包之后序列化加载的仍然是旧的序列化文件,这就会导致报错或者依旧执行旧代码。有的同
学可能会说,既然如此,直接把上次的 checkpoint 删除了,不就能启动了吗? 确实是能启动,但
是一旦你删除了旧的 checkpoint ,新启动的程序,只能从 kafka smallest 或者 largest 的偏移量消
费,默认是从最新的,如果是最新的,而不是上一次程序停止的那个偏移量就会导致有数据丢失,
如果是老的,那么就会导致数据重复。
6.5.2. Kafka Itself
kafka 本身就有机制可以定时存储消费者分组的偏移量,但是这样会有重复消费的情况 , 还如果采用
这种方式那么就是将 kafka offset 全部交给 kafka 管理
kafka 的的数据其实也是内存和磁盘存储的,如果数据量上来了,无疑也是对 kafka 集群的一种压
力。
因为我们项目在实际开发中的时候,遇到数据峰值很高的时候 kafka 集群的磁盘 io 是特别高的这样
是非常不安全的。
6.5.3. Your own data store
自己写代码管理 offset ,其实就是把每批次 offset 存储到一个外部的存储系统里面包括( Hbase
HDFS,Zookeeper Kafka DB 等等)
当一个新的 spark streaming+kafka 的流式项目第一次启动的时候,这个时候发现外部的存储
系统并没有记录任何有关这个 topic 所有分区的偏移量,所以就从
KafkaUtils.createDirectStream 直接创建 InputStream 流,默认是从最新的偏移量消费,如果
是第一次其实最新和最旧的偏移量时相等的都是 0 ,然后在以后的每个批次中都会把最新的
offset 给存储到外部存储系统中,不断的做更新。
当流式项目停止后再次启动,会首先从外部存储系统读取是否记录的有偏移量,如果有的话,
就读取这个偏移量,然后把偏移量集合传入到 KafkaUtils.createDirectStream 中进行构建
InputSteam ,这样的话就可以接着上次停止后的偏移量继续处理,然后每个批次中仍然的不
断更新外部存储系统的偏移量,这样以来就能够无缝衔接了,无论是故障停止还是升级应用,
都是透明的处理。
对正在运行的一个 spark streaming+kafka 的流式项目,我们在程序运行期间增加了 kafka
分区个数,请注意:这个时候新增的分区是不能被正在运行的流式项目感应到的,如果想要程
序能够识别新增的分区,那么 spark streaming 应用程序必须得重启,同时如果你还使用的是
自己写代码管理的 offset 就千万要注意,对已经存储的分区偏移量,也要把新增的分区插入进
去,否则你运行的程序仍然读取的是原来的分区偏移量,这样就会丢失一部分数据。 7. 数据的反压机制
数据流入的速度远高于数据处理的速度,对流处理系统构成巨大的负载压力,如果不能正确处理,
可能导致集群资源耗尽最终集群崩溃,因此有效的反压机制 (backpressure) 对保障流处理系统的稳
定至关重要。
7.1. Storm 反压
旧版本
开启了 acker 机制的 storm 程序,可以通过设置 conf.setMaxSpoutPending 参数来实现反压效
果,如果下游组件 (bolt) 处理速度跟不上导致 spout 发送的 tuple 没有及时确认的数超过了参数
设定的值, spout 会停止发送数据
但是 conf.setMaxSpoutPending 参数的设置很难达到最好的反压效果
设小了会导致吞吐上不去
设大了会导致 worker OOM ;有震荡,数据流会处于一个颠簸状态,效果不如逐级反
压;
另外对于关闭 acker 机制的程序无效;
新版本
storm 自动反压机制 (Automatic Back Pressure) 通过监控 bolt 中的接收队列的情况,当超过
高水位值时专门的线程会将反压信息写到 Zookeeper Zookeeper 上的 watch 会通知该拓扑
的所有 Worker 都进入反压状态,最后 Spout 降低 tuple 发送的速度。
7.2. Spark 反压
旧版本
Spark Streaming 程序中当计算过程中出现 batch processing time > batch interval 的情况
时, ( 其中 batch processing time 为实际计算一个批次花费时间, batch interval Streaming
应用设置的批处理间隔 ) 意味着处理数据的速度小于接收数据的速度,如果这种情况持续过长的时间,会造成数据在内
存中堆积,导致 Receiver 所在 Executor 内存溢出等问题 ( 如果设置 StorageLevel 包含 disk, 则内
存存放不下的数据会溢写至 disk, 加大延迟 )
可以通过设置参数 spark.streaming.receiver.maxRate 来限制 Receiver 的数据接收速率,此举
虽然可以通过限制接收速率,来适配当前的处理能力,防止内存溢出,但也会引入其它问题。
比如: producer 数据生产高于 maxRate ,当前集群处理能力也高于 maxRate ,这就会造成资
源利用率下降等问题。
新版本
Spark Streaming v1.5 开始引入反压机制( back-pressure , 通过动态控制数据接收速率来
适配集群数据处理能力
Spark Streaming Backpressure: 根据 JobScheduler 反馈作业的执行信息来动态调整
Receiver 数据接收率。
spark streaming 的数据源方式有两种:
若是基于 Receiver 的数据源,可以通过设置 spark.streaming.receiver.maxRate 来控制最
大输入速率;
若是基于 Direct 的数据源 ( Kafka Direct Stream) ,则可以通过设置
spark.streaming.kafka.maxRatePerPartition 来控制最大输入速率。
若设置 spark.streaming.backpressure.enabled true Spark Streaming 会自动根据处
理能力来调整输入速率,从而在流量高峰时仍能保证最大的吞吐和性能。
反压原理
在原架构的基础上加上一个新的组件 RateController, 这个组件负责监听 “OnBatchCompleted”
事件,然后从中抽取 processingDelay schedulingDelay 信息 . Estimator 依据这些信息估算
出最大处理速度( rate ),最后由基于 Receiver Input Stream rate 通过 ReceiverTracker
ReceiverSupervisorImpl 转发给 BlockGenerator (继承自 RateLimiter .
Spark Streaming 的反压机制中,有以下几个重要的组件:
RateController
RateController 继承自接口 StreamingListener, 并实现了 onBatchCompleted
法。每一个 Batch 处理完成后都会调用此方法 override def onBatchCompleted ( batchCompleted :
StreamingListenerBatchCompleted ) {
val elements = batchCompleted . batchInfo . streamIdToInputInfo
for {
// 处理结束时间
processingEnd <-
batchCompleted . batchInfo . processingEndTime
// 处理时间 , `processingEndTime` - `processingStartTime`
workDelay <- batchCompleted . batchInfo . processingDelay
// 在调度队列中的等待时间 , `processingStartTime` -
`submissionTime`
waitDelay <- batchCompleted . batchInfo . schedulingDelay
// 当前批次处理的记录数
elems <- elements . get ( streamUID ). map ( _ . numRecords )
} computeAndPublish ( processingEnd , elems , workDelay ,
waitDelay )
}
RateEstimator
RateEstimator 是速率估算器,主要用来估算最大处理速率
private def computeAndPublish ( time : Long , elems : Long ,
workDelay : Long , waitDelay : Long ): Unit = Future [ Unit ] {
// 根据处理时间、调度时间、当前 Batch 记录数,预估新速率
val newRate = rateEstimator . compute ( time , elems , workDelay ,
waitDelay )
newRate . foreach { s =>
// 设置新速率
rateLimit . set ( s . toLong )
// 发布新速率
publish ( getLatestRate ())
}
}
RateLimiter
RateLimiter 是一个抽象类,它并不是 Spark 本身实现的,而是借助了第三方 Google
GuavaRateLimiter 来产生的
它实质上是一个限流器,也可以叫做令牌,如果 Executor task 每秒计算的速度大
于该值则阻塞,如果小于该值则通过
将流数据加入缓存中进行计算。这种机制也可以叫做令牌桶机制 令牌桶机制: 大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不
被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后
面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大
小。当进行某操作时需要令牌时会从令牌桶中取出相应的令牌数,如果获取到则继续操
作,否则阻塞。用完之后不用放回。
参数设置 参数名称
默认值
含义
spark.streaming.backpressure.enabled
false
是否启用反压机制
spark.streaming.backpressure.initialRate
no set
每个接收器初次接收数据
的最大速率
spark.streaming.blockInterval
200ms
数据块分块的时间间隔
spark.streaming.receiver.maxRate
no set
每个接收器最大接收速率
(每秒记录数)
spark.streaming.receiver.writeAheadLog.enable
false
启用预写日志
spark.streaming.unpersist
true
清除 sparkstreaming 持久
化的 RDD
spark.streaming.stopGracefullyOnShutdown
false
Spark
StreamingContext
JVM 关闭时立即关闭
spark.streaming.kafka.maxRatePerPartition
no set
从每个 kafka 分区中读取
数据的速率(每秒记录
数)
spark.streaming.kafka.minRatePerPartition
1
从每个 kafka 分区中读取
数据的速率(每秒记录
数)
spark.streaming.kafka.maxRetries
1
驱动程序在每个 kafka
区的 leader 中寻找最新的
偏移量的最大重试次数
(设置为 1 ,表示重试次
数为 2
spark.streaming.ui.retainedBatches
1000
在垃圾收集之前, Spark
Streaming UI 和状态 API
记住的批次数量
spark.streaming.driver.writeAheadLog.closeFileAfterWrite
false
在驱动器端写入预写日志
后是否关闭文件
spark.streaming.receiver.writeAheadLog.closeFileAfterWrite
false
在接收器端写入预写日志
后是否关闭文件
注意
反压机制真正起作用时需要至少处理一个批
由于反压机制需要根据当前批的速率,预估新批的速率,所以反压机制真正起作用前,
应至少保证处理一个批。
如何保证反压机制真正起作用前应用不会崩溃
要保证反压机制真正起作用前应用不会崩溃 , 需要控制每个批次最大摄入速率。
若为 Direct Stream ,如 Kafka Direct Stream, 则可以通过
spark.streaming.kafka.maxRatePerPartition 参数来控制。
此参数代表了 每秒每个分区最大摄入的数据条数。
// 启用反压
conf . set ( "spark.streaming.backpressure.enabled" , "true" )
// 最小摄入条数控制
conf . set ( "spark.streaming.backpressure.pid.minRate" , "1" )
// 最大摄入条数控制
conf . set ( "spark.streaming.kafka.maxRatePerPartition" , "12" ) 假设 BatchDuration 10
spark.streaming.kafka.maxRatePerPartition 12
kafka topic 分区数为 3
则一个批 (Batch) 最大读取的数据条数为 360 (3 12 10=360)
该参数也代表了整个应用生命周期中的最大速率,即使是背压调整的最大值也不会超过
该参数。
8. SparkStreaming 事务
Exactly-once 语义是实时计算的难点之一。要做到每一条记录只会被处理一次,即使服务器或网络
发生故障时也能保证没有遗漏,这不仅需要实时计算框架本身的支持,还对上游的消息系统、下游
的数据存储有所要求。
一个 Spark Streaming 程序由三步组成:输入、处理逻辑、输出。要达到 exactly once 的理想状
态,需要三步协同进行,而不是只与处理逻辑有关。
8.1. 逻辑处理
Spark 容错分为: Driver 级别的容错和 Executor 级别的容错。
Driver 级别的容错具体为 DAG 生成的模板,即 DStreamGraph RecevierTracker 中存储的
元数据信息和 JobScheduler 中存储的 Job 进行的进度情况等信息,只要通过 checkpoint 就可以
了,每个 Job 生成之前进行 checkpoint ,在 Job 生成之后再进行 checkpoint ,如果出错的话就
checkpoint 中恢复。
Executor 级别的容错具体为接收数据的安全性和任务执行的安全性。在接收数据安全性方
面,一种方式是 Spark Streaming 接收到数据默认为 MEMORY_AND_DISK_2 的方式,在两台
机器的内存中,如果一台机器上的 Executor 挂了,立即切换到另一台机器上的 Executor ,这
种方式一般情况下非常可靠且没有切换时间。另外一种方式是 WAL Write Ahead Log ),在
数据到来时先通过 WAL 机制将数据进行日志记录,如果有问题则从日志记录中恢复,然后再
把数据存到 Executor 中,再进行其他副本的复制。 WAL 这种方式对性能有影响,在生产环境
中不常用,一般使用 Kafka 存储, Spark Streaming 接收到数据丢失时可以从 Kafka 中回放。在
任务执行的安全性方面,靠 RDD 的容错。
Spark Streaming 的容错机制是基于 RDD 的容错机制。
checkpoint
2. 基于血统( lineage )的高度容错机制
3. 出错了之后会从出错的位置从新计算,而不会导致重复计算
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值