背景
上一篇文章:
高威:Flink集群搭建zhuanlan.zhihu.com介绍了Flink集群的搭建(单机版),其中介绍了当前最流行的3中实时开发中间件,其中spark中有两个:sparkStreaming和Structured Streaming。
Sparkstreaming首次引入在0.*版本,其核心思想是利用spark批处理框架,以microbatch(以一段时间的流作为一个batch)的方式,完成对流数据的处理。其核心思想是建立DStream进行微批处理,依旧属于批处理的一部分。不满足基于Event Time的实时处理需求。
Structured Streaming是Spark2.0版本提出的新的实时流框架(2.0和2.1是实验版本,从Spark2.2开始为稳定版本),相比于Spark Streaming,优点如下:
1、同样能支持多种数据源的输入和输出,Kafka、flume、Socket、Json。
2、基于Event-Time,相比于Spark Streaming的Processing-Time更精确,更符合业务场景。
Event time 事件时间: 就是数据真正发生的时间,比如用户浏览了一个页面可能会产生一条用户的该时间点的浏览日志。
Process time 处理时间: 则是这条日志数据真正到达计算框架中被处理的时间点,简单的说,就是你的Spark程序是什么时候读到这条日志的。
事件时间是嵌入在数据本身中的时间。对于许多应用程序,用户可能希望在此事件时间操作。例如,如果要获取IoT设备每分钟生成的事件数,则可能需要使用生成数据的时间(即数据中的事件时间),而不是Spark接收他们的时间。事件时间在此模型中非常自然地表示 - 来自设备的每个事件都是表中的一行,事件时间是该行中的一个列值。
3、支持spark2的dataframe处理。
4、解决了Spark Streaming存在的代码升级,DAG图变化引起的任务失败,无法断点续传的问题。
5、基于SparkSQL构建的可扩展和容错的流式数据处理引擎,使得实时流式数据计算可以和离线计算采用相同的处理方式(DataFrame&SQL)。
6、可以使用与静态数据批处理计算相同的方式来表达流计算。
7、结构化流查询(Structured Streaming Query)内部默认使用微批处理引擎( micro-batch processing engine),它将数据流看作一系列小的批任务(batch jobs)来处理,从而达到端到端如100毫秒这样低的延迟以及只执行一次容错的保证。然而,从Spark 2.3,我们已经引入了一个新的低延迟处理方式——连续处理(Continuous Processing),可以达到端到端如1毫秒这样低的延迟至少一次保证。不用改变查询中DataSet/DataFrame的操作,你就能够选择基于应用要求的查询模式。
Structured Streaming支持的Source:
1、File Source:从给定的目录读取数据,目前支持的格式有text,csv,json,parquet,容错。
2、Kafka Source:从kafka拉取数据。仅兼容kafka 0.10.0或者更高版本,容错。
3、Socket Source(for testing):从一个连接中读取UTF8编码的文本数据,不容错。
Structured Streaming的输出:
1、Append mode(default):仅仅从上次触发计算到当前新增的行会被输出到sink。仅仅支持行数据插入结果表后不进行更改的query操作。因此,这种方式能保证每行数据仅仅输出一次。例如,带有Select,where,map,flatmap,filter,join等的query操作支持append模式。
2、Complete mode:每次trigger都会将整个结果表输出到sink。这个是针对聚合操作的。
3、Update mode:仅仅是自上次trigger之后结果表有变更的行会输出到sink。在以后的版本中会有更详细的信息。
StructuredStreaming目前支持的sink只有FileSink、KafkaSink、ConsoleSink、MemorySink和ForeachSink。
其中最常用的是ForeachSink。
具体详情见官网:
Structured Streaming Programming Guidespark.apache.org环境:
JDK:1.8.0_221
Spark:2.1.1
Hadoop:2.7.7
Maven:3.6.1
Kafka:0.11.0.0
Scala:2.11.8
测试:
1、数据源为socket。
Socket方式是最简单的数据输入源,用户只需要指定"socket"形式并配置监听的IP和Port即可。
spark自带的测试包:
1、开一个终端输入:
nc -lk 9999
2、进入spark的bin目录,执行
./run-example org.apache.spark.examples.sql.streaming.StructuredNetworkWordCount localhost 9999
输出结果:
或者spark-shell的交互模式(nc -lk 9999依旧执行):
import org.apache.spark.sql.functions._
import org.apache.spark.sql.SparkSession
val spark = SparkSession.builder.appName("StructuredNetworkWordCount").getOrCreate()
import spark.implicits._
val lines = spark.readStream.format("socket").option("host", "localhost").option("port", 9999).load()
val words = lines.as[String].flatMap(_.split(" "))
val wordCounts = words.groupBy("value").count()
val query = wordCounts.writeStream.outputMode("complete").format("console").start()
输出:
Jar包工程:
代码:
package test
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
/** *
*
* @autor gaowei
* @Date 2020-04-13 11:11
*/
object WordCountTest {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
val spark = SparkSession.builder()
.master("local")
.appName("WordCount1")
.getOrCreate()
// 读取socket流数据,监听端口9998
val lines = spark.readStream
.format("socket")
.option("host", "localhost")
.option("port", "9999")
.load()
// 隐式转换
import spark.implicits._
// 将一行数据进行空格分割后打平
val words = lines.as[String].flatMap(_.split(" "))
// 根据value进行groupby,计算出count
val wordCounts = words.groupBy("value").count()
// 将流数据写入console控制台
val query = wordCounts
.writeStream
.format("console")
.outputMode("complete")
.start()
query.awaitTermination()
}
}
打包后执行
spark-submit --class test.WordCountTest --master yarn --deploy-mode client ./data-analysis-1.0-SNAPSHOT-jar-with-dependencies.jar
2、数据源为kafka。
交互模式:
注意:需要手动引入Kafka包,一般在kafka的libs下面会有,并指定
1、进入Kafka目录,开启Kafka生产者模式:
./kafka-console-producer.sh --broker-list localhost:9092 --topic test1
2、启动spark交互界面
spark-shell --jars /Users/gaowei/Package/kafka_2.11-0.11.0.0/libs/kafka-clients-0.11.0.0.jar --packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.2.0
3、代码:
import org.apache.spark.sql.functions._
import org.apache.spark.sql.SparkSession
val df = spark.readStream.format("kafka").option("kafka.bootstrap.servers", "localhost:9092").option("subscribe", "test1").load()
import spark.implicits._
val lines= df.selectExpr("CAST(value AS STRING)").as[String]
val wordCounts = lines.groupBy("value").count()
val query = wordCounts.writeStream.outputMode("complete").format("console").start()
kafka生产者:
Structured Streaming消费:
Jar包模式:
IDEA中使用maven构建工程,因此pom.xml添加如下依赖:
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.11.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_${scala.version}</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql-kafka-0-10_${scala.version}</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_${scala.version}</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_${scala.version}</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_${scala.version}</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-mllib_${scala.version}</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.10.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.4</version>
<!--<scope>test</scope>-->
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
代码:
package test
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
/** *
*
* @autor gaowei
* @Date 2020-04-13 11:11
*/
object WordCountTest {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
val spark = SparkSession.builder()
.master("local")
.appName("WordCount1")
.getOrCreate()
// 读取socket流数据,监听端口9998
val df = spark.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("subscribe", "test1").load()
// 隐式转换
import spark.implicits._
val lines= df.selectExpr("CAST(value AS STRING)").as[String]
val wordCounts = lines.groupBy("value").count()
val query = wordCounts
.writeStream
.format("console")
.outputMode("complete")
.start()
query.awaitTermination()
}
}
kafka生产者:
执行:
spark-submit --jars /Users/gaowei/Package/kafka_2.11-0.11.0.0/libs/kafka-clients-0.11.0.0.jar --class test.WordCountTest --packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.2.0 --master yarn --deploy-mode client ./data-analysis-1.0-SNAPSHOT-jar-with-dependencies.jar
如果maven中添加了依赖可以把
--packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.2.0去掉
输出结果:
问题:
出现这个问题是找不到spark连接kafka的包,需要添加依赖,或者手动引入。
添加依赖:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql-kafka-0-10_${scala.version}</artifactId>
<version>${spark.version}</version>
</dependency>
或者手动引入:
--packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.2.0
报的kafka的反序列化问题,顺着提示的jar包位置,需要手动引入Kafka的包,spark提交任务不会用kafka自带的jar包,所以需要指定kafka的libs下面kafka-clients-0.11.0.0.jar包。
常见项目:
需求:APP有一个项目,上线一个活动,实时更新商品详情页面的热度。
开发:structuredStreaming开发,直接读取kafka数据,然后complete模式统计历史输出。
处理逻辑:
APP页面点击 -> nginx服务器 -> 通过lua发送kafka -> structuredStreaming读取kafka日志 ->自定义输出到mysql -> 后端通过接口实时刷数据。
其中下面代码展现的是kafka -> structuredStreaming读取kafka日志 ->自定义输出到mysql 这个流程。
代码:
package test
import org.apache.spark.sql.{DataFrame, ForeachWriter, Row, SparkSession}
import org.apache.spark.sql.streaming.{ProcessingTime, Trigger}
import java.sql.{Connection, DriverManager}
/** *
*
* @autor gaowei
* @Date 2020-04-13 17:59
*/
object kafkaToMysqlTest {
class MysqlSink(url: String, user: String, pwd: String) extends ForeachWriter[Row] {
var conn: Connection = _
override def open(partitionId: Long, epochId: Long): Boolean = {
Class.forName("com.mysql.jdbc.Driver")
conn = DriverManager.getConnection(url, user, pwd)
true
}
override def process(value: Row): Unit = {
val p = conn.prepareStatement("replace into test(pid,pv) values(?,?)")
p.setString(1, value(0).toString)
p.setLong(2, value(1).toString.toLong)
p.execute()
}
override def close(errorOrNull: Throwable): Unit = {
conn.close()
}
}
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().appName("kafkaToMysqlTest").getOrCreate()
val brokers = "localhost:9092"
val topics = "test1"
val df = spark.readStream.format("kafka").option("kafka.bootstrap.servers", brokers).option("subscribe", topics).load()
import spark.implicits._
val kafkaDf = df.selectExpr("CAST(value AS STRING)").as[String]
val dataFrame = kafkaDf.groupBy("value").count().
toDF("pid","pv")
//todo 将数据写到MYSQL
val mysqlSink = new MysqlSink("jdbc:mysql://localhost:3306/warehouse", "root", "410410410")
val query = dataFrame.writeStream.outputMode("complete").foreach(mysqlSink).start()
query.awaitTermination()
}
}
开一个终端作为Kafka的生产者:
./kafka-console-producer.sh --broker-list localhost:9092 --topic test1
代码打包后执行:
spark-submit --jars /Users/gaowei/Package/kafka_2.11-0.11.0.0/libs/kafka-clients-0.11.0.0.jar --class test.kafkaToMysqlTest ./data-analysis-1.0-SNAPSHOT-jar-with-dependencies.jar
结果: