DStream 其实是 RDD 的序列,它的语法与 RDD 类似,分为 transformation(转换) 和 output(输出) 两种操作;

DStream 的转换操作分为 无状态转换 和 有状态转换,且 tansformation 也是惰性的;

DStream 的输出操作请参考 我的博客 Streaming

 

无状态转换

转换操作只作用于单个 RDD,即单个数据流的 batch;

例如,每次根据采集到的数据流统计单词个数,第一次采集到的是  a 2个 b 1个,第二次采集到的是 a 1个 b 1个,那么第二次的输出也是 a 1 b 1,并不与前面的累计

 

DStream 的转换操作最终还是会转化成 RDD 的转换操作,这个转化由 spark streaming 完成

本文只介绍部分 API,详细请参考源码 /usr/lib/spark/python/pyspark/streaming/dstream.py

 

基本转换

为了解释下面的操作,假定输入都一样,如下

hellp spark
hello hadoop hive

 

map(self, f, preservesPartitioning=False):生成一个新的 DStream,不解释了

[u'hellp', u'spark']
[u'hello', u'hadoop', u'hive']

flatMap(self, f, preservesPartitioning=False):把一个 func 作用于 DStream 的所有元素,并将生成的结果展开

hellp
spark
hello
hadoop
hive

filter(self, f):不解释了

repartition(self, numPartitions):把 DStream 中每个 RDD 进行分区,用于提高并发

union(self, other):在一个 RDD 上再加一个 RDD

 

聚合转换

count(self):

reduce(self, func):

countByValue(self):返回一个由键值对型 RDD 构成的 DStream

# 输入 
a
a
b
# 输出
(u'a', 2)
(u'b', 1)

 

键值对转换

cogroup(self, other, numPartitions=None):把两个 kv 构成的 DStream 根据 key 进行合并,注意是取 key 的并集

sc = SparkContext()

ssc = StreamingContext(sc, 30)
line1 = ssc.socketTextStream('192.168.10.11', 9999)
out1 = line1.map(lambda x: (len(x), x)).groupByKey()    # value 没有进行任何操作
out1.pprint()

line2 = ssc.socketTextStream('192.168.10.11', 9998)
out2 = line2.map(lambda x: (len(x), x)).groupByKey()
out2.pprint()

out3 = out1.cogroup(out2)
out3.pprint()

ssc.start()
ssc.awaitTermination()

很遗憾,合并之后,value 是个对象集,不可见

 

join(self, other, numPartitions=None):把两个 kv 构成的 DStream 连接起来

如 (K,V) 构成的 DStream 和 (K,W) 构成的 DStream 连接后是 (K, (V,W))

同样有 左连接、右连接、全连接

out3 = out1.join(out2)      # 内连接   二者都有
out3 = out1.leftOuterJoin(out2) # 左连接   左边有,找对应右边的
out3 = out1.rightOuterJoin(out2)    # 右连接  右边有,找对应左边的
out3 = out1.fullOuterJoin(out2)     # 全连接   笛卡尔内积

 

reduceByKey(self, func, numPartitions=None):

groupByKey(self, numPartitions=None):将构成 DStream 的 RDD 中的元素进行 分组 

 

还有很多,不一一解释了,记住本质,这些虽然是 DStream 的 API,但是这些 API 其实是作用于 构成 DStream 的 RDD 上的 

 

transform:这个比较特殊

transform(self, func):
        """
        Return a new DStream in which each RDD is generated by applying a function
        on each RDD of this DStream.

        `func` can have one argument of `rdd`, or have two arguments of
        (`time`, `rdd`)
        """

输入一个 函数,这个函数作用于 构成 DStream 的每个 RDD ,并返回新的 RDD ,重新构成一个 DStream

transform 用于 机器学习 或者 图计算 时优势明显

 

示例

from pyspark import SparkContext
from pyspark.streaming import StreamingContext

sc = SparkContext()
ssc = StreamingContext(sc, 20)
line1 = ssc.socketTextStream('192.168.10.11', 9999)
out1 = line1.map(lambda x: (len(x), x))
### 输入为
# a
# aaa
# aa
out1.pprint()
# (1, u'a')
# (3, u'aaa')
# (2, u'aa')        这是一个 RDD,被 transform 的 func 作用

out2 = out1.transform(lambda x: x.sortByKey())
out2.pprint()
# (1, u'a')
# (2, u'aa')
# (3, u'aaa')

out3 = out1.transform(lambda x: x.mapValues(lambda m: ''))  
out3.pprint()

ssc.start()
ssc.awaitTermination()

 

有状态转换

转换操作作用于之前所有的 RDD 上

例如,每次根据采集到的数据流统计单词个数,第一次采集到的是  a 2个 b 1个,第二次采集到的是 a 1个 b 1个,那么第二次的输出也是 a 3 b 2,累计之前的

 

updateStateByKey(self, updateFunc, numPartitions=None, initialRDD=None):更新当前 RDD 的状态,不好解释,用例子帮助理解

需要设置检查点, 用于保存状态

示例

from pyspark import SparkContext
from pyspark.streaming import StreamingContext

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

sc = SparkContext()
ssc = StreamingContext(sc, 20)
ssc.checkpoint('/usr/lib/spark')        # 必须有检查点

line1 = ssc.socketTextStream('192.168.10.11', 9999)
out1 = line1.map(lambda x: (x, 1)).reduceByKey(lambda x, y: x + y)    # 对当前 RDD 的处理

runningCounts = out1.updateStateByKey(updateFunction)   # 更新状态
runningCounts.pprint()

ssc.start()
ssc.awaitTermination()

示例操作

 

由于统计全局,所以需要checkpoint数据会占用较大的存储。而且效率也不高。所以很多时候不建议使用updateStateByKey

 

窗口操作

updateStateByKey 也是一种窗口,只是窗口大小不固定;

这里的窗口就是指滑窗,跟 均值滤波里面的滑窗意思一样;

这里的滑窗内的元素是 RDD;

滑窗有 窗口尺寸 和 滑动步长 两个概念

滑窗也是有状态的转换

这里的 尺寸 和 步长 都是用时间来描述;

尺寸 是 采集周期的 N 倍,步长 也是 采集周期的 N 倍

 

window(self, windowDuration, slideDuration=None):windowDuration 为 窗口时长,slideDuration 为步长

示例

from pyspark import SparkContext
from pyspark.streaming import StreamingContext

sc = SparkContext()
ssc = StreamingContext(sc, 20)
line1 = ssc.socketTextStream('192.168.10.11', 9999)

## 先处理再滑窗
out1 = line1.map(lambda x: (len(x), x))
out2 = out1.window(40)
# out2 = out2.groupByKey()    # 滑窗后可继续处理
out2.pprint()

ssc.start()
ssc.awaitTermination()

示例操作

可以看到有重复计算的内容

 

reduceByWindow(self, reduceFunc, invReduceFunc, windowDuration, slideDuration):当 slideDuration < windowDuration 时,计算是有重复的,那么我们可以不用重新获取或者计算,而是通过获取旧信息来更新新的信息,这样可以提高效率

 

函数解释

window 计算方式如下===

win1 = time1 + time2 + time3

win2 = time3 + time4 + time5

显然 time3 被重复获取并计算

 

reduceByWindow 计算方式如下====

win1 = time1 + time2 + time3

win2 = win1 + time4 + time5 -time1 -time2

 

reduceFunc 是对新产生的数据(time4,time5) 进行计算;

invReduceFunc 是对之前的旧数据(time1,time3) 进行计算;

 

示例

from pyspark import SparkContext
from pyspark.streaming import StreamingContext

def func1(x, y):
    return x+y

def func2(x, y):
    return -x-y

sc = SparkContext(appName="windowStream", master="local[*]")
# 第二个参数指统计多长时间的数据
ssc = StreamingContext(sc, 10)
ssc.checkpoint("/tmp/window")
lines = ssc.socketTextStream('192.168.10.11', 9999)
# 第一个参数执行指定函数, 第二个参数是窗口长度, 第三个参数是滑动间隔
lines = lines.flatMap(lambda x: x.split(' ')).map(lambda x: (x, 1))
dstream = lines.reduceByWindow(func1, func2, 20, 10)
dstream.pprint()

ssc.start()
ssc.awaitTermination()

 

reduceByWindow 和 window 可实现相同效果;

reduceByWindow 的底层是 reduceByKeyAndWindow,用法也完全相同,reduceByKeyAndWindow 的效率更高

需要设置检查点, 用于保存状态

 

reduceByKeyAndWindow(self, func, invFunc, windowDuration, slideDuration=None,
numPartitions=None, filterFunc=None):与 reduceByWindow 的区别是 它的输入需要 kv 对

dstream = lines.reduceByKeyAndWindow(func1, func2, 20, 10)

 

countByWindow(self, windowDuration, slideDuration):计数

需要设置检查点, 用于保存状态

示例

sc = SparkContext()
ssc = StreamingContext(sc, 20)
line1 = ssc.socketTextStream('192.168.10.11', 9999)

ssc.checkpoint('/usr/lib/spark')

## 先处理再滑窗
out1 = line1.map(lambda x: x)
out2 = out1.countByWindow(40, 20)
# out2 = out2.groupByKey()    # 滑窗后可继续处理
out2.pprint()

ssc.start()
ssc.awaitTermination()

 

countByValueAndWindow(self, windowDuration, slideDuration, numPartitions=None):

groupByKeyAndWindow(self, windowDuration, slideDuration, numPartitions=None):

 

不一一解释了

 

总结

1. 凡是带 key 的都需要输入 kv 对

2. 凡是需要记录上个 状态的 都需要设置检查点

 

 

参考资料:

《Spark大数据分析核心概念技术及实践OCR-2017》 电子书

 https://www.cnblogs.com/libin2015/p/6841177.html