spark学习之旅(4)之Streaming的使用

Spark Streaming类似于Apache Storm,用于流式数据的处理。所谓流式处理其实指的就是实时数据,之前的spark都是处理离线数据的,就是直接处理数据文件,而streaming是一直检测数据,数据出来一条,处理一条。根据其官方文档介绍,Spark Streaming有高吞吐量和容错能力强等特点。Spark Streaming支持的数据输入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如:map、reduce、join、window等进行运算。而结果也能保存在很多地方,如HDFS,数据库等。另外Spark Streaming也能和MLlib(机器学习)以及Graphx完美融合。

Spark Streaming使用离散化流(discretized stream)作为抽象表示,叫作DStream。DStream 是随时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为 RDD 存在,而 DStream 是由这些 RDD 所组成的序列(因此 得名“离散化”)。

DStream 可以从各种输入源创建,比如 Flume、Kafka 或者 HDFS。创建出来的DStream 支持两种操作,一种是转化操作(transformation),会生成一个新的DStream,另一种是输出操作(output operation),可以把数据写入外部系统中。DStream 提供了许多与 RDD 所支持的操作相类似的操作支持,还增加了与时间相关的新操作,比如滑动窗口。

Stream的简单使用

我们首先创建代码用于发送消息

// 主要按照套接字发送数据
object CreateData {
    
    
  def main(args: Array[String]): Unit = {
    
    
    // 通过套接字发送数据
    val listener = new ServerSocket(9888)
    while(true){
    
    
      val socket = listener.accept()
      new Thread(){
    
    
        override def run() = {
    
    
          println("Got client connected from :"+ socket.getInetAddress)
          val out = new PrintWriter(socket.getOutputStream,true)
          while(true){
    
    
            Thread.sleep(1000)
            val context1 = "张三~李四~王五~张三"
            out.write(context1 + '\n')
            out.flush()
          }
          socket.close()
        }
      }.start()
    }
  }
}

然后创建stream用于接收参数

streaming代码接收并处理,

这里注意:每个接收器都以 Spark 执行器程序中一个长期运行的任务的形式运行,因此会占据分配给应用的 CPU 核心。此外,我们还需要有可用的 CPU 核心来处理数据。这意味着如果要运行多个接收器,就必须至少有和接收器数目相同的核心数,还要加上用来完成计算所需要的核心数。

如果我们想要在流计算应用中运行 10 个接收器,那么至少需要为应用分配 11 个 CPU 核心

简单点讲就是最好不使用local或者local[1]

object StreamingDemo {
    
    

  def main(args: Array[String]): Unit = {
    
    
    // Seconds 数据间隔的时间
    val ssc = new StreamingContext(new SparkConf()
      .setAppName("stream")
      .setMaster("local[2]"), Seconds(10))

    ssc.sparkContext.setLogLevel("WARN")

    // 设置检查点
    ssc.checkpoint("./check")

    val data = ssc.socketTextStream("localhost",9888)

    data.print()

    ssc.start()
    ssc.awaitTermination()
  }

}

在上述的代码中,可以看出Streaming本身是采用“微批次”的架构,就是把流处理转变成一系列的连续的批处理对待

常用算法

Stream中常用的方法大致分为转换方法和输出方法,转换方法分为无状态转换和有状态转换。

普通转换方法

常用算法和RDD中的算法差不多,不同的是由原来的RDD类型换成了DStream罢了。

方法名 具体作用
map(func) 将源DStream中的每个元素通过一个函数func从而得到新的DStreams。
flatMap(func) 和map类似,但是每个输入的项可以被映射为0或更多项。
filter(func) 选择源DStream中函数func判为true的记录作为新DStreams
repartition(numPartitions) 通过创建更多或者更少的partition来改变此DStream的并行级别
union(otherStream) 联合源DStreams和其他DStreams来得到新DStream
count() 统计源DStreams中每个RDD所含元素的个数得到单元素RDD的新DStreams
reduce(func) 通过函数func(两个参数一个输出)来整合源DStreams中每个RDD元素得到单元素RDD的DStreams。
reduceByKey(func, [numTasks]) 对(K,V)对的DStream调用此函数,返回同样(K,V)对的新DStream,但是新DStream中的对应V为使用reduce函数整合而来。注意:默认情况下,这个操作使用Spark默认数量的并行任务(本地模式为2,集群模式中的数量取决于配置参数spark.default.parallelism)。你也可以传入可选的参数numTaska来设置不同数量的任务。
join(otherStream, [numTasks]) 两DStream分别为(K,V)和(K,W)对,返回(K,(V,W))对的新DStream。
cogroup(otherStream, [numTasks]) 两DStream分别为(K,V)和(K,W)对,返回(K,(Seq[V],Seq[W])对新DStreams

有状态的转换UpdateStateByKey

在streaming中有时候需要进行跨批次处理数据,比如统计所有批次中单词出现数量。updateStateByKey() 提供了对一个状态变量的访问,用于键值对形式的 DStream。给定一个由(键,事件)对构成的 DStream,并传递一个指定如何根据新的事件 更新每个键对应状态的函数,它可以构建出一个新的 DStream,其内部数据为(键,状态) 对。

我们以一个单词统计的例子来使用这个方法,具体代码:

def main(args: Array[String]): Unit = {
    
    

    // 创建对象
    val ssc = new StreamingContext(
      new SparkConf()
        .setAppName("streaming")
        .setMaster("local[2]"),Seconds(5)
    )

    ssc.checkpoint("./check")

    val data = ssc
      .socketTextStream("127.0.0.1",9888)

    // 计算每个人的数量
    val names = data.flatMap(
      line => {
    
    
        val name = line.split("~")
        name
      }
    ).map((_,1))

    val result = names.updateStateByKey[Int](
      (values:Seq[Int],state:Option[Int]) => {
    
    
        // (当前key值的集合,之前累加的状态) => { 返回值 }
        // 获得之前状态中的数据
        var count = state.getOrElse(0)
        // 遍历当前批次中的数据
        for(value <- values){
    
    
          // 累加
          count += value
        }
        // Some:Option子类,表示有值
        Some(count)
      }
    )

    result.print()

    ssc.start()
    ssc.awaitTermination()
  }
streaming存入数据库

数据库的存取需要借助JDBC处理,接下来以词频统计为例展示如何存入数据库。数据库的存储需要先创建表

CREATE TABLE `word` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `key` varchar(255) DEFAULT NULL,
  `value` int(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8;

然后执行JDBC存储

def main(args: Array[String]): Unit = {
    
    
    // StreamingContext(spark配置,时间间隔)
    val ssc = new StreamingContext(
      new SparkConf().setAppName("streaming")
        .setMaster("local[2]"),Seconds(3)
    )
    ssc.sparkContext.setLogLevel("ERROR")

    // 设置检查点 streaming是24*7
    ssc.checkpoint("./check_port")

    println("这里是Streaming")

    val data = ssc.socketTextStream("127.0.0.1",9888)

    val mapData = data
      .flatMap(_.split("~"))
      .map((_,1)) // (张三,1) , (李四,1)

    // state 状态,此时数据
    // updateStateByKey[返回值中数据的类型]
    val result = mapData.updateStateByKey[Int](
      // values => 此批次中相同key的值集合
      // key是张三的数据,放在values (1,1,1...)
      // state => 之前这个key计算的数据(状态) (张三,12)
      (values:Seq[Int],state:Option[Int]) => {
    
    
        var count = state.getOrElse(0)
        for(v <- values){
    
    
          count += v
        }
        Some(count)
      }
    )

    result.print()
    result.foreachRDD(
      item => {
    
    
        if(!item.isEmpty()){
    
     // 判断RDD是否为空
          item.foreach{
    
     // 遍历RDD中数据,分别取出存入mysql
            case(key,count) => {
    
    
              // 获得连接
              val connection = getConnection()
              /* 定义SQL,注意word数据库需要提前创建 */
              val sql = "insert into `word` (`key`,`value`) values (?,?)"
              /* 创建prepareStatement对象,用于执行SQL */
              val state = connection.prepareStatement(sql)
              /* 占位符(?的位置)插入数据 */
              state.setString(1,key)
              state.setInt(2,count)
              /* 执行SQL */
              state.execute()
            }
          }
        }
      }
    )

    ssc.start()
    ssc.awaitTermination()

  }

  def getConnection() : Connection = {
    
    
    Class.forName("com.mysql.jdbc.Driver")
    DriverManager.getConnection(
      "jdbc:mysql://127.0.0.1:3306/ssm",
      "root","root")
  }
streaming窗口方法

基于时间顺序批量处理数据流,所以引入另外一个概念,时间窗口,后面简称窗口,这个窗口有点像一个批次内的所有数据

每一个窗口是用窗口长度和 滑动间隔定义,两者都必须是 StreamContext 的批次间隔的整数倍,窗口长度指的是获取该时间长度内接受的RDD,滑动间隔指的是每隔多少秒去获得数据

这里是个简单的窗口例子,数据生产者还是上面的代码


def main(args: Array[String]): Unit = {
    
    

    // 创建对象
    val ssc = new StreamingContext(
      new SparkConf()
        .setAppName("streaming")
        .setMaster("local[2]"),Seconds(5)
    )

    ssc.sparkContext.setLogLevel("ERROR")
    ssc.checkpoint("./stream_checkpoint")

    val data = ssc
      .socketTextStream("127.0.0.1",9888)

    // 创建一个15秒窗口时间,5秒间隔时间的窗口
    val winData = data.window(Seconds(15),Seconds(5))
    winData.print()

    ssc.start()
    ssc.awaitTermination()
  }

常用方法

方法名称 方法描述
window(窗口长度, 间隔) 基于对源DStream窗化的批次进行计算返回一个新的DStream
countByWindow(窗口长度, 间隔) 返回一个滑动窗口计数流中的元素。
reduceByWindow(func, windowLength, slideInterval) 通过使用自定义函数整合滑动区间流元素来创建一个新的单元素流。
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) 当在一个(K,V)对的DStream上调用此函数,会返回一个新(K,V)对的DStream,此处通过对滑动窗口中批次数据使用reduce函数来整合每个key的value值。**注意:**默认情况下,这个操作使用Spark的默认数量并行任务(本地是2),在集群模式中依据配置属性(spark.default.parallelism)来做grouping。你可以通过设置可选参数numTasks来设置不同数量的tasks。

猜你喜欢

转载自blog.csdn.net/lihao1107156171/article/details/115587995