DStream上的输出操作

DStream上的输出操作

输出操作允许将DStream的数据推出到外部系统,例如数据库或文件系统。由于输出操作实际上允许外部系统使用转换后的数据,因此它们会触发所有DStream转换的实际执行(类似于RDD的操作)。当前,定义了以下输出操作:

输出操作

含义

print()

在运行流应用程序的驱动程序节点上,打印DStream中每批数据的前十个元素。这对于开发和调试很有用。
Python API在Python API中称为 pprint()

saveAsTextFiles(prefix, [suffix])

将此DStream的内容另存为文本文件。基于产生在每批间隔的文件名的前缀后缀“前缀TIME_IN_MS [.suffix]”

saveAsObjectFiles(prefix, [suffix])

将此DStream的内容保存为SequenceFiles序列化Java对象的内容。基于产生在每批间隔的文件名的前缀和 后缀“前缀TIME_IN_MS [.suffix]”
Python API这在Python API中不可用。

saveAsHadoopFiles(prefix, [suffix])

将此DStream的内容另存为Hadoop文件。基于产生在每批间隔的文件名的前缀后缀“前缀TIME_IN_MS [.suffix]”
Python API这在Python API中不可用。

foreachRDDfunc

最通用的输出运算符,将函数func应用于从流生成的每个RDD。此功能应将每个RDD中的数据推送到外部系统,例如将RDD保存到文件或通过网络将其写入数据库。请注意,函数func在运行流应用程序的驱动程序进程中执行,并且通常在其中具有RDD操作,这将强制计算流RDD。

 

 

使用print操作:

// nc -l -p 9999
import org.apache.spark._
import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.socketTextStream("192.168.79.138", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)
wordCounts.print()
ssc.start()

使用saveAsTextFiles操作

// nc -l -p 9999
import org.apache.spark._
import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.socketTextStream("192.168.79.138", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)
wordCounts.print()
wordCounts.saveAsTextFiles("/user/dataDirectory/saveAsTextFiles", "txt")
ssc.start()

使用saveAsObjectFiles操作

// nc -l -p 9999
import org.apache.spark._
import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.socketTextStream("192.168.79.138", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)
wordCounts.print()
wordCounts.saveAsObjectFiles("/user/dataDirectory/saveAsObjectFiles")
ssc.start()

使用saveAsHadoopFiles操作

import org.apache.spark._
import org.apache.spark.streaming._
 
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.TextOutputFormat;

val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.socketTextStream("192.168.79.138", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)
wordCounts.print()

wordCounts.saveAsHadoopFiles("/user/dataDirectory/", "txt", classOf[Text],classOf[IntWritable],classOf[TextOutputFormat[Text,IntWritable]]);
ssc.start();

使用foreachRDD的设计模式

dstream.foreachRDD是一个强大的原语,可以将数据发送到外部系统。但是,重要的是要了解如何正确有效地使用此原语。应避免的如下一些常见错误。

通常,将数据写入外部系统需要创建一个连接对象(例如,到远程服务器的TCP连接),并使用该对象将数据发送到远程系统。为此,开发人员可能会无意间尝试在Spark驱动程序中创建连接对象,然后尝试在Spark辅助程序中使用该对象以将记录保存在RDD中。例如(在Scala中),

  • Scala
  • Java
  • Python

dstream.foreachRDD { rdd =>

  val connection = createNewConnection()

  // executed at the driver

  rdd.foreach { record =>

connection.send(record) 

// executed at the worker

  }

}

例如:

hadoop fs -mkdir /user
hadoop fs -mkdir /user/dataDirectory
nc -l -p 9999
cd /usr/spark/spark-3.0.0/bin
spark-shell --driver-class-path ../examples/jars/mysql-connector-java-5.1.47-bin.jar --jars ../examples/jars/mysql-connector-java-5.1.47-bin.jar

import org.apache.spark.streaming._
import java.sql.DriverManager

val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.socketTextStream("192.168.79.138", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)

wordCounts.print()

wordCounts.foreachRDD(
  rdd=>{
    Class.forName("com.mysql.jdbc.Driver").newInstance 
    val connection = DriverManager.getConnection("jdbc:mysql://192.168.79.1:3306/spark?useUnicode=true&characterEncoding=utf8&useSSL=false", "root", "123456")    
    rdd.foreach(record => {
        val prep = connection.prepareStatement("INSERT INTO wordcount (word, count) VALUES (?, ?)")
        prep.setString(1, record._1.toString )
        prep.setLong(2,record._2)
        prep.executeUpdate
      })
    connection.close()
  }
)

ssc.start()

这是不正确的,因为这要求将连接对象序列化并从驱动程序发送给工作程序。这样的连接对象很少能在机器之间转移。此错误可能表现为序列化错误(连接对象不可序列化),初始化错误(连接对象需要在工作程序中初始化)等。正确的解决方案是在工作程序中创建连接对象。

但是,这可能会导致另一个常见错误-为每个记录创建一个新的连接。例如,

  • Scala
  • Java
  • Python

dstream.foreachRDD { rdd =>

  rdd.foreach { record =>

    val connection = createNewConnection()

    connection.send(record)

    connection.close()

  }

}

例如:

hadoop fs -mkdir /user
hadoop fs -mkdir /user/dataDirectory
nc -l -p 9999
cd /usr/spark/spark-3.0.0/bin
spark-shell --driver-class-path ../examples/jars/mysql-connector-java-5.1.47-bin.jar --jars ../examples/jars/mysql-connector-java-5.1.47-bin.jar

import org.apache.spark.streaming._
import java.sql.DriverManager

val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.socketTextStream("192.168.79.138", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)

wordCounts.print()

wordCounts.foreachRDD(
  rdd=>{
    rdd.foreach(record => {
        Class.forName("com.mysql.jdbc.Driver").newInstance 
		val connection = DriverManager.getConnection("jdbc:mysql://192.168.79.1:3306/spark?useUnicode=true&characterEncoding=utf8&useSSL=false", "root", "123456")    
		val prep = connection.prepareStatement("INSERT INTO wordcount (word, count) VALUES (?, ?)")
        prep.setString(1, record._1.toString )
        prep.setLong(2,record._2)
        prep.executeUpdate
		connection.close()
    })    
  }
)

ssc.start()

通常,创建连接对象会浪费时间和资源。因此,为每个记录创建和销毁连接对象会导致不必要的高开销,并且会大大降低系统的整体吞吐量。更好的解决方案是使用 rdd.foreachPartition-创建单个连接对象,并使用该连接在RDD分区中发送所有记录。

  • Scala
  • Java
  • Python

dstream.foreachRDD { rdd =>

  rdd.foreachPartition { partitionOfRecords =>

    val connection = createNewConnection()

    partitionOfRecords.foreach(record => connection.send(record))

    connection.close()

  }}

例如:

hadoop fs -mkdir /user
hadoop fs -mkdir /user/dataDirectory
nc -l -p 9999
cd /usr/spark/spark-3.0.0/bin
spark-shell --driver-class-path ../examples/jars/mysql-connector-java-5.1.47-bin.jar --jars ../examples/jars/mysql-connector-java-5.1.47-bin.jar

import org.apache.spark.streaming._
import java.sql.DriverManager

val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.socketTextStream("192.168.79.138", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)

wordCounts.print()

wordCounts.foreachRDD(
  rdd=>{
    rdd.foreachPartition(
    rddPartition=>{
      Class.forName("com.mysql.jdbc.Driver").newInstance 
      val connection = DriverManager.getConnection("jdbc:mysql://192.168.79.1:3306/spark?useUnicode=true&characterEncoding=utf8&useSSL=false", "root", "123456")
      rddPartition.foreach(record => {
        val prep = connection.prepareStatement("INSERT INTO wordcount (word, count) VALUES (?, ?)")
        prep.setString(1, record._1.toString )
        prep.setLong(2,record._2)
        prep.executeUpdate
      })
      connection.close()
    })
  }
)

ssc.start()

这将分摊许多记录上的连接创建开销。

最后,可以通过在多个RDD /批次之间重用连接对象来进一步优化。与将多个批次的RDD推送到外部系统时可以重用的连接对象相比,它可以维护一个静态的连接对象池,从而进一步减少了开销。

  • Scala
  • Java
  • Python

dstream.foreachRDD { rdd =>

  rdd.foreachPartition { partitionOfRecords =>

    // ConnectionPool is a static, lazily initialized pool of connections

    val connection = ConnectionPool.getConnection()

    partitionOfRecords.foreach(record => connection.send(record))

ConnectionPool.returnConnection(connection)  

// return to the pool for future reuse

  }

}

请注意,应按需延迟创建池中的连接,如果一段时间不使用,则超时。这样可以最有效地将数据发送到外部系统。

其他要记住的要点:

  •  

DStream由输出操作延迟执行,就像RDD由RDD操作延迟执行一样。具体来说,DStream输出操作内部的RDD动作会强制处理接收到的数据。因此,如果您的应用程序没有任何输出操作,或者dstream.foreachRDD()内部没有任何RDD操作,就不会执行任何输出操作。系统将仅接收数据并将其丢弃。

  •  

默认情况下,输出操作一次执行一次。它们按照在应用程序中定义的顺序执行。

你可能感兴趣的:(DStream上的输出操作)