前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >学习笔记:StructuredStreaming入门(十二)

学习笔记:StructuredStreaming入门(十二)

作者头像
Maynor
发布2022-11-30 13:55:59
1.7K0
发布2022-11-30 13:55:59
举报

Spark Day12:Structured Streaming

image-20210506095958240
image-20210506095958240

01-[了解]-上次课程内容回顾

​ 主要讲解SparkStreaming如何企业开发:集成Kafka、三大应用场景(实时增量ETL、状态累加统计、窗口分析统计)。

代码语言:javascript
复制
1、集成Kafka
	由于Kafka框架提供2套Consumer API,所以集成Kafka时,也提供2套API,但是推荐使用New Consumer API
		- KafkaConsumer
		- ConsumerRecord<Key, Value>,都是String类型
	http://spark.apache.org/docs/2.2.0/streaming-kafka-0-10-integration.html
		GAV:org.apache.spark:spark-streaming-kafka-0-10_2.11:2.4.5
	方法:
		val kafkaDStream: DStream[String, String] = KafkaUtils.createDirectStream
		直接从Kafka消费数据获取数据流中,每批次RDD是KafkaRDD
	原理:
		每批次BatchInterval时间间隔,依据偏移量范围到Kafka Topic中各个分区获取相应范围数据
image-20210506145535122
image-20210506145535122

从Kafka消费数据时,属性设置:"enable.auto.commit" -> (false: java.lang.Boolean) Kafka New Consumer API,默认情况下,提供一种机制,从Kafka Topic消费数据以后,可以定时异步或同步将消费偏移量信息存储到:__consumer__offsets,当设置属性为false时,表示不需要提交保存偏移量

代码语言:javascript
复制
从Kafka消费数据时,不仅可以指定某个Topic获取或某些Topic,而且还有指定正则表达式,很方便消费多个Topic
image-20210506150255502
image-20210506150255502

SparkStreaming流式计算模块,在实际项目中有3大应用场景:主要如下所示

代码语言:javascript
复制
2、实时增量ETL,【实际项目中,此种应用类型最多】
	实时将海量业务数据,进行实时ETL转换,存储到外部存储引擎,以便系统进行分析处理
		业务数据一产生发送到 Kafka Topic -> 流式应用程序:ETL转换 -> HBase/ES  
	使用2个函数:
    	transform转换函数,针对每批次RDD进行转换处理,返回还是RDD
    	foreachRDD输出函数,针对每批次RDD进行输出,返回值为Unit
   	输出函数模式:
   		dstream.forearchRDD((rdd, batchTime) => {
   			// 每批次RDD针对每个分区数据进行操作,适当考虑是否降低分区数目
   			rdd.coalease(1).forearchPartition{iter =>
   				// 从连接池中获取连接
   				val conn: Connection 
   				
   				// 将每个分区数据进行保存,考虑批量保存
   				iter.foreach{item =>
   				
   				}
   				
   				// 将连接放回连接池中
   				conn.release()
   			}
   		})

3、状态累加统计
	实时对数据进行聚合操作,并且状态属于累加统计的,比如双11大屏计算销售额
		updateStateByKey 函数
			依据Key更新状态的,需要定义状态更新函数,表示如何更新状态
			updateFunc:
				(values: Seq[V], state: Option[S]) => Option[S]
					values: 表示当前批次中Key对应的所有Value的值
					state:表示当前Key以前的状态,如果没有状态就是None
		mapWithState 函数
			依据Key更新状态,当Key存在时,才更新状态,否则不更新,性能远远由于updateStateByKey
			StateSpec对象
				StateSpec.function函数创建实例,传递map映射函数,针对每条数据进行状态更新处理
					(KeyType, Option[ValueType], State[StateType]) => MappedType
					Key类型		Value值数据类型       状态数据类型         返回数据类型
	保存以前状态State,所以设置Checkpoint检查的目录,存储State数据
		ssc.checkpoint("datas/streaming-ckpt-999999")
		

4、窗口分析统计
	描述需求:每隔多长时间,统计最近多久范围内数据情况(趋势统计)
		比如每隔1分钟统计最近20分钟内数据情况
	窗口统计:
		window size = 20 分钟
		slide size = 1 分钟
	分为2种类型窗口:
		当window size = slide size : 滚动窗口,数据不会被重复处理
		当window sieze > slide size : 滑动窗口,数据会被重复处理
	函数:
    	window函数,设置窗口大小和滑动大小
    	将聚合函数和窗口函数合在一起:
    		reduceByKeyAndWindow
    			窗口大小和滑动大小,还需要设置聚合函数

02-[了解]-今日课程内容提纲

2个方面内容:偏移量管理(Checkpoint检查点)和StructuredStreaming入门(新的流式计算模块)

代码语言:javascript
复制
1、偏移量管理
	SparkStreaming从Kafka消费数据时,如何管理偏移量,实现实时流式应用容灾恢复
	方式一:
		Checkpoint检查点恢复偏移量,继续消费数据
	方式二:
		用户手动管理偏移量,进行存储和读取,续集消费数据
		推荐此种方式,相当来说比较麻烦,了解思路即可
【此部分内容,属于SparkStreaming模块处理流式数据一个不足之处,一大软点,使得用户不喜欢框架】


2、StructuredStreaming 快速入门
	数据结构:DataFrame/Dataset,流式数据集
	- 2.x提出结构化流模块处理流式数据
		SparkStreaming不足之处
		StructuredStreaming 设计原理和编程模型
	- 入门案例:词频统计WordCount
		实时累加统计
		代码就是SparkSQL词频统计代码(DSL和SQL)
	- 内置数据源,了解即可,几乎项目不用
	- StructuredStreaming应用程序基本设置

03-[理解]-偏移量管理之引例和概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6wtQxLP6-1626354186973)(/img/image-20210506154426999.png)]

  • 方式一:Checkpoint 恢复
image-20210506154756236
image-20210506154756236

  • 方式二:手动管理偏移量和加载状态
image-20210506154813079
image-20210506154813079
代码语言:javascript
复制
程序中指定加载上一次状态信息,继续运行累加计算状态。

04-[理解]-偏移量管理之重构代码

​ 实际项目开发中,为了代码重构复用和代码简洁性,将【从数据源读取数据、实时处理及结果输出】封装到方法【processData】中,类的结构如下:

image-20210506155237200
image-20210506155237200

Streaming流式应用模板完整代码:

代码语言:javascript
复制
package cn.itcast.spark.app.ckpt

import cn.itcast.spark.app.StreamingContextUtils
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, State, StateSpec, StreamingContext}

/**
 * 实时消费Kafka Topic数据,累加统计各个搜索词的搜索次数,实现百度搜索风云榜
 */
object _01StreamingTemplate {
	
	/**
	 * 抽象一个函数:专门从数据源读取流式数据,经过状态操作分析数据,最终将数据输出
	 * @param ssc 流式上下文StreamingContext实例对象
	 */
	def processData(ssc: StreamingContext): Unit = {
		// 1. 从Kafka消费数据,使用Kafka New Consumer API
		val kafkaDStream: DStream[ConsumerRecord[String, String]] = StreamingContextUtils
			.consumerKafka(ssc, "search-log-topic")
		
		// 2. 对每批次的数据进行搜索词进行次数统计
		val reduceDStream: DStream[(String, Int)] = kafkaDStream.transform{ rdd =>
			val reduceRDD: RDD[(String, Int)] = rdd
				// 过滤不合格的数据
				.filter{ record =>
					val message: String = record.value()
					null != message && message.trim.split(",").length == 4
				}
				// 提取搜索词,转换数据为二元组,表示每个搜索词出现一次
				.map{record =>
					val keyword: String = record.value().trim.split(",").last
					keyword -> 1
				}
				// 按照单词分组,聚合统计
				.reduceByKey((tmp, item) => tmp + item) // TODO: 先聚合,再更新,优化
			// 返回
			reduceRDD
		}
		
		// 3、实时累加统计搜索词搜索次数,使用mapWithState函数
		/*
		  def mapWithState[StateType: ClassTag, MappedType: ClassTag](
		      spec: StateSpec[K, V, StateType, MappedType]
		    ): MapWithStateDStream[K, V, StateType, MappedType]
		 */
		// 状态更新函数,针对每条数据进行更新状态
		val spec: StateSpec[String, Int, Int, (String, Int)] = StateSpec.function(
			// mappingFunction: (KeyType, Option[ValueType], State[StateType]) => MappedType
			(keyword: String, countOption: Option[Int], state: State[Int]) => {
				// a. 获取当前批次中搜索词搜索次数
				val currentState: Int = countOption.getOrElse(0)
				// b. 从以前状态中获取搜索词搜索次数
				val previousState = state.getOption().getOrElse(0)
				// c. 搜索词总的搜索次数
				val latestState = currentState + previousState
				// d. 更行状态
				state.update(latestState)
				// e. 返回最新搜索次数
				(keyword, latestState)
			}
		)
		// 调用mapWithState函数进行实时累加状态统计
		val stateDStream: DStream[(String, Int)] = reduceDStream.mapWithState(spec)
		
		// 5. 将结果数据输出 -> 将每批次的数据处理以后输出
		stateDStream.print()
	}
	
	def main(args: Array[String]): Unit = {
		// 1. 获取StreamingContext实例对象
		/*
		  def getActiveOrCreate(
		      checkpointPath: String, // 检查点目录
		      creatingFunc: () => StreamingContext,
		      hadoopConf: Configuration = SparkHadoopUtil.get.conf,
		      createOnError: Boolean = false
		    ): StreamingContext
		 */
		val ssc: StreamingContext = {
			// a. 创建SparkConf对象,设置应用配置信息
			val sparkConf = new SparkConf()
				.setAppName(this.getClass.getSimpleName.stripSuffix("$"))
				.setMaster("local[3]")
				// TODO: 设置消费最大数量
				/*
					每秒钟消费每个分区数据最大量:1W
						topic: 3个分区,batchInterval:5s
					问:每批次数据消费最大量是多少?
						1w * 3 * 5 = 15W
				 */
				.set("spark.streaming.kafka.maxRatePerPartition", "10000")
			// b. 传递SparkConf和BatchInterval创建流式上下对象
			val context = new StreamingContext(sparkConf, Seconds(5))
			// c. 返回实例对象
			context
		}
		// TODO: 设置检查点目录
		ssc.checkpoint("datas/streaming/state-8888")
		
		// TODO:实时处理流式数据
		processData(ssc)
		
		// TODO: 启动流式应用,等待终止(人为或程序异常)
		ssc.start()
		ssc.awaitTermination() // 流式应用启动以后,一直等待终止,否则一直运行
		// 无论是否异常最终关闭流式应用(优雅的关闭)
		ssc.stop(stopSparkContext = true, stopGracefully = true)
	}
	
}

扩展知识点:Scala语言中设计模式【贷出模式

  • 贷出函数:管理资源【获取资源和关闭资源】
  • 用户函数:业务逻辑实现地方
  • MAIN方法,调用贷出函数,将用户函数传递给贷出函数

05-[理解]-偏移量管理之Checkpoint编码实现

针对Spark Streaming状态应用程序,设置Checkpoint检查点目录,其中存储两种类型数据:

image-20210506160354537
image-20210506160354537

Metadata Checkpointing 用来恢复 DriverData Checkpointing用来容错stateful的数据处理失败的场景 。

​ 当我们再次运行Streaming Application时,只要从Checkpoint 检查点目录恢复,构建StreamingContext应用,就可以继续从上次消费偏移量消费数据。

image-20210506160549555
image-20210506160549555

使用StreamingContext中【getActiveOrCreate】方法构建StreamingContext实例对象,方法声明如下:

image-20210506160808441
image-20210506160808441

若Application为首次重启,将创建一个新的StreamingContext实例;如果Application从失败中重启,从checkpoint目录导入checkpoint数据来重新创建StreamingContext实例。

image-20210506161045918
image-20210506161045918

修改上述案例代码:

代码语言:javascript
复制
package cn.itcast.spark.app.ckpt

import cn.itcast.spark.app.StreamingContextUtils
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, State, StateSpec, StreamingContext}

/**
 * 实时消费Kafka Topic数据,累加统计各个搜索词的搜索次数,实现百度搜索风云榜
 */
object _02StreamingStateCkpt {
	
	/**
	 * 抽象一个函数:专门从数据源读取流式数据,经过状态操作分析数据,最终将数据输出
	 * @param ssc 流式上下文StreamingContext实例对象
	 */
	def processData(ssc: StreamingContext): Unit = {
		// 1. 从Kafka消费数据,使用Kafka New Consumer API
		val kafkaDStream: DStream[ConsumerRecord[String, String]] = StreamingContextUtils
			.consumerKafka(ssc, "search-log-topic")
		
		// 2. 对每批次的数据进行搜索词进行次数统计
		val reduceDStream: DStream[(String, Int)] = kafkaDStream.transform{ rdd =>
			val reduceRDD: RDD[(String, Int)] = rdd
				// 过滤不合格的数据
				.filter{ record =>
					val message: String = record.value()
					null != message && message.trim.split(",").length == 4
				}
				// 提取搜索词,转换数据为二元组,表示每个搜索词出现一次
				.map{record =>
					val keyword: String = record.value().trim.split(",").last
					keyword -> 1
				}
				// 按照单词分组,聚合统计
				.reduceByKey((tmp, item) => tmp + item) // TODO: 先聚合,再更新,优化
			// 返回
			reduceRDD
		}
		
		// 3、实时累加统计搜索词搜索次数,使用mapWithState函数
		/*
		  def mapWithState[StateType: ClassTag, MappedType: ClassTag](
		      spec: StateSpec[K, V, StateType, MappedType]
		    ): MapWithStateDStream[K, V, StateType, MappedType]
		 */
		// 状态更新函数,针对每条数据进行更新状态
		val spec: StateSpec[String, Int, Int, (String, Int)] = StateSpec.function(
			// mappingFunction: (KeyType, Option[ValueType], State[StateType]) => MappedType
			(keyword: String, countOption: Option[Int], state: State[Int]) => {
				// a. 获取当前批次中搜索词搜索次数
				val currentState: Int = countOption.getOrElse(0)
				// b. 从以前状态中获取搜索词搜索次数
				val previousState = state.getOption().getOrElse(0)
				// c. 搜索词总的搜索次数
				val latestState = currentState + previousState
				// d. 更行状态
				state.update(latestState)
				// e. 返回最新搜索次数
				(keyword, latestState)
			}
		)
		// 表示,在启动应用时,可以初始化状态,比如从Redis中读取状态数据,转换为RDD,进行赋值初始化操作
		/*
		 def initialState(rdd: RDD[(KeyType, StateType)]): this.type
		 */
		//spec.initialState()
		
		// 调用mapWithState函数进行实时累加状态统计
		val stateDStream: DStream[(String, Int)] = reduceDStream
			.mapWithState(spec)
			.filter(tuple => tuple._2 >= 10)
		
		// 5. 将结果数据输出 -> 将每批次的数据处理以后输出
		stateDStream.print()
	}
	
	
	def main(args: Array[String]): Unit = {
		//TODO: 检查点目录
		val CKPT_DIR: String = "datas/streaming/state-1000"
		
		// 1. 获取StreamingContext实例对象
		/*
		  def getActiveOrCreate(
		      checkpointPath: String, // 检查点目录
		      creatingFunc: () => StreamingContext,
		      hadoopConf: Configuration = SparkHadoopUtil.get.conf,
		      createOnError: Boolean = false
		    ): StreamingContext
		 */
		val ssc: StreamingContext = StreamingContext.getActiveOrCreate(
			CKPT_DIR, // 如果目录存在,从Checkpoint数据恢复构建StreamingContext对象,包括DStream创建、转换和输出
			// 匿名函数,函数参数没有,返回值要求:StreamingContext对象
			() => { // CKPT不存在时,调用此函数构建StreamingContext对象,读取数据,转换和输出
				// a. 创建SparkConf对象,设置应用配置信息
				val sparkConf = new SparkConf()
					.setAppName(this.getClass.getSimpleName.stripSuffix("$"))
					.setMaster("local[3]")
					// 设置消费最大数量
					.set("spark.streaming.kafka.maxRatePerPartition", "10000")
				// b. 传递SparkConf和BatchInterval创建流式上下对象
				val context = new StreamingContext(sparkConf, Seconds(5))
				// c. TODO: 处理数据
				processData(context)
				// d. 返回流式上下文对象
				context
			}
		)
		
		// TODO: 设置检查点目录
		ssc.checkpoint(CKPT_DIR)
		
		// TODO: 启动流式应用,等待终止(人为或程序异常)
		ssc.start()
		ssc.awaitTermination() // 流式应用启动以后,一直等待终止,否则一直运行
		// 无论是否异常最终关闭流式应用(优雅的关闭)
		ssc.stop(stopSparkContext = true, stopGracefully = true)
	}
}

​ 当Streaming Application再次运行时,从Checkpoint检查点目录恢复时,有时有问题,比如修改程序,再次从运行时,可能出现类型转换异常,如下所示:

image-20210506162150441
image-20210506162150441

原因在于修改DStream转换操作,在检查点目录中存储的数据没有此类的相关代码,ClassCastException异常。 此时无法从检查点读取偏移量信息和转态信息,所以SparkStreaming中Checkpoint功能,属于鸡肋,食之无味,弃之可惜。

06-[理解]-偏移量管理之手动管理偏移量和状态思路

SparkStreaming中Checkpoint功能,属于鸡肋,食之无味,弃之可惜

  • 解决问题一:状态State,针对实数累计统计来说,再次运行流式应用,获取上次状态
  • 解决问题二:偏移量,从Kafka消费数据位置,再次运行应用时,继续上次消费位置消费数据

解决方案:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WBJj1lCY-1626354186980)(/img/image-20210506164820304.png)]

代码语言:javascript
复制
当运行流式应用程序时,首先从状态存储系统获取状态数据,进行状态初始化操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GwzyGO7b-1626354186981)(/img/image-20210506164851608.png)]

代码语言:javascript
复制
保存每批次数据偏移量信息到存储系统中,比如MySQL表、Zookeeper节点等,当再次运行流式应用时,从保存系统加载偏移量消息,继续消费数据。
image-20210506165038189
image-20210506165038189

考虑第一个问题:状态恢复如何完成呢???从存储状态系统加载状态State,进行初始化操作。

image-20210506165433772
image-20210506165433772

07-[理解]-偏移量管理之MySQL存储偏移量

此处将偏移量数据存储到MySQL表中,数据库及表的DDL和DML语句如下:

代码语言:javascript
复制
-- 1. 创建数据库的语句
CREATE DATABASE IF NOT EXISTS db_spark DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
USE db_spark ;

-- 2. 创建表的语句
CREATE TABLE `tb_offset` (
`topic` varchar(255) NOT NULL,
`partition` int NOT NULL,
`groupid` varchar(255) NOT NULL,
`offset` bigint NOT NULL,
PRIMARY KEY (`topic`,`partition`,`groupid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ;

-- 3. 插入数据语句replace
replace into tb_offset (`topic`, `partition`, `groupid`, `offset`) values(?, ?, ?, ?)
--/*
--
replace语句执行时,分以下两种情况:
--
- 情况1:insert,当不存或唯一索引冲突,相当于insert操作
--
- 情况2:delete and insert,当存在主键冲突或唯一索引冲突,相当于delete操作,加insert操作
--*/

-- 4. 查询数据语句select
select * from tb_offset where topic in ('xx', 'yy') AND groupid = 'gid001' ;
select * from tb_offset where topic in (?) and groupid = ? ;
image-20210506170036587
image-20210506170036587

​ 工具类OffsetsUtils从MySQL数据库表中读取消费的偏移量信息和保存最近消费的偏移量值,示意图如下所示:

image-20210506170102219
image-20210506170102219

​ 工 具 类 中 包 含 如 何 保 存 偏 移 量 【 saveOffsetsToTable 】 和 读 取 偏 移 量【getOffsetsToMap】两个函数,具体代码如下:

代码语言:javascript
复制
package cn.itcast.spark.app.offset

import java.sql.{Connection, DriverManager, PreparedStatement, ResultSet}

import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange

import scala.collection.mutable

/**
 * 将消费Kafka Topic偏移量数据存储MySQL数据库,工具类用于读取和保存偏移量数据
 */
object OffsetsUtils {

	/**
	 * 依据Topic名称和消费组GroupId获取各个分区的偏移量
	 *
	 *@param topicNames Topics名称
	 *@param groupId 消费组ID
	 **/
	def getOffsetsToMap(topicNames: Array[String], groupId: String): Map[TopicPartition, Long] ={
		// 构建集合
		val map: mutable.Map[TopicPartition, Long] = scala.collection.mutable.Map[TopicPartition, Long]()
		// 声明变量
		var conn: Connection = null
		var pstmt: PreparedStatement = null
		var result: ResultSet = null
		try{
			// a. 加载驱动类
			Class.forName("com.mysql.cj.jdbc.Driver")
			// b. 获取连接
			conn = DriverManager.getConnection(
				"jdbc:mysql://node1.itcast.cn:3306/?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true", //
				"root", //
				"123456" //
			)
			// c. 编写SQL,获取PreparedStatement对象
			// Array("1-topic", "2-topic") ->  topic in ("1-topic", "2-topic")
			val topicNamesStr = topicNames.map(topicName => s"\'$topicName\'").mkString(", ")
			val querySQL =
				s"""
				   |SELECT
				   |  `topic`, `partition`, `groupid`, `offset`
				   |FROM
				   |  db_spark.tb_offset
				   |WHERE
				   |  groupid = ? AND topic in ($topicNamesStr)
				   |""".stripMargin
			pstmt = conn.prepareStatement(querySQL)
			pstmt.setString(1, groupId)
			// d. 查询数据
			result = pstmt.executeQuery()
			// e. 遍历获取值
			while (result.next()){
				val topicName = result.getString("topic")
				val partitionId = result.getInt("partition")
				val offset = result.getLong("offset")
				// 加入集合中
				map += new TopicPartition(topicName, partitionId) -> offset
			}
		}catch {
			case e: Exception => e.printStackTrace()
		}finally {
			if(null != result) result.close()
			if(null != pstmt) pstmt.close()
			if(null != conn) conn.close()
		}
		// 返回集合,转换为不可变的
		map.toMap
	}
	
	/**
	 * 保存Streaming每次消费Kafka数据后最新偏移量到MySQL表中
	 *
	 * @param offsetRanges Topic中各个分区消费偏移量范围
	 * @param groupId 消费组ID
	 */
	def saveOffsetsToTable(offsetRanges: Array[OffsetRange], groupId: String): Unit = {
		// 声明变量
		var conn: Connection = null
		var pstmt: PreparedStatement = null
		try{
			// a. 加载驱动类
			Class.forName("com.mysql.cj.jdbc.Driver")
			// b. 获取连接
			conn = DriverManager.getConnection(
				"jdbc:mysql://node1.itcast.cn:3306/?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true", //
				"root", //
				"123456" //
			)
			// c. 编写SQL,获取PreparedStatement对象
			val insertSQL = "replace into db_spark.tb_offset (`topic`, `partition`, `groupid`, `offset`) values (?, ?, ?, ?)"
			pstmt = conn.prepareStatement(insertSQL)
			// d. 设置参数
			offsetRanges.foreach{offsetRange =>
				println(offsetRange.toString())
				pstmt.setString(1, offsetRange.topic)
				pstmt.setInt(2, offsetRange.partition)
				pstmt.setString(3, groupId)
				pstmt.setLong(4, offsetRange.untilOffset)
				// 加入批次
				pstmt.addBatch()
			}
			// e. 批量插入
			pstmt.executeBatch()
		}catch {
			case e: Exception => e.printStackTrace()
		}finally {
			if(null != pstmt) pstmt.close()
			if(null != conn) conn.close()
		}
	}
	
}

从Kafka Topic消费数据时,首先从MySQL数据库加载偏移量,如果有值,使用如下函数:

image-20210506170921342
image-20210506170921342

从Kafka Topic消费数据时,直接获取的DStream中每批次RDD都是KafkaRDD,可以获取数据偏移量范围信息OffsetRanges。

image-20210506171016452
image-20210506171016452

修改前面实时订单消费额统计代码,自己管理消费偏移量,存储到MySQL表中,代码如下:

代码语言:javascript
复制
package cn.itcast.spark.app.offset

import org.apache.commons.lang3.time.FastDateFormat
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.kafka010._
import org.apache.spark.streaming.{Seconds, State, StateSpec, StreamingContext}

/**
 * 实时消费Kafka Topic数据,累加统计各个搜索词的搜索次数,实现百度搜索风云榜
 */
object _03StreamingManagerOffsets {
	
	/**
	 * 抽象一个函数:专门从数据源读取流式数据,经过状态操作分析数据,最终将数据输出
	 * @param ssc 流式上下文StreamingContext实例对象
	 */
	def processData(ssc: StreamingContext): Unit = {
		val groupId = "gui_0001" // 消费组ID
		
		// 1. 从Kafka消费数据,使用Kafka New Consumer API
		val kafkaDStream: DStream[ConsumerRecord[String, String]] = {
			// i.位置策略
			val locationStrategy: LocationStrategy = LocationStrategies.PreferConsistent
			// ii.读取哪些Topic数据
			val topics = Array("search-log-topic")
			// iii.消费Kafka 数据配置参数
			val kafkaParams = Map[String, Object](
				"bootstrap.servers" -> "node1.itcast.cn:9092",
				"key.deserializer" -> classOf[StringDeserializer],
				"value.deserializer" -> classOf[StringDeserializer],
				"group.id" -> groupId,
				"auto.offset.reset" -> "latest",
				"enable.auto.commit" -> (false: java.lang.Boolean)
			)
			// iv.消费数据策略
			/*
				1. 从MySQL表加载偏移量
				2. 判断是否有值,如果没有值,表示第一次消费数据,从最新偏移量开始
				3. 如果有值,从指定偏移量消费数据
			 */
			// TODO: a. 从MySQL表加载偏移量
			val map: Map[TopicPartition, Long] = OffsetsUtils.getOffsetsToMap(topics, groupId)
			val consumerStrategy: ConsumerStrategy[String, String] = if(map.isEmpty){
				// TODO: b. 判断是否有值,如果没有值,表示第一次消费数据,从最新偏移量开始
				ConsumerStrategies.Subscribe(topics, kafkaParams)
			}else{
				// TODO: c.  如果有值,从指定偏移量消费数据
				ConsumerStrategies.Subscribe(topics, kafkaParams, map)
			}
			
			// v.采用新消费者API获取数据,类似于Direct方式
			val kafkaDStream: DStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
				ssc, locationStrategy, consumerStrategy
			)
			// vi.返回DStream
			kafkaDStream
		}
		
		// TODO:其一、定义数组存储每批次数据对应RDD中各个分区的Topic Partition中偏移量信息
		var offsetRanges: Array[OffsetRange] = Array.empty
		
		// 2. 对每批次的数据进行搜索词进行次数统计
		val reduceDStream: DStream[(String, Int)] = kafkaDStream.transform{ rdd =>
			// TODO:其二、直接从Kafka获取的每批次KafkaRDD中获取偏移量信息
			offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
			
			val reduceRDD: RDD[(String, Int)] = rdd
				// 过滤不合格的数据
				.filter{ record =>
					val message: String = record.value()
					null != message && message.trim.split(",").length == 4
				}
				// 提取搜索词,转换数据为二元组,表示每个搜索词出现一次
				.map{record =>
					val keyword: String = record.value().trim.split(",").last
					keyword -> 1
				}
				// 按照单词分组,聚合统计
				.reduceByKey((tmp, item) => tmp + item) // TODO: 先聚合,再更新,优化
			// 返回
			reduceRDD
		}
		
		// 3、实时累加统计搜索词搜索次数,使用mapWithState函数
		/*
		  def mapWithState[StateType: ClassTag, MappedType: ClassTag](
		      spec: StateSpec[K, V, StateType, MappedType]
		    ): MapWithStateDStream[K, V, StateType, MappedType]
		 */
		// 状态更新函数,针对每条数据进行更新状态
		val spec: StateSpec[String, Int, Int, (String, Int)] = StateSpec.function(
			// mappingFunction: (KeyType, Option[ValueType], State[StateType]) => MappedType
			(keyword: String, countOption: Option[Int], state: State[Int]) => {
				// a. 获取当前批次中搜索词搜索次数
				val currentState: Int = countOption.getOrElse(0)
				// b. 从以前状态中获取搜索词搜索次数
				val previousState = state.getOption().getOrElse(0)
				// c. 搜索词总的搜索次数
				val latestState = currentState + previousState
				// d. 更行状态
				state.update(latestState)
				// e. 返回最新搜索次数
				(keyword, latestState)
			}
		)
		// 调用mapWithState函数进行实时累加状态统计
		val stateDStream: DStream[(String, Int)] = reduceDStream.mapWithState(spec)
		
		// 5. 将结果数据输出 -> 将每批次的数据处理以后输出
		stateDStream.foreachRDD{(resultRDD, batchTime) =>
			// 将batchTime进行转换:yyyy-MM-dd HH:mm:ss
			val formatTime = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss")
				.format(batchTime.milliseconds)
			println("-------------------------------------------")
			println(s"Time: $formatTime")
			println("-------------------------------------------")
			// 判断结果RDD是否有数据,没有数据不要打印
			if(!resultRDD.isEmpty()){
				// TODO: 针对RDD数据进行输出,以前在SparkCore中怎么编写此处就编写
				resultRDD.coalesce(1).foreachPartition(iter => iter.foreach(println))
			}
			
			// TODO: 其三、当每批次结果RDD保存至外部存储系统以后,保存偏移量
			OffsetsUtils.saveOffsetsToTable(offsetRanges, groupId)
		}
	}
	
	def main(args: Array[String]): Unit = {
		// 1. 获取StreamingContext实例对象
		val ssc: StreamingContext = {
			// a. 创建SparkConf对象,设置应用配置信息
			val sparkConf = new SparkConf()
				.setAppName(this.getClass.getSimpleName.stripSuffix("$"))
				.setMaster("local[3]")
				// TODO: 设置消费最大数量
				.set("spark.streaming.kafka.maxRatePerPartition", "10000")
			// b. 传递SparkConf和BatchInterval创建流式上下对象
			val context = new StreamingContext(sparkConf, Seconds(5))
			// c. 返回实例对象
			context
		}
		// TODO: 设置检查点目录
		ssc.checkpoint("datas/streaming/state-9999")
		
		// TODO: 调用处理流式数据代码
		processData(ssc)
		
		// TODO: 启动流式应用,等待终止(人为或程序异常)
		ssc.start()
		ssc.awaitTermination() // 流式应用启动以后,一直等待终止,否则一直运行
		// 无论是否异常最终关闭流式应用(优雅的关闭)
		ssc.stop(stopSparkContext = true, stopGracefully = true)
	}
}
image-20210506171850778
image-20210506171850778

实际项目中将偏移量保存至Zookeeper上或者Redis中,原因如下:

image-20210506171919863
image-20210506171919863
image-20210506171930208
image-20210506171930208

08-[了解]-Spark Streaming不足

StructuredStreaming结构化流:

  • 第一点、从Spark 2.0开始出现新型的流式计算模块
  • 第二点、Spark 2.2版本,发布Release版本,可以用于实际生产环境中
  • 第三点、Spark 2.3版本,提供ContinuesProcessing持续流处理,原生流处理模式,来一条数据处理一条数据,达到实时性
image-20210506172756499
image-20210506172756499

本质上,这是一种micro-batch(微批处理)的方式处理,用批的思想去处理流数据。这种设计让Spark Streaming面对复杂的流式处理场景时捉襟见肘。 Spark Streaming 存在哪些不足,总结一下主要有下面几点:

代码语言:javascript
复制
第一点:使用 Processing Time 而不是 Event Time
	窗口分析时
	使用处理时间进行窗口分析不合理的
            每个小时订单销售额				23  -  00 
                23:59:58 产生订单   
            流式应用程序接受时间              00  -  01    
                00:00:05 获取数据
                
第二点:Complex, low-level api
	编程复杂,底层RDD
	
第三点:reason about end-to-end application
	很难支持流式应用端到端精确性一次语义
	DStream 只能保证自己的一致性语义是 exactly-once 的

第四点:批流代码不统一
	批处理:Dataset、DataFrame
	流计算:DStream

流式计算一直没有一套标准化、能应对各种场景的模型,直到2015年Google发表了TheDataflow Model的论文( https://yq.aliyun.com/articles/73255 )。

image-20210506173553828
image-20210506173553828

09-[掌握]-Structured Streaming编程模型

​ Structured Streaming是一个基于Spark SQL引擎的可扩展、容错的流处理引擎。随着数据不断地到达,Spark 引擎会以一种增量的方式来执行这些操作,并且持续更新结算结果。 结构化流StructuredStreaming模块仅仅就是SparkSQL中针对流式数据处理功能模块而已。

image-20210506175231522
image-20210506175231522
代码语言:javascript
复制
1、流式处理引擎,基于SparkSQL引擎之上
	DataFrame/Dataset
	处理数据时,使用Catalyst优化器

2、富有的、统一的、高级API
	DataFrame/Dataset
		数据源、数据处理、数据输出
	DSL或SQL分析数据
	
3、数据源比较丰富
	提供一套流式数据源接口,只要实现,就可以流式读取和保存

Structured Streaming 在 Spark 2.0 版本于 2016 年引入,设计思想参考很多其他系统的思想,

image-20210506175700692
image-20210506175700692

Structured Streaming 和其他系统的显著区别主要如下:

image-20210506175850499
image-20210506175850499

编程模型:将流式数据当做一张没有限制(无界)表,将源源不断地数据追加到表中,默认情况下,只要表中一有数据(有1条数据或多条数据),就会立即进行处理分析(增量处理,本质来说,还是微批处理,底层使用SparkSQL引擎)。

image-20210506180056502
image-20210506180056502
代码语言:javascript
复制
在这个模型中,主要存在下面几个组成部分:
1、第一部分:unbounded table(input table)
	输入表,将流式数据放在表中

2、第二部分:Query(查询)
	当输入表input table中一有数据,立即处理分析
	增量查询分析
	
3、第三部分:Result Table
	Query 产生的结果表

4、第四部分:Output
	Result Table 的输出,依据设置的输出模式OutputMode输出结果;
image-20210506180511112
image-20210506180511112

​ Structured Streaming最核心的思想就是将实时到达的数据看作是一个不断追加的unbound table无界表,到达流的每个数据项就像是表中的一个新行被附加到无边界的表中,用静态结构化数据的批处理查询方式进行流计算。

image-20210506180641970
image-20210506180641970

10-[掌握]-入门案例WordCount之功能演示

需求:入门案例与SparkStreaming的入门案例基本一致:实时从TCP Socket读取数据(采用nc)实时进行词频统计WordCount,并将结果输出到控制台Console。

以词频统计WordCount案例,Structured Streaming实时处理数据的示意图如下,各行含义:

image-20210506180908652
image-20210506180908652
代码语言:javascript
复制
第一行、表示从TCP Socket不断接收数据,使用【nc -lk 9999】;
第二行、表示时间轴,每隔1秒进行一次数据处理;
第三行、可以看成是“input unbound table",当有新数据到达时追加到表中;
第四行、最终的wordCounts是结果表,新数据到达后触发查询Query,输出的结果;
第五行、当有新的数据到达时,Spark会执行“增量"查询,并更新结果集;该示例设置为CompleteMode,因此每次都将所有数据输出到控制台;

​ 使用Structured Streaming处理实时数据时,会负责将新到达的数据与历史数据进行整合,并完成正确的计算操作,同时更新Result Table

运行词频统计WordCount程序,从TCP Socket消费数据,官方演示说明截图如下:

image-20210506181304502
image-20210506181304502

演示运行案例步骤:

  • 第一步、打开终端Terminal,运行NetCat,命令为:nc -lk 9999
  • 第二步、打开另一个终端Terminal,执行如下命令
代码语言:javascript
复制
# 官方入门案例运行:词频统计
/export/server/spark/bin/run-example \
--master local[2] \
--conf spark.sql.shuffle.partitions=2 \
org.apache.spark.examples.sql.streaming.StructuredNetworkWordCount \
node1.itcast.cn 9999

# 测试数据
spark hadoop spark hadoop spark hive
spark spark spark
spark hadoop hive

发送数据以后,最终统计输出结果如下:

image-20210506181902407
image-20210506181902407

11-[掌握]-入门案例WordCount之编程实现

需求:编程使用StructuredStreaming词频统计WordCount程序,从TCP Socket消费数据,最终结果打印控制台

  • Socket 数据源
image-20210506182318064
image-20210506182318064
  • Console 接收器
image-20210506182426561
image-20210506182426561
image-20210506182518054
image-20210506182518054
代码语言:javascript
复制
第一点、程序入口SparkSession,加载流式数据:spark.readStream

第二点、数据封装Dataset/DataFrame中,分析数据时,建议使用DSL编程,调用API,很少使用SQL方式

第三点、启动流式应用,设置Output结果相关信息、start方法启动应用
代码语言:javascript
复制
package cn.itcast.spark.start

import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.sql.functions._
import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}

/**
 * 使用Structured Streaming从TCP Socket实时读取数据,进行词频统计,将结果打印到控制台。
	 * 第一点、程序入口SparkSession,加载流式数据:spark.readStream
	 * 第二点、数据封装Dataset/DataFrame中,分析数据时,建议使用DSL编程,调用API,很少使用SQL方式
	 * 第三点、启动流式应用,设置Output结果相关信息、start方法启动应用
 */
object _04StructuredWordCount {
	
	def main(args: Array[String]): Unit = {
		
		// TODO: step1. 构建SparkSession实例对象,相关配置进行设置
		val spark: SparkSession = SparkSession.builder()
    		.appName(this.getClass.getSimpleName.stripSuffix("$"))
    		.master("local[2]")
    		.config("spark.sql.shuffle.partitions", "2")
    		.getOrCreate()
		import spark.implicits._
		
		// TODO: step2. 从TCP Socket加载数据,读取数据列名称为value,类型是String
		val inputStreamDF: DataFrame = spark.readStream
    		.format("socket")
    		.option("host", "node1.itcast.cn")
    		.option("port", "9999")
    		.load()
		/*
		root
			|-- value: string (nullable = true)
		 */
		//inputStreamDF.printSchema()
		
		
		// TODO: step3. 进行词频统计
		/*
			table: words , column: value
					SQL: SELECT word, COUNT(1) AS count  FROM words GROUP BY word
		 */
		val resultStreamDF: DataFrame =  inputStreamDF
			// hadoop spark hadoop spark spark  -> 分割单词,并且扁平化
			.select(explode(split(trim($"value"), "\\s+")).as("word"))
			.groupBy("word").count()
		/*
		root
		 |-- word: string (nullable = true)
		 |-- count: long (nullable = false)
		 */
		
		// TODO: step4. 将结果输出(ResultTable结果输出,此时需要设置输出模式)
		val query: StreamingQuery = resultStreamDF.writeStream
			.outputMode(OutputMode.Update()) // 表示当词频更新时,再输出
			.format("console")
    		.option("numRows", "10")
    		.option("truncate", "false")
			// 启动流式应用
			.start()
		
		// TODO: step5. 启动流式应用后,等待终止
		query.awaitTermination()
		query.stop()
	}
	
}

amDF: DataFrame = spark.readStream
    		.format("socket")
    		.option("host", "node1.itcast.cn")
    		.option("port", "9999")
    		.load()
		/*
		root
			|-- value: string (nullable = true)
		 */
		//inputStreamDF.printSchema()
		
		
		// TODO: step3. 进行词频统计
		/*
			table: words , column: value
					SQL: SELECT word, COUNT(1) AS count  FROM words GROUP BY word
		 */
		val resultStreamDF: DataFrame =  inputStreamDF
			// hadoop spark hadoop spark spark  -> 分割单词,并且扁平化
			.select(explode(split(trim($"value"), "\\s+")).as("word"))
			.groupBy("word").count()
		/*
		root
		 |-- word: string (nullable = true)
		 |-- count: long (nullable = false)
		 */
		
		// TODO: step4. 将结果输出(ResultTable结果输出,此时需要设置输出模式)
		val query: StreamingQuery = resultStreamDF.writeStream
			.outputMode(OutputMode.Update()) // 表示当词频更新时,再输出
			.format("console")
    		.option("numRows", "10")
    		.option("truncate", "false")
			// 启动流式应用
			.start()
		
		// TODO: step5. 启动流式应用后,等待终止
		query.awaitTermination()
		query.stop()
	}
	
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-11-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Spark Day12:Structured Streaming
    • 01-[了解]-上次课程内容回顾
      • 02-[了解]-今日课程内容提纲
        • 03-[理解]-偏移量管理之引例和概述
          • 04-[理解]-偏移量管理之重构代码
            • 05-[理解]-偏移量管理之Checkpoint编码实现
              • 06-[理解]-偏移量管理之手动管理偏移量和状态思路
                • 07-[理解]-偏移量管理之MySQL存储偏移量
                  • 08-[了解]-Spark Streaming不足
                    • 09-[掌握]-Structured Streaming编程模型
                      • 10-[掌握]-入门案例WordCount之功能演示
                        • 11-[掌握]-入门案例WordCount之编程实现
                        相关产品与服务
                        云数据库 SQL Server
                        腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档