Spark Streaming

Spark Streaming

官网介绍:https://spark.apache.org/streaming/

之前的学习:

批处理/离线处理【batch jobs】 --> Spark 更擅长

流计算/ 实时处理【streaming jobs】 --> Spark(ss 、sss)、FlinkStrom

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。

使用

  1. 导包
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-streaming_2.12</artifactId>
        <version>3.2.0</version>
    </dependency>
    
  2. 构建 ssc
    val sparkConf = new SparkConf().setAppName("StreamingSocketApp").setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf, Seconds(5))
    
  3. 接收数据
    val lines: ReceiverInputDStream[String] = ssc.socketTextStream("gargantua", 9527) 
    
    // socketTextStream也是在调用socketStream,再调用reciverSocketStream
    
    // textFileStream 和 fileStream 是不带reciver
    
  4. 业务逻辑
    // 流处理ssc 和 批处理RDD 算子的写法没有区别
    val value = lines.flatMap(_.split(",")).map((_, 1)).reduceByKey(_ + _)   
    println(value)
    
  5. 启动
    // 最后启动,启动后再添加的业务逻辑将不会执行
    ssc.start()   
    ssc.awaitTermination()
    
  6. 流处理不需要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)

  1. print 打印到终端

  2. saveAsTextFiles 写到文件

  3. saveAsHadoopFiles 写到文件系统
    流处理的结果不适合生成到文件和文件系统,会全是小文件,每5秒一个

  4. 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 来完成流处理。

  1. 获取SparkSession
  2. DStream 转 DF
  3. 把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地址,能够所在行政区划。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值