DStream上的输出操作
输出操作允许将DStream的数据推出到外部系统,例如数据库或文件系统。由于输出操作实际上允许外部系统使用转换后的数据,因此它们会触发所有DStream转换的实际执行(类似于RDD的操作)。当前,定义了以下输出操作:
输出操作 | 含义 |
print() | 在运行流应用程序的驱动程序节点上,打印DStream中每批数据的前十个元素。这对于开发和调试很有用。 |
saveAsTextFiles(prefix, [suffix]) | 将此DStream的内容另存为文本文件。基于产生在每批间隔的文件名的前缀和后缀:“前缀TIME_IN_MS [.suffix]”。 |
saveAsObjectFiles(prefix, [suffix]) | 将此DStream的内容保存为SequenceFiles序列化Java对象的内容。基于产生在每批间隔的文件名的前缀和 后缀:“前缀TIME_IN_MS [.suffix]”。 |
saveAsHadoopFiles(prefix, [suffix]) | 将此DStream的内容另存为Hadoop文件。基于产生在每批间隔的文件名的前缀和后缀:“前缀TIME_IN_MS [.suffix]”。 |
foreachRDD(func) | 最通用的输出运算符,将函数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中),
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()
这是不正确的,因为这要求将连接对象序列化并从驱动程序发送给工作程序。这样的连接对象很少能在机器之间转移。此错误可能表现为序列化错误(连接对象不可序列化),初始化错误(连接对象需要在工作程序中初始化)等。正确的解决方案是在工作程序中创建连接对象。
但是,这可能会导致另一个常见错误-为每个记录创建一个新的连接。例如,
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分区中发送所有记录。
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推送到外部系统时可以重用的连接对象相比,它可以维护一个静态的连接对象池,从而进一步减少了开销。
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操作,就不会执行任何输出操作。系统将仅接收数据并将其丢弃。
默认情况下,输出操作一次执行一次。它们按照在应用程序中定义的顺序执行。