Spark Streaming
官网介绍:https://spark.apache.org/streaming/
之前的学习:
批处理/离线处理【batch jobs】 --> Spark 更擅长
流计算/ 实时处理【streaming jobs】 --> Spark(ss 、sss)、Flink、Strom
Flink、Strom 才是正儿八经的流处理,但Storm 只能流处理,不能批流一体。
Spark 和 Flink 都是批流一体。
ss: Spark Streaming 并不是真正的实时处理,是按照指定的时间间隔,拆分成多个微批处理(mini batch)
[.countByWindow(Seconds(5))]:每隔5s统计一个 window
ss 不是重点,以后会朝着sss发展。
flume、kafka --> Spark Streaming --> DB
flume、kafka:如果flume采集后直接去到 spark streaming,高峰期处理速度可能跟不上采集速度,需要加一层kafka削峰。在最新版本中已经不能再直接对接flume。
使用
- 导包
<dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-streaming_2.12</artifactId> <version>3.2.0</version> </dependency>
- 构建 ssc
val sparkConf = new SparkConf().setAppName("StreamingSocketApp").setMaster("local[2]") val ssc = new StreamingContext(sparkConf, Seconds(5))
- 接收数据
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("gargantua", 9527) // socketTextStream也是在调用socketStream,再调用reciverSocketStream // textFileStream 和 fileStream 是不带reciver
- 业务逻辑
// 流处理ssc 和 批处理RDD 算子的写法没有区别 val value = lines.flatMap(_.split(",")).map((_, 1)).reduceByKey(_ + _) println(value)
- 启动
// 最后启动,启动后再添加的业务逻辑将不会执行 ssc.start() ssc.awaitTermination()
- 流处理不需要stop
数据源
receiver 会使用一个线程来监听数据源的变化。
对于有 receiver 的 DStream 。 不要使用local 和 local[1] ,不然唯一的线程被recevier使用,所以没有资源跑计算。
local[n] : n 必须大于 receiver 的数量
socket、kafka 都有 receiver , 代码里也需要监听指定的地址。
而HDFS 没有 receiver , 只需要通过文件系统区读取即可。
sparkstreaming中socketTextStream策略默认是采用 MEMORY_AND_DISK_SER_2。
transformtions
大部分算子和rdd中是一致的。区别有transform和updateStateByKey
transform
实现DStream和RDD的互操作。
当数据一部分来自流处理,一部分来自批处理,DStream 无法和 RDD join ,所以 DStream需要 transform 转换为RDD
dstream.transform(rdd = > {
val newRdd = rdd.leftjoin(blackRDD)
})
updateStateByKey
每一个批次的数据,都是各自处理的,批次之间没有关联,无状态的。
updateStateByKey 返回一个有状态的DStream,可以不同批次的数据合并到这个有状态的DStream。
带状态的ssc ,需要setCheckpoint(), 指定一个位置用于存档的检查点。
val lines = ssc.socketTextStream("hadoop000", 9528)
lines.flatMap(_.split(","))
.map((_,1))
.updateStateByKey(updateFunction)
.print()
updateStateByKey 要求传入一个Function,指定将上一批次数据和当前数据的操作
// newValues 当前的值, preValues 上一个值,可能没有,所以类型是Option
def updateFunction(newValues: Seq[Int], preValues: Option[Int]): Option[Int] = {
val curr = newValues.sum
val pre = preValues.getOrElse(0)
Some(curr + pre)
}
mapWithState
除了updateStateByKey ,还可以使用mapWithState,传入 StateSpec.function(stateFunction),也要自定义Function
lines.flatMap(_.split(","))
.map((_,1))
.mapWithState(StateSpec.function(stateFunction))
val stateFunction = (word:String,option:Option[Int],state:State[Int]) => {
val sum = option.getOrElse(0) + state.getOption().getOrElse(0)
val wordFreq = (word, sum)
state.update(sum)
wordFreq
}
Checkpoint 可以实现一个StreamingContext里不同批次的关联。但是重启后就是新的StreamingContext,和新的Checkpoint。不能将两次启动的数据关联上。
官网提供的解决办法
把整个代码顺序调整,连ssc都先尝试从Checkpoint去拿,没有才创建。 Checkpoint 不单保存了DStream数据,还保存了元数据?
object StreamingState01 {
val checkpointDirectory = "./checkpoint" // 设置存放数据的目录
def main(args: Array[String]): Unit = {
val ssc = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext)
ssc.start()
ssc.awaitTermination()
}
def functionToCreateContext(): StreamingContext = {
val sparkConf = new SparkConf()
.setAppName("StreamingState01")
.setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(5)) // 处理的时间间隔
ssc.checkpoint(checkpointDirectory) // set checkpoint directory
val lines = ssc.socketTextStream("hadoop000", 9528)
lines.flatMap(_.split(","))
.map((_,1))
.updateStateByKey(updateFunction)
.print()
ssc
}
// newValues 当前的值, preValues 上一个值,可能没有,所以类型是Option
def updateFunction(newValues: Seq[Int], preValues: Option[Int]): Option[Int] = {
val curr = newValues.sum
val pre = preValues.getOrElse(0)
Some(curr + pre)
}
}
要想实现带状态的数据,其实输出到数据库就行,每一个批次都去查询修改。也能实现。还避免了小文件输出到文件系统。
output (action)
-
print 打印到终端
-
saveAsTextFiles 写到文件
-
saveAsHadoopFiles 写到文件系统
流处理的结果不适合生成到文件和文件系统,会全是小文件,每5秒一个 -
foreachRDD 通用的输出算子,输出到文件或DB都可以
把ss 的结果写出去,都用 foreachRDD完成
foreachRDD结果写到 mysql
注意,在 driver建立连接,而执行sql是在 excuter 的话,会报错 不能序列化, Connection 的确是没有序列化的,所以建立连接不能写在 driver。
而将 建立连接在 excuter,又太多连接了。
应该用 rdd.foreachPartition( ) , 在每一个分区中建立连接(分区太多时,反复创建连接开销仍然很大,更好的方式是创建连接池)
/**
*
* 每一个分区建立一次连接。partition.foreach()
*
* 两个优化的点:
* 每个分区创建连接,分区很多时开销还是大,应该使用连接池。
* 每一条数据执行一条sql。sql效率还是低,优化为批量insert:
*
* result 有2w条数据 ==> batch的方式 满1w条就写一次数据库
*/
def save2MySQL02(result:DStream[(String, Int)]): Unit = {
result.foreachRDD(rdd => {
rdd.foreachPartition(partition => { // 每个分区一个connection
val connection = ConnectionPool.getConnection// MySQLUtils.getConnection() // executor
logError("....." + connection)
partition.foreach(pair => {
val sql = s"insert into wc(word,cnt) values('${pair._1}', ${pair._2})"
connection.createStatement().execute(sql) // executor
})
// MySQLUtils.closeConnection(connection) // 用完关掉
ConnectionPool.returnConnection(connection)
})
})
}
foreachRDD结果写到 redis
导入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
def main(args: Array[String]): Unit = {
val ssc = ContextUtils.getStreamingContext("StreamingSocketWCApp01", 5, "local[2]")
val lines = ssc.socketTextStream("gargantua", 9527)
lines.flatMap(_.split(","))
.map((_, 1))
.reduceByKey(_ + _)
.foreachRDD(rdd => {
rdd.foreachPartition(partition => {
val jedis = RedisUtils.getJedis
jedis.select(13) // // Redis 默认有16个库,默认使用的是0号库。select(13) 切换到13号库
// wc : (String, Int)
partition.foreach(x => {
println("-_-----------------" + x._1 + "==>" + x._2)
jedis.hincrBy("streaming_redis_wc", x._1, x._2) // hash 相同的key会累加
})
jedis.close()
})
})
ssc.start()
ssc.awaitTermination()
}
注意
一旦启动了上下文,就不能设置或添加新的流计算。
一旦上下文被停止,就不能重新启动。
在JVM中同时只能有一个StreamingContext是活动的。
一个SparkContext可以被重用来创建多个StreamingContext,只要在创建下一个StreamingContext之前停止上一个StreamingContext(而不停止SparkContext)。
StreamingContext上的stop()也会停止SparkContext。想要仅停止StreamingContext,可以将名为stopSparkContext的stop()的可选参数设置为false。
streaming 转 DF、SQL
可以不再用DStream算子,而使用 SQL 来完成流处理。
- 获取SparkSession
- DStream 转 DF
- 把DF注册成视图
val words: DStream[String] = lines.flatMap(_.split(","))
words.foreachRDD(word => {
val spark = SparkSession.builder().config(word.sparkContext.getConf).getOrCreate()
import spark.implicits._ // toDF 需要导包
val wordDF = word.toDF("word") // DStream 转 DF
wordDF.createOrReplaceTempView("words") // 把DF注册成视图
spark.sql(
"""
|select
|word, count(1) cnt
|from
|words
|group by word
|""".stripMargin).show(false)
})
解析IP地址、经纬度
代码里发送http请求,可以使用 httpclient
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
地理/逆地理编码:提供结构化地址与经纬度之间的相互转化的能力
可以使用高德API: https://lbs.amap.com/api/webservice/guide/api/georegeo
第一步,申请”web服务 API”密钥(Key);
第二步,拼接HTTP请求URL,第一步申请的Key需作为必填参数一同发送;
第三步,接收HTTP请求返回的数据(JSON或XML格式),解析数据。
def main(args: Array[String]): Unit = {
val longtitude = 116.480881
val latitude = 39.989410
val url = s"https://restapi.amap.com/v3/geocode/regeo?output=json&key=${SecurityUtils.GAODE_KEY.trim}&location=$longtitude,$latitude"
var httpClient:HttpClient = null
var response: HttpResponse = null
try {
httpClient = HttpClients.createDefault() // 获取 httpClient
val httpGet = new HttpGet(url) // 新建一个GET请求
response = httpClient.execute(httpGet) // 发送这个请求,得到响应
val status = response.getStatusLine.getStatusCode
val entity = response.getEntity
var province = ""
if (200 == status) {
// 响应消息内容是json格式
val result = EntityUtils.toString(entity, "UTF-8")
val jSONObject = JSON.parseObject(result)
val regeocodes = jSONObject.getJSONObject("regeocodes")
if (null != regeocodes && !regeocodes.isEmpty) {
val addressComponent = regeocodes.getJSONObject("addressComponent")
province = addressComponent.getString("province")
println(province + "-------")
}
}
} catch {
case e:Exception => e.printStackTrace()
} finally {
// if(null != response) response.close
}
}
解析IP地址
IP定位是一套简单的HTTP接口,根据输入的IP地址,能够所在行政区划。