Structured Streaming基础入门

Structured Streaming

1. 回顾和展望

1.1. Spark 编程模型的进化过程

在这里插入图片描述

RDD
rdd.flatMap(_.split(" "))
   .map((_, 1))
   .reduceByKey(_ + _)
   .collect

针对自定义数据对象进行处理, 可以处理任意类型的对象, 比较符合面向对象

RDD 无法感知到数据的结构, 无法针对数据结构进行编程

DataFrame
spark.read
     .csv("...")
     .where('name =!= "")
     .groupBy($"name")
     .show()

DataFrame 保留有数据的元信息, API 针对数据的结构进行处理, 例如说可以根据数据的某一列进行排序或者分组

DataFrame 在执行的时候会经过 Catalyst 进行优化, 并且序列化更加高效, 性能会更好

DataFrame 只能处理结构化的数据, 无法处理非结构化的数据, 因为 DataFrame 的内部使用 Row 对象保存数据

SparkDataFrame 设计了新的数据读写框架, 更加强大, 支持的数据源众多

Dataset
spark.read
     .csv("...")
     .as[Person]
     .where(_.name != "")
     .groupByKey(_.name)
     .count()
     .show()

Dataset 结合了 RDDDataFrame 的特点, 从 API 上即可以处理结构化数据, 也可以处理非结构化数据

DatasetDataFrame 其实是一个东西, 所以 DataFrame 的性能优势, 在 Dataset 上也有

总结

RDD 的优点

  1. 面向对象的操作方式
  2. 可以处理任何类型的数据

RDD 的缺点

  1. 运行速度比较慢, 执行过程没有优化
  2. API 比较僵硬, 对结构化数据的访问和操作没有优化

DataFrame 的优点

  1. 针对结构化数据高度优化, 可以通过列名访问和转换数据
  2. 增加 Catalyst 优化器, 执行过程是优化的, 避免了因为开发者的原因影响效率

DataFrame 的缺点

  1. 只能操作结构化数据
  2. 只有无类型的 API, 也就是只能针对列和 SQL 操作数据, API 依然僵硬

Dataset 的优点

  1. 结合了 RDDDataFrameAPI, 既可以操作结构化数据, 也可以操作非结构化数据
  2. 既有有类型的 API 也有无类型的 API, 灵活选择

1.2. Spark 的 序列化 的进化过程

什么是序列化和序列化

Java 中, 序列化的代码大概如下

public class JavaSerializable implements Serializable {
  NonSerializable ns = new NonSerializable();
}

public class NonSerializable {

}

public static void main(String[] args) throws IOException {
  // 序列化
  JavaSerializable serializable = new JavaSerializable();
  ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("/tmp/obj.ser"));
  // 这里会抛出一个 "java.io.NotSerializableException: cn.itcast.NonSerializable" 异常
  objectOutputStream.writeObject(serializable);
  objectOutputStream.flush();
  objectOutputStream.close();

  // 反序列化
  FileInputStream fileInputStream = new FileInputStream("/tmp/obj.ser");
  ObjectInputStream objectOutputStream = new ObjectInputStream(fileInputStream);
  JavaSerializable serializable1 = objectOutputStream.readObject();
}

序列化的作用就是可以将对象的内容变成二进制, 存入文件中保存

反序列化指的是将保存下来的二进制对象数据恢复成对象

序列化对对象的要求
  • 对象必须实现 Serializable 接口
  • 对象中的所有属性必须都要可以被序列化, 如果出现无法被序列化的属性, 则序列化失败
限制
  • 对象被序列化后, 生成的二进制文件中, 包含了很多环境信息, 如对象头, 对象中的属性字段等, 所以内容相对较大
  • 因为数据量大, 所以序列化和反序列化的过程比较慢
序列化的应用场景
  • 持久化对象数据
  • 网络中不能传输 Java 对象, 只能将其序列化后传输二进制数据
在 Spark 中的序列化和反序列化的应用场景
Task分发

在这里插入图片描述
Task 是一个对象, 想在网络中传输对象就必须要先序列化

RDD 缓存
val rdd1 = rdd.flatMap(_.split(" "))
   .map((_, 1))
   .reduceByKey(_ + _)

rdd1.cache

RDD 中处理的是对象, 例如说字符串, Person 对象等

如果缓存 RDD 中的数据, 就需要缓存这些对象

对象是不能存在文件中的, 必须要将对象序列化后, 将二进制数据存入文件

广播变量

在这里插入图片描述
广播变量会分发到不同的机器上, 这个过程中需要使用网络, 对象在网络中传输就必须先被序列化

Shuffle 过程

在这里插入图片描述
Shuffle 过程是由 ReducerMapper 中拉取数据, 这里面涉及到两个需要序列化对象的原因

  • RDD 中的数据对象需要在 Mapper 端落盘缓存, 等待拉取
  • MapperReducer 要传输数据对象
Spark Streaming 的 Receiver

在这里插入图片描述
Spark Streaming 中获取数据的组件叫做 Receiver, 获取到的数据也是对象形式, 在获取到以后需要落盘暂存, 就需要对数据对象进行序列化

算子引用外部对象
class Unserializable(i: Int)

rdd.map(i => new Unserializable(i))
   .collect
   .foreach(println)

map 算子的函数中, 传入了一个 Unserializable 的对象

map 算子的函数是会在整个集群中运行的, 那 Unserializable 对象就需要跟随 map 算子的函数被传输到不同的节点上,如果 Unserializable 不能被序列化, 则会报错

RDD 的序列化

在这里插入图片描述
RDD 的序列化只能使用 Java 序列化器, 或者 Kryo 序列化器

  • RDD 中存放的是数据对象, 要保留所有的数据就必须要对对象的元信息进行保存, 例如对象头之类的
  • 保存一整个对象, 内存占用和效率会比较低一些

Kryo 是什么

  • KryoSpark 引入的一个外部的序列化工具, 可以增快 RDD 的运行速度
  • 因为 Kryo 序列化后的对象更小, 序列化和反序列化的速度非常快
  • RDD 中使用 Kryo 的过程如下
val conf = new SparkConf()
  .setMaster("local[6]")
  .setAppName("KyroTest")

conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
conf.registerKryoClasses(Array(classOf[Person]))

val sc = new SparkContext(conf)
...
rdd.map(arr => Person(arr(0), arr(1), arr(2)))
DataFrame 和 Dataset 中的序列化

DataFrameDataset 是为结构化数据优化的

DataFrameDataset 中, 数据和数据的 Schema 是分开存储的

spark.read
     .csv("...")
     .where($"name" =!= "")
     .groupBy($"name")
     .map(row: Row => row)
     .show()

DataFrame 中没有数据对象这个概念, 所有的数据都以行的形式存在于 Row 对象中, Row 中记录了每行数据的结构, 包括列名, 类型等
在这里插入图片描述
Dataset 中上层可以提供有类型的 API, 用以操作数据, 但是在内部, 无论是什么类型的数据对象 Dataset 都使用一个叫做 InternalRow 的类型的对象存储数据

val dataset: Dataset[Person] = spark.read.csv(...).as[Person]
元信息独立

RDD 不保存数据的元信息, 所以只能使用 Java Serializer 或者 Kyro Serializer 保存 整个对象

DataFrameDataset 中保存了数据的元信息, 所以可以把元信息独立出来分开保存
在这里插入图片描述
一个 DataFrame 或者一个 Dataset 中, 元信息只需要保存一份, 序列化的时候, 元信息不需要参与
在这里插入图片描述
在反序列化 ( InternalRow → Object ) 时加入 Schema 信息即可
在这里插入图片描述
元信息不再参与序列化, 意味着数据存储量的减少, 和效率的增加

使用堆外内存

DataFrameDataset 不再序列化元信息, 所以内存使用大大减少. 同时新的序列化方式还将数据存入堆外内存中, 从而避免 GC 的开销.

堆外内存又叫做 Unsafe, 之所以叫不安全的, 因为不能使用 Java 的垃圾回收机制, 需要自己负责对象的创建和回收, 性能很好, 但是不建议普通开发者使用, 毕竟不安全

总结
  1. 当需要将对象缓存下来的时候, 或者在网络中传输的时候, 要把对象转成二进制, 在使用的时候再将二进制转为对象, 这个过程叫做序列化和反序列化
  2. Spark 中有很多场景需要存储对象, 或者在网络中传输对象
    1. Task 分发的时候, 需要将任务序列化, 分发到不同的 Executor 中执行
    2. 缓存 RDD 的时候, 需要保存 RDD 中的数据
    3. 广播变量的时候, 需要将变量序列化, 在集群中广播
    4. RDDShuffle 过程中 MapReducer 之间需要交换数据
    5. 算子中如果引入了外部的变量, 这个外部的变量也需要被序列化
  3. RDD 因为不保留数据的元信息, 所以必须要序列化整个对象, 常见的方式是 Java 的序列化器, 和 Kyro 序列化器
  4. DatasetDataFrame 中保留数据的元信息, 所以可以不再使用 Java 的序列化器和 Kyro 序列化器, 使用 Spark 特有的序列化协议, 生成 UnsafeInternalRow 用以保存数据, 这样不仅能减少数据量, 也能减少序列化和反序列化的开销, 其速度大概能达到 RDD 的序列化的 20 倍左右

1.3. Spark Streaming 和 Structured Streaming

Spark Streaming 时代

在这里插入图片描述
Spark Streaming 其实就是 RDDAPI 的流式工具, 其本质还是 RDD, 存储和执行过程依然类似 RDD

Structured Streaming 时代

在这里插入图片描述
Structured Streaming 其实就是 DatasetAPI 的流式工具, APIDataset 保持高度一致

Structured Streaming 相比于 Spark Streaming 的进步就类似于 Dataset 相比于 RDD 的进步

另外还有一点, Structured Streaming 已经支持了连续流模型, 也就是类似于 Flink 那样的实时流, 而不是小批量, 但在使用的时候仍然有限制, 大部分情况还是应该采用小批量模式

2.2.0 以后 Structured Streaming 被标注为稳定版本, 意味着以后的 Spark 流式开发不应该在采用 Spark Streaming

2. Structured Streaming 入门案例

2.1. 需求梳理

需求

在这里插入图片描述
编写一个流式计算的应用, 不断的接收外部系统的消息

对消息中的单词进行词频统计

统计全局的结果

整体结构

在这里插入图片描述

  1. Socket Server 等待 Structured Streaming 程序连接
  2. Structured Streaming 程序启动, 连接 Socket Server, 等待 Socket Server 发送数据
  3. Socket Server 发送数据, Structured Streaming 程序接收数据
  4. Structured Streaming 程序接收到数据后处理数据
  5. 数据处理后, 生成对应的结果集, 在控制台打印
开发方式和步骤

Socket server 使用 Netcat nc 来实现

Structured Streaming 程序使用 IDEA 实现, 在 IDEA 中本地运行

  1. 编写代码
  2. 启动 nc 发送 Socket 消息
  3. 运行代码接收 Socket 消息统计词频

2.2. 代码实现

object SocketProcessor {

  def main(args: Array[String]): Unit = {

    // 1. 创建 SparkSession
    val spark = SparkSession.builder()
      .master("local[6]")
      .appName("socket_processor")
      .getOrCreate()

    spark.sparkContext.setLogLevel("WARN")   

    import spark.implicits._

    // 2. 读取外部数据源, 并转为 Dataset[String]
    val source = spark.readStream
      .format("socket")
      .option("host
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值