输出操作允许将Dstream的数据推送到外部系统,例如数据库或文件系统。由于输出操作实际上允许外部系统使用变换后的数据,所以他们触发所有DStream变换的实际执行(类似于RDD的action操作)。目前定义了以下输出操作:
使用foreachRDD的设计模式
dstream.freachRDD是一个强大的语句,允许将数据发送到外部系统。但是,了解如何正确有效地使用这个语句很重要。下面提供以下常见的错误:
通常向外部系统写入数据需要创建一个连接对象(例如远程服务器的TCP连接)并使用它将数据发送到运城系统。为此,开发人员可能会无意中尝试在Spark 的Driver中创建连接对象,然后尝试在Spark Worker中使用它来在RDD中保存记录。例如:
dstream.foreachRDD { rdd =>
val connection = createNewConnection() // executed at the driver
rdd.foreach { record =>
connection.send(record) // executed at the worker
}
}
这是不正确的,因为这需要将连接对象序列化并从驱动程序发送到工作人员。这样的连接对象很少能跨机器传输。此错误可能会显示为序列化错误(连接对象不可序列化),初始化错误(连接对象需要在Worker初始化)等。正确的解决方案是在Worker创建连接对象。
但是,这可能会导致另一个常见的错误,为每个记录创建一个新的连接。例如:
dstream.foreachRDD { rdd =>
rdd.foreach { record =>
val connection = createNewConnection()
connection.send(record)
connection.close()
}
}
通常,创建连接对象具有时间和资源开销。因此,创建和销毁每个记录的连接对象可能会引起不必要的高开销,并可显著降低系统的总体吞吐量。一个更好的解决方案是使用rdd.foreachPartition-创建一个连接对象,并使用该连接对象在RDD分区中发送所有记录。
dstream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
val connection = createNewConnection()
partitionOfRecords.foreach(record => connection.send(record))
connection.close()
}
}
这样可以在多个记录上分摊连接创建开销。
最后,可以通过跨多个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
}
}
请注意,池中的连接应根据需要是懒加载,如果一段时间不使用,则会超时。这实现了最有效的将数据发送到外部系统。
其他需要记住的点
1.DStreams通过输出操作进行延迟执行,就像RDD由RDD action操作进行延迟执行。具体来说,DStream输出操作中的RDD动作强制处理接收到的数据。因此,如果你的应用程序没有任何输出操作,或者具有dstream.foreach()等输出操作,而在其中又没有任何RDD操作,则不会执行任何操作。系统将简单的接收数据并将其丢弃。
2.默认情况下,输出操作是一个一个执行的。他们按照他们在应用程序中定义的顺序执行。