Linux->ZooKeeper开机启动的俩种方式 :
Linux创建用户、用户组 及 删除;运维有关命令:
Linux定时任务工具:crontab
自由设置秒级任务:
Linux的 shell通配符、shell元字符和 正则表达式:
=~ 运算符:进行正则表达式判断之用,左侧的字符串会依右侧的正则表达式做匹配,若匹配则结果为true,不匹配则结果为false。比如:if ! [[ $LAUNCHER_EXIT_CODE =~ ^[0-9]+$ ]];
Linux下的二进制文件的编辑和查看:
Linux下实现免密码登陆:
Linux指令之 读取指定范围行的文件:
HDFS元数据管理原理:
HDFS NameNode 数据结构(插入3个):https://baike.baidu.com/item/NameNode/17989399?fr=aladdin
摘抄:
hadoop-2.6.0/dfs/name/current就是存放namenode元数据信息的地方
fsimage是镜像文件,包含了namenode所有的元数据信息。
VERSION中有namespaceID/clusterID/cTime/storagetype/bolckpoolID/layoutVersion
namespaceID是文件系统的唯一标识符,格式化文件系统后就会生成这个ID
clusterID是系统生成的集群的ID;
cTime是namenode存储系统创建是时间,第一次格式化系统就是0,再次格式化时就会更新;
toragetype说明文件存储的是什么系统存储的信息,可能是namenode/datanode
bolckpoolID是针对每一个namespace对应的bolckpool的ID,包含存储节点的IP等信息
seen_txid:存放transactionID,格式化文件系统后这个数字是0,代表一系列edits_*文件的尾数,namenode重启时会循环从0001到seen_txid中的数字,hdfs重启时会比对这个数字是不是edits文件的尾数,如果不是的话可能会有元数据丢失。
fsimage是整个namenode的镜像,包含临时文件edits。后面还会讲解。
in_user.lock是因为我们启动了集群。
实际生产环境下集群搭建
一. 前期准备
生产环境的准备主要分为两个方面讲解,分别为:系统层面和软件层面
一. 系统层面
- IP地址的选择,尽可能的将集群的IP地址选在同一个网段。
- 主机名的命名方式 。比如:bigdata-cdh01.test.com bigdata-cdh02.test.com bigdata-cdh03.test.com
- 修改
各台主机的主机名,运行如下命令(必须是在root用户下才能修改主机名):
[root@python5 hadoop]# hostname bigdata-cdh01.test.com #临时修改,系统重启失效
[root@python5 hadoop]# vi /etc/sysconfig/network #永久修改主机名,修改结果如下图所示
4. 在每台机器上做IP与主机名的映射处理,执行如下命令,然后禁用IPv6:(注意:如果是在windows下访问,在win下也要配置,在root用户下执行)
root@python5 hadoop]# vi /etc/hosts 执行结果如下截图所示
root@python5 hadoop]#echo "alia net-pf-10 off" >> /etc/modprobe.d/dist.conf
root@python5 hadoop]#echo "alia ipv6 off" >> /etc/modprobe.d/dist.conf
5. 创建普通用户,用于安装软件,执行如下命令(注意:此操作必须在root用户下执行,并且每台机器的这个用户名和密码必须必须必须一致):
root@python5 hadoop]#useradd tes(test是用户名) #后面为用户名,根据自己的情况自己定义
root@python5 hadoop]#passwd test #后面为密码,根据自己的情况定义
root@python5 hadoop]#su - 用户名 #切换账号
6. 配置普通用户的sudo权限,执行如下命令(注意:此操作依旧在root用户下执行):
root@python5 hadoop]#chmod u+w /etc/sudoers #为该文件附写文件的权限。备注:此文件默认只读
root@python5 hadoop]#vi /etc/sudoers # 编辑该文件
在文件的首行添加的内容如下:
test(test是用户名) ALL=(root) NOPASSWD:ALL
root@python5 hadoop]#chmod u-w /etc/sudoers #收回权限
7. 关闭防火墙,禁用Selinux:
[hadoop@python5 etc]$ systemctl stop firewalld #关闭防火墙
[hadoop@python5 etc]$ sudo vi /etc/sysconfig/selinux #修改selinux,修改内容如下截图所示
8. 卸载Linux自带的JDK,执行如下命令:
[hadoop@python5 etc]$sudo rpm -qa | grep java
[hadoop@python5 etc]$sudo rpm -e --nodeps XXXXXXXX
9. 设置文件打开数据和用户的最大进程:
查看文件打开数量(ulimit -a)
查看用户的最大进程数(ulimit -u)
执行如下命令修改这两个参数:
[hadoop@python5 etc]$sudo vi /etc/security/limits.conf
添加的内容如下:
* soft nofile 65535
* hard nofile 65535
* soft nproc 32000
* hard nproc 32000
二. 软件层面的配置
1. 集群时间的同步(以集群中的一台机器作为时间服务器):
[hadoop@python5 etc]$sudo rpm -qa | grep ntp #查看是否安装了ntp服务
[hadoop@python5 etc]$sudo vim /etc/ntp.conf #修改ntp的配置文件,修改内容如下图1所示
[hadoop@pyt
hon5 etc]$sudo vim /etc/sysconfig/ntpd #同步硬件时间,增加如下一行内容即可
SYNC_HWCLOCK=yes
[hadoop@python5 etc]$service ntpd status #查看ntp是否启动
或:
[hadoop@python5 etc]$systemctl status ntpd #查看ntp是否启动
[hadoop@python5 etc]$service ntpd start #启动ntp服务
或
[hadoop@python5 etc]$sudo systemctl start ntpd #启动ntp服务
[hadoop@python5 etc]$sudo chkconfig ntpd on #永久启动ntp服务
在要执行时间同步的机器上做一个定时任务,执行如下命令,必须在root用户下
[hadoop@python5 etc]$crontab -l #查看定时任务
[hadoop@python5 etc]$crontab -e #编写定时任务,编辑内容如下:
0-59/10 * * * * /usr/sbin/ntpdate bigdata-cdh01.test.com #(这里的bigdata-cdh01.test.com指的是被指定的时间服务器)
spark
1. spark集群术语。
Application | 基于Spark的用户程序。由集群上的一个驱动程序和多个执行器组成。 |
Application jar | 包含用户的Spark应用程序的JAR。在某些情况下,用户希望创建一个“uber jar”,其中包含他们的应用程序及其依赖项。用户的JAR不应该包括Hadoop或Spark库,但是这些库将在运行时添加。 |
Driver program | 运行应用程序的main()函数并创建SparkContext的进程 |
Cluster manager | 用于在集群上获取资源的外部服务(例如,standalone manager、Meos、Sharn) |
Deploy mode | 区分驱动程序进程运行的位置。在“cluster”模式下,框架在集群内部启动驱动程序。在“client”模式下,提交者在集群外部启动驱动程序。 |
Worker node | 可以在群集中运行应用程序代码的任何节点 |
Executor | 在一个worker节点上为一个应用程序启动的进程,它运行tasks,并将数据保存在内存或磁盘存储器中。每个应用程序都有自己的executors。 |
Task | 将发送给一个执行者的工作单元。 |
Job | 由多个任务组成的并行计算,这些任务是为响应一个spark操作而生成的(例如save、collect);您将在驱动程序日志中看到这个术语。 |
Stage | 每个作业被划分为更小的任务集,称为相互依赖的stages(类似于MapReduce中的map和reduce阶段);您将在驱动程序日志中看到这个术语。 |
Streaming
一. StructuredStreaming
http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html
概述
结构化流是一个基于Spark SQL引擎的可扩展、容错的流处理引擎。您可以用在静态数据上表示批处理计算的方式来表示流计算。Spark SQL引擎将负责递增和连续地运行它,并在流数据继续到达时更新最终结果。可以使用Scala、Java、Python或R中的DataSet/DataFrame API来表示streaming aggreations(流聚合),event-time-windows(事件时间窗口),stream-to-batch 连接等。计算是在同一个优化的Spark SQL引擎上执行的。最后,系统通过检查点和提前写入日志来确保端到端的容错性。简言之,结构化流提供了快速、可扩展、容错、端到端的一次性流处理,而用户无需考虑流。
编程模型
结构化流中的关键思想是将实时数据流视为一个不断追加的表。这导致了一个新的流处理模型,与批处理模型非常相似。您将把流计算表示为与静态表类似的标准批处理查询,spark将在无边界输入表上以增量查询的形式运行它。让我们更详细地了解这个模型。
基本概念
将输入数据流视为“输入表”。到达流中的每个数据项都像是追加到输入表中的新行。
对输入的查询将生成“结果表”。每一个触发间隔(比如说,每1秒),新的行都会追加到输入表中,这最终会更新结果表。每当结果表更新时,我们都希望将更改后的结果行写入外部接收器。
“Output”定义为写入外部存储器的内容。可以在不同的模式下定义output:
. Complete Mode -即完整模式。整个更新后的结果表将写入外部存储器。由存储连接器决定如何处理整个表的写入。
. Append Mode - 即追加模式。只有自上一个触发器以来追加到结果表中的新行才会写入外部存储器。这仅适用于结果表中不希望更改现有行的查询。
. Update Mode - 更新模式。只有自上一个触发器以来在结果表中更新的行才会写入外部存储器(从spark 2.1.1开始可用)。请注意,这与完整模式不同,因为此模式只输出自上一个触发器以来已更改的行。如果查询不包含聚合,则等同于追加模式。
请注意,每个模式适用于某些类型的查询。这将在后面详细讨论。
为了说明这个模型的使用,让我们在上面的Quick Example的上下文中理解这个模型。开始的lines DataFrame是输入表,最后的wordcounts DataFrame是结果表。请注意,在流式lines DataFrame上生成wordcounts的查询与静态DataFrame的查询完全相同。但是,当这个查询启动时,Spark将不断检查来自套接字连接的新数据。如果有新的数据,spark将运行一个“增量”查询,将以前运行的计数与新的数据结合起来,以计算更新的计数,如下所示。
请注意,结构化流并没有具体化整个表。它从流数据源读取最新的可用数据,它从流数据源中读取最新的可用数据,接着递增处理以更新结果,然后丢弃源数据。它只保留更新结果所需的最小中间状态数据(例如前面示例中的中间计数)。
此模型与许多其他流处理引擎显著不同。许多流系统要求用户自己维护正在运行的聚合,因此必须考虑容错性和数据一致性(at-least-once,或者at-most-once或者exactly-once)。在这个模型中,Spark负责在有新数据时更新结果表,从而减少用户对结果表的推理。作为一个例子,让我们看看这个模型如何处理基于事件时间的处理和延迟到达的数据。
Handling Event-time and Late Data(处理事件时间和延迟数据)
事件时间是嵌入到数据本身中的时间。对于许多应用程序,您可能希望在此事件时间上操作。例如,如果您希望获得每分钟由物联网设备生成的事件数,那么您可能希望使用生成数据的时间(即数据中的事件时间),而不是Spark接收数据的时间。这个事件事件很自然的用这个模型表示---设备中的每个事件是表中的一行,事件时间是此行中的一个列值。这允许基于窗口的聚合(例如,每分钟事件数)只是事件时间列上特殊类型的分组和聚合---每个时间窗口都是一个组,并且每一行可以属于多个窗口/组。因此,这种基于事件时间窗口的聚合查询既可以在静态数据集(例如,从收集的设备事件日志中)上定义,也可以在数据流上定义,从而使用户的生活更加容易。
此外,该模型根据事件时间自然地处理比预期晚到达的数据。由于Spark正在更新结果表,因此它可以完全控制 在有延迟数据时更新旧聚合,以及清除旧聚合以限制中间状态数据的大小。自Spark2.1以来,我们支持水印技术,允许用户指定延迟数据的阈值,并允许引擎相应地清除旧状态。稍后将在 Window Operations(窗口操作)部分中更详细地解释这些内容。
Fault Tolerance Semantics(容错语义)
提供一次确切的端到端语义是结构化流设计背后的关键目标之一。为了实现这一点,我们设计了结构化的流媒体源、接收器和执行引擎,以便可靠地跟踪处理的确切进度,以便通过重新启动和/或重新处理来处理任何类型的故障。假设每个流源都有偏移量(类似于Kafka偏移量或Kinesis序列号),以跟踪流中的读取位置。引擎使用检查点和提前写入日志来记录每个触发器中正在处理的数据的偏移范围。流sinks设计成等量处理后处理。流sinks的设计是等幂的处理后处理。通过使用可重放源和幂等汇聚,结构化流可以确保在任何失败情况下端到端精确地使用一次语义。
API using Datasets and DataFrames
由于Spark 2.0, DataFrames和Datasets可以表示静态的有界数据,也可以表示流式的无界数据。与静态DataSets/DataFrame类似,您可以使用公共入口点 SparkSession
(Scala/Java/Python/R docs) 从流源创建DataFrames/Datasets ,并将它们作为静态DataFrames/Datasets应用相同的操作。
Creating streaming DataFrames and streaming Datasets(创建流式DataFarmes和流式DataSets)
Streaming DataFrames 可以通过由SparkSession.readStream()返回的
DataStreamReader接口
(Scala/Java/Python docs)来创建。
在R中,使用 read.stream()
方法。与创建静态DataFrame的读取接口类似,您可以指定源的详细信息—data format(数据格式)、shcema(表头)、options(选项)等。
Input Sources
有一些内置资源。
. File source - 以数据流的形式读取目录中写入的文件。支持的文件格式有text、csv、json、orc、parquet。请参阅DatastreamReader界面的文档以获取更新的列表,以及每个文件格式支持的选项。注意,文件必须原子地放置在给定的目录中,在大多数文件系统中,可以通过文件移动操作来实现。
. Kafka source - 从Kafka读取数据。它与Kafka broker 0.10.0或更高版本兼容。有关详细信息,请参阅 Kafka Integration Guide。
. Socket source (for testing) - 从一个套接字连接读取UTF8文本数据。侦听服务器套接字位于驱动程序处(即同一台计算机)。请注意,这只能用于测试,因为这不提供端到端的容错保证。
. Rate source (for testing) - 以每秒指定的行数生成数据,每个输出行包含timestamp
和 value
。其中timestamp
是包含消息调度时间的timestamp
类型,value
是包含消息计数的Long类型,从0开始作为第一行。此源用于测试和基准测试。
有些源不具有容错性,因为它们不能保证在失败后可以使用检查点偏移量重播数据。请参见前面关于 fault-tolerance semantics.的部分。以下是Spark中所有来源的详细信息。
Source | Option | Fault-tolerant | Notes |
File Source | : 输入目录的路径,以及所有文件格式的通用路径。 : 每个触发中要考虑的新文件的最大数目(默认值:无最大值) : 是否首先处理最新的文件,在有大量积压文件时很有用(默认值:false) : 是否仅基于文件名而不是完整路径检查新文件(默认值:false)。将此设置为`true`时,以下文件将被视为相同的文件,因为它们的文件名“dataset.txt”相同: "file:///dataset.txt" | Yes | 支持全局路径,但不支持多个逗号分隔的路径/全局 |
Socket Source | : 连接到的主机,必须指定 : 连接到的端口,必须指定 | No | |
Rate Source | (例如100,默认:1) : 每秒应生成多少行。 (例如5s,默认:0s) : 在生成速度变为 之前,需要多长时间进行爬坡。使用比秒细的粒度将被截断为整数秒。 (例如10,默认:Spark默认的并行性) : 生成的行的分区数。
此源将尽力达到 ,但是查询可能会受到资源限制,可以调整 以帮助达到所需要的速度 | Yes | |
Kafka Source | See the Kafka Integration Guide. | Yes | |
如下一些示例:
spark = SparkSession. ...
# Read text from socket
socketDF = spark \
.readStream \
.format("socket") \
.option("host", "localhost") \
.option("port", 9999) \
.load()
socketDF.isStreaming() # 对于具有流源的DataFrame返回true
socketDF.printSchema()
# Read all the csv files written atomically in a directory#读取以原子方式写入目录中的所有csv文件
userSchema = StructType().add("name", "string").add("age", "integer")
csvDF = spark \
.readStream \
.option("sep", ";") \
.schema(userSchema) \
.csv("/path/to/directory") # Equivalent to format("csv").load("/path/to/directory")
这些示例生成未类型化的流式DataFrame,这意味着在编译时不检查DataFrame的schema,只在提交查询时 在运行时检查。一些操作(如 map
, flatMap
等)需要在编译时知道类型。为此,您可以使用与静态DataFrame相同的方法将这些非类型化的流DataFrame转换为类型化的流数据集。有关详细信息,请参阅SQL Programming Guide。此外,有关支持的流源(streaming source)的更多详细信息将在文档的后面讨论。
Schema inference and partition of streaming DataFrames/Datasets
默认情况下,来自基于文件的源的结构化流要求您指定schema,而不是依靠Spark自动推断schema。此限制确保流式查询使用一致的模式,即使在失败的情况下也是如此。对于特殊的用例,可以通过将
设置为true来重新启用schema推断。当名为
的子目录存在时会进行分区查询, 并且列表将自动递归到这些目录中去。如果这些列出现在用户提供的schema中,它们将由spark根据正在读取的文件的路径填充。组成分区schema的目录必须在查询开始时存在,并且必须保持静态。例如,可以在/data/year=2015/存在时添加/data/year=2016/但更改分区列无效(即通过创建目录/data/date=2016-04-17/)。
Operations on streaming DataFrames/Datasets(流式DataFrames/Datasets上的操作)
您可以对流式DataFrames/Datasets应用各种操作—从非类型化、类SQL操作(例如select、where、groupBy)到类型化的类RDD操作(例如map、filter、flatMap)。有关详细信息,请参阅 SQL programming guide 。让我们来看几个您可以使用的示例操作。
Basic Operations - Selection, Projection, Aggregation(基本操作-选择、投影、聚合)
DataFrame/Dataset上的大多数常见操作都支持流式处理。本节稍后将讨论一些不受支持的操作。
df = ... # streaming DataFrame with IOT device data with schema { device: string, deviceType: string, signal: double, time: DateType }
# Select the devices which have signal more than 10
df.select("device").where("signal > 10")
# Running count of the number of updates for each device type
df.groupBy("deviceType").count()
还可以将流式DataFrame/Dataset注册为临时视图,然后对其应用SQL命令。
df.createOrReplaceTempView("updates")
spark.sql("select count(*) from updates") # returns another streaming DF
注意,可以使用
来判断DataFrame/Dataset是否具有流数据。
df.isStreaming
Window Operations on Event Time(事件时间的窗口操作)
滑动事件时间窗口上的聚合对于结构化流非常简单,并且与分组聚合非常相似。在分组聚合中,为 用户指定的分组列中的每个唯一值 维护聚合值(例如计数)。对于基于窗口的聚合,将为行的事件时间所在的每个窗口维护聚合值。让我们用一个例子来理解这一点。
假设我们的quick example被修改了,流现在包含了行以及行生成的时间。我们不需要运行单词计数,而是希望在10分钟的窗口内对单词进行计数,每5分钟更新一次。也就是说,单词计数是在10分钟的窗口12:00-12:10、12:05-12:15、12:10-12:20等之间接收的单词上进行。请注意,12:00-12:10表示12:00之后但12:10之前到达的数据。现在,考虑一下12:07收到的一个word。这个word应该增加对应于两个窗口12:00-12:10和12:05-12:15的计数。因此,计数将由分组键(即此word)和窗口(可以从事件时间计算)这两个参数索引。
结果表如下所示。
由于此窗口化与分组类似,因此在代码中,可以使用
和
操作来表示窗口化聚合。您可以看到以下示例的完整代码。
words = ... # streaming DataFrame of schema { timestamp: Timestamp, word: String }
# Group the data by window and word and compute the count of each group
windowedCounts = words.groupBy(
window(words.timestamp, "10 minutes", "5 minutes"),
words.word
).count()
Handling Late Data and Watermarking (处理延迟数据和水印)
现在考虑一下如果其中一个事件延迟到达应用程序会发生什么。例如,在12:04(即事件时间)生成的单词可以在12:11被应用程序接收。应用程序应使用时间12:04而不是12:11更新窗口12:00-12:10的旧计数。这在基于窗口的分组中自然发生——结构化流可以长时间保持部分聚合的中间状态,以便后期数据可以正确更新旧窗口的聚合,如下图所示。
但是,要运行这个查询几天,系统必须绑定它在内存状态中累积的中间量。这意味着系统需要知道何时可以从内存状态中除去旧聚合,因为应用程序将不再接收该聚合的延迟数据。为了实现这一点,在Spark2.1中,我们引入了watermarking(水印)技术,它允许引擎自动跟踪数据中的当前事件时间,并尝试相应地清除旧状态。您可以通过指定事件时间列和阈值来定义查询的水印,该阈值说明数据在事件时间方面的预计延迟时间。对于在时间
结束的特定窗口,引擎将保持状态并允许延迟数据更新状态,直到(引擎看到的最大事件时间-late threshold>T)。换句话说,阈值内的延迟数据将被聚合,但超过阈值的数据将开始被删除(有关确切的保证,请参阅本节后面的部分)。让我们用一个例子来理解这一点。我们可以很容易地使用
在前面的示例中定义水印,如下所示。
words = ... # streaming DataFrame of schema { timestamp: Timestamp, word: String }
# Group the data by window and word and compute the count of each group
windowedCounts = words \
.withWatermark("timestamp", "10 minutes") \
.groupBy(
window(words.timestamp, "10 minutes", "5 minutes"),
words.word) \
.count()
此例中,我们对“timestamp”列的值定义查询水印,还定义了“10分钟”作为允许数据延迟的阈值。如果在更新输出模式下运行此查询(稍后在输出模式部分中讨论),则引擎将继续更新结果表中窗口的计数,直到窗口比水印旧,而水印比“timestamp”列中的当前事件时间落后10分钟。这有一个例子。
如图所示,引擎跟踪的最大事件时间是蓝色虚线,每个触发器开始时设置为(最大事件时间-“10分钟”)的水印是红线。例如,当引擎观察数据(12:14,dog)时,它将下一个触发器的水印设置为12:04。此水印允许引擎在额外的10分钟内保持中间状态,以便计算延迟的数据。例如,数据(12:09,cat)出现故障和延迟,并落在Windows 12:00-12:10和12:05-12:15中。例如,数据(12:09,cat)出现故障和延迟,并落在Windows 12:00-12:10和12:05-12:15中。因为它仍然在触发器中的水印12:04之前,引擎仍保持中间计数状态,并正确更新相关窗口的计数。但是,当水印更新到12:11时,窗口的中间状态(12:00-12:10)被清除,所有后续数据(例如(12:04,donkey))被视为“too late”,因此被忽略。请注意,在每个触发器之后,更新的计数(即紫色行)都会写入sink(接收器)作为触发器输出,这由Update模式决定。
某些sinks(接收器)(如文件)可能不支持Update模式所需的细粒度更新。为了使用它们,我们还支持Append模式,其中只有最终计数被写入sink。如下所示。
请注意,对非流式数据集使用
是不起作用的。由于水印不应以任何方式影响任何批查询,因此我们将直接忽略它。
与之前的Update模式类似,引擎为每个窗口保持中间计数。但是,部分计数不会更新到结果表,也不会写入接收器。引擎等待10分钟计算延迟数据,然后将一个小于watermark的窗口删除,并将最终计数追加到结果表/接收器。
引擎等待“10分钟”计算延迟日期,然后将窗口的中间状态<水印,并将最终计数附加到结果表/接收器。例如,只有在水印更新为12:11之后,才会将窗口12:00-12:10的最终计数追加到结果表中。
Conditions for watermarking to clean aggregation state(水印清除聚合状态的条件)
需要注意的是,水印必须满足以下条件才能清除聚合查询中的状态(从spark 2.1.1开始,以后可能会更改)。
- Output mode must be Append or Update. 完整模式要求保留所有聚合数据,因此不能使用水印删除中间状态。
- 聚合必须具有事件时间列或事件时间列上的一个窗口。
- 必须在 与聚合中使用的时间戳列相同的列上 调用。例如,在Append输出模式下是无效的,因为watermark被定义在与聚合不同的列上。
- 必须在聚合之前调用才能使用水印详细信息。例如,在Append输出模式下是无效的。
Semantic Guarantees of Aggregation with Watermarking(水印聚合的语义保证)
- 水印延迟(用
- 设置)为“2小时”,保证引擎不会丢弃任何延迟时间小于2小时的数据。也就是说,任何比最新处理的数据晚2小时(就事件时间而言)以内的数据都保证被聚合。
- 但是,担保只在一个方向上是严格的。延迟超过2小时的数据不一定会被删除;它可能会被聚合,也可能不会被聚合。数据越晚,引擎处理数据的可能性就越小。
Join Operations
结构化流支持将流Dataset/DataFrame与静态Dataset/DataFrame以及另一个流Dataset/DataFrame连接起来。流连接的结果是递增生成的,类似于上一节中的流聚合结果。在本节中,我们将探讨在上述情况下支持的连接类型(即inner、outer等)。请注意,在所有支持的联接类型中,使用流Dataset/DataFrame进行联接的结果将与使用流中包含相同数据的Dataset/DataFrame时的结果完全相同。
Stream-static Joins (流静态连接)
自从Spark2.0引入以来,结构化流支持流和静态Dataset/DataFrame之间的连接(内部连接和某些类型的外部连接)。下面是一个简单的例子。
staticDf = spark.read. ...
streamingDf = spark.readStream. ...
streamingDf.join(staticDf, "type") # inner equi-join with a static DF
streamingDf.join(staticDf, "type", "right_join") # right outer join with a static DF
注意,流静态连接不是有状态的,因此不需要状态管理。但是,还不支持几种类型的流静态外部联接。
Stream-stream Joins(流流连接)
在Spark 2.3中,我们添加了对流-流连接的支持,也就是说,您可以连接两个流Datasets/DataFrames。在两个数据流之间生成连接结果的挑战在于,在任何时候,对于连接的两端,数据集的视图都是不完整的,这使得在输入之间查找匹配变得更加困难。从一个输入流接收的任何行都可以与来自另一个输入流的任何将来尚未接收的行匹配。因此,对于这两个输入流,我们将过去的输入缓冲为流状态,这样我们就可以将每个未来的输入与过去的输入匹配起来,从而生成连接的结果。此外,与流聚合类似,我们自动处理延迟的、无序的数据,并可以使用水印限制状态。让我们讨论受支持的不同类型的流-流连接以及如何使用它们。
Inner Joins with optional Watermarking (带有可选水印的内部连接)
支持任何类型列上的内部连接以及任何类型的连接条件。然而,随着流的运行,流状态的大小将无限期地增长,因为必须保存所有过去的输入,因为任何新输入都可以匹配来自过去的任何输入。为了避免无界状态,您必须定义附加的连接条件,使不确定的旧输入不能与将来的输入匹配,因此可以从状态中清除。换句话说,您必须在连接中执行以下附加步骤。
1. 在两个输入上定义水印延迟,以便引擎知道输入的延迟程度(类似于流聚合)
2. 在两个输入之间定义一个事件时间约束,这样引擎就可以计算出一个输入的旧行何时不需要(即不满足时间约束)与另一个输入匹配。这个约束可以用两种方法之一定义。
1> 时间范围连接条件(例如,...JOIN ON leftTime BETWEEN rightTime AND rightTime + INTERVAL 1 HOUR)
2>在事件时间窗口中加入(例如, ...JOIN ON leftTimeWindow = rightTimeWindow
)
让我们用一个例子来理解这一点。
假设我们想要将一个广告印象流(当一个广告被显示时)与另一个用户点击广告流连接起来,以便在印象导致可货币化的点击时进行关联。要允许流-流连接中的状态清理,您必须指定水印延迟和时间约束,如下所示。
1> 水印延迟:例如,印象和相应的点击在事件时间上可能分别延迟2小时和3小时。
2>事件时间范围条件:例如,在相应的印象之后0秒到1小时的时间范围内可以发生单击。
代码应该是这样的。
from pyspark.sql.functions import expr
impressions = spark.readStream. ...
clicks = spark.readStream. ...
# Apply watermarks on event-time columns
impressionsWithWatermark = impressions.withWatermark("impressionTime", "2 hours")
clicksWithWatermark = clicks.withWatermark("clickTime", "3 hours")
# Join with event-time constraints
impressionsWithWatermark.join(
clicksWithWatermark,
expr("""
clickAdId = impressionAdId AND
clickTime >= impressionTime AND
clickTime <= impressionTime + interval 1 hour
""")
)
流-流内部连接与水印的语义保证:
这类似于对聚合进行水印提供的保证。水印延迟为“2小时”,保证引擎不会丢弃任何延迟小于2小时的数据。但是延迟超过2小时的数据可能会被处理,也可能不会被处理。
Outer Joins with Watermarking (带水印的外部联接)
虽然水印+事件时间约束对于内部连接是可选的,但是对于左右外部连接,必须指定它们。这是因为为了在外部连接中生成NULL结果,引擎必须知道何时输入行与将来的任何东西不匹配。因此,必须指定水印+事件时间约束才能生成正确的结果。因此,带有outer-join的查询看起来很像前面的广告货币化示例,只是会有一个额外的参数将其指定为一个outer-join。
impressionsWithWatermark.join(
clicksWithWatermark,
expr("""
clickAdId = impressionAdId AND
clickTime >= impressionTime AND
clickTime <= impressionTime + interval 1 hour
"""),
"leftOuter" # can be "inner", "leftOuter", "rightOuter"
)
流-流外部连接与水印的语义保证:
外部联接与内部联接在水印延迟以及是否删除数据方面具有相同的保证。
告诫:
关于外连结果是如何产生的,需要注意一些重要特征。
. 外连的空结果将产生一个延迟,这取决于指定的水印延迟和时间范围条件。这是因为引擎必须等待很长时间,以确保没有匹配项,并且将来也不会再有匹配项。
. 在目前微批引擎的实现中,水印是在微批结束时提出的,下一个微批使用更新后的水印来清理状态并输出外连结果。因为我们只在处理新数据时触发微批处理,所以如果流中没有接收到新数据,那么外连结果的生成可能会延迟。简而言之,如果连接的两个输入流中的任何一个在一段时间内都没有接收到数据,那么外连(左或右)输出可能会延迟。
Support matrix for joins in streaming queries (流查询中支持矩阵连接)
Left Input | Rigth Input | Join Type | |
Static | Static | All Types | 支持,因为它不在流数据上,即使它可以出现在流查询中 |
Stream | Static | Inner | 支持,非状态 |
Left Outer | 支持,非状态 | ||
Rigth Outer | 不支持 | ||
Full Outer | 不支持 | ||
Static | Stream | Inner | 支持,非状态 |
Left Outer | 不支持 | ||
Right Outer | 支持,非状态 | ||
Full Outer | 不支持 | ||
Stream | Stream | Inner | 支持,可选指定两边的水印+时间限制用于状态清除 |
Left Outer | 有条件支持,必须在右侧上指定水印+正确结果的时间约束,可选的在左侧上指定水印以进行所有状态清理。 | ||
Right Outer | 有条件支持,必须在左侧上指定水印+正确结果的时间约束,可选的在右侧上指定水印以进行所有状态清理 | ||
Full Outer | 不支持 |
有关支持的联接的其他详细信息:
. 联接可以级联,也就是说,您可以执行
. 从Spark 2.3开始,您只能在查询处于Append输出模式时使用连接。其他输出模式还不支持。
. 从spark 2.3开始,在连接之前不能使用其他非映射类操作。以下是一些无法使用的示例。
。在联接之前不能使用流聚合。
。在联接之前,不能在Update模式中使用mapGroupsWithState和flatMapGroupsWithState。
Streaming Deduplication (流重复数据删除)
您可以使用事件中的唯一标识符来删除数据流中重复的记录。这与使用唯一标识符列在静态上进行重复数据删除完全相同。查询将存储来自以前记录的必要数据量,以便过滤重复的记录。与聚合类似,您可以使用带或不带水印的重复数据删除。
. With watermark : 如果重复记录到达的时间有上限,则可以在事件时间列上定义水印,并使用guid和事件时间列进行重复数据消除。查询将使用水印从过去的记录中删除旧的状态数据,这些记录不希望再得到任何重复数据。这限制了查询必须维护的状态量。
. Without watermark : 由于重复记录可能到达的时间没有界限,查询将所有过去记录的数据存储为状态。
streamingDf = spark.readStream. ...
# Without watermark using guid column
streamingDf.dropDuplicates("guid")
# With watermark using guid and eventTime columns
streamingDf \
.withWatermark("eventTime", "10 seconds") \
.dropDuplicates("guid", "eventTime")
Policy for handling multiple watermarks (处理多个水印的策略)
流查询可以有多个联合或连接在一起的输入流。每个输入流可以有一个不同的延迟数据阈值,对于有状态的操作,这些阈值需要被容忍。在每个输入流上使用withWatermarks(“eventtime”,delay)指定这些阈值。例如,考虑inputstream1和inputstream2之间流连接的进行查询。
inputStream1.withWatermark(“eventTime1”, “1 hour”) .join( inputStream2.withWatermark(“eventTime2”, “2 hours”), joinCondition)
在执行查询时,结构化流分别跟踪每个输入流中看到的最大事件时间,根据相应的延迟计算水印,并选择一个全局水印用于有状态操作。默认情况下,选择最小值作为全局水印,因为它可以确保如果某个流落后于其他流(例如,其中一个流由于上游失败而停止接收数据),就不会出现数据意外丢失的情况。换句话说,全局水印将以最慢的流的速度安全地移动,查询输出也将相应地延迟。
在执行查询时,结构化流分别跟踪每个输入流中看到的最大事件时间,根据相应的延迟计算水印,并选择一个全局水印用于有状态操作。默认情况下,选择最小值作为全局水印,因为它可以确保如果某个流落后于其他流(例如,其中一个流由于上游失败而停止接收数据),就不会出现数据意外丢失的情况。换句话说,全局水印将以最慢的流的速度安全地移动,查询输出也将相应地延迟。然而,在某些情况下,您可能希望获得更快的结果,即使这意味着从最慢的流中删除数据。由于Spark 2.4,您可以通过设置SQL配置Spark . SQL .streaming来设置多重水印策略,选择最大值作为全局水印。multipleWatermarkPolicy to max(默认为min)。这使得全局水印以最快的流的速度移动。然而,作为一个副作用,来自较慢流的数据将被大量删除。因此,明智地使用这种配置。
Arbitrary Stateful Operations(任意状态操作)
许多用例需要比聚合更高级的有状态操作。例如,在许多用例中,必须从事件的数据流跟踪会话。要进行这种会话,您必须将任意类型的数据保存为状态,并在每个触发器中使用数据流事件对状态执行任意操作。从Spark 2.2开始,这可以使用操作mapGroupsWithState和功能更强大的操作flatMapGroupsWithState来完成。这两个操作都允许在分组数据集中应用用户定义的代码来更新用户定义的状态。
Unsupported Operations
流式DataFrame/Dataset 不支持一些DataFrame/Dataset 操作。其中一些如下。
. 流数据集尚不支持多个流聚合(即流数据集中的聚合链)
. 流数据集不支持限制行和取前N行。
. 不支持对流数据集执行不同(distinct)的操作。
. 只有在聚合之后并且处于完全(complete)输出模式时,流数据集才支持排序操作。
. 不支持流数据集上的几种类型的外部联接。有关详细信息,请参阅 support matrix in the Join Operations section 。
此外,还有一些数据集方法不适用于流数据集。它们是将立即运行查询并返回结果的操作,这对流数据集没有意义。相反,这些功能可以通过显式启动流式查询来完成(请参见下一节)。
. - 无法从流数据集中返回单个计数。相反,使用ds.groupby().count()返回包含运行计数的流数据集。
. - 而是使用ds.writestream.foreach(...) (请参见下一节)。
. - 而是使用控制台接收器(请参见下一节)。
如果您尝试这些操作,您将看到一个 AnalysisException
,比如: “operation XYZ is not supported with streaming DataFrames/Datasets”。虽然其中一些可能在未来的spark版本中得到支持,但还有一些基本上难以有效地在流数据上实现。例如,不支持对输入流进行排序,因为它需要跟踪流中接收的所有数据。因此,从根本上说,这很难有效地执行。
Starting Streaming Queries (开始流式查询)
一旦定义了最终结果 DataFrame/Dataset,剩下的就是开始流计算。为此,必须使用通过
返回的
(Scala/Java/Python docs) 。您必须在这个接口中指定一个或多个以下内容。
. 输出接收器的详细信息(Details of the output sink):数据格式、位置等。
. 输出模式(Output mode) :指定写入输出接收器的内容。
. 查询名(Query name) :可选地,为标识指定查询的唯一名称。
. 触发间隔(Trigger interval) :可选地,指定触发间隔。如果未指定,系统将在上一次处理完成后立即检查新数据的可用性。如果由于前一个处理未完成而错过触发时间,则系统将立即触发处理。
. 检查点位置(Checkpoint location) :对于一些可以保证端到端容错的输出接收器,指定系统写入所有检查点信息的位置。这应该是HDFS兼容的容错文件系统中的一个目录。下一节将更详细地讨论检查点的语义。
Output Modes
有几种类型的输出模式。
. Append mode(default) -这是默认模式,其中只有自上一个触发器以来添加到结果表中的新行才会输出到接收器。只有添加到结果表的行永远不会更改 的查询才支持此功能。因此,此模式保证每行只输出一次(假设是容错的接收器)。例如,只有select
, where
, map
, flatMap
, filter
, join
等的查询将支持Append模式。
. Complete mode - 每次触发后,整个结果表都将输出到接收器。聚合查询支持此功能。
. Update mode - (自Spark 2.1.1起提供)只有结果表中自上次触发器以来更新的行才会输出到接收器。更多信息将添加到将来的版本中。
不同类型的流式查询支持不同的输出模式。下面是兼容性矩阵。
Query Type | Supported Output Modes | Notes | |
Queries with aggregation | Aggregation on event-time with watermark | Append, Update, Complete | Append模式使用水印来删除旧的聚合状态。但是,窗口聚合的输出被延迟到“withWatermark()”中指定的延迟阈值,因为根据模式语义,行在最后完成后(即水印交叉后)只能添加到结果表中一次。有关详细信息,请参阅 Late Data 部分。 Updae模式使用水印删除旧的聚合状态。 Complete模式不会删除旧的聚合状态,因为根据定义,此模式保留结果表中的所有数据。 |
Other aggregations | Complete, Update | 由于未定义水印(仅在其他类别中定义),因此不会删除旧的聚合状态。 不支持Append模式,因为聚合可能更新,从而违反了此模式的语义 | |
Queries with | Update | | |
Queries with | Append operation mode | Append | flatMapGroupsWithState之后允许聚合。 |
Update operation mode | Update | flatMapGroupsWithState之后不允许聚合。 | |
Queries with | Append | 尚不支持Update和Complete模式。 | |
Other queries | Append, Update | 不支持Complete模式,因为无法将所有未聚合的数据保留在结果表中。 |
Output Sinks
有几种内置的输出接收器。
. File sink - Stores the output to a directory.
writeStream
.format("parquet") // can be "orc", "json", "csv", etc.
.option("path", "path/to/destination/dir")
.start()
. Kafka sink - 将输出存储到Kafka中的一个或多个主题。
writeStream
.format("kafka")
.option("kafka.bootstrap.servers", "host1:port1,host2:port2")
.option("topic", "updates")
.start()
. Foreach sink - 对输出中的记录执行任意计算。有关详细信息,请参阅本节后面的内容。
writeStream
.foreach(...)
.start()
. Console sink (for debugging) - 每次有触发器时都将输出打印到console/stdout。支持附加和完整输出模式。这应该用于在低数据量上进行调试,因为在每次触发之后,整个输出都被收集并存储在驱动程序的内存中。
writeStream
.format("console")
.start()
. Memory sink (for debuggin) - 输出作为内存中的表存储在内存中。支持Append和Complete输出模式。当整个输出被收集并存储在驱动程序的内存中这应该用于调试低数据量的目的,因为整个输出被收集并存储在驱动程序的内存中。因此,请谨慎使用。
writeStream
.format("memory")
.queryName("tableName")
.start()
有些接收器不能容错,因为它们不能保证输出的持久性,并且仅用于调试目的。请参见前面关于 fault-tolerance semantics 的部分。以下是Spark中所有接收器的细节。
Sink | Supported Output modes | Options | Fault-tolerant | Notes |
File Sink | Append |
用于文件格式特定选项,请参阅DataFrameWriter (Scala/Java/Python/R)中的相关方法。例如,"parquet"格式选项请参见 | Yes(exactly-once:正好一次) | 支持写入分区表。按时间划分可能有用。 |
Kafka Sink | Append, Update, Complete | Yes(at-least-once:至少一次) | Kafka Integration Guide中更多详细信息 | |
Foreach Sink | Append, Update, Complete | None | 取决于ForEachWriter实现 | 下一节将详细介绍 |
ForeachBath Sink | Append, Update, Complete | None | 取决于实现 | 下一节将详细介绍 |
Console Sink | Append, Update, Complete |
| No | |
Memory Sink | Append, Complete | None | No.但在完整模式下,重新启动的查询将重新创建完整表。 | 表名是查询名。 |
注意,必须调用start()
才能实际开始执行查询。这将返回一个StreamingQuery对象,该对象是连续运行执行的句柄。您可以使用这个对象来管理查询,我们将在下一小节中讨论这个问题。现在,让我们用几个例子来理解这一切。
# ========== DF with no aggregations ==========
noAggDF = deviceDataDf.select("device").where("signal > 10")
# Print new data to console
noAggDF \
.writeStream \
.format("console") \
.start()
# Write new data to Parquet files
noAggDF \
.writeStream \
.format("parquet") \
.option("checkpointLocation", "path/to/checkpoint/dir") \
.option("path", "path/to/destination/dir") \
.start()
# ========== DF with aggregation ==========
aggDF = df.groupBy("device").count()
# Print updated aggregations to console
aggDF \
.writeStream \
.outputMode("complete") \
.format("console") \
.start()
# Have all the aggregates in an in-memory table. The query name will be the table name
aggDF \
.writeStream \
.queryName("aggregates") \
.outputMode("complete") \
.format("memory") \
.start()
spark.sql("select * from aggregates").show() # interactively query in-memory table
Using Foreach and ForeachBatch ()
foreach
和 foreachBatch
操作允许您对流式查询的输出应用任意操作和写入逻辑。它们有稍微不同的用法——虽然foreach
允许在每一行上自定义写入逻辑,但是foreachBatch
允许在每个微批的输出上执行任意操作和自定义逻辑。让我们更详细地了解它们的用法。
1>
允许您指定对流式查询的每个微批的输出数据执行的函数。自Spark 2.4以来,Scala、Java和Python都支持此功能。它接受两个参数:一个 DataFrame 或 Dataset,该数据集具有微批处理的输出数据和微批处理的惟一ID。
def foreach_batch_function(df, epoch_id):
# Transform and write batchDF
pass
streamingDF.writeStream.foreachBatch(foreach_batch_function).start()
使用 foreachBatch
可以执行以下操作。
. Reuse existing batch data sources (重用现有批处理数据源)- 对于许多存储系统,可能还没有可用的流接收器(streaming sink),但可能已经存在用于批处理查询的数据编写器(data writer)。使用 foreachBatch
,可以在每个微批的输出上使用批处理数据编写器。
. Write to multiple locations (写入多个位置) - 如果要将流式查询的输出写入多个位置,则只需多次写入输出DataFrame/Dataset。但是,每次尝试写入都会导致重新计算输出数据(包括可能重新读取输入数据)。为了避免重新计算,应该缓存输出DataFrame/Dataset,将其写入多个位置,然后取消缓存。这里有一个概述:
streamingDF.writeStream.foreachBatch {
(batchDF: DataFrame, batchId: Long) => batchDF.persist()
batchDF.write.format(…).save(…) // location 1
batchDF.write.format(…).save(…) // location 2
batchDF.unpersist() }
. Apply additional DataFrame operations (应用其它DataFrame操作) - 流式DataFrames中不支持许多DataFrame 和 Dataset操作,因为Spark在这些情况下不支持生成增量计划。使用 foreachBatch
,您可以对每个微批量输出应用其中一些操作。但是,您必须对自己执行该操作的端到端语义进行推理:。
注意:
. 默认情况下,foreachbatch至少提供一次写入保证。但是,您可以使用提供给函数的batchID作为消除重复输出并获得一次性保证的方法。
. foreachbatch不使用连续处理模式,因为它从根本上依赖于流式查询的微批处理执行。如果以连续模式写入数据,请改用foreach。
2>
如果foreachBatch不是一个选项(例如,不存在相应的批数据写入器,或者连续处理模式),那么可以使用foreach来表示自定义写入器逻辑。具体地说,可以将数据写逻辑划分为三种方法来表示:open、process和close。自从Spark 2.4以来,foreach在Scala、Java和Python中都可用。
在Python中,可以通过两种方式调用foreach:在函数或对象中。该函数提供了一种表达处理逻辑的简单方法,但不允许在失败导致重新处理某些输入数据时对生成的数据去重。对于这种情况,必须在对象中指定处理逻辑。
(1)此函数接受一行作为输入。
def process_row(row):
# Write row to storage
pass
query = streamingDF.writeStream.foreach(process_row).start()
(2)此对象有一个process方法和可选的open和close方法
class ForeachWriter:
def open(self, partition_id, epoch_id):
# Open connection. This method is optional in Python.
pass
def process(self, row):
# Write row to connection. This method is NOT optional in Python.
pass
def close(self, error):
# Close the connection. This method in optional in Python.
pass
query = streamingDF.writeStream.foreach(ForeachWriter()).start()
Execution semantics (执行语义)当启动流式查询时,spark以 以下方式调用函数或对象的方法:
. 此对象的单个副本负责 查询中单个任务生成的所有数据。换句话说,一个实例负责处理以分布式方式生成的数据的一个分区。
. 此对象必须是可序列化的,因为每个任务都将获得所提供对象的新的序列化反序列化副本。因此,强烈建议对写入数据进行任何初始化(例如。打开连接或启动事务)是在调用open()方法之后完成的,这意味着任务已准备好生成数据。
. 方法的生命周期如下:
。对于每个具有分区ID的分区:
. 对于具有epoch_id的流数据每个bath/epoch
. 方法open(partitionId, epochId)被调用。
. 如果open(…)返回true,则对分区和batch/epoch中的每一行调用方法process(row)。
. 当处理行时,会调用带有错误(如果有的话)的close(error)。
. close()方法(如果存在)在open()方法存在并成功返回时调用(无论返回值如何),除非JVM或Python进程在中间崩溃。
. 注意: 当失败导致重处理一些输入数据时,可以用open()方法中的partitionId 和 epochId对生成对生成的数据进行去重。这取决于查询的执行模式。如果流式查询是在微批处理模式下执行的,那么由一个唯一元组(partition_id,epoch_id)表示的每个分区都保证具有相同的数据。因此,(partition_id,epoch_id)可用于消除重复和/或事务性提交数据,并实现一次性保证。但是,如果流式查询是在连续模式下执行的,则此保证不适用,因此不应用于重复数据去重。
Triggers
流式查询的触发器设置定义了流式数据处理的时间,无论该查询是作为具有固定批处理间隔的微批处理查询还是作为连续处理查询执行。以下是支持的不同类型的触发器。
Trigger Type | Description |
unspecified(default) | 如果没有显式指定触发器设置,那么默认情况下,查询将以微批处理模式(micro-batch mode)执行,在此模式下,前一个微批处理完成后,将立即生成微批处理。 |
Fixed interval micro-batches | 查询将以微批处理模式(micro-batches mode)执行,微批处理将按用户指定的时间间隔启动。 . 如果前一个微批处理在间隔内完成,那么引擎将等到间隔结束后再启动下一个微批处理。 . 如果前一个微批次的完成时间比间隔时间长(即,如果缺少间隔边界),则下一个微批次将在前一个微批次完成后立即开始(即,它不会等待下一个间隔边界)。 . 如果没有新数据可用,则不会启动微批处理。 |
One-time micro-batch | 查询将只执行一个微批处理来处理所有可用的数据,然后自行停止。在希望定期启动集群、处理自上一阶段以来可用的所有内容,然后关闭集群的场景中,这非常有用。在某些情况下,这可能导致显著的成本节约。 |
Continuous with fixed checkpoint interval | 查询将在新的低延迟、连续处理模式下执行。请在下面的 Continuous Processing section 中阅读有关此内容的更多信息。 |
如下少许代码示例.
# Default trigger (runs micro-batch as soon as it can)
df.writeStream \
.format("console") \
.start()
# ProcessingTime trigger with two-seconds micro-batch interval
df.writeStream \
.format("console") \
.trigger(processingTime='2 seconds') \
.start()
# One-time trigger
df.writeStream \
.format("console") \
.trigger(once=True) \
.start()
# Continuous trigger with one-second checkpointing interval
df.writeStream
.format("console")
.trigger(continuous='1 second')
.start()
Managing Streaming Queries (管理流式查询)
启动查询时创建的StreamingQuery
对象可用于监视和管理查询。
query = df.writeStream.format("console").start() # get the query object
query.id # get the unique identifier of the running query that persists across restarts from checkpoint data
#获取 从检查点数据重新启动时一直存在的正在运行的查询的唯一标识符
query.runId # get the unique id of this run of the query, which will be generated at every start/restart
#获取此查询运行的唯一ID,该ID将在每次启动/重新启动时生成。
query.name # get the name of the auto-generated or user-specified name
#获取自动生成的名称或用户指定的名称
query.explain() # print detailed explanations of the query#打印查询的详细说明
query.stop() # stop the query#停止查询
query.awaitTermination() # block until query is terminated, with stop() or with error
#阻塞,直到使用stop()或错误终止查询
query.exception() # the exception if the query has been terminated with error
#查询因错误而终止时的异常
query.recentProgress # an array of the most recent progress updates for this query
#此流式查询的最新进度更新
query.lastProgress # the most recent progress update of this streaming query
您可以在单个SparkSession中启动任意数量的查询。它们将同时运行,共享集群资源。可以使用 sparkSession.streams
获得 StreamingQueryManager
(Scala/Java/Python docs),它可以用来管理当前活动的查询.
spark = ... # spark session
spark.streams.active # get the list of currently active streaming queries
spark.streams.get(id) # get a query object by its unique id
spark.streams.awaitAnyTermination() # block until any one of them terminates
Monitoring Streaming Queries(监控流式查询)
有多种方法可以监视活动的流式查询。您可以使用Spark的Dropwizard度量支持将度量推送到外部系统,也可以通过编程方式访问它们。
交互读取度量值
可以使用 streamingQuery.lastProgress和
streamingQuery.status
直接获取活动查询的当前状态和度量。 streamingQuery.lastProgress
在Scala和Java中返回一个 StreamingQueryProgress
对象,在Python中返回一个具有相同字段的字典。它包含有关流最后一个触发器中的进度的所有信息—处理了哪些数据、处理速率、延迟等。还有streamingQuery.recentProgress
,它返回最后几个进度的数组。
示例如下:
query = ... # a StreamingQuery
print(query.lastProgress)
'''
Will print something like the following.
{u'stateOperators': [], u'eventTime': {u'watermark': u'2016-12-14T18:45:24.873Z'}, u'name': u'MyQuery', u'timestamp': u'2016-12-14T18:45:24.873Z', u'processedRowsPerSecond': 200.0, u'inputRowsPerSecond': 120.0, u'numInputRows': 10, u'sources': [{u'description': u'KafkaSource[Subscribe[topic-0]]', u'endOffset': {u'topic-0': {u'1': 134, u'0': 534, u'3': 21, u'2': 0, u'4': 115}}, u'processedRowsPerSecond': 200.0, u'inputRowsPerSecond': 120.0, u'numInputRows': 10, u'startOffset': {u'topic-0': {u'1': 1, u'0': 1, u'3': 1, u'2': 0, u'4': 1}}}], u'durationMs': {u'getOffset': 2, u'triggerExecution': 3}, u'runId': u'88e2ff94-ede0-45a8-b687-6316fbef529a', u'id': u'ce011fdc-8762-4dcb-84eb-a77333e28109', u'sink': {u'description': u'MemorySink'}}
'''
print(query.status)
'''
Will print something like the following.
{u'message': u'Waiting for data to arrive', u'isTriggerActive': False, u'isDataAvailable': False}
'''
Reporting Metrics programmatically using Asynchronous APIs(使用异步API以编程方式报告度量)
您还可以通过附加 StreamingQueryListener
(Scala/Java docs)来异步监视与 SparkSession
相关的所有查询。一旦使用sparkSession.streams.attachListener()
附加自定义StreamingQueryListener
对象,当查询启动和停止以及在活动查询中取得进展时,您将得到回调。举个例子,
Not available in Python.
Reporting Metrics using Dropwizard(使用DropWizard报告度量)
Spark支持使用 Dropwizard Library报告度量.为了同时报告结构化流查询的指标,您必须显式地启用SparkSession中的配置spark.sql.streaming.metricsEnabled
spark.conf.set("spark.sql.streaming.metricsEnabled", "true")
# or
spark.sql("SET spark.sql.streaming.metricsEnabled=true")
启用此配置后在SparkSession中启动的所有查询都将通过DropWizard向已配置的任何接收器(例如Ganglia、Graphite、JMX等)报告度量。
Recovering from Failures with Checkpointing(使用检查点从失败中恢复)
如果出现故障或有意关闭,您可以恢复以前查询的进度和状态,并在停止的地方继续。这是使用检查点和提前写入日志完成的。您可以使用检查点位置配置查询,该查询将把所有进度信息(即每个触发器中处理的偏移范围)和正在运行的聚合(如quick example中的字数)保存到检查点位置。此检查点位置必须是HDFS兼容文件系统中的路径,并且可以在starting a query时在DatastreamWriter中设置为选项。
aggDF \
.writeStream \
.outputMode("complete") \
.option("checkpointLocation", "path/to/HDFS/dir") \
.format("memory") \
.start()
Recovery Semantics after Changes in a Streaming Query(流查询中更改后的恢复语义)
在从相同检查点位置重新启动之间,允许流查询中的哪些更改是有限制的。以下是几种不允许的更改,或者更改的效果没有定义好。对于所有它们:
. 术语allowed意味着您可以进行指定的更改,但其效果的语义是否定义良好取决于查询和更改。
. 术语“not allowed”意味着您不应该进行指定的更改,因为重新启动的查询可能会失败,并出现不可预知的错误。
表示使用sparkSession.readStream生成的流式DataFrame/Dataset。
Types of changes(变化的类型)
- 输入源的编号或类型(即不同的源)发生更改:这是不允许的。
- 输入源参数的更改:是否允许,更改的语义是否定义良好,取决于源和查询。下面是几个例子。
。允许 添加/删除/修改速率限制: spark.readStream.format("kafka").option("subscribe", "topic")
到 spark.readStream.format("kafka").option("subscribe", "topic").option("maxOffsetsPerTrigger", ...)
。
通常不允许更改订阅的主题/文件,因为结果不可预测:spark.readStream.format("kafka").option("subscribe", "topic")
到spark.readStream.format("kafka").option("subscribe", "newTopic")
- 输出接收器类型的更改:允许在几个特定的接收器组合之间进行更改。这需要逐个验证。下面是几个例子。
。允许文件接收器到Kafka接收器。Kafka只能看到新的数据。
。不允许Kafka接收器到文件接收器。
。允许 Kafka 接收器改为foreach,反之亦然。
- 输出接收器参数的更改:是否允许这样做以及更改的语义是否定义良好取决于接收器和查询。下面是几个例子。
。不允许更改文件接收器的输出目录: sdf.writeStream.format("parquet").option("path", "/somePath")
to sdf.writeStream.format("parquet").option("path", "/anotherPath")
。
允许更改输出主题: sdf.writeStream.format("kafka").option("topic", "someTopic")
to sdf.writeStream.format("kafka").option("topic", "anotherTopic")
。
允许更改用户定义的foreach接收器(即 ForeachWriter
代码),但更改的语义取决于代码。
- 在projection / filter / map-like 操作中的更改:某些情况下是允许的。例如:
。允许添加/删除 筛选器(filters):sdf.selectExpr("a")
到 sdf.where(...).selectExpr("a").filter(...)
。
允许更改具有 相同输出schema的投影(projection): sdf.selectExpr("stringColumn AS json").writeStream
到 sdf.selectExpr("anotherStringColumn AS json").writeStream
。
有条件地允许更改具有不同输出schema的投影:只有当输出接收器允许schema从“a”更改为“b”时,sdf.selectExpr("a").writeStream
更改为sdf.selectExpr("b").writeStream
才被允许。
- 状态操作中的更改:流式查询中的一些操作需要维护状态数据,以便持续更新结果。结构化流自动将状态数据检查点到容错存储(例如,HDFS、AWS S3、Azure Blob存储),并在重启后恢复。但是,这假定状态数据的schema在重启期间保持不变。这意味着在重新启动之间不允许对流查询的有状态操作进行任何更改(即添加、删除或模式修改)。以下是有状态操作的列表,为了确保状态恢复,在重启之间不应该更改这些操作的schema:
。Streaming aggregation(流聚合):例如,sdf.groupBy("a").agg(...)
. 不允许对分组键或聚合的数量或类型进行任何更改。
。Streaming deduplication(流去重) :例如,sdf.dropDuplicates("a")
.不允许对分组键或聚合的数量或类型进行任何更改。
。Stream-stream join(流流连接):例如,sdf1.join(sdf2, ...)
(即,两个输入都是用sparkSession.readStream
生成的)。不允许更改schema或同等联接列。不允许更改联接类型(外部或内部)。不允许更改联接类型(外部或内部)。联接条件中的其他更改定义错误。
。Arbitrary stateful operation(任意状态操作):例如,sdf.groupByKey(...).mapGroupsWithState(...)
or sdf.groupByKey(...).flatMapGroupsWithState(...)
. 不允许对用户定义状态的schema和超时类型进行任何更改。允许在用户定义的状态映射函数中进行任何更改,但是更改的语义效果取决于用户定义的逻辑。如果您真的希望支持状态schema更改,那么可以使用支持schema迁移的编码/解码方案显式地将复杂的状态数据结构编码/解码为字节。例如,如果您将状态保存为Avro编码的字节,那么您就可以在查询重新启动之间自由地更改Avro-state-schema,因为二进制状态总是会成功地恢复。
Continuous Processing (持续处理)
[Experimental]
Continuous processing 连续处理是Spark 2.3中引入的一种新的、实验性的流式执行模式,它支持低(~1 ms)端到端延迟,并至少保证一次容错。将其与默认的微批处理引擎进行比较,该引擎(默认的微批处理引擎)可以实现一次完全保证,但最多只能实现约100毫秒的延迟。对于某些类型的查询(下面讨论),您可以选择在不修改应用程序逻辑(即,不更改数据帧/数据集操作)的情况下执行它们的模式。
要在连续处理模式下运行受支持的查询,只需指定 一个具有所需检查点间隔的连续触发器作为参数。例如,
spark \
.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "host1:port1,host2:port2") \
.option("subscribe", "topic1") \
.load() \
.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") \
.writeStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "host1:port1,host2:port2") \
.option("topic", "topic1") \
.trigger(continuous="1 second") \ # only change in query
.start()
检查点间隔为1秒意味着连续处理引擎将每秒记录查询的进度。生成的检查点的格式与微批处理引擎兼容,因此任何查询都可以用任何触发器重新启动。例如,支持的以微批处理模式启动的查询可以在连续模式下重新启动,反之亦然。请注意,任何时候切换到连续模式时,至少会得到一次容错保证。
Supported Queries
从spark 2.3开始,在连续处理模式中只支持以下类型的查询。
. Operations: 在连续模式下仅支持类似map的Dataset/DataFrame操作,即仅支持projections(投影)(select
, map
, flatMap
, mapPartitions等
)和selections(选择)(where
, filter等
)。
。除了聚合函数(因为还不支持聚合)、current_timestamp()
和current_date()
(使用时间进行确定性计算很有挑战性)之外,所有SQL函数都受到支持。
. Sources:
。Kafka source:支持所有选项。
。Rate source:适合测试。在连续模式中只支持numPartitions
和rowsPerSecond
选项。
. Sinks:
。Kafka source:支持所有选项。
。Memory sink: 有利于调试。
。Console sink:有利于调试。支持所有选项。注意,控制台将打印在连续触发器中指定的每个检查点间隔。
有关输入源和输出接收器的详细信息,请参阅Input Sources 和 Output Sinks部分。尽管控制台接收器适合测试,但可以最好地观察到以Kafka为源和接收器的端到端低延迟处理,因为这允许引擎 在输入主题中输入数据可用的毫秒内 处理数据并使结果在输出主题中可用。
Caveats(注意事项)
- 连续处理引擎启动多个长时间运行的任务,这些任务不断地从源读取数据、处理数据并不断地向接收器写入数据。查询所需的任务数取决于查询可以并行从源读取多少分区。因此,在开始连续处理查询之前,必须确保集群中有足够的核心来并行执行所有任务。例如,如果您正在读取具有10个分区的Kafka主题,那么集群必须至少有10个核心才能使查询取得进展。
- 停止连续处理流可能会产生虚假的任务终止警告。这些可以被安全地忽略。
- 前没有失败任务的自动重试。任何失败都将导致查询停止,需要从检查点手动重新启动查询。
Additional Information
Further Reading
。关于如何运行Spark示例的Instructions(说明)。
- 阅读 Structured Streaming Kafka Integration Guide中有关与Kafka集成的信息
- 阅读Spark SQL Programming Guide中有关使用DataFrames/Datasets的详细信息
- 第三方博客帖子
- Real-time Streaming ETL with Structured Streaming in Apache Spark 2.1 (Databricks Blog)
- Real-Time End-to-End Integration with Apache Kafka in Apache Spark’s Structured Streaming (Databricks Blog)
- Event-time Aggregation and Watermarking in Apache Spark’s Structured Streaming (Databricks Blog)
二. Spark Streaming
http://spark.apache.org/docs/latest/streaming-programming-guide.html
概述
Park Streaming是核心Spark API的扩展,它支持对实时数据流进行可扩展、高吞吐量、容错的流处理。数据可以从Kafka、Flume、Kinesis或TCP套接字等许多源中摄取,并且可以使用复杂的算法进行处理,这些算法使用诸如map
, reduce
, join
和 window
.等高级函数表示。最后,可以将处理过的数据推送到文件系统、数据库和实时仪表板(live dashboards)中。实际上,您可以在数据流上应用Spark的 machine learning(机器学习)和graph processing(图形处理算法)。
在内部,它的工作原理如下。Spark Streaming接收实时输入数据流,并将数据分为批,然后由Spark engine进行处理,以批量生成最终结果流。
Spark流提供了一种高级抽象,称为 discretized stream(离散流)或 DStream,它表示连续的数据流。 DStream可以从输入数据流(如Kafka、Flume和Kinesis)中创建,也可以通过对其他DStreams应用高级操作来创建。在内部,一个DStreams表示为一个 RDDs序列。
本指南向您展示如何开始使用DStream编写Spark流式程序。您可以在Scala、Java或Python(在Spark1.2中引入)中编写Spark Streaming程序,所有这些都在本指南中介绍。您将在本指南中找到允许您在不同语言的代码段之间进行选择的选项卡。
在Python中有一些不同或不可用的API。在本指南中,您将发现标记
突出显示了这些差异。
A Quick Example(一个简单的例子)
在我们详细介绍如何编写自己的Spark Streaming程序之前,让我们快速了解一下一个简单的Spark Streaming程序是什么样子的。假设我们想计算从一个监听TCP套接字的数据服务器接收到的文本数据中的字数。你需要做的就是如下。
首先,我们导入StreamingContext,它是所有流功能的主要入口点。我们创建了一个本地StreamingContext ,其中有两个执行线程,批处理间隔为1秒。
from pyspark import SparkContext
from pyspark.streaming import StreamingContext
# Create a local StreamingContext with two working thread and batch interval of 1 second
sc = SparkContext("local[2]", "NetworkWordCount")
ssc = StreamingContext(sc, 1)
使用此上下文,我们可以创建一个表示来自指定主机名(例如localhost
)和端口(例如 9999
)的TCP源的流数据的DStream。
# Create a DStream that will connect to hostname:port, like localhost:9999
lines = ssc.socketTextStream("localhost", 9999)
此lines
DStream表示将从数据服务器接收的数据流。此DStream中的每个记录都是一行文本。接下来,我们要按空格将行拆分为单词。
# Split each line into words
words = lines.flatMap(lambda line: line.split(" "))
flatMap
是一对多DStream操作,它通过从源DStream中的每个记录生成多个新记录来创建新的DStream。在这种情况下,每行将被拆分为多个单词,单词流表示为words
DStream。接下来,我们要计数这些单词。
# Count each word in each batch
pairs = words.map(lambda word: (word, 1))
wordCounts = pairs.reduceByKey(lambda x, y: x + y)
# Print the first ten elements of each RDD generated in this DStream to the console
wordCounts.pprint()
words
DStream被进一步映射(一对一转换)到(word, 1)对的数据流,然后将其归约(reduce)以获得每批数据中单词的频率。最后, wordCounts.pprint()
将打印 每秒生成的计数的 一部分。
请注意,当执行这些行时,Saprk Streaming仅设置它在启动时将执行的计算,并且尚未启动真正的处理。为了在所有变换都设置好之后开始处理,我们最后调用
ssc.start() # Start the computation
ssc.awaitTermination() # Wait for the computation to terminate
如果您已经下载并构建了Spark,那么您可以如下运行此示例。您首先需要使用
$ nc -lk 9999
然后,在另一个终端中,可以使用
$ ./bin/spark-submit examples/src/main/python/streaming/network_wordcount.py localhost 9999
然后,在运行netcat服务器的终端中键入的任何行都将被计数并每秒在屏幕上打印。它看起来像下面这样。
Basic Concepts(基本概念)
接下来,我们将超越简单的示例,并详细介绍Spark Streaming的基础知识。
Linking
与Spark类似,Spark流可以通过Maven Central获得。要编写自己的Spark Streaming程序,必须将以下依赖项添加到SBT或Maven项目中。
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>2.4.3</version>
<scope>provided</scope>
</dependency>
如果要从诸如kafka、flume和kinesis之类的源中摄取Spark Streaming 核心API中不存在的数据,则必须将相应的工件spark-streaming-xyz_2.12
添加到依赖项中。例如,一些常见的例子如下。
Source | Artifact |
Kafka | spark-streaming-kafka-0-10_2.12 |
Flume | spark-streaming-flume_2.12 |
Kinesis | spark-streaming-kinesis-asl_2.12[Amazon Software License] |
有关最新的列表,请参阅 Maven repository (Maven存储库),以获取支持的源和工件的完整列表。
Initializing StreamingContext
要初始化一个Spark Streaming程序,必须创建一个StreamingContext对象,该对象是所有火花流功能的主要入口点。
可以从一个SparkContext 对象创建一个StreamingContextt对象。
from pyspark import SparkContext
from pyspark.streaming import StreamingContext
sc = SparkContext(master, appName)
ssc = StreamingContext(sc, 1)
appName
参数是应用程序要在集群UI上显示的名称。 master是一个
Spark, Mesos or YARN 集群URL或者是一个特殊的
“local[*]”字符以运行在本地模式。实际上,在集群上运行时,您不希望在程序中硬编码 master
,而是使用spark-submit 启动应用程序并在那里接收它。但是,对于本地测试和单元测试,您可以通过“local[*]”来运行进程中的Spark Streaming(检测本地系统中的核心数量)。
批处理间隔必须根据应用程序的延迟要求和可用的群集资源来设置。有关详细信息,请参阅Performance Tuning(性能调整)部分。
定义上下文后,必须执行以下操作。
1. 通过创建输入DStreams定义输入源。
2. 通过对DStreams应用变换和输出操作来定义流计算。
3. 开始接收数据并使用 streamingContext.start()
进行处理。
4. 使用streamingContext.awaitTermination()
等待停止处理(手动或由于任何错误)。
5. 可以使用streamingContext.stop()手
动停止处理。
Points to remember(要记住的要点):
. 一旦启动了上下文,就不能设置或添加新的流计算。
. 一旦上下文停止,就不能重新启动。
. 一个JVM中只能同时有一个StreamingContext处于活动状态。
. StreamingContext上的stop()也会停止SparkContext。要仅停止StreamingContext,请将被调用的stop()
的可选参数stopsparkContext设置为false。
. 只要在创建下一个StreamingContext 之前停止上一个StreamingContext (不停止SparkContext),就可以重新使用SparkContext来创建多个StreamingContexts。
Discretized Streams (DStreams)(离散流)
Discretized Stream 或 DStream 是Spark Streaming提供的基本抽象。它表示一个连续的数据流,要么是从源接收的输入数据流,要么是通过转换输入流生成的已处理数据流。在内部,数据流由一系列连续的RDD表示,这是对不可变的分布式数据集的Spark抽象。数据流中的每个RDD都包含来自一个特定间隔的数据,如下图所示。
应用于一个DStream上的任何操作都转换为底层RDDs上的操作。例如,在前面将行流转换为字的示例中,对lines
DStream中的每个RDD应用flatMap
操作,以生成 words
DStream的RDDs。如下图所示。
这些底层的RDD变换是由Spark引擎计算出来的。DStream 操作隐藏了这些细节中的大部分,并为开发人员提供了更高级别的API以方便使用。这些操作将在后面的章节中详细讨论。
Input DStreams and Receivers(输入数据流和接收器)
输入DStream是表示从流源接收的输入数据流的DStream。在 quick example中,lines
一个输入DStream,因为它表示从netcat服务器接收的数据流。每个输入DStream(除本节后面讨论的文件流之外)都与接收器--Receiver(Scala doc, Java doc)对象相关联,后者接收来自源的数据并将其存储在Spark内存中进行处理。
Spark流提供两类内置流源。
. Basic sources: 在streamingContext API中直接可用的源。示例:文件系统和套接字连接。
. Advanced sources: 诸如Kafka、Flume、Kinesis等源可以通过额外的实用程序类获得。如linking部分所述,这些需要针对额外依赖项进行链接。
我们将在本节后面讨论每个类别中存在的一些来源。
请注意,如果您希望在流应用程序中并行接收多个数据流,则可以创建多个输入DStreams (在性能优化(Performance Tuning )部分进一步讨论)。这将创建多个接收器,同时接收多个数据流。但是请注意,Spark Worker/Executor是一个长期运行的任务,因此它占用分配给Spark流应用程序的一个核(core)。因此,重要的是要记住,Spark流应用程序需要分配足够的核(或线程,如果在本地运行)来处理接收到的数据以及运行接收器--receiver(s)。
要记住的要点
. 在本地运行Spark流程序时,不要使用“local”或“local[1]”作为master URL。这意味着只有一个线程将用于本地运行任务。如果您使用的是基于接收器的输入数据流(如sockets、kafka、flume等),那么将使用单线程来运行接收器,不留下任何线程来处理接收到的数据。因此,在本地运行时,始终使用“local[n]”作为master URL,其中n>要运行的接收器数(有关如何设置master的信息,请参阅 Spark Properties)。
. 将逻辑扩展到在集群上运行,分配给Spark流应用程序的核数必须大于接收器(recivers)数。否则,系统将接收数据,但无法对其进行处理。
Basic Sources
我们已经在quick example中查看了ssc.socketTextStream(...)
,该示例从通过一个TCP套接字连接接收的文本数据创建数据流。除了套接字之外,StreamingContext API还提供了从文件中创建数据流作为输入源的方法。
File Streams
对于从与HDFS API兼容的任何文件系统(即HDFS、S3、NFS等)上的文件读取数据,可以通过StreamingContext.fileStream[KeyClass, ValueClass, InputFormatClass]
创建DStream。
文件流不需要运行接收器,因此不需要为接收文件数据分配任何核。
对于简单文本文件,最简单的方法是 StreamingContext.textFileStream(dataDirectory)。
Python API中没有fileStream
;只有textFileStream
可用。
streamingContext.textFileStream(dataDirectory)
如何监视目录
spark streaming将监视目录dataDirectory
并处理该目录中创建的任何文件。
. 可以监视一个简单的目录,比如“hdfs://namenode:8040/logs/”。所有直接位于该路径下的文件将在被发现时被处理。
. 可以提供一个 POSIX glob pattern 模式,例如“hdfs://namenode:8040/logs/2017/*”。在这里,DStream将由与模式匹配的目录(directories)中的所有文件组成。也就是说:它是目录(directories)的模式,而不是目录中的文件。即,“*”代表的是“hdfs://namenode:8040/logs/2017/”下的所有目录。
. 所有文件必须采用相同的数据格式。
. 文件被认为是基于其修改时间而不是创建时间的时间段的一部分。
. 处理后,对当前窗口中的文件所做的更改将不会导致重新读取该文件。即:更新被忽略。
. 目录下的文件越多,扫描更改所需的时间就越长——即使没有修改过任何文件。
. 如果使用通配符来标识目录,如“hdfs://namenode:8040/logs/2016-*”,则重命名整个目录以匹配路径会将该目录添加到监视目录列表中。只有修改时间(备注:此目录的修改时间)位于当前窗口内的此目录中的文件才会包含在流中。
. 调用FileSystem.setTimes() 修复时间戳是在以后的窗口中获取文件的一种方法,即使文件的内容没有更改。
使用对象存储作为数据源
“Full”文件系统(如HDFS)倾向于在创建输出流时设置文件的修改时间。当一个文件被打开时,即使在数据被完全写入之前,它也可能被包括在 DStream
中——在这之后,对同一窗口中文件的更新将被忽略。也就是说:更改可能会丢失,数据会从流中被忽略。
为确保在窗口中获取更改,请将文件写入未受监控的目录,然后在关闭输出流后立即将其重命名为目标目录。如果在创建文件的窗口中,重命名的文件出现在扫描的目标目录中,则将提取新数据。
相反,Amazon S3和Azure Storage等对象存储通常具有较慢的重命名操作,因为数据实际上是复制的。此外,重命名的对象可能将rename()操作的时间作为修改时间,因此可能不会被认为是窗口的一部分,而原始创建时间暗示它们是窗口的一部分。
需要对目标对象存储进行仔细的测试,以验证存储的时间戳行为与Spark流所期望的一致。直接写入目标目录可能是通过所选对象存储 流数据的合适策略。
有关此主题的更多详细信息,请参阅Hadoop文件系统规范( Hadoop Filesystem Specification)。
Streams based on Custom Receivers(基于自定义接收器的流)
可以使用通过自定义接收器接收的数据流创建DStream。有关详细信息,请参阅Custom Receiver Guide 。
Queue of RDDs as a Stream(作为一个流的RDDs队列)
对于使用测试数据测试Spark流应用程序,还可以使用streamingContext.queueStream(queueOfRDDs)创建一个基于一个RDDs队列的DStream。推入队列的每个RDD将被视为数据流中的一批数据,并像流一样进行处理。
有关套接字和文件流的更多细节,请参见Scala的 StreamingContext 中的相关函数的API文档、Java的 StreamingContext 和Python的 StreamingContext 。
Advanced Sources
从spark 2.4.3开始,在这些源代码中,kafka、kinesis和flume在python API中可用。
这类源需要与外部非spark库进行连接,其中一些库具有复杂的依赖关系(例如Kafka和Flume)。因此,为了最小化与依赖关系的版本冲突相关的问题,从这些源创建DStreams的功能已经转移到单独的库中,在必要时可以显式地链接到这些库。
请注意,这些高级源在Spark Shell中不可用,因此基于这些高级源的应用程序不能在Shell中进行测试。如果您真的想在spark shell中使用它们,那么您必须下载相应的Maven artifact's JAR及其依赖项,并将其添加到类路径中。
其中一些高级资源如下所示:
. Kafka: Spark Streaming 2.4.3与Kafka Broker 0.8.2.1或更高版本兼容。有关详细信息,请参阅Kafka Integration Guide。
. Flume: Spark Streaming 2.4.3与Flume 1.6.0兼容。有关详细信息,请参阅 Flume Integration Guide。
. Kinesis: Spark Streaming 2.4.3与Kinesis客户端库1.2.1兼容。有关详细信息, Kinesis Integration Guide 。
Custom Sources
这在Python中尚不受支持。
也可以从自定义数据源中创建输入DStreams。您所要做的就是实现一个用户定义的接收器--receiver(请参见下一节了解这是什么),它可以从自定义源接收数据并将其推入Spark。有关详细信息,请参阅 Custom Receiver Guide。
Receiver Reliability
根据数据源的可靠性-- reliability,可以有两种数据源。源(如Kafka和Flume)允许确认传输的数据。如果从这些可靠来源接收数据的系统正确地确认接收到的数据,则可以确保不会因任何故障而丢失任何数据。这会导致两种接收器。
1. Reliable Receiver (可靠的接收器)- 当数据被接收并存储在带有复制的Spark中时,可靠的接收器正确地向可靠的源发送确认。
2. Unreliable Receiver -不可靠的接收器不会向源发送确认。这可以用于不支持确认的源,甚至可以用于不希望或不需要进行复杂确认的可靠源。
有关如何编写可靠接收器的详细信息,请参阅 Custom Receiver Guide。
Transformations on DStreams
与RDD类似,转换允许修改来自输入数据流的数据。数据流支持普通spark RDD上的许多可用转换。其中一些常见的转换如下。
Transformation | Meaning |
map(func) | 通过将源DStream 的每个元素通过函数func返回一个新的DStream 。 |
flatMap(func) | 与map类似,但每个输入项都可以映射到0个或多个输出项。 |
filter(func) | 通过只选择func返回true的源DStream的记录,返回新的DStream。 |
repartition(numPartitions) | 通过创建更多或更少的分区来更改此DStream中的并行级别 |
union(otherStream) | 返回包含源DStream和otherDStream中元素的联合的新DStream。 |
count() | 通过计算源 DStream中每个RDD中的元素数,返回单元素RDD的新 DStream。 |
reduce(func) | 通过使用函数func(它接受两个参数并返回一个)聚合源DStream的每个RDD中的元素,返回一个新的单元素RDDs DStream。这个函数应该具有结合律和交换律,这样才能并行计算。 |
countByValue() | 当在k类型元素的DStream 上调用时,返回一个(k,long)对的新DStream ,其中每个键的值是它(键)在源DStream 的每个RDD中的频率 |
reduceByKey(func, [numTasks]) | 当调用 (K, V) 对的DStream 时,返回(K, V) 对的新DStream ,其中每个键的值使用给定的reduce函数进行聚合。注意:默认情况下,这将使用Spark的默认并行任务数量(本地模式为2个,集群模式下该数量由配置属性 |
join(otherStream, [numTasks]) | 当调用 (K, V)和 (K, W) 对的两个DStream 时,返回一个(K, (V, W)) 对的新DStream ,其中每个键都有一对元素。 |
cogroup(otherStream, [numTasks]) | 当调用 (K, V)和 (K, W) 对的两个DStream 时,返回(K, [ Seq[V], Seq[W] ])元组的新DStream 。 |
transform(func) | 通过将RDD-to-RDD函数应用于源DStream的每个RDD,返回一个新的DStream。这可以用于在DStream上执行任意的RDD操作。 |
updateStateByKey(func) | 返回一个新的“state” DStream,其中通过对键的前一个状态和键的新值应用给定的函数更新每个键的状态。这可以用于维护每个键的任意状态数据。 |
其中一些转换值得更详细地讨论。
UpdateStateByKey Operation
updateStateByKey
操作允许您在使用新信息持续更新任意状态的同时保持该状态。要使用它,您需要执行两个步骤。
1. Define the state (定义状态) -状态可以是任意数据类型。
2. Define the state update function (定义状态更新函数) - 使用函数指定如何使用以前的状态和输入流中的新值更新状态。
在每个批处理中,Spark将为所有现有键应用状态更新函数,而不管它们是否在批处理中有新数据。如果update函数 返回None,那么键值对将被删除。
让我们用一个例子来说明这一点。假设您想要维护在文本数据流中看到的每个单词的运行计数。这里,运行计数是状 态,它是一个整数。我们将更新函数定义为:
def updateFunction(newValues, runningCount):
if runningCount is None:
runningCount = 0
return sum(newValues, runningCount) # add the new values with the previous running count to get the new count
这适用于包含words的DStream(例如,前面示例中包含(word, 1)
对的pairs
DStream)。
runningCounts = pairs.updateStateByKey(updateFunction)
The update function will be called for each word, with newValues
having a sequence of 1’s (from the (word, 1)
pairs) and the runningCount
having the previous count. 对于完整的python代码,请看一看 stateful_network_wordcount.py示例。
注意,使用 updateStateByKey
需要配置检查点目录,这将在 checkpointing 部分详细讨论。
Transform Operation
transform
操作(及其变体,如 transformWith
))允许在DStream上应用任意的RDD-to-RDD函数。它可以用于应用DStream API中没有公开的任何RDD操作。例如,DStream API中没有直接公开将数据流中的每个批处理与另一个数据集连接起来的功能。但是,您可以很容易地使用transform来实现这一点。这带来了非常强大的可能性。例如,可以通过将输入数据流与预先计算的垃圾邮件信息(也可以使用Spark生成)连接起来,然后根据这些信息进行过滤,从而进行实时数据清理。
spamInfoRDD = sc.pickleFile(...) # RDD containing spam information
# join data stream with spam information to do data cleaning
cleanedDStream = wordCounts.transform(lambda rdd: rdd.join(spamInfoRDD).filter(...))
请注意,所提供的函数在每个批处理间隔中都会被调用。这允许您执行时变RDD操作,即RDD操作、分区数、广播变量等可以在批之间更改。
Window Operations
Spark流还提供窗口计算,允许您在滑动数据窗口上应用转换。下图说明了这个滑动窗口。
如图所示,每次窗口在源DStream上滑动时,落到窗口中的源RDD都被组合并操作,以生成窗口DStream的RDD。在这种特定情况下,该操作将应用于过去3个时间单位的数据,并按2个时间单位滑动。这表明任何窗口操作都需要指定两个参数。
. window length - 窗口的持续时间(图中为3)。
. sliding interval - 执行窗口操作的间隔(图中为2)。
这两个参数必须是源DStream批处理间隔的倍数(图中为1)。
一些常见的窗口操作如下。所有这些操作都采用上述两个参数 - windowLength 和 slideInterval.。
Transformation | Meaning |
window(windowLength, slideInterval) | 返回一个新的 DStream,该 DStream是根据源 DStream的窗口批计算的。 |
countByWindow(windowLength, slideInterval) | 返回流中元素的滑动窗口计数。 |
reduceByWindow(func, windowLength, slideInterval) | 返回一个新的单元素流,该流是使用func通过滑动间隔聚合流中的元素而创建的。这个函数应该是结合律和交换律,这样才能正确地并行计算。 |
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) | 当对(K, V)对的DStream调用时,返回一个新的(K, V)对的DStream,其中每个键的值都使用指定的reduce函数func在滑动窗口中的批上聚合。注意:默认情况下,这将使用Spark的默认并行任务数量(本地模式为2个,集群模式下该数量由配置属性 |
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]) | 上面 |
countByValueAndWindow(windowLength,slideInterval, [numTasks]) | 当调用一个(K, V)对的DStream时,返回一个新的(K, Long)对的DStream,其中每个键的值是它在滑动窗口中的频率。与 |
Join Operations
最后,值得强调的是,您可以多么容易地在Spark流中执行不同类型的连接。
Stream-stream joins
流可以很容易地与其他流连接。
stream1 = ...
stream2 = ...
joinedStream = stream1.join(stream2)
在这里,在每个批处理间隔中,由 stream1
生成的RDD将与由 stream2
生成的RDD连接起来。你也可以做 leftOuterJoin
, rightOuterJoin
, fullOuterJoin
。此外,在流的窗口上进行连接通常非常有用。这也很简单。
windowedStream1 = stream1.window(20)
windowedStream2 = stream2.window(60)
joinedStream = windowedStream1.join(windowedStream2)
Stream-dataset joins
在解释 DStream.transform
操作时,前面已经显示了这一点。这里还有一个将窗口化流与数据集结合的示例。
事实上,您还可以动态地更改要连接的数据集。为transform
提供的函数将在每个批处理间隔被评估,因此将使用 dataset
引用指向的当前数据集。
API文档中提供了完整的DStream转换列表。对于Python API,请参见 DStream。
Output Operations on DStreams
输出操作允许将DStream的数据推出到外部系统,如数据库或文件系统。由于输出操作实际上允许外部系统使用转换后的数据,因此它们会触发所有DStream转换的实际执行(类似于RDD的操作--actions)。目前,定义了以下输出操作:
Output Operation | Meaning |
pprint() | 在运行流应用程序的驱动节点上,打印数据流中每批数据的前十个元素。这对于开发和调试很有用。 |
saveAsTextFiles(prefix, [suffix]) | 将此DStream的内容保存为文本文件。每个批处理间隔的文件名是根据 prefix 和 suffix生成的:"prefix-TIME_IN_MS[.suffix]"。 |
saveAsObjectFiles(prefix, [suffix]) | 将这个DStream的内容保存为序列化Java对象的 Python API :This is not available in the Python API. |
saveAsHadoopFiles(prefix, [suffix]) | 将此DStream的内容另存为Hadoop文件。每个批处理间隔的文件名是根据 prefix 和 suffix生成的:"prefix-TIME_IN_MS[.suffix]"。 Python API :This is not available in the Python API. |
foreachRDD(func) | 将函数 func应用于从流生成的每个RDD的最通用的输出运算符。此函数应该将每个RDD中的数据推送到外部系统,例如将RDD保存到文件中,或者通过网络将其写入数据库。请注意,函数 func是在运行流应用程序的驱动程序进程中执行的,通常会在函数 func中执行RDD操作--actions,从而强制计算流RDDs。 |
Design Patterns for using foreachRDD(使用foreachRDD设计模式)
dstream.foreachRDD
是一个功能强大的原语,它允许将数据发送到外部系统。然而,理解如何正确有效地使用这个原语是很重要的。以下是一些需要避免的常见错误。
通常,向外部系统写入数据需要创建一个连接对象(例如,到远程服务器的TCP连接),并使用它将数据发送到远程系统。为此,开发人员可能无意中尝试在Spark驱动程序中创建一个连接对象,然后尝试在Spark Worker中使用它来将保存RDD中的记录。例如(在Python中),
def sendRecord(rdd):
connection = createNewConnection() # executed at the driver
rdd.foreach(lambda record: connection.send(record))
connection.close()
dstream.foreachRDD(sendRecord)
这是不正确的,因为这要求将连接对象序列化并从驱动程序(dirver)发送到工作程序(worker)。这样的连接对象很少可以跨机器进行传输。此错误可能表现为序列化错误(连接对象不可序列化)、初始化错误(连接对象需要在工作区(workers)初始化)等。正确的解决方案是在工作区(worker)创建连接对象。
然而,这可能导致另一个常见的错误——为每个记录创建新的连接。例如,
def sendRecord(record):
connection = createNewConnection()
connection.send(record)
connection.close()
dstream.foreachRDD(lambda rdd: rdd.foreach(sendRecord))
通常,创建连接对象需要时间和资源开销。因此,为每个记录创建和销毁连接对象可能会产生不必要的高开销,并可能显著降低系统的总体吞吐量。更好的解决方案是使用rdd.foreachPartition
- 创建一个连接对象,并使用该连接发送一个RDD分区中的所有记录。
def sendPartition(iter):
connection = createNewConnection()
for record in iter:
connection.send(record)
connection.close()
dstream.foreachRDD(lambda rdd: rdd.foreachPartition(sendPartition))
这会将连接创建开销分摊到许多记录上。
最后,可以通过跨多个RDDs/批重用连接对象来进一步优化。可以维护一个静态连接对象池,当多个批的RDDs被推送到外部系统时,可以重用该连接对象池,从而进一步减少开销。
def sendPartition(iter):
# ConnectionPool is a static, lazily initialized pool of connections
connection = ConnectionPool.getConnection()
for record in iter:
connection.send(record)
# return to the pool for future reuse
ConnectionPool.returnConnection(connection)
dstream.foreachRDD(lambda rdd: rdd.foreachPartition(sendPartition))
请注意,池中的连接应按需延迟创建,如果一段时间不使用一段时间,则超时。这实现了向外部系统最有效地发送数据。
其他需要记住的要点:
. DStreams由输出操作延迟执行,就像RDD操作延迟执行RDDs一样。具体来说,DStream输出操作中的RDD操作强制处理接收到的数据。因此,如果您的应用程序没有任何输出操作,或者有像 dstream.foreachRDD()
这样的输出操作,而其中没有任何RDD操作,那么什么也不会执行。系统将简单地接收数据并丢弃它。
. 默认情况下,每次执行一个输出操作。它们是按照应用程序中定义的顺序执行的。
DataFrame and SQL Operations
您可以轻松地对流数据使用 DataFrames and SQL操作。您必须使用StreamingContext正在使用的SparkContext创建一个SparkSession。此外,还必须这样做,以便在驱动程序出现故障时重新启动。这是通过创建SparkSession的一个延迟实例化的单实例来完成的。这在下面的示例中显示。它修改了前面的 word count example ,以使用DataFrames和SQL生成单词计数。每个RDD都转换为一个DataFrame,(此DataFrame)被注册为临时表,然后使用SQL进行查询。
# Lazily instantiated global instance of SparkSession
def getSparkSessionInstance(sparkConf):
if ("sparkSessionSingletonInstance" not in globals()):
globals()["sparkSessionSingletonInstance"] = SparkSession \
.builder \
.config(conf=sparkConf) \
.getOrCreate()
return globals()["sparkSessionSingletonInstance"]
...
# DataFrame operations inside your streaming program
words = ... # DStream of strings
def process(time, rdd):
print("========= %s =========" % str(time))
try:
# Get the singleton instance of SparkSession
spark = getSparkSessionInstance(rdd.context.getConf())
# Convert RDD[String] to RDD[Row] to DataFrame
rowRdd = rdd.map(lambda w: Row(word=w))
wordsDataFrame = spark.createDataFrame(rowRdd)
# Creates a temporary view using the DataFrame
wordsDataFrame.createOrReplaceTempView("words")
# Do word count on table using SQL and print it
wordCountsDataFrame = spark.sql("select word, count(*) as total from words group by word")
wordCountsDataFrame.show()
except:
pass
words.foreachRDD(process)
完整源代码:
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
r"""
Use DataFrames and SQL to count words in UTF8 encoded, '\n' delimited text received from the
network every second.
Usage: sql_network_wordcount.py <hostname> <port>
<hostname> and <port> describe the TCP server that Spark Streaming would connect to receive data.
To run this on your local machine, you need to first run a Netcat server
`$ nc -lk 9999`
and then run the example
`$ bin/spark-submit examples/src/main/python/streaming/sql_network_wordcount.py localhost 9999`
"""
from __future__ import print_function
import sys
from pyspark import SparkContext
from pyspark.streaming import StreamingContext
from pyspark.sql import Row, SparkSession
def getSparkSessionInstance(sparkConf):
if ('sparkSessionSingletonInstance' not in globals()):
globals()['sparkSessionSingletonInstance'] = SparkSession\
.builder\
.config(conf=sparkConf)\
.getOrCreate()
return globals()['sparkSessionSingletonInstance']
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: sql_network_wordcount.py <hostname> <port> ", file=sys.stderr)
sys.exit(-1)
host, port = sys.argv[1:]
sc = SparkContext(appName="PythonSqlNetworkWordCount")
ssc = StreamingContext(sc, 1)
# Create a socket stream on target ip:port and count the
# words in input stream of \n delimited text (eg. generated by 'nc')
lines = ssc.socketTextStream(host, int(port))
words = lines.flatMap(lambda line: line.split(" "))
# Convert RDDs of the words DStream to DataFrame and run SQL query
def process(time, rdd):
print("========= %s =========" % str(time))
try:
# Get the singleton instance of SparkSession
spark = getSparkSessionInstance(rdd.context.getConf())
# Convert RDD[String] to RDD[Row] to DataFrame
rowRdd = rdd.map(lambda w: Row(word=w))
wordsDataFrame = spark.createDataFrame(rowRdd)
# Creates a temporary view using the DataFrame.
wordsDataFrame.createOrReplaceTempView("words")
# Do word count on table using SQL and print it
wordCountsDataFrame = \
spark.sql("select word, count(*) as total from words group by word")
wordCountsDataFrame.show()
except:
pass
words.foreachRDD(process)
ssc.start()
ssc.awaitTermination()
您还可以对定义在来自不同线程(即与运行中的StreamingContext异步)的流数据上的表运行SQL查询。只要确保设置了StreamingContext来记住足够的流数据,以便查询可以运行。否则,StreamingContext(它不知道任何异步SQL查询)将在查询完成之前删除旧的流数据。例如,如果您想查询最后一批数据,但是您的查询可能需要5分钟才能运行,那么请调用 streamingContext.remember(Minutes(5))
(在Scala中,或者在其他语言中使用相同的方法)。
有关 DataFrames的更多信息,请参阅 DataFrames and SQL。
MLlib Operations
您还可以轻松使用 MLlib提供的机器学习算法。首先,有流式机器学习算法(如Streaming Linear Regression--流式线性回归、 Streaming KMeans--流式KMeans等)可以同时从流式数据中学习,并将模型应用于流式数据。除此之外,对于更大类别的机器学习算法,您可以离线学习一个学习模型(即使用历史数据),然后将该模型在线应用于流数据。有关详细信息,请参阅 MLlib指南。
Caching / Persistence(缓存/持久性)
与RDDs类似,DStreams还允许开发人员将流的数据持久化到内存中。也就是说,在DStream上使用 persist()
方法将自动将该DStream的每个RDD持久化到内存中。如果DStream中的数据将被计算多次(例如,对同一数据进行多次操作),这将非常有用。对于基于窗口的操作,如 reduceByWindow
和 reduceByKeyAndWindow
,以及基于状态的操作,如updateStateByKey
,这是隐式正确的。因此,由基于窗口的操作生成的DStreams将自动持久化到内存中,而不需要开发人员调用 persist()
。
对于通过网络接收数据的输入流(如kafka、flume、sockets等),默认的持久性级别设置为将数据复制到两个节点以实现容错。
注意,与RDDs不同,DStreams的默认持久性级别将数据序列化在内存中。这将在 Performance Tuning --性能调优一节中进一步讨论。有关不同持久性级别的更多信息可以在 Spark Programming Guide中找到。
Checkpointing
流式应用程序必须24/7全天候运行,因此必须能够适应与应用程序逻辑无关的故障(例如,系统故障、JVM崩溃等)。
为了实现这一点,Spark流需要将足够的信息检查到容错存储系统,以便它能够从故障中恢复。检查点有两种类型的数据。
. Metadata checkpointing - 将定义流计算的信息保存到容错存储(如HDFS)。这用于从运行流应用程序驱动程序的节点的故障中恢复(稍后将详细讨论)。元数据包括:
。 Configuration - 用于创建流应用程序的配置。
。 DStream operations -定义流应用程序的DStream操作集。
。 Incomplete batches - 其作业已排队但尚未完成的批处理-batches。
. Data checkpointing - 将生成的RDD保存到可靠存储。在一些跨多个批处理组合数据的状态转换中,这是必需的。在这种转换中,生成的RDDs依赖于以前批处理的RDDs,这会导致依赖链的长度随着时间不断增加。为了避免恢复时间的无限增长(与依赖链成比例),状态转换的中间RDDs会被定期检查到可靠的存储(如HDFS)以切断依赖链。
总而言之,元数据检查点主要用于从驱动程序故障中恢复,而数据或RDD检查点甚至对于若使用有状态转换的基本功能也是必要的。
When to enable Checkpointing(何时启用检查点)
必须为具有下列任何要求的应用程序启用检查点:
. Usage of stateful transformations (状态转换的使用) -- 如果在应用程序中使用 updateStateByKey
和 reduceByKeyAndWindow
(带有反函数),则必须提供检查点目录,以允许定期进行RDD检查点。
. Recovering from failures of the driver running the application (从运行应用程序的驱动程序的故障中恢复) --元数据检查点用于使用进度信息进行恢复。
注意,没有上述状态转换的简单流式应用程序可以在不启用检查点的情况下运行。在这种情况下,从驱动程序故障中恢复也是部分的(一些已接收但未处理的数据可能会丢失)。这通常是可以接受的,并且许多应用程序都是以这种方式运行Spark流应用程序的。对非Hadoop环境的支持有望在未来得到改善。
How to configure Checkpointing(如何配置检查点)
检查点可以通过在容错、可靠的文件系统(如HDFS、S3等)中设置一个目录来启用,检查点信息将保存到该目录中。这是通过使用 streamingContext.checkpoint(checkpointDirectory)
完成的。这将允许您使用前面提到的状态转换。此外,如果要使应用程序从驱动程序故障中恢复,则应重写流应用程序以具有以下行为。
. 当程序第一次启动时,它将创建一个新的StreamingContext,设置所有的流,然后调用start()。
. 当程序在失败后重新启动时,它将从检查点目录中的检查点数据重新创建一个StreamingContext。
通过使用 StreamingContext.getOrCreate
简化此行为。使用方法如下
# Function to create and setup a new StreamingContext
def functionToCreateContext():
sc = SparkContext(...) # new context
ssc = StreamingContext(...)
lines = ssc.socketTextStream(...) # create DStreams
...
ssc.checkpoint(checkpointDirectory) # set checkpoint directory
return ssc
# Get StreamingContext from checkpoint data or create a new one
context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext)
# Do additional setup on context that needs to be done,irrespective of whether it is being started or restarted
#在需要执行的上下文上执行其他设置,而不管它是正在启动还是重新启动。
context. ...
# Start the context
context.start()
context.awaitTermination()
如果 checkpointDirectory
存在,那么将从检查点数据重新创建上下文。如果目录不存在(即第一次运行),则将调用函数 functionToCreateContext
以创建新上下文并设置数据流。请参阅python示例recoverable_network_wordcount.py。此示例将网络数据的字数附加到文件中。
示例:recoverable_network_wordcount.py
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
Counts words in text encoded with UTF8 received from the network every second.
Usage: recoverable_network_wordcount.py <hostname> <port> <checkpoint-directory> <output-file>
<hostname> and <port> describe the TCP server that Spark Streaming would connect to receive
data. <checkpoint-directory> directory to HDFS-compatible file system which checkpoint data
<output-file> file to which the word counts will be appended
To run this on your local machine, you need to first run a Netcat server
`$ nc -lk 9999`
and then run the example
`$ bin/spark-submit examples/src/main/python/streaming/recoverable_network_wordcount.py \
localhost 9999 ~/checkpoint/ ~/out`
If the directory ~/checkpoint/ does not exist (e.g. running for the first time), it will create
a new StreamingContext (will print "Creating new context" to the console). Otherwise, if
checkpoint data exists in ~/checkpoint/, then it will create StreamingContext from
the checkpoint data.
"""
from __future__ import print_function
import os
import sys
from pyspark import SparkContext
from pyspark.streaming import StreamingContext
# Get or register a Broadcast variable
def getWordBlacklist(sparkContext):
if ('wordBlacklist' not in globals()):
globals()['wordBlacklist'] = sparkContext.broadcast(["a", "b", "c"])
return globals()['wordBlacklist']
# Get or register an Accumulator
def getDroppedWordsCounter(sparkContext):
if ('droppedWordsCounter' not in globals()):
globals()['droppedWordsCounter'] = sparkContext.accumulator(0)
return globals()['droppedWordsCounter']
def createContext(host, port, outputPath):
# If you do not see this printed, that means the StreamingContext has been loaded
# from the new checkpoint
print("Creating new context")
if os.path.exists(outputPath):
os.remove(outputPath)
sc = SparkContext(appName="PythonStreamingRecoverableNetworkWordCount")
ssc = StreamingContext(sc, 1)
# Create a socket stream on target ip:port and count the
# words in input stream of \n delimited text (eg. generated by 'nc')
lines = ssc.socketTextStream(host, port)
words = lines.flatMap(lambda line: line.split(" "))
wordCounts = words.map(lambda x: (x, 1)).reduceByKey(lambda x, y: x + y)
def echo(time, rdd):
# Get or register the blacklist Broadcast
blacklist = getWordBlacklist(rdd.context)
# Get or register the droppedWordsCounter Accumulator
droppedWordsCounter = getDroppedWordsCounter(rdd.context)
# Use blacklist to drop words and use droppedWordsCounter to count them
def filterFunc(wordCount):
if wordCount[0] in blacklist.value:
droppedWordsCounter.add(wordCount[1])
return False
else:
return True
counts = "Counts at time %s %s" % (time, rdd.filter(filterFunc).collect())
print(counts)
print("Dropped %d word(s) totally" % droppedWordsCounter.value)
print("Appending to " + os.path.abspath(outputPath))
with open(outputPath, 'a') as f:
f.write(counts + "\n")
wordCounts.foreachRDD(echo)
return ssc
if __name__ == "__main__":
if len(sys.argv) != 5:
print("Usage: recoverable_network_wordcount.py <hostname> <port> "
"<checkpoint-directory> <output-file>", file=sys.stderr)
sys.exit(-1)
host, port, checkpoint, output = sys.argv[1:]
ssc = StreamingContext.getOrCreate(checkpoint,
lambda: createContext(host, int(port), output))
ssc.start()
ssc.awaitTermination()
还可以使用StreamingContext.getOrCreate(checkpointDirectory, None)
.从检查点数据显式创建 StreamingContext
,并启动计算。
除了使用 getOrCreate
,还需要确保驱动程序进程在失败时自动重启。这只能由用于运行应用程序的部署基础设施来完成。这将在 Deployment一节中进一步讨论。
注意,RDDs的检查点会导致节省可靠存储的成本。这可能会导致RDDs检查点所在批次的处理时间增加。因此,需要仔细设置检查点的间隔。相反,检查点太少会导致沿袭和任务大小增长,这可能会产生有害的影响。对于需要RDD检查点的有状态转换,默认间隔是至少10秒的批处理间隔的倍数。它可以通过使用 dstream.checkpoint(checkpointInterval)
来设置。通常,一个DStream的检查点间隔为5 - 10个滑动间隔是一个很好的设置。
Accumulators, Broadcast Variables, and Checkpoints(累加器、广播变量和检查点)
累加器和广播变量无法从Spark流中的检查点恢复。如果启用检查点并同时使用累加器或广播变量,则必须为累加器和广播变量创建延迟实例化的单例实例,以便在驱动程序失败重启后重新实例化它们。如下面的示例所示。
def getWordBlacklist(sparkContext):
if ("wordBlacklist" not in globals()):
globals()["wordBlacklist"] = sparkContext.broadcast(["a", "b", "c"])
return globals()["wordBlacklist"]
def getDroppedWordsCounter(sparkContext):
if ("droppedWordsCounter" not in globals()):
globals()["droppedWordsCounter"] = sparkContext.accumulator(0)
return globals()["droppedWordsCounter"]
def echo(time, rdd):
# Get or register the blacklist Broadcast
blacklist = getWordBlacklist(rdd.context)
# Get or register the droppedWordsCounter Accumulator
droppedWordsCounter = getDroppedWordsCounter(rdd.context)
# Use blacklist to drop words and use droppedWordsCounter to count them
def filterFunc(wordCount):
if wordCount[0] in blacklist.value:
droppedWordsCounter.add(wordCount[1])
False
else:
True
counts = "Counts at time %s %s" % (time, rdd.filter(filterFunc).collect())
wordCounts.foreachRDD(echo)
请参阅完整的源代码。
Deploying Applications(部署应用)
本节讨论部署一个Spark流应用程序的步骤。
Requirements(要求)
要运行Spark流应用程序,您需要具备以下条件。
. Cluster with a cluster manager - 这是任何Spark应用程序的一般需求,并在 deployment guide-部署指南中进行了详细讨论。
. Package the application JAR - 您必须将流应用程序编译到一个JAR中。如果使用 spark-submit启动应用程序,则不需要在JAR中提供Spark和Spark流。但是,如果您的应用程序使用advanced sources--高级源(例如Kafka、Flume),那么您必须将它们链接到的额外工件及其依赖项打包到用于部署应用程序的JAR中。例如,使用 KafkaUtils
的应用程序必须在应用程序JAR中包含 spark-streaming-kafka-0-10_2.12
及其所有传递依赖项。
. Configuring sufficient memory for the executors (为执行器配置足够的内存) - 由于接收到的数据必须存储在内存中,因此必须为执行器配置足够的内存来保存接收到的数据。请注意,如果要执行10分钟的窗口操作,系统必须在内存中保留至少最后10分钟的数据。因此,应用程序的内存需求取决于其中使用的操作。
. Configuring checkpointing - 如果流应用程序需要它,则必须将与Hadoop API兼容且容错存储(例如HDFS、S3等)中的一个目录配置为检查点目录 和 以检查点信息可用于故障恢复的方式编写的流应用程序。有关更多细节,请参见 checkpointing 部分。
. Configuring automatic restart of the application driver (配置应用程序驱动程序的自动重新启动) - 要从驱动程序失败中自动恢复,用于运行流应用程序的部署基础设施必须监视驱动程序进程,如果驱动程序失败,则必须重新启动驱动程序。不同的 cluster managers--集群管理器有不同的工具来实现这一点。
。Spark Standalone - 可以提交一个Spark应用程序驱动程序在Spark独立集群中运行(请参阅 cluster deploy mode--集群部署模式),也就是说,应用程序驱动程序本身运行在一个工作节点上。此外,可以指示独立集群管理器supervise--监视驱动程序,如果驱动程序由于非零退出码或运行驱动程序的节点失败而失败,则可以重新启动它。有关详细信息,请参阅集群模式并在 Spark Standalone guide中进行监视。
。YARN -Yarn支持类似的自动重启应用程序的机制。有关详细信息,请参阅YARN文档 。
。Mesos - Marathon已经用Mesos实现了这一点。
. Configuring write-ahead logs (配置 写前 日志) -自Spark 1.2以来,我们引入了write-ahead -写前日志,以实现强大的容错保证。如果启用,则从接收器接收到的所有数据都将写入配置检查点目录中的写前日志。这可以防止驱动程序恢复时的数据丢失,从而确保零数据丢失(在 Fault-tolerance Semantics--容错语义一节中详细讨论)。这可以通过设置配置参数spark.streaming.receiver.writeAheadLog.enable
为 true
来启用。然而,这些更强的语义可能以单个接收器的接收吞吐量为代价。这可以通过并行运行更多的接收器来纠正,以增加总吞吐量。此外,建议在启用写前日志时禁用Spark中接收数据的复制,因为日志已经存储在复制的存储系统中。此外,建议在启用写前日志时禁用Spark中接收数据的复制,因为日志已经存储在复制的存储系统中。这可以通过将输入流的存储级别设置为StorageLevel.MEMORY_AND_DISK_SER
来实现。当使用S3(或任何不支持刷新的文件系统)进行写前日志时,请记住启用spark.streaming.driver.writeAheadLog.closeFileAfterWrite
和 spark.streaming.receiver.writeAheadLog.closeFileAfterWrite
.有关详细信息,请参阅 Spark Streaming Configuration 。注意,当启用I/O加密时,Spark不会加密写入write-ahead日志的数据。如果需要对写前日志数据进行加密,则应该将其存储在本地支持加密的文件系统中。
. Setting the max receiving rate (设置最大接收速率)- 如果群集资源不够大,流应用程序无法以接收数据的速度处理数据,则可以通过设置以记录/秒为单位的最大速率限制来限制接收器的速率。参见配置参数spark.streaming.receiver.maxRate
用于接收器和 spark.streaming.kafka.maxRatePerPartition
用于直接Kafka方法。在Spark 1.5中,我们引入了一个称为 backpressure 的特性,消除了设置此速率限制的必要性,因为Spark流会自动计算出速率限制,并在处理条件发生变化时动态调整它们。可以通过将配置参数 spark.streaming.backpressure.enabled
设置为 true
来启用此 backpressure 。
Upgrading Application Code(升级应用程序代码)
如果运行中的Spark流应用程序需要用新的应用程序代码升级,那么有两种可能的机制。
. 升级后的Spark流应用程序与现有应用程序并行启动和运行。一旦新的(接收到与旧的相同的数据)被预热并准备好迎接黄金时间,旧的可以被调低。请注意,对于支持将数据发送到两个目的地(即早期和升级的应用程序)的数据源,可以这样做。
. 现有应用程序正常关闭(有关正常关闭选项,请参阅 StreamingContext.stop(...) 或 JavaStreamingContext.stop(...)以确保在关闭之前完全处理接收到的数据。然后可以启动升级后的应用程序,它将从先前应用程序停止的同一点开始处理。请注意,这只能在支持源端缓冲的输入源(如Kafka和Flume)上完成,因为在上一个应用程序关闭且升级的应用程序尚未启动时需要缓冲数据。无法从升级前代码的早期检查点信息重新启动。检查点信息基本上包含序列化的Scala/Java/Python对象,并且尝试用新的、修改过的类反序列化对象可能会导致错误。在这种情况下,要么用不同的检查点目录启动升级的应用程序,要么删除以前的检查点目录。
Monitoring Applications(监控应用程序)
除了Spark的监视功能之外,还有一些特定于Spark流的附加功能。当使用StreamingContext时, Spark web UI会显示一个附加的 Streaming
选项卡,其中显示关于正在运行的接收器(接收器是否活动、接收到的记录数量、接收器错误等)和已完成的批(批处理时间、排队延迟等)的统计信息。这可以用来监视流应用程序的进程。
Web UI中的以下两个指标尤其重要:
. Processing Time - 处理每批数据的时间。
. Scheduling Delay - 批处理在队列中等待上一批处理完成的时间。
如果批处理时间始终大于批处理间隔和/或队列延迟不断增加,则表明系统无法像生成批处理时那样快速地处理它们,并且正在落后。在这种情况下,考虑减少批处理时间。
Spark流程序的进程也可以使用StreamingListener接口进行监视,该接口允许您获得接收器状态和处理时间。注意,这是一个开发人员API,它可能会得到改进(即,更多信息报道)在未来。
Performance Tuning
要获得集群上Spark流应用程序的最佳性能,需要进行一些调优。本节将解释一些参数和配置,这些参数和配置可以进行调优,以提高应用程序的性能。在高层次上,你需要考虑两件事:
1. 通过有效地使用集群资源来减少每批数据的处理时间。
2. 设置正确的批大小,以便在接收到批数据时可以尽快处理这些批数据(即,数据处理与数据接收保持同步)
Reducing the Batch Processing Times(减少批量处理时间)
可以在Spark中进行许多优化,以最小化每批处理时间。这些已经在 Tuning Guide中进行了详细讨论。本节重点介绍一些最重要的内容。
Level of Parallelism in Data Receiving(数据接收的并行级别)---备注:此副标题有误
通过网络接收数据(如Kafka、Flume、socket等)需要对数据进行反序列化并存储在Spark中。如果数据接收成为系统中的瓶颈,则考虑并行化数据接收。注意,每个输入DStream创建一个接收单个数据流的接收器(运行在worker机器上)。因此,可以通过创建多个输入DStreams并将其配置为从源接收数据流的不同分区来实现接收多个数据流。例如,接收两个数据主题的单个Kafka输入DStream 可以分成两个Kafka输入流 ,每个输入流只接收一个主题。这将运行两个接收器,允许并行接收数据,从而增加总吞吐量。这些多个DStreams可以联合在一起以创建单个DStream。然后,在单个输入 DStream上应用的转换可以应用于统一流。具体操作如下。
numStreams = 5
kafkaStreams = [KafkaUtils.createStream(...) for _ in range (numStreams)]
unifiedStream = streamingContext.union(*kafkaStreams)
unifiedStream.pprint()
另一个需要考虑的参数是接收器的块间隔,它由配置参数spark.streaming.blockInterval
决定。对于大多数接收器,接收到的数据在存储在Spark的内存中之前被合并成数据块。每个批中的块数决定了在类似map的转换中用于处理接收数据的任务数。每批每个接收器的任务数量大约为(批处理间隔/块间隔)。例如,200 ms的块间隔将每2秒创建10个任务。如果任务数量过低(也就是说,少于每台机器的内核数量),那么它将是低效的,因为所有可用的内核都不会用于处理数据。若要增加给定批处理间隔的任务数量,请减小块间隔。但是,建议的最小块间隔值为50 ms左右,低于这个值可能会导致任务启动开销出现问题。
使用多个输入流/接收器接收数据的另一种方法是显式重新分区输入数据流(使用 inputStream.repartition(<number of partitions>)
)。这将在进一步处理之前将接收到的批数据分布到集群中指定数量的机器上。
关于直接流,请参阅 Spark Streaming + Kafka Integration Guide。
Level of Parallelism in Data Receiving(数据接收的并行级别)
如果在计算的任何阶段中使用的并行任务数量都不够多,则集群资源可能没有得到充分利用。例如,对于 reduceByKey
和 reduceByKeyAndWindow
等分布式reduce操作,并行任务的默认数量由spark.default.parallelism
配置属性控制。您可以将并行度级别作为参数传递(请参阅 PairDStreamFunctions文档),或者设置 spark.default.parallelism
配置属性来更改默认值。
Data Serialization
通过调整序列化格式,可以减少数据序列化的开销。对于流,有两种类型正在被序列化的数据。
. Input data: 默认情况下,通过接收者接收到的输入数据通过StorageLevel.MEMORY_AND_DISK_SER_2存储在执行器的内存中。也就是说,将数据序列化为字节以减少GC开销,并复制以容忍执行器失败。此外,数据首先保存在内存中,只有当内存不足以容纳流计算所需的所有输入数据时,数据才会溢出到磁盘上。这种序列化显然有一些开销——接收必须对接收到的数据进行反序列化,并使用Spark的序列化格式对其进行重新序列化。
. Persisted RDDs generated by Streaming Operations(流操作生成的持久化RDD): 流计算生成的RDDs可以保存在内存中。例如,窗口操作将数据保存在内存中,因为它们将被多次处理。不过,与Spark Core默认的StorageLevel.MEMORY_ONLY不同。通过流计算生成的RDDs将使用 StorageLevel.MEMORY_ONLY_SER(即序列化)持久化,以最小化GC(garbage collector --垃圾收集器)开销。
在这两种情况下,使用Kryo序列化可以减少CPU和内存开销。有关详细信息,请参阅 Spark Tuning Guide。对于Kryo,请考虑注册自定义类,并禁用对象引用跟踪(请参阅 Configuration Guide)中与Kryo相关的配置)。
在需要为流应用程序保留的数据量不大的特定情况下,可以将数据(两种类型)持久存储为反序列化对象,而不会导致过多的GC开销。例如,如果使用几秒钟的批处理间隔,并且没有窗口操作,那么可以尝试通过显式地设置相应的存储级别来禁用持久数据中的序列化。这将减少由于序列化而导致的CPU开销,从而在没有太多GC开销的情况下提高性能。
Task Launching Overheads(任务启动开销)
如果每秒启动的任务数很高(例如,每秒启动50个或更多),那么将任务发送到从属服务器的开销可能会非常大,并且很难实现次秒延迟。通过以下更改可以降低开销:
. Execution mode: 在独立模式或粗粒度Mesos模式下运行Spark可以比细粒度Mesos模式获得更好的任务启动时间。有关详细信息,请参阅 Running on Mesos guide。
这些更改可能会将批处理时间缩短100毫秒,从而使次秒批处理大小可行。
Setting the Right Batch Interval(设置正确的批处理间隔)
要使运行在集群上的Spark流应用程序稳定,系统应该能够像接收数据一样快速处理数据。换句话说,批量数据的处理速度应该和它们生成的速度一样快。对于应用程序,可以通过monitoring 流式Web UI中的处理时间来找到是否为真,其中批处理时间应小于批处理间隔。
根据流计算的性质,使用的批处理间隔可能对应用程序在一组固定的集群资源上能够维持的数据速率产生显著影响。例如,让我们考虑一下前面的 WordCountNetwork示例。对于特定的数据速率,系统可能能够每2秒(即2秒的批处理间隔)而不是每500毫秒更新一次报告字计数。因此,需要设置批处理间隔,以保持生产中的预期数据速率。
为应用程序确定正确的批大小的一个好方法是使用保守的批间隔(例如,5-10秒)和较低的数据速率对其进行测试。要验证系统是否能够跟上数据速率,可以检查每个处理批所经历的端到端延迟值(在Spark 驱动 log4j日志中查找“总延迟”,或者使用 StreamingListener接口)。如果延迟保持与批量大小相当,那么系统是稳定的。否则,如果延迟持续增加,则意味着系统无法跟上,因此不稳定。一旦您有了稳定配置的想法,就可以尝试增加数据速率和/或减小批处理大小。请注意,由于临时数据速率增加而导致的延迟瞬间增加可能很好,只要延迟减小到一个较低的值(即小于批量大小)。
Memory Tuning(内存调优)
调优Spark应用程序的内存使用和GC行为在 Tuning Guide中有详细的讨论。强烈建议您阅读。在本节中,我们将专门讨论流应用程序上下文中的一些优化参数。
Spark流应用程序所需的集群内存量在很大程度上取决于所使用的转换类型。例如,如果您想对最后10分钟的数据使用窗口操作,那么您的集群应该有足够的内存来在内存中保存10分钟的数据。或者,如果您想对大量的键使用updateStateByKey
,那么所需的内存就会很大。相反,如果您想做一个简单的map-filter-store操作,那么所需的内存将会很低。
一般来说,由于通过接收器接收到的数据通过StorageLevel.MEMORY_AND_DISK_SER_2存储,因此不适合内存的数据将溢出到磁盘。这可能会降低流应用程序的性能,因此建议根据流应用程序的需要提供足够的内存。最好尝试在小范围内查看内存使用情况,并相应地进行估计。
内存调优的另一个方面是垃圾收集。对于需要低延迟的流应用程序,不希望有由JVM垃圾收集引起的大停顿。
有几个参数可以帮助您调整内存使用和GC开销:
. Persistence Level of DStreams(DStreams的持久性级别):正如前面在 Data Serialization一节中提到的,输入数据和RDDs默认情况下作为序列化字节保存。与反序列化持久性相比,这减少了内存使用和GC开销。启用Kryo序列化进一步减少了序列化的大小和内存使用。通过压缩(参见spark配置 spark.rdd.compress
),可以进一步减少内存使用量,代价是占用CPU时间。
. Clearing old data: 默认情况下,所有输入数据和由DStream转换生成的持久RDDs都会自动清除。例如,如果您使用的是10分钟的窗口操作,那么Spark流将保留大约最后10分钟的数据,并主动丢弃旧数据。通过设置 streamingContext.remember
,可以将数据保留更长的时间(例如交互查询较旧的数据)。
. CMS Garbage Collector: 强烈建议使用 并发标记和扫描 GC,以保持GC相关暂停始终较低。尽管已知并发GC会降低系统的总体处理吞吐量,但仍然建议使用它来实现更一致的批处理时间。确保在驱动程序(在 spark-submit
中使用 --driver-java-options
)和执行器(使用 Spark configuration spark.executor.extraJavaOption
)上都设置了CMS GC。
. Other tips: 为了进一步降低GC开销,这里还有一些要尝试的技巧。
。使用 OFF_HEAP
存储级别持久化RDDs。有关更多细节,请参阅 Spark Programming Guide。
。使用更多的执行器和更小的堆大小。这将减少每个JVM堆中的GC压力。
Important points to remember(要记住的要点):
. 一个DStream与单个接收器相关联。为了获得读取并行性,需要创建多个接收器,即多个DStreams。接收器在执行器中运行。它占据一个核心。确保在预订接收器插槽后有足够的内核用于处理,即spark.cores.max应考虑接收器插槽。接收者以循环方式分配给执行者。
. 当从流源接收数据时,接收器创建数据块。每隔BlockInterval毫秒生成一个新的数据块。在batchInterval期间创建N个数据块,其中N=batchInterval/blockInterval。这些块由当前执行器的BlockManager--块管理器分发给其他执行器的块管理器。之后,运行在驱动程序上的网络输入跟踪器将被通知有关块位置的信息,以便进一步处理。
. 在驱动程序上为批处理间隔期间创建的块创建一个RDD。批处理间隔期间生成的块是RDD的分区。每个分区都是Spark中的一个任务。blockinterval==batchinterval意味着创建了单个分区,可能在本地处理它。
. 块上的映射--map任务在执行器(接收块的执行器和复制块的执行器)中进行处理,这些执行器具有不考虑块间隔的块,除非启动了非本地调度。具有更大的块间隔意味着更大的块。spark.locality.wait
的高值增加了在本地节点上处理块的机会。需要在这两个参数之间找到平衡,以确保在本地处理较大的块。
. 您可以通过调用 inputDstream.repartition(n)
来定义分区的数量,而不是依赖于batchInterval和blockInterval。这将随机重组RDD中的数据,以创建n个分区。是的,为了更好的并行性。但这是以洗牌为代价的。RDD的处理由驱动程序的作业调度程序作为作业来调度。在给定的时间点上,只有一个作业是活动的。因此,如果一个作业正在执行,其他作业将排队。
. 如果您有两个数据流,则将形成两个RDD,并将创建两个作业,这些作业将逐个安排。为了避免这种情况,可以联合两个dstreams。这将确保为dstreams的两个RDDs形成一个unionRDD。然后,这个unionRDD被视为单个作业。但是,RDDs的分区不受影响。
. 如果批处理时间超过batchinterval,那么显然接收器的内存将开始填满,并最终抛出异常(很可能是BlockNotFoundException)。目前,没有办法暂停接收器。使用SparkConf配置spark.streaming.receiver.maxRate
,接收器的速率可以被限制。
Fault-tolerance Semantics(容错语义)
在本节中,我们将讨论发生故障时Spark流应用程序的行为。
Background
为了理解Spark流提供的语义,让我们记住Spark的RDDs的基本容错语义。
1. RDD是一个不可变的、确定性可重新计算的分布式数据集。每个RDD都会记住确定性操作的沿袭,这些操作是在容错输入数据集上用来创建它(此RDD)的。
2. 如果一个RDD的任何分区由于工作节点故障而丢失,那么可以使用操作沿袭(lineage)从原始容错数据集重新计算该分区。
3. 假设所有的RDD转换都是确定性的,那么最终转换的RDD中的数据将总是相同的,而不考虑Spark集群中的故障。
Spark操作容错文件系统(如HDFS或S3)中的数据。因此,所有由容错数据生成的RDD都是容错的。但是,Spark流并不是这样,因为在大多数情况下,数据是通过网络接收的(使用 fileStream
时除外)。为了对所有生成的RDDs实现相同的容错属性,接收到的数据在集群中工作节点的多个Spark执行器之间进行复制(默认复制系数为2)。这会导致系统中有两种数据在发生故障时需要恢复:
1>. Data received and replicated (接收和复制的数据) - 由于一个工作节点的副本存在于另一个节点上,因此该数据在单个工作节点的故障中幸存下来。
2>. Data received but buffered for replication (已接收但为复制而缓冲的数据) - 由于未复制此数据,恢复此数据的唯一方法是从源中再次获取它。
此外,我们应该关注两种故障:
1>. Failure of a Worker Node (工作节点故障) - 运行执行器的任何工作节点都可能失败,并且这些节点上的所有内存中数据都将丢失。如果任何接收器在失败的节点上运行,那么它们的缓冲数据将丢失。
2>. Failure of the Driver Node(驱动节点故障) - 如果运行Spark流应用程序的驱动程序节点失败,那么显然SparkContext会丢失,并且所有具有内存数据的执行器都会丢失。
利用这一基础知识,让我们了解Spark流的容错语义。
Definitions
流系统的语义通常是根据系统可以处理每条记录的次数来捕获的。在所有可能的运行条件下(尽管有故障等),系统可以提供三种类型的保证。
1. At most once (最多一次): 每个记录要么处理一次,要么根本不处理。
2. At least once(至少一次): 每个记录将被处理一次或多次。这比 At most once --最多一次强,因为它确保不会丢失任何数据。但是可能有重复的。
3. Exactly once(就一次): 每个记录将被精确地处理一次-不会丢失任何数据,也不会多次处理任何数据。这显然是三者中最强壮的保证。
Basic Semantics(基本语义)
一般来说,在任何流处理系统中,处理数据都有三个步骤。
1. Receiving the data: 使用接收器或其他方式从源接收数据
2. Transforming the data:接收到的数据通过DStream和RDD转换进行转换。
3. Pushing out the data:最终转换的数据被推送到外部系统,如文件系统、数据库、仪表盘等。
如果流应用程序必须实现端到端的一次性保证,那么每个步骤都必须提供一次性保证。也就是说,每个记录必须精确地接收一次,精确地转换一次,精确地推送到下游系统一次。让我们在Spark流上下文中理解这些步骤的语义。
1. Receiving the data:不同的输入源提供不同的保证。这将在下一小节中详细讨论。
2.Transforming the data: 由于RDD提供了保证,所有接收到的数据都将只处理一次。即使存在故障,只要接收到的输入数据是可访问的,最终转换的RDD将始终具有相同的内容。
3. Pushing out the data: 默认情况下,输出操作至少确保一次语义,因为它取决于输出操作的类型(幂等性或非幂等性)和下游系统的语义(是否支持事务)。但是用户可以实现自己的事务机制来实现精确的一次语义。本节稍后将对此进行更详细的讨论。
Semantics of Received Data(接收数据的语义)
不同的输入源提供不同的保证,从至少一次到完全一次。阅读了解更多详细信息。
With Files(使用文件)
如果所有输入数据都已经存在于HDFS这样的容错文件系统中,Spark流总是可以从任何故障中恢复并处理所有数据。这提供了精确的一次语义,这意味着无论发生什么故障,所有数据都将被精确地处理一次。
With Receiver-based Sources(使用基于接收器的源)
对于基于接收器的输入源,容错语义取决于故障场景和接收器类型。如前所述,有两种类型的接收器:
1. Reliable Receiver - 只有在确保接收到的数据已被复制之后,这些接收器才会确认可靠的来源。如果这样的接收器失败,则源不会收到缓冲(未复制)数据的确认。因此,如果重新启动接收器,源将重新发送数据,并且不会因失败而丢失任何数据。
2. Unreliable Receiver - 这类接收器不发送确认,因此当它们由于worker或driver故障而失败时,可能会丢失数据。
根据使用的接收器类型,我们实现以下语义。如果一个工作节点失败,那么对于可靠的接收器就不会丢失数据。对于不可靠的接收器,接收到但未复制的数据可能会丢失。如果驱动程序节点失败,那么除了这些丢失之外,在内存中接收和复制的所有过去的数据都将丢失。这将影响状态转换的结果。
为了避免丢失过去接收到的数据,Spark 1.2引入了写前日志,将接收到的数据保存到容错存储中。启用写前日志和可靠的接收器后,数据丢失为零。在语义方面,它提供了至少一次保证。
下表总结了失败时的语义:
Deployment Scenario (部署方案) | Worker Failure | Driver Failure |
Spark 1.1 or earlier, OR Spark 1.2 or later without write-ahead logs | Buffered data lost with unreliable receivers Zero data loss with reliable receivers At-least once semantics | Buffered data lost with unreliable receivers Past data lost with all receivers Undefined semantics |
Spark 1.2 or later with write-ahead logs | Zero data loss with reliable receivers At-least once semantics | Zero data loss with reliable receivers and files At-least once semantics |
With Kafka Direct API
在Spark 1.3中,我们引入了一个新的Kafka Direct API,它可以保证所有的Kafka数据都被Spark流精确地接收一次。与此同时,如果您实现精确的一次输出操作,就可以实现端到端精确的一次保证。 Kafka Integration Guide.将进一步讨论这种方法。
Semantics of output operations(输出操作的语义)
输出操作(如 foreachRDD
)有至少一次语义,也就是说,在worker出现故障时,转换后的数据可能被多次写入外部实体。虽然这对于使用 saveAs***Files
操作将文件保存到文件系统是可以接受的(因为文件会被相同的数据覆盖),但是可能需要额外的工作来实现精确的一次语义。有两种方法。
. Idempotent updates(等幂更新)- 多次尝试总是写入相同的数据。例如,saveAs***Files
总是向生成的文件写入相同的数据。
. Transactional updates(事务性更新): - 所有更新都是通过事务方式进行的,因此更新仅原子性地进行一次。一种方法是这样做。
。使用批处理时间(在foreachRDD中可用)和RDD的分区索引创建标识符。这个标识符惟一地标识流应用程序中的blob数据。
。使用标识符以事务方式(即,完全一次,原子方式)更新外部系统。也就是说,如果标识符尚未提交,则自动提交分区数据和标识符。否则,如果已提交,则跳过更新。
dstream.foreachRDD { (rdd, time) =>
rdd.foreachPartition { partitionIterator =>
val partitionId = TaskContext.get.partitionId()
val uniqueId = generateUniqueId(time.milliseconds, partitionId)
// use this uniqueId to transactionally commit the data in partitionIterator
}
}
Where to Go from Here
- Additional guides
- Third-party DStream data sources can be found in Third Party Projects
- API documentation
- Scala docs
- Java docs
- Python docs