DStreams转化
与RDDs相似,转化允许DStream输入的数据被修改。DStreams支持很多在一般Spark RDD中的转化,一些常用的如下:
map(func)
flatMap(func)
filter(func)
repartition(numPartitions)
union(otherStream)
count()
reduce(func)
countByValue()
reduceByKey(func, [num Tasks])
join(otherStream, [num Tasks])
cogroup(otherStream, [num Tasks])
transform(func)
updateStateByKey(func)
我们将具体探讨下面几个操作
UpdateStateByKey
这个操作允许保持任意状态并同时持续使用新信息更新,它分成两步
1、定义状态——可以是任意数据类型
2、定义状态更新函数——指明一个如何使用此前状态和来自于输入流的新值更新状态的函数
在每一个批次,Spark会将状态更新函数应用于所有现存的keys,无论他们是否在这一批次有新的数据。如果更新函数返回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
这将被用于一个包含词的DStream(例如在之前的例子中pairs DStream包含(word, 1))
runningCounts = pairs.updateStateByKey(updateFunction)
更新函数会被每个词调用,newValues是一系列1(来自(word,1)),runningCount有此前的计数,完整的代码见https://github.com/apache/spark/blob/v2.1.1/examples/src/main/python/streaming/stateful_network_wordcount.py
注意使用updateStateByKey要求配置检查点文件夹,详见http://spark.apache.org/docs/latest/streaming-programming-guide.html#checkpointing
Transform
这个操作以及它的变体如transformWith允许任意RDD-RDD函数被应用于DStream。可以用于任何不暴露与DStream API的RDD操作。例如 将数据流中的每个批次与另外一个数据集结合起来。这使得可以通过将输入数据流与预计算的垃圾邮件信息结合起来进行实时数据清洗然后基于此进行过滤。
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
Spark Streaming还提供窗口计算,允许将转化应用于数据的一个滑动窗口,图示如下
如图所示,每个时间窗口在源DStream上滑动,窗口内的源RDDs被结合操作产生该窗口DStream的RDDs。在这个特定的例子中, 操作被应用于最后3个时间单元的数据,每次滑动2个时间单元,这表明每个窗口操作要指明两个参数
窗口长度——窗口的持续时间
滑动间隔——窗口操作执行的间隔
这两个参数必须是源DStream批次间隔的倍数。
举个例子,例如之前的例子扩展到计数最后30秒的数据,间隔为10秒。我们要应用reduceByKey操作到最后30秒数据(word,1)的pairs DStream,使用操作reduceByKeyAndWindow
# Reduce last 30 seconds of data, every 10 seconds
windowedWordCounts = pairs.reduceByKeyAndWindow(lambda x, y: x + y, lambda x, y: x - y, 30, 10)
一些常用的窗口操作
window(windowLength, slideInterval)
countByWindow(windowLength, slideInterval)
reduceByWindow(func, windowLength, slideInterval)
reduceByKeyAndWindow(func, windowLength, slideInterval, [num Tasks])
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [num Tasks])
countByValueAndWindow(windowLength, slideInterval, [num Tasks])
Join
最后我们看一些在Spark Streaming中进行不同的联合操作。
流-流连接
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)
流-数据集连接
dataset = ... # some RDD
windowedStream = stream.window(20)
joinedStream = windowedStream.transform(lambda rdd: rdd.join(dataset))
实际上,我们还可以动态的调整被连接的数据集。transform提供的函数在每个批次间被评估因此会使用dataset引用指向的当前数据集。
DStreams的输出操作
输出操作使DStream的数据被推到外部系统如数据库或文件系统,他们触发实际执行DStream的所有转化(与RDDs的action类似),目前输出操作有:
print()
saveAsTextFiles(prefix, [suffix])
saveAsObjectFiles(prefix, [suffix])
saveAsHadoopFiles(prefix, [suffix])
foreachRDD(func)
使用foreachRDD的设计模式
dstream.foreachRDD功能强大,允许数据被送往外部系统,但要理解如何正确有效的使用该功能。一些应避免的常见错误有:总是将数据写到需要创建连接对象的外部系统(例如TCP连接到一个远程服务器)并使用它将数据送往远程系统。开发者可能不经意的在Spark driver中建立一个连接对象,并试图使用Spark woker保存在RDDs中的记录,例如
def sendRecord(rdd):
connection = createNewConnection() # executed at the driver
rdd.foreach(lambda record: connection.send(record))
connection.close()
dstream.foreachRDD(sendRecord)
这不正确。因为它要求连接对象被序列化并从驱动着送到工作者。这样的连接对象难以在机器间传输。这个错误可能以序列化错误(连接对象无法序列化)、初始化错误(连接对象需要在工作者中初始化)等形式显现。正确的方法是在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/批次反复使用连接对象以优化。
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))
注意池中的连接如果一段时间不用会超时。这是将数据送往外部系统最有效的方法。
其他需要注意的点:
DStream以懒惰的方式被输出操作执行。在DStream输出操作中的RDD action实际对接收数据进行处理。因此,如果应用中没有输出操作或者如dstream.foreachRDD()这样的输出操作中没有RDD执行,那么系统只是简单的接收并丢弃数据而没有任何执行。
默认的,输出操作是一次一执行,并且以在应用中定义的顺序执行。