导读
1,流式计算的场景
2,流式计算框架
3,Spark Streaming 的特点
通过对现阶段一些常见的需求进行整理, 我们要问自己一个问题, 这些需求如何解决?
商品推荐:
京东和淘宝这样的商城在购物车, 商品详情等地方都有商品推荐的模块
工业大数据:
现在的工场中, 设备是可以联网的, 汇报自己的运行状态, 在应用层可以针对这些数据来分析运行状况和稳健程度, 展示工件完成情况, 运行情况等
监控:
一般的大型集群和平台, 都需要对其进行监控
监控的需求
这样的需求, 可以通过传统的批处理来完成吗?
流计算
流和批的架构组合
流和批都是有意义的, 有自己的应用场景, 那么如何结合流和批呢? 如何在同一个系统中使用这两种不同的解决方案呢?
混合架构
混合架构的名字叫做 Lambda 架构, 混合架构最大的特点就是将流式计算和批处理结合起来
后在进行查询的时候分别查询流系统和批系统, 最后将结果合并在一起
优点
缺点
流式架构
优点
问题
关于架构的问题, 很多时候往往是无解的, 在合适的地方使用合适的架构, 在项目课程中, 还会进行更细致的讨论
Spark Streaming 的特点
特点 | 说明 |
---|---|
Spark Streaming 是 Spark Core API 的扩展 | Spark Streaming 具有类似 RDD 的 API, 易于使用, 并可和现有系统共用相似代码一个非常重要的特点是, Spark Streaming 可以在流上使用基于 Spark 的机器学习和流计算, 是一个一站式的平台 |
Spark Streaming 具有很好的整合性 | Spark Streaming 可以从 Kafka, Flume, TCP 等流和队列中获取数据,Spark Streaming 可以将处理过的数据写入文件系统, 常见数据库中 |
Spark Streaming 是微批次处理模型 | 微批次处理的方式不会有长时间运行的 Operator, 所以更易于容错设计,微批次模型能够避免运行过慢的服务, 实行推测执行 |
导读
1,环境准备
2,工程搭建
3,代码编写
4,总结
Netcat 的使用
Step 1: Socket 回顾
Socket 是 Java 中为了支持基于 TCP / UDP 协议的通信所提供的编程模型
Socket 分为 Socket server 和 Socket client
TCP 三次握手建立连接
Step 1
Client 向 Server 发送 SYN(j), 进入 SYN_SEND 状态等待 Server 响应
Step 2
Server 收到 Client 的 SYN(j) 并发送确认包 ACK(j + 1), 同时自己也发送一个请求连接的 SYN(k) 给 Client, 进入 SYN_RECV 状态等待 Client 确认
Step 3
Client 收到 Server 的 ACK + SYN, 向 Server 发送连接确认 ACK(k + 1), 此时, Client 和 Server 都进入 ESTABLISHED 状态, 准备数据发送
Step 2: Netcat
Netcat 是一个非常常见的 Socket 工具, 可以使用 nc 建立 Socket server 也可以建立 Socket client
nc -l 建立 Socket server, l 是 listen 监听的意思
nc host port 建立 Socket client, 并连接到某个 Socket server
创建工程
目标:
使用 Spark Streaming 程序和 Socket server 进行交互, 从 Server 处获取实时传输过来的字符串, 拆开单词并统计单词数量, 最后打印出来每一个小批次的单词数量
创建 IDEA Maven 工程, 步骤省略, 参考 Spark 第一天工程建立方式
导入 Maven 依赖, 省略, 参考 Step 2
创建 main/scala 文件夹和 test/scala 文件夹
创建包 cn.itcast.streaming
创建对象 StreamingWordCount
Step 2: Maven 依赖
如果使用 Spark Streaming, 需要使用如下 Spark 的依赖
Spark Core: Spark 的核心包, 因为 Spark Streaming 要用到
Spark Streaming
Step 3: 编码
object StreamingWordCount {
def main(args: Array[String]): Unit = {
if (args.length < 2) {
System.err.println("Usage: NetworkWordCount " )
System.exit(1)
}
val sparkConf = new SparkConf().setAppName("NetworkWordCount")
val ssc = new StreamingContext(sparkConf, Seconds(1)) //1
val lines = ssc.socketTextStream( //2
hostname = args(0),
port = args(1).toInt,
storageLevel = StorageLevel.MEMORY_AND_DISK_SER) //3
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print() //4
ssc.start() //5
ssc.awaitTermination() //6
}
}
Step 4: 部署和上线
使用 Maven 命令 package 打包
在 node02 上使用 nc 开启一个 Socket server, 接受 Streaming 程序的连接请求, 从而建立连接发送消息给 Streaming 程序实时处理
nc -lk 9999
在 node01 执行如下命令运行程序
spark-submit --class cn.itcast.streaming.StreamingWordCount --master local[6] original-streaming-0.0.1.jar node02 9999
Step 5: 总结和知识落地
注意点
Spark Streaming 并不是真正的来一条数据处理一条
Spark Streaming 的处理机制叫做小批量, 英文叫做 mini-batch, 是收集了一定时间的数据后生成 RDD, 后针对 RDD 进行各种转换操作, 这个原理提现在如下两个地方
Spark Streaming 中至少要有两个线程
在使用 spark-submit 启动程序的时候, 不能指定一个线程
创建 StreamingContext
val conf = new SparkConf().setAppName(appName).setMaster(master)
val ssc = new StreamingContext(conf, Seconds(1))
各种算子
算子 | 释义 |
---|---|
flatMap | lines.flatMap(_.split(" ")) 将一个数据一对多的转换为另外的形式, 规则通过传入函数指定 |
map | words.map(x => (x, 1)) 一对一的转换数据 |
reduceByKey | words.reduceByKey(_ + _) 这个算子需要特别注意, 这个聚合并不是针对于整个流, 而是针对于某个批次的数据 |
1,总章
2,静态 DAG
3,动态切分
4,数据流入
5,容错机制
总章
Spark Streaming 的特点
Spark Streaming 是按照时间切分小批量
可以理解为 DStream 是一个管道, 数据源源不断的从这个管道进去, 被处理, 再出去
但是需要注意的是, DStream 并不是严格意义上的实时流, 事实上, DStream 并不处理数据, 而是处理 RDD
所以针对以上的解读, 可能会产生一种疑惑
如何处理数据?
如下代码
val lines: DStream[String] = ssc.socketTextStream(
hostname = args(0),
port = args(1).toInt,
storageLevel = StorageLevel.MEMORY_AND_DISK_SER)
val words: DStream[String] = lines
.flatMap(_.split(" "))
.map(x => (x, 1))
.reduceByKey(_ + _)
可以看到
有一个疑惑
Spark Streaming 是流计算, 流计算的数据是无限的
什么系统可以产生无限的数据?
无限的数据一般指的是数据不断的产生, 比如说运行中的系统, 无法判定什么时候公司会倒闭, 所以也无法断定数据什么时候会不再产生数据
那就会产生一个问题
如何不简单的读取数据, 如何应对数据量时大时小?
如何数据是无限的, 意味着可能要一直运行下去
那就会又产生一个问题
Spark Streaming 不会出错吗? 数据出错了怎么办?
总结
总结下来, 有四个问题
DAG 的定义
RDD 和 DStream 的 DAG
如果是 RDD 的 WordCount, 代码大致如下
val textRDD = sc.textFile(...)
val splitRDD = textRDD.flatMap(_.split(" "))
val tupleRDD = splitRDD.map((_, 1))
val reduceRDD = tupleRDD.reduceByKey(_ + _)
用图形表示如下
val lines: DStream[String] = ssc.socketTextStream(...)
val words: DStream[String] = lines.flatMap(_.split(" "))
val wordCounts: DStream[(String, Int)] = words.map(x => (x, 1)).reduceByKey(_ + _)
同理, DStream 也可以形成 DAG 如下
RDD 和 DStream 的区别
DStream 如何形式 DAG
切分流, 生成小批量
静态和动态
根据前面的学习, 可以总结一下规律
但是回顾前面的内容, RDD 的运行分为逻辑计划和物理计划
但是在 DStream 中则不能这么简单的划分, 因为 DStream 中有一个非常重要的逻辑, 需要按照时间片划分小批量
上述两点, 其实描述的是静态的一张 DAG, 数据处理过程, 但是 Streaming 是动态的, 数据是源源不断的来的
所以, 在 DStream 中, 静态和动态是两个概念, 有不同的流程
动态生成 RDD DAG 的过程
RDD DAG 的生成是按照时间来切片的, Streaming 会维护一个 Timer, 固定的时间到达后通过如下五个步骤生成一个 RDD DAG 后调度执行
数据的产生和导入
Receiver
在 Spark Streaming 中一个非常大的挑战是, 很多外部的队列和存储系统都是分块的, RDD 是分区的, 在读取外部数据源的时候, 会用不同的分区对照外部系统的分片, 例如
答案是不行, 因为需要一套单独的机制来保证并行的读取外部数据源, 这套机制叫做 Receiver
Receiver 的结构
为了保证并行获取数据, 对应每一个外部数据源的分区, 所以 Receiver 也要是分布式的, 主要分为三个部分
Receiver 的执行过程
容错
因为要非常长时间的运行, 对于任何一个流计算系统来说, 容错都是非常致命也非常重要的一环, 在 Spark Streaming 中, 大致提供了如下的容错手段
热备
还记得这行代码吗
这行代码中的 StorageLevel.MEMORY_AND_DISK_SER 的作用是什么? 其实就是热备份
当 Receiver 获取到数据要存储的时候, 是交给 BlockManager 存储的
如果设置了 StorageLevel.MEMORY_AND_DISK_SER, 则意味着 BlockManager 不仅会在本机存储, 也会发往其它的主机进行存储, 本质就是冗余备份
如果某一个计算失败了, 通过冗余的备份, 再次进行计算即可
冷备
冷备在 Spark Streaming 中的手段叫做 WAL (预写日志)
重放
导读
这一小节主要目的是为了了解 Spark Streaming 一些特别特殊和重要的操作, 一些基本操作基本类似 RDD
updateStateByKey
需求: 统计整个流中, 所有出现的单词数量, 而不是一个批中的数量
统计总数
入门案例中, 只能统计某个时间段内的单词数量, 因为 reduceByKey 只能作用于某一个 RDD, 不能作用于整个流
如果想要求单词总数该怎么办?
状态
可以使用状态来记录中间结果, 从而每次来一批数据, 计算后和中间状态求和, 于是就完成了总数的统计
val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[6]")
val sc = new SparkContext(sparkConf)
sc.setLogLevel("ERROR")
val ssc = new StreamingContext(sc, Seconds(1))
val lines: DStream[String] = ssc.socketTextStream(
hostname = "localhost",
port = "9999".toInt,
storageLevel = StorageLevel.MEMORY_AND_DISK_SER)
val words = lines.flatMap(_.split(" ")).map(x => (x, 1))
// 使用 updateStateByKey 必须设置 Checkpoint 目录
ssc.checkpoint("checkpoint")
// updateStateByKey 的函数
def updateFunc(newValue: Seq[Int], runningValue: Option[Int]) = {
// newValue 之所以是一个 Seq, 是因为它是某一个 Batch 的某个 Key 的全部 Value
val currentBatchSum = newValue.sum
val state = runningValue.getOrElse(0)
// 返回的这个 Some(count) 会再次进入 Checkpoint 中当作状态存储
Some(currentBatchSum + state)
}
// 调用
val wordCounts = words.updateStateByKey[Int](updateFunc)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
window 操作
需求: 计算过 30s 的单词总数, 每 10s 更新一次
实现
val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[6]")
val sc = new SparkContext(sparkConf)
sc.setLogLevel("ERROR")
val ssc = new StreamingContext(sc, Seconds(1))
val lines: DStream[String] = ssc.socketTextStream(
hostname = "localhost",
port = 9999,
storageLevel = StorageLevel.MEMORY_AND_DISK_SER)
val words = lines.flatMap(_.split(" ")).map(x => (x, 1))
// 通过 window 操作, 会将流分为多个窗口
val wordsWindow = words.window(Seconds(30), Seconds(10))
// 此时是针对于窗口求聚合
val wordCounts = wordsWindow.reduceByKey((newValue, runningValue) => newValue + runningValue)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
既然 window 操作经常配合 reduce 这种聚合, 所以 Spark Streaming 提供了较为方便的方法
val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[6]")
val sc = new SparkContext(sparkConf)
sc.setLogLevel("ERROR")
val ssc = new StreamingContext(sc, Seconds(1))
val lines: DStream[String] = ssc.socketTextStream(
hostname = "localhost",
port = 9999,
storageLevel = StorageLevel.MEMORY_AND_DISK_SER)
val words = lines.flatMap(_.split(" ")).map(x => (x, 1))
// 开启窗口并自动进行 reduceByKey 的聚合
val wordCounts = words.reduceByKeyAndWindow(
reduceFunc = (n, r) => n + r,
windowDuration = Seconds(30),
slideDuration = Seconds(10))
wordCounts.print()
ssc.start()
ssc.awaitTermination()
窗口时间
滑动时间的问题
但是其实无论谁比谁大, 都不能算错, 例如, 我的需求有可能就是统计一小时内的数据, 一天刷新两次