初识Structured Streaming

第16章:Spark Streaming与Structured Streaming 摘要: Spark Streaming采用微批处理模式,通过DStream将实时数据流切分为RDD序列进行处理,支持无状态和有状态转换(如窗口操作),但存在处理时间语义单一、API不统一等局限。Structured Streaming基于Spark SQL引擎,将流数据视为无限表,提供统一的DataFrame API、事件时间处理(支持Watermarking解决乱序问题)及端到端精确一次语义,支持多种输出模式(Append/Complete/Update)。示例展示了从Kafka读取JSON数据,通过Wa 阅读详情

公众号后台回复关键词:pyspark,获取本项目github地址。

设想我们要设计一个交易数据展示系统,实时呈现比特币最近1s钟的成交均价。

我们可以通过交易数据接口以非常低的延迟获得全球各个比特币交易市场的每一笔比特币的成交价,成交额,交易时间。

由于比特币交易事件一直在发生,所以交易事件触发的交易数据会像流水一样源源不断地通过交易接口传给我们。

如何对这种流式数据进行实时的计算呢?我们需要使用流计算工具,在数据到达的时候就立即对其进行计算。

市面上主流的开源流计算工具主要有 Storm, Flink 和 Spark。

其中Storm的延迟最低,一般为几毫秒到几十毫秒,但数据吞吐量较低,每秒能够处理的事件在几十万左右,建设成本高。

Flink是目前国内互联网厂商主要使用的流计算工具,延迟一般在几十到几百毫秒,数据吞吐量非常高,每秒能处理的事件可以达到几百上千万,建设成本低。

Spark通过Spark Streaming或Spark Structured Streaming支持流计算。但Spark的流计算是将流数据按照时间分割成一个一个的小批次(mini-batch)进行处理的,其延迟一般在1秒左右。吞吐量和Flink相当。值得注意的是Spark Structured Streaming 现在也支持了Continous Streaming 模式,即在数据到达时就进行计算,不过目前还处于测试阶段,不是特别成熟。

虽然从目前来看,在流计算方面,Flink比Spark更具性能优势,是当之无愧的王者。但由于Spark拥有比Flink更加活跃的社区,其流计算功能也在不断地完善和发展,未来在流计算领域或许足以挑战Flink的王者地位。

import pyspark
from pyspark.sql import SparkSession
from pyspark.sql import types as T
from pyspark.sql import functions as F 
import time,os,random

#本文主要用小数据测试,设置较小的分区数可以获得更高性能
spark = SparkSession.builder \
        .appName("structured streaming") \
        .config("spark.sql.shuffle.partitions","8") \
        .config("spark.default.parallelism","8") \
        .config("master","local[4]") \
        .enableHiveSupport() \
        .getOrCreate()

sc = spark.sparkContext

一,Structured Streaming 基本概念

流计算(Streaming)和批计算(Batch):

批计算或批处理是处理离线数据。单个处理数据量大,处理速度比较慢。

流计算是处理在线实时产生的数据。单次处理的数据量小,但处理速度更快。


Spark Streaming 和 Spark Structured Streaming:

Spark在2.0之前,主要使用的Spark Streaming来支持流计算,其数据结构模型为DStream,其实就是一个个小批次数据构成的RDD队列。

目前,Spark主要推荐的流计算模块是Structured Streaming,其数据结构模型是Unbounded DataFrame,即没有边界的数据表。

相比于 Spark Streaming 建立在 RDD数据结构上面,Structured Streaming 是建立在 SparkSQL基础上,DataFrame的绝大部分API也能够用在流计算上,实现了流计算和批处理的一体化,并且由于SparkSQL的优化,具有更好的性能,容错性也更好。


source 和 sink:

source即流数据从何而来。在Spark Structured Streaming 中,主要可以从以下方式接入流数据。

1, Kafka Source。当消息生产者发送的消息到达某个topic的消息队列时,将触发计算。这是structured Streaming 最常用的流数据来源。

2, File Source。当路径下有文件被更新时,将触发计算。这种方式通常要求文件到达路径是原子性(瞬间到达,不是慢慢写入)的,以确保读取到数据的完整性。在大部分文件系统中,可以通过move操作实现这个特性。

3, Socket Source。需要制定host地址和port端口号。这种方式一般只用来测试代码。linux环境下可以用nc命令来开启网络通信端口发送消息测试。

sink即流数据被处理后从何而去。在Spark Structured Streaming 中,主要可以用以下方式输出流数据计算结果。

1, Kafka Sink。将处理后的流数据输出到kafka某个或某些topic中。

2, File Sink。将处理后的流数据写入到文件系统中。

3, ForeachBatch Sink。对于每一个micro-batch的流数据处理后的结果,用户可以编写函数实现自定义处理逻辑。例如写入到多个文件中,或者写入到文件并打印。

4, Foreach Sink。一般在Continuous触发模式下使用,用户编写函数实现每一行的处理处理。

5,Console Sink。打印到Driver端控制台,如果日志量大,谨慎使用。一般供调试使用。

6,Memory Sink。输出到内存中,供调试使用。

append mode, complete mode 和 update mode:

这些是流数据输出到sink中的方式,叫做 output mode。

append mode 是默认方式,将新流过来的数据的计算结果添加到sink中。

complete mode 一般适用于有aggregation查询的情况。流计算启动开始到目前为止接收到的全部数据的计算结果添加到sink中。

update mode 只有本次结果中和之前结果不一样的记录才会添加到sink中。


operation 和 query:

在SparkSQL批处理中,算子被分为Transformation算子和Action算子。Spark Structured Streaming 有所不同,所有针对流数据的算子都是懒惰执行的,叫做operation。

DataFrame的Action算子(例如show,count,reduce)都不可以在Spark Structured Streaming中使用,而大部分Transformation算子都可以在Structured Streaming中使用(例如select,where,groupBy,agg)。

但也有些操作不可以(例如sort, distinct,某些类型的join操作,以及连续的agg操作等)。

如果要触发执行,需要通过writeStream启动一个query,指定sink,output mode,以及触发器trigger类型。

从一定意义上,可以将writeStream理解成Structured Streaming 唯一的 Action 算子。

Spark Structured Streaming支持的触发器trigger类型主要有以下一些。

1,unspecified。不指定trigger类型,以micro-batch方式触发,当上一个micro-batch执行完成后,将中间收到的数据作为下一个micro-batch的数据。

2,fixed interval micro-batches。指定时间间隔的micro-batch。如果上一个micro-batch在间隔时间内完成,需要等待指定间隔时间。如果上一个micro-batch在间隔时间后才完成,那么会在上一个micro-batch执行完成后立即执行。

3,one-time micro-batch。只触发一次,以micro-batch方式触发。一种在流计算模式下执行批处理的方法。

4,continuous with fixed checkpoint interval。每个事件触发一次,真正的流计算,这种模式目前还处于实验阶段。


event time, processing time 和 watermarking:

event time 是流数据的发生时间,一般嵌入到流数据中作为一个字段。

processing time 是指数据被处理的时间。

Spark Structured Streaming 一般 使用 event time作为 Windows切分的依据,例如每秒钟的成交均价,是取event time中每秒钟的数据进行处理。

考虑到数据存在延迟,如果一个数据到达时,其对应的时间批次已经被计算过了,那么会重新计算这个时间批次的数据并更新之前的计算结果。但是如果这个数据延迟太久,那么可以设置watermarking(水位线)来允许丢弃 processing time和event time相差太久的数据,即延迟过久的数据。注意这种丢弃是或许会发生的,不是一定会丢弃。


at-most once,at-least once 和 exactly once:

这是分布式流计算系统在某些机器发生发生故障时,对结果一致性(无论机器是否发生故障,结果都一样)的保证水平。反应了分布式流计算系统的容错能力。

at-most once,最多一次。每个数据或事件最多被程序中的所有算子处理一次。这本质上是一种尽力而为的方法,只要机器发生故障,就会丢弃一些数据。这是比较低水平的一致性保证。

at-least once,至少一次。每个数据或事件至少被程序中的所有算子处理一次。这意味着当机器发生故障时,数据会从某个位置开始重传。但有些数据可能在发生故障前被所有算子处理了一次,在发生故障后重传时又被所有算子处理了一次,甚至重传时又有机器发生了故障,然后再次重传,然后又被所有算子处理了一次。因此是至少被处理一次。这是一种中间水平的一致性保证。

exactly once,恰好一次。从计算结果看,每个数据或事件都恰好被程序中的所有算子处理一次。这是一种最高水平的一致性保证。

spark structured streaming 在micro-batch触发器类型下,sink是File情况下,可以保证为exactly once的一致性水平。

但是在continuou触发器类型下,只能保证是at-least once的一致性水平。

详情参考如下文章:《谈谈流计算中的『Exactly Once』特性》

https://segmentfault.com/a/1190000019353382

二,word count 基本范例

下面范例中,我们将用Python代码在一个目录下不断生成一些简单句子组成的文件。

然后用pyspark读取文件流,并进行词频统计,并将结果打印。

下面是生成文件流的代码。并通过subprocess.Popen调用它异步执行。

%%writefile make_streamming_data.py
import random 
import os 
import time 
import shutil
sentences = ["eat tensorflow2 in 30 days","eat pytorch in 20 days","eat pyspark in 10 days"]
data_path = "./data/streamming_data"

if os.path.exists(data_path):
    shutil.rmtree(data_path)
    
os.makedirs(data_path)

for i in range(20):
    line = random.choice(sentences)
    tmp_file = str(i)+".txt"
    with open(tmp_file,"w") as f:
        f.write(line)
        f.flush()
    shutil.move(tmp_file,os.path.join(data_path,tmp_file))
    time.sleep(1)
    
# 在后台异步生成文件流
import subprocess
cmd = ["python", "make_streamming_data.py"]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

#process.wait() #等待结束

#通过 readStream 创建streaming dataframe
schema = T.StructType().add("value", "string")
data_path = "./data/streamming_data"

dflines = spark \
    .readStream \
    .option("sep", ".") \
    .schema(schema) \
    .csv(data_path)

dflines.printSchema() 
print(dflines.isStreaming) 
root
 |-- value: string (nullable = true)

True
#实施operator转换
dfwords = dflines.select(F.explode(F.split(dflines.value, " ")).alias("word"))
dfwordCounts = dfwords.groupBy("word").count()

#执行query, 注意是异步方式执行, 相当于是开启了后台进程

def foreach_batch_function(df, epoch_id):
    print("Batch: ",epoch_id)
    df.show()

query = dfwordCounts \
    .writeStream \
    .outputMode("complete")\
    .foreachBatch(foreach_batch_function) \
    .start()

#query.awaitTermination() #阻塞当前进程直到query发生异常或者被stop

print(query.isActive)

#60s后主动停止query
time.sleep(30)
query.stop()

print(query.isActive)


True
Batch:  0
+-----------+-----+
|       word|count|
+-----------+-----+
|        eat|   10|
|       days|   10|
|         20|    4|
|tensorflow2|    3|
|         30|    3|
|         10|    3|
|    pyspark|    3|
|         in|   10|
|    pytorch|    4|
+-----------+-----+

Batch:  1
+-----------+-----+
|       word|count|
+-----------+-----+
|        eat|   13|
|       days|   13|
|         20|    4|
|tensorflow2|    5|
|         30|    5|
|         10|    4|
|    pyspark|    4|
|         in|   13|
|    pytorch|    4|
+-----------+-----+

Batch:  2
+-----------+-----+
|       word|count|
+-----------+-----+
|        eat|   15|
|       days|   15|
|         20|    5|
|tensorflow2|    5|
|         30|    5|
|         10|    5|
|    pyspark|    5|
|         in|   15|
|    pytorch|    5|
+-----------+-----+

Batch:  3
+-----------+-----+
|       word|count|
+-----------+-----+
|        eat|   18|
|       days|   18|
|         20|    6|
|tensorflow2|    5|
|         30|    5|
|         10|    7|
|    pyspark|    7|
|         in|   18|
|    pytorch|    6|
+-----------+-----+

Batch:  4
+-----------+-----+
|       word|count|
+-----------+-----+
|        eat|   20|
|       days|   20|
|         20|    7|
|tensorflow2|    5|
|         30|    5|
|         10|    8|
|    pyspark|    8|
|         in|   20|
|    pytorch|    7|
+-----------+-----+

False

三,创建Streaming DataFrame

可以从Kafka Source,File Source 以及 Socket Source 中创建 Streaming DataFrame。

1,从Kafka Source 创建

需要安装kafka,并加载其jar包到依赖中。

详细参考:http://spark.apache.org/docs/2.2.0/structured-streaming-kafka-integration.html

以下代码仅供示范,运行需要配置相关kafka环境。

df = spark \
  .read \
  .format("kafka") \
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2") \
  .option("subscribe", "topic1") \
  .load()

2,从File Source 创建

支持读取parquet文件,csv文件,json文件,txt文件目录。需要指定schema。

schema = T.StructType().add("name","string").add("age","integer").add("score","double")
dfstudents = spark.readStream.schema(schema).json("./data/students_json")
dfstudents.printSchema() 
query = dfstudents.writeStream \
    .outputMode("append")\
    .format("parquet") \
    .option("checkpointLocation", "./data/checkpoint/") \
    .option("path", "./data/students_parquet/") \
    .start()

#query.awaitTermination()

3,从Socket Source创建

在bash中输入nc -lk 9999 开启socket网络通信端口,然后在其中输入一些句子,如:

hello world
hello China
hello Beijing
dflines = spark \
    .readStream \
    .format("socket") \
    .option("host", "localhost") \
    .option("port", 9999) \
    .load()

四,使用operator转换

可以在Streaming DataFrame上使用Static DataFrame大部分常规Transformation算子。

还可以针对event time进行滑动窗口(window)操作,可以通过设置水位线(watermarking)来丢弃延迟过久的数据。

不仅如此,可以对Streaming DataFrame和 Static DataFrame 进行表连接 join操作。

甚至两个Streaming DataFrame之前也是可以join的。

1,Basic Operators

一些常用的Transformation算子都可以在Unbounded DataFrame上使用,例如select,selectExpr, where, groupBy, agg等等。

也可以像批处理中的静态的DataFrame那样,注册临时视图,然后在视图上使用SQL语法。

schema = T.StructType().add("name","string").add("age","integer").add("score","double")
dfstudents = spark.readStream.schema(schema).json("./data/students_json")
dfstudents.printSchema() 

dfstudents.createOrReplaceTempView("students")

dfstudents_old = spark.sql("select * from students where age >25")
print(dfstudents_old.isStreaming)

2, Window Operations on Event Time

基于事件时间滑动窗上的聚合操作和其它列的goupBy操作非常相似,落在同一个时间窗的记录就好像具有相同的key,它们将进行聚合。

下面我们通过一个虚拟的比特币交易价格的例子来展示基于事件时间滑动窗上的聚合操作。

%%writefile make_trading_data.py

import random 
import os 
import time 
import datetime 
import json 
import shutil

data_path = "./data/trading_data"

if os.path.exists(data_path):
    shutil.rmtree(data_path)
    
os.makedirs(data_path)

for i in range(20):
    now =  datetime.datetime.now()
    now_str =  now.strftime("%Y-%m-%d %H:%M:%S.%f")
    
    #构造延迟数据, 延迟20min左右
    right_now = now - datetime.timedelta(minutes = 20)
    right_now_str = right_now.strftime("%Y-%m-%d %H:%M:%S.%f")
    
    if i%2==0:
        dic = {"dt": now_str, "amount": 100, "price": 10000.0+random.choice(range(5))}
    else:
        dic = {"dt": right_now_str, "amount": 100 ,"price": 100.0-random.choice(range(5))}
        
    tmp_file = str(i)+".json"
    with open(tmp_file,"w") as f:
        json.dump(dic,f)
    shutil.move(tmp_file,os.path.join(data_path,tmp_file))
    time.sleep(10)
# 在后台异步生成文件流
import subprocess
cmd = ["python", "make_trading_data.py"]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

#process.wait() #等待结束
data_path = "./data/trading_data"
schema = T.StructType().add("dt","string").add("amount","integer").add("price","double")
dfprice_raw = spark.readStream.schema(schema).json("./data/trading_data")
dfprice_raw.printSchema() 
dfprice = dfprice_raw.selectExpr("cast(dt as timestamp) as dt","amount","price", "amount*price as volume")
dfprice.printSchema() 
# 控制台方式输出,可能需要在jupyter 的log界面查看输出日志

query = dfprice.writeStream \
    .outputMode("append")\
    .format("console") \
    .start()

time.sleep(20)
query.stop()

#query.awaitTermination()

-------------------------------------------
Batch: 0
-------------------------------------------
+--------------------+------+-------+---------+
|                  dt|amount|  price|   volume|
+--------------------+------+-------+---------+
|2020-12-21 08:11:...|   100|10004.0|1000400.0|
+--------------------+------+-------+---------+

-------------------------------------------
Batch: 1
-------------------------------------------
+--------------------+------+-----+------+
|                  dt|amount|price|volume|
+--------------------+------+-----+------+
|2020-12-21 07:51:...|   100| 99.0|9900.0|
+--------------------+------+-----+------+

-------------------------------------------
Batch: 2
-------------------------------------------
+--------------------+------+-------+---------+
|                  dt|amount|  price|   volume|
+--------------------+------+-------+---------+
|2020-12-21 08:12:...|   100|10003.0|1000300.0|
+--------------------+------+-------+---------+

#下面我们将dfprice按照时间分窗,窗口范围为10min,滑动周期为5min,并统计滑动窗口内的平均交易价格

dfprice_avg = dfprice.groupBy(F.window(dfprice.dt, "10 minutes", "5 minutes")) \
   .agg(F.sum("amount").alias("amount"), F.sum("volume").alias("volume")) \
   .selectExpr("window","window.start","window.end","volume/amount as avg_price")

dfprice_avg.printSchema() 
query = dfprice_avg.writeStream \
    .outputMode("complete")\
    .format("console") \
    .start()

time.sleep(60)
query.stop()

#query.awaitTermination()

-------------------------------------------
Batch: 0
-------------------------------------------
+--------------------+-------------------+-------------------+---------+
|              window|              start|                end|avg_price|
+--------------------+-------------------+-------------------+---------+
|[2020-12-21 08:25...|2020-12-21 08:25:00|2020-12-21 08:35:00|  10004.0|
|[2020-12-21 08:00...|2020-12-21 08:00:00|2020-12-21 08:10:00|     97.0|
|[2020-12-21 08:05...|2020-12-21 08:05:00|2020-12-21 08:15:00|     97.0|
|[2020-12-21 08:20...|2020-12-21 08:20:00|2020-12-21 08:30:00|  10004.0|
+--------------------+-------------------+-------------------+---------+

-------------------------------------------
Batch: 1
-------------------------------------------
+--------------------+-------------------+-------------------+---------+
|              window|              start|                end|avg_price|
+--------------------+-------------------+-------------------+---------+
|[2020-12-21 08:25...|2020-12-21 08:25:00|2020-12-21 08:35:00|  10002.0|
|[2020-12-21 08:00...|2020-12-21 08:00:00|2020-12-21 08:10:00|     97.0|
|[2020-12-21 08:05...|2020-12-21 08:05:00|2020-12-21 08:15:00|     97.0|
|[2020-12-21 08:20...|2020-12-21 08:20:00|2020-12-21 08:30:00|  10002.0|
+--------------------+-------------------+-------------------+---------+

-------------------------------------------
Batch: 2
-------------------------------------------
+--------------------+-------------------+-------------------+---------+
|              window|              start|                end|avg_price|
+--------------------+-------------------+-------------------+---------+
|[2020-12-21 08:25...|2020-12-21 08:25:00|2020-12-21 08:35:00|  10002.0|
|[2020-12-21 08:00...|2020-12-21 08:00:00|2020-12-21 08:10:00|     98.5|
|[2020-12-21 08:05...|2020-12-21 08:05:00|2020-12-21 08:15:00|     98.5|
|[2020-12-21 08:20...|2020-12-21 08:20:00|2020-12-21 08:30:00|  10002.0|
+--------------------+-------------------+-------------------+---------+
#进一步地,我们设置watermarking(水位线)为20分钟, 则超出水位线的数据将允许被丢弃(但不一定被丢弃)

dfprice_avg = dfprice.withWatermark("dt", "20 minutes") \
   .groupBy(F.window(dfprice.dt, "10 minutes", "5 minutes")) \
   .agg(F.sum("amount").alias("amount"), F.sum("volume").alias("volume")) \
   .selectExpr("window","window.start","window.end","volume/amount as avg_price")

dfprice_avg.printSchema() 


#设置水位线后, outputMode必须是append或者update
query = dfprice_avg.writeStream \
    .outputMode("update")\
    .format("console") \
    .start()

time.sleep(60)
query.stop()

#query.awaitTermination()
-------------------------------------------
Batch: 0
-------------------------------------------
+--------------------+-------------------+-------------------+---------+
|              window|              start|                end|avg_price|
+--------------------+-------------------+-------------------+---------+
|[2020-12-21 08:00...|2020-12-21 08:00:00|2020-12-21 08:10:00|     96.0|
|[2020-12-21 08:15...|2020-12-21 08:15:00|2020-12-21 08:25:00|  10001.0|
|[2020-12-21 08:20...|2020-12-21 08:20:00|2020-12-21 08:30:00|  10001.0|
|[2020-12-21 07:55...|2020-12-21 07:55:00|2020-12-21 08:05:00|     96.0|
+--------------------+-------------------+-------------------+---------+

-------------------------------------------
Batch: 1
-------------------------------------------
+------+-----+---+---------+
|window|start|end|avg_price|
+------+-----+---+---------+
+------+-----+---+---------+

-------------------------------------------
Batch: 2
-------------------------------------------
+--------------------+-------------------+-------------------+---------+
|              window|              start|                end|avg_price|
+--------------------+-------------------+-------------------+---------+
|[2020-12-21 08:00...|2020-12-21 08:00:00|2020-12-21 08:10:00|     97.5|
|[2020-12-21 07:55...|2020-12-21 07:55:00|2020-12-21 08:05:00|     97.5|
+--------------------+-------------------+-------------------+---------+

-------------------------------------------
Batch: 3
-------------------------------------------
+--------------------+-------------------+-------------------+------------------+
|              window|              start|                end|         avg_price|
+--------------------+-------------------+-------------------+------------------+
|[2020-12-21 08:15...|2020-12-21 08:15:00|2020-12-21 08:25:00|10000.666666666666|
|[2020-12-21 08:20...|2020-12-21 08:20:00|2020-12-21 08:30:00|10000.666666666666|
+--------------------+-------------------+-------------------+------------------+

-------------------------------------------
Batch: 4
-------------------------------------------
+------+-----+---+---------+
|window|start|end|avg_price|
+------+-----+---+---------+
+------+-----+---+---------+

-------------------------------------------
Batch: 5
-------------------------------------------
+--------------------+-------------------+-------------------+---------+
|              window|              start|                end|avg_price|
+--------------------+-------------------+-------------------+---------+
|[2020-12-21 08:00...|2020-12-21 08:00:00|2020-12-21 08:10:00|     98.0|
|[2020-12-21 07:55...|2020-12-21 07:55:00|2020-12-21 08:05:00|     98.0|
+--------------------+-------------------+-------------------+---------+

3, Join Operations

Streaming DataFrame 可以和 Static DataFrame 进行 Inner 或者 Left Outer 连接操作。join后的结果依然是一个 Streaming DataFrame。

此外 Streaming  DataFrame 也可以和  Streaming  DataFrame 进行 Inner join.

这种join机制是通过追溯被join的 Streaming DataFrame 已经接收到的流数据和主动 join的 Streaming DataFrame的当前批次进行key的配对,为了避免追溯过去太久的数据造成性能瓶颈,可以通过设置 watermark 来清空过去太久的历史数据的State,数据被清空State后将允许不被配对查询。

schema = T.StructType().add("name","string").add("age","integer").add("score","double")
dfstudents = spark.readStream.schema(schema).json("./data/students_json")
dfstudents.printSchema() 

下面是Streaming DataFrame 和 Static DataFrame 进行 join的示范。

dfclasses = spark.createDataFrame([("LiLei","class1"),("Hanmeimei","class2"),("Lily","class3")]).toDF("name","class")
dfclasses.printSchema() 
# 示范 Streaming DataFrame  inner join Static DataFrame
dfjoin_inner = dfstudents.join(dfclasses, "name", "inner")
dfjoin_inner.printSchema()
print(dfjoin_inner.isStreaming)
root
 |-- name: string (nullable = true)
 |-- age: integer (nullable = true)
 |-- score: double (nullable = true)
 |-- class: string (nullable = true)

True
query = dfjoin_inner.writeStream \
    .outputMode("append")\
    .format("console") \
    .start()

time.sleep(10)
query.stop()

#query.awaitTermination()
-------------------------------------------
Batch: 0
-------------------------------------------
+---------+---+-----+------+
|     name|age|score| class|
+---------+---+-----+------+
|    LiLei| 12| 75.5|class1|
|Hanmeimei| 16| 90.0|class2|
|     Lily| 15| 68.0|class3|
+---------+---+-----+------+
# 示范 Streaming DataFrame  left join Static DataFrame
dfjoin_left = dfstudents.join(dfclasses, "name", "left")
dfjoin_left.printSchema()
print(dfjoin_left.isStreaming)

query = dfjoin_left.writeStream \
    .outputMode("append")\
    .format("console") \
    .start()

time.sleep(10)
query.stop()

#query.awaitTermination()
-------------------------------------------
Batch: 0
-------------------------------------------
+---------+---+-----+------+
|     name|age|score| class|
+---------+---+-----+------+
|    LiLei| 12| 75.5|class1|
|   Justin| 19| 87.0|  null|
|     Lily| 15| 68.0|class3|
|     Andy| 17| 80.0|  null|
|Hanmeimei| 16| 90.0|class2|
|  Michael| 20| 70.5|  null|
+---------+---+-----+------+

下面是一个简单的Streaming DataFrame inner join Streaming DataFrame示范。


dfhometown = dfstudents.selectExpr("name","if(rand()>0.5,'China','USA') as hometown")
dfhometown.printSchema()
print(dfhometown.isStreaming)

root
 |-- name: string (nullable = true)
 |-- hometown: string (nullable = false)

True
dfjoin_streaming = dfstudents.join(dfhometown,"name","inner")
dfjoin_streaming.printSchema()
print(dfjoin_streaming.isStreaming)
query = dfjoin_streaming.writeStream \
    .outputMode("append")\
    .format("console") \
    .start()

time.sleep(10)
query.stop()

#query.awaitTermination()
-------------------------------------------
Batch: 0
-------------------------------------------
+---------+---+-----+--------+
|     name|age|score|hometown|
+---------+---+-----+--------+
|    LiLei| 12| 75.5|     USA|
|   Justin| 19| 87.0|   China|
|     Lily| 15| 68.0|   China|
|Hanmeimei| 16| 90.0|   China|
|     Andy| 17| 80.0|     USA|
|  Michael| 20| 70.5|   China|
+---------+---+-----+--------+

五,输出 Structured Streaming 的结果

Streaming DataFrame 支持以下类型的结果输出:

  • Kafka Sink。将处理后的流数据输出到kafka某个或某些topic中。

  • File Sink。将处理后的流数据写入到文件系统中。

  • ForeachBatch Sink。对于每一个micro-batch的流数据处理后的结果,用户可以编写函数实现自定义处理逻辑。例如写入到多个文件中,或者写入到文件并打印。

  • Foreach Sink。一般在Continuous触发模式下使用,用户编写函数实现每一行的处理。

  • Console Sink。打印到Driver端控制台,如果日志量大,谨慎使用。一般供调试使用。

  • Memory Sink。输出到内存中,供调试使用。

1,输出到Kafka Sink

示范代码如下,注意,df 应当具备以下列:topic, key 和 value.

query = df.selectExpr("topic", "CAST(key AS STRING)", "CAST(value AS STRING)") \
  .writeStream()\
  .format("kafka")\
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")\
  .start()

2,输出到File Sink

schema = T.StructType().add("name","string").add("age","integer").add("score","double")
dfstudents = spark.readStream.schema(schema).json("./data/students_json")

query = dfstudents \
    .writeStream\
    .format("csv") \
    .option("checkpointLocation", "./data/checkpoint") \
    .option("path", "./data/students_csv") \
    .start()

time.sleep(5)
query.stop()

3, 输出到ForeachBatch Sink

对于每一个Batch,可以当做一个Static DataFrame 进行处理。

schema = T.StructType().add("name","string").add("age","integer").add("score","double")
dfstudents = spark.readStream.schema(schema).json("./data/students_json")

def foreach_batch_function(df, epoch_id):
    print("epoch_id = ",epoch_id)
    df.show()
    print("rows = ",df.count())
    
query = dfstudents.writeStream.foreachBatch(foreach_batch_function).start()  

time.sleep(3)
query.stop()
epoch_id =  0
+---------+---+-----+
|     name|age|score|
+---------+---+-----+
|    LiLei| 12| 75.5|
|Hanmeimei| 16| 90.0|
|     Lily| 15| 68.0|
|  Michael| 20| 70.5|
|     Andy| 17| 80.0|
|   Justin| 19| 87.0|
+---------+---+-----+

rows =  6

4, 输出到Console Sink

将结果输出到终端,对于jupyter 环境调试,可能需要在jupyter 的 log 日志中去查看。

schema = T.StructType().add("name","string").add("age","integer").add("score","double")
dfstudents = spark.readStream.schema(schema).json("./data/students_json")

dfstudents.writeStream \
  .format("console") \
  .trigger(processingTime='2 seconds') \
  .start()
-------------------------------------------
Batch: 0
-------------------------------------------
+---------+---+-----+
|     name|age|score|
+---------+---+-----+
|    LiLei| 12| 75.5|
|Hanmeimei| 16| 90.0|
|     Lily| 15| 68.0|
|  Michael| 20| 70.5|
|     Andy| 17| 80.0|
|   Justin| 19| 87.0|
+---------+---+-----+

5, 输出到Memory Sink

schema = T.StructType().add("name","string").add("age","integer").add("score","double")
dfstudents = spark.readStream.schema(schema).json("./data/students_json")


#设置的queryName 将成为需要查询的表的名称
query = dfstudents \
    .writeStream \
    .queryName("dfstudents") \
    .outputMode("append") \
    .format("memory") \
    .start()

time.sleep(3)
query.stop()

dfstudents_static = spark.sql("select * from dfstudents")
dfstudents_static.show() 


+---------+---+-----+
|     name|age|score|
+---------+---+-----+
|    LiLei| 12| 75.5|
|Hanmeimei| 16| 90.0|
|     Lily| 15| 68.0|
|  Michael| 20| 70.5|
|     Andy| 17| 80.0|
|   Justin| 19| 87.0|
+---------+---+-----+

如果本书对你有所帮助,想鼓励一下作者,记得给本项目加一颗星星star⭐️,并分享给你的朋友们喔????!

公众号后台回复关键词:pyspark,获取本项目github地址。

【秒懂StructuredStreaming】手把手教你写StructuredStreaming + Kafka程序 上一篇博客《StructuredStreaming是何方神圣》已经介绍了StructuredStreaming的相关概念,这篇博客我们介绍基于StructuredStreaming进行实时流算子开发,并将结果输出到kafka中。 StructuredStreaming使用的数据类型是DataFrame和Dataset。 从Spark 2.0开始,DataFra... 阅读详情

相关推荐

【Structured Streaming】简单介绍

一、Spark Streaming曲折发展史 Spark Streaming针对实时数据流,提供了一套可扩展、高吞吐、可容错的流式计算模型。Spark Streaming接收实时数据源的数据,切分成很多小的batches,然后被Spark Engine执行,产出同样由很多小的batchs组成的结果流。本质上,这是一种micro-batch(微批处理)的方式处理 不足在于处理延时较高(无法优...

依旧的博客 4750

Spark(37):Streaming DataFrame 和 Streaming DataSet 创建

使用 Structured Streaming 最重要的就是对 Streaming DataFrame 和 Streaming DataSet 进行各种操作。从 Spark2。0 开始, DataFrame 和 DataSet 可以表示静态有界的表, 也可以表示流式无界表。与静态 Datasets/DataFrames 类似,我们可以使用公共入口点 SparkSession 从流数据源创建流式 Datasets/DataFrames,并对它们应用与静态 Datasets/DataFrames 相同的操作。

yang_shibiao的博客 1366

Structured Streaming总结

文章目录写在前面关于介绍API主要学习读取Socket数据读取目录下文本数据整合Kafka整合环境准备代码实现整合MySQL代码实现 写在前面 晚上好我是啊晨 ,一个大数据分享者 今天说一下Spark的Structured Streaming,工作中据我了解不是很多,但也有使用。 毕竟是微批处理的,做标准实时的话,还要看flink。 如有需要flink文章请阅读我的其它大数据文章,谢谢 中间有什么问题请留言,请珍惜现在的时间: 关于介绍 这个建议大家看官网,写的非常详细,毕竟官网学习还是很有效率的。官

啊晨 975

智能点滴自动监控方法设计(源码+万字报告+实物)

Spark Structured Streaming 一、概述 http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html Structured Streaming构建在Spark SQL基础之上的一个可靠且容错的流数据处理引擎。 简短来说,Structured Streaming提供快速、可靠、容...

qq_40828705的博客 503

Structured Streaming基础--学习笔记

Structured的基本使用,编程模型,数据处理,以及数据的输出指定

yfq_29的博客 1302

Spark Structured Streaming 概论

对于 Source 与 Sink,你需要知道,在 Structured Streaming 框架下,Spark 都能提供哪些具体的支持。以 Source 为例,Spark 支持 Socket、File 和 Kafka,而对于 Sink,Spark 支持 Console、File、Kafka 和 Foreach(Batch)。对于结果的输出,我们需要了解,在不同的场景下,Structured Streaming 支持不同的输出模式。

2401_84052244的博客 4178

Spark Structured Streaming使用教程

Structured Streaming是一个基于Spark SQL引擎的可扩展和容错流处理引擎,Spark SQL引擎将负责增量和连续地运行它,并在流数据继续到达时更新最终结果。

penngo的专栏 1818

PySpark Structured Streaming kafka示例

PySpark Structured Streaming kafka示例 直接上代码: #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pyspark.sql import SparkSession from pyspark.sql.functions import from_json from pyspark.sql.types...

Widsom的博客 3265

Structured-Streaming初识

Structured Streaming是一个基于SparkSQL引擎构建的可扩展且容错的流处理引擎。可以像在静态数据上表达批量计算一样表达流计算。SparkSQL引擎将负责以增量方式连续运行它,并在流数据继续到达时更新最终结果。可以使用Scala、Java、Python或R中的Dataset/DataFrame API来进行流聚合、事件时间窗口、流到批处理连接等的开发。它基于SparkSQL引擎优化而执行。最后,系统通过 checkpoint 和预写日志确保端到端的精确一次容错保证。

lu070828的博客 991

Structured_Streaming和Kafka整合

结构化流与Kafka的整合

qq_50215015的博客 1618

大数据Spark:实时数据处理案例—基于Structured Streaming的实时数据ETL架构

通过准备数据源、设置环境、创建SparkSession、定义数据模式、读取实时数据、进行数据转换和输出结果,我们可以实现对实时数据的提取、转换和加载操作。接下来,我们需要设置Spark和相关依赖的环境。在这个示例中,我们首先将数据的"value"列转换为字符串类型,然后使用"split"函数将其拆分为"id"、"name"和"age"列,并将它们转换为相应的类型。以上是本文的详细描述和示例代码,希望对您理解大数据Spark中的实时数据ETL架构有对您理解大数据Spark中的实时数据ETL架构有所帮助。

code_welike的博客 380

初识Spark Structured Streaming结构化流

http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html官网 Overview概述 Structured Streaming is a scalable and fault-tolerant stream processing engine built on the Spark SQL engine. 结构化流是一个基于Spark SQL可扩展以及容错的流式处理引擎 You can express yo.

゛Never give up~ 645

sqoop抽取mysql的数据到hive表里面,字段内容被自动截取,导致字段不全的问题解决

今天央视重新转播NBA,写篇博客纪念一下这个重要的日子,湖人的表现就呵呵了!!!! 强行转到博客的主题来 mysql的字段属性:该字段能存下的值比如:“山东” `province` varchar(3) NOT NULL COMMENT '省份' 用sqoop sqoop import --connect "jdbc:mysql://xxxxxx:3306/xxxxxx?tinyInt1isBit=false&zeroDateTimeBehavior=convertToNull..

放肆桀骜 966

Structured Streaming详解

目录一、Structured Streaming概述(1)Structured Streaming背景(2)Structured Streaming概念二、Structured Streaming编程模型(1)编程模型(2)EventTime(3)容错语义三、基于WordCount程序讲解Structured Streaming编程模型 一、Structured Streaming概述 (1)Structured Streaming背景 大多数的流式计算引擎(比如storm、spark streaming等

蔡政洁的博客 8999

【硬件】通俗易懂的讲解晶体管(三极管和MOS管)的工作原理

晶体管是一个简单的元器件,可用于构建许多有趣的项目。在本文中,我将用通俗易懂的语言给您讲解晶体管的工作原理,以便您可以在电路设计中更好的使用静态管。一旦你学习这些基础知识,对以后的设计和使用来说,将会变得非常容易。我们将重点介绍两种最常见的晶体管:双极型晶体管(三极管)和MOSFET。晶体管的工作原理其实是类似于电子开关。它可以打开和关闭电路。一个简单的思考方法是将晶体管视为无源的继电器。晶体管类似于继电器,从某种意义上说,您可以使用它来打开和关闭某些东西。但晶体管也可以部分导通,一般在放大电路中使用,这部

1万+

学术版GPT安装教程——保姆版(附带原生GPT注册教程)

学术版GPT图文安装教程

DBenD的博客 2万+

Structured Streaming 和 Spark streaming的区别

我先简单的说一下Spark streaming,然后在想对比的说一下Structured Streaming Spark streaming的概述 Spark Streaming是一个基于Spark Core之上的实时计算框架,可以从很多数据源消费数据并对数据进行实时的处理 Spark Streaming类似于Apache Storm,用于流式数据的处理。 根据其官方文档介绍...

MannerXu的博客 5998

【推荐100个unity插件之8】实现多人在线联机游戏——Mirror插件的使用介绍(附项目源码)

基于Mirror的Unity多人游戏开发指南 本文介绍了使用Mirror插件开发Unity多人游戏的基本流程。Mirror是Unity的高级网络API,替代了已废弃的UNet系统。文章详细讲解了如何导入Mirror插件,创建NetworkManager网络管理器,处理常见错误,以及创建玩家角色和出生点。 主要内容包括: 创建场景NetworkManager并配置必要组件 为玩家添加NetworkIdentity和NetworkTransform组件 设置玩家初始生成位置和生成方式 常见错误及解决方法 通过M

向宇的博客,专注php/web全栈 unity游戏开发,欢迎大家评论纠错 1万+

spark:Structured Streaming介绍

spark:Structured Streaming介绍

m0_70882914的博客 1685

Android Shadow插件化框架分析与集成(一)

到此为止,本篇文章先介绍到这里,本篇文章主要介绍了:从源码的角度去集成shadow框架,从下载源码开始,到集成到自己的项目中,其中插件项目我是新建的demo应用,宿主应用是之前搭的一个MVP框架。文章中一步一步详细的介绍了宿主该如何集成,并提供了源码;介绍了插件该如何集成,同时也提供了源码。文章中还介绍了gradle的版本与AGP版本使用不当造成的问题,以及shadow源码的gradle版本与APG版本的配置,及和宿主插件应用gradle版本与AGP版本的关系。

superzhang6666的博客 1235

晶闸管选型参数,结构原理,工艺与注意问题总结

闸管(Thyristor)是一种半导体开关器件,也被称为可控硅整流器(SCR, Silicon Controlled Rectifier)。它可以在高电压和大电流条件下工作,常被用于交流电的控制和整流。本文对晶闸管的工作原理,结构特点,工艺流程,选型参数及设计注意事项进行总结。

fydar的博客 1286

一篇吃饱 Structured Streaming

目录 Structured Streaming曲折发展史 Spark Streaming Structured Streaming Structured Streaming实战 (三种方式 WorldCount) Socket source方式 Json source方式 Kafka source方式 Structured Streaming...

最慢的步伐不是跬步,而是徘徊;最快的脚步不是冲刺,而是坚持 8681

SAP 联产品测试

最近领导一直要求研究一下SAP联产品的使用场景,之前只是自己摸索过一次,这个重新对联产品进行一个测试。 1、首先准备好物料主数据 注意主数据中需要勾选联产品标识。 设置好物料的分摊的百分比。 2、检查BOM 在BOM中也需要维护联产品的标识 只有当组件数量未负数时,才能勾选联产品的标识。如果组件数量正值,勾选联产品的标识时会报错。 3、我们创建生产订单 可以看到按正常的生产订单下达时,系统就报错了,问题点在于离散制造的生产订单基本是结算到物料的。但是联产品是按订单结算的, 所以需要对订单类型中的结算参

我小时候很黑的博客 2116

大数据系列之Spark Streaming和Structured Streaming对比

本文对Spark Streaming和Structured Streaming在流模型、API使用、时延性能以及和Kafka对接等方面进行了对比

牧羊人的方向 5049

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

Structured Streaming && Spark Streaming

第一件事儿先贴官方文档,由此可见,英文的重要性啊,哈哈... structured streaming :http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html spark Streaming :http://spark.apache.org/docs/latest/streamin...

阿尼古 1422

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

上一篇: 探索MLlib机器学习
下一篇: 使用pytorch-lightning漂亮地进行深度学习研究
算法美食屋
博客等级 码龄9年 2万+粉丝 154原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值