如何更新火花流中的广播变量?

时间:2015-10-27 15:38:29

标签: java scala apache-spark spark-streaming broadcast

我相信,我有一个相对常见的火花流用例:

我想根据一些参考数据过滤一系列对象

最初,我认为使用广播变量实现这是一件非常简单的事情:

public void startSparkEngine {
    Broadcast<ReferenceData> refdataBroadcast
      = sparkContext.broadcast(getRefData());

    final JavaDStream<MyObject> filteredStream = objectStream.filter(obj -> {
        final ReferenceData refData = refdataBroadcast.getValue();
        return obj.getField().equals(refData.getField());
    }

    filteredStream.foreachRDD(rdd -> {
        rdd.foreach(obj -> {
            // Final processing of filtered objects
        });
        return null;
    });
}

然而,虽然不经常,我的参考数据会定期更改

我的印象是我可以在驱动程序上修改并重新广播我的变量,它会传播给每个工作者,但是Broadcast对象不是{ {1}}并且需要Serializable

我有哪些替代品?我能想到的三个解决方案是:

  1. 将参考数据查找移动到finalforEachPartition,以使其完全驻留在工作人员身上。但是,参考数据存在于REST API中,因此我还需要以某种方式存储计时器/计数器以阻止对流中的每个元素访问远程数据。

  2. 每次refdata更改时,使用新的广播变量重新启动Spark上下文。

  3. 将参考数据转换为 RDD ,然后将forEachRdd流转换为我现在正在传输join的方式,尽管这将提供参考每个对象的数据。

6 个答案:

答案 0 :(得分:23)

通过@Rohan Aletty扩展答案。这是一个BroadcastWrapper的示例代码,它根据某些ttl

刷新广播变量
public class BroadcastWrapper {

    private Broadcast<ReferenceData> broadcastVar;
    private Date lastUpdatedAt = Calendar.getInstance().getTime();

    private static BroadcastWrapper obj = new BroadcastWrapper();

    private BroadcastWrapper(){}

    public static BroadcastWrapper getInstance() {
        return obj;
    }

    public JavaSparkContext getSparkContext(SparkContext sc) {
       JavaSparkContext jsc = JavaSparkContext.fromSparkContext(sc);
       return jsc;
    }

    public Broadcast<ReferenceData> updateAndGet(SparkContext sparkContext){
        Date currentDate = Calendar.getInstance().getTime();
        long diff = currentDate.getTime()-lastUpdatedAt.getTime();
        if (var == null || diff > 60000) { //Lets say we want to refresh every 1 min = 60000 ms
            if (var != null)
               var.unpersist();
            lastUpdatedAt = new Date(System.currentTimeMillis());

            //Your logic to refresh
            ReferenceData data = getRefData();

            var = getSparkContext(sparkContext).broadcast(data);
       }
       return var;
   }
}

您的代码如下:

public void startSparkEngine() {

    final JavaDStream<MyObject> filteredStream = objectStream.transform(stream -> {
        Broadcast<ReferenceData> refdataBroadcast = BroadcastWrapper.getInstance().updateAndGet(stream.context());

        stream.filter(obj -> obj.getField().equals(refdataBroadcast.getValue().getField()));
    });

    filteredStream.foreachRDD(rdd -> {
        rdd.foreach(obj -> {
        // Final processing of filtered objects
        });
        return null;
    });
}

这也适用于多群集。 希望这有帮助

答案 1 :(得分:6)

几乎每个处理流应用程序的人都需要一种方法来编织(过滤,查找等)参考数据(从数据库,文件等)到流数据。我们对整个两部分进行了部分解决方案

  1. 查找要在流操作中使用的参考数据

    • 使用所需的缓存TTL
    • 创建CacheLookup对象
    • 将它包装在Broadcast
    • 使用CacheLookup作为流逻辑的一部分
  2. 在大多数情况下,这项工作正常,但以下

    除外
    1. 更新参考数据

      尽管有这些主题中的建议,但是没有明确的方法来实现这一点,即:杀死先前的广播变量并创建新的变量。多个未知数,例如这些操作之间的预期。

    2. 这是一个常见的需求,如果有办法将信息发送到广播变量通知更新,它会有所帮助。这样,就可以使&#34; CacheLookup&#34;

      中的本地缓存无效

      问题的第二部分仍未解决。如果有任何可行的方法,我会感兴趣

答案 2 :(得分:3)

不确定您是否已经尝试过此功能,但我认为可以在不关闭SparkContext的情况下更新广播变量。通过使用unpersist()方法,广播变量的副本在每个执行器上被删除,并且需要是需要重新广播的变量才能再次访问。对于您的用例,当您想要更新广播时,您可以:

  1. 等待执行者完成当前的一系列数据

  2. 取消播放广播变量

  3. 更新广播变量

  4. 重新广播以将新参考数据发送给执行者

  5. 我从this post大量抽签,但做出最后一次回复的人声称已经让它在本地工作。重要的是要注意你可能想要在unpersist上设置阻塞到true,这样你就可以确保执行者摆脱旧数据了(所以过时的值不会被再次阅读)在下一次迭代)。

答案 3 :(得分:3)

最近与此有关。认为这可能对Scala用户有用。

BroadCastWrapper的标量处理方式类似于下面的示例。

import java.io.{ ObjectInputStream, ObjectOutputStream }
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.streaming.StreamingContext
import scala.reflect.ClassTag

/* wrapper lets us update brodcast variables within DStreams' foreachRDD
 without running into serialization issues */
case class BroadcastWrapper[T: ClassTag](
 @transient private val ssc: StreamingContext,
  @transient private val _v: T) {

  @transient private var v = ssc.sparkContext.broadcast(_v)

  def update(newValue: T, blocking: Boolean = false): Unit = {

    v.unpersist(blocking)
    v = ssc.sparkContext.broadcast(newValue)
  }

  def value: T = v.value

  private def writeObject(out: ObjectOutputStream): Unit = {
    out.writeObject(v)
  }

  private def readObject(in: ObjectInputStream): Unit = {
    v = in.readObject().asInstanceOf[Broadcast[T]]
  }
}

每次需要调用更新功能来获取新的广播变量。

答案 4 :(得分:0)

实现的最简单方法,下面的代码读取每个批次的维度数据文件夹,但请记住,新的维度数据值(在我的情况下为国家/地区名称)必须是一个新文件。

package com.databroccoli.streaming.dimensionupateinstreaming

import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.{DataFrame, ForeachWriter, Row, SparkSession}
import org.apache.spark.sql.functions.{broadcast, expr}
import org.apache.spark.sql.types.{StringType, StructField, StructType, TimestampType}

object RefreshDimensionInStreaming {

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

    @transient lazy val logger: Logger = Logger.getLogger(getClass.getName)

    Logger.getLogger("akka").setLevel(Level.WARN)
    Logger.getLogger("org").setLevel(Level.ERROR)
    Logger.getLogger("com.amazonaws").setLevel(Level.ERROR)
    Logger.getLogger("com.amazon.ws").setLevel(Level.ERROR)
    Logger.getLogger("io.netty").setLevel(Level.ERROR)

    val spark = SparkSession
      .builder()
      .master("local")
      .getOrCreate()

    val schemaUntyped1 = StructType(
      Array(
        StructField("id", StringType),
        StructField("customrid", StringType),
        StructField("customername", StringType),
        StructField("countrycode", StringType),
        StructField("timestamp_column_fin_1", TimestampType)
      ))

    val schemaUntyped2 = StructType(
      Array(
        StructField("id", StringType),
        StructField("countrycode", StringType),
        StructField("countryname", StringType),
        StructField("timestamp_column_fin_2", TimestampType)
      ))

    val factDf1 = spark.readStream
      .schema(schemaUntyped1)
      .option("header", "true")
      .csv("src/main/resources/broadcasttest/fact")

    var countryDf: Option[DataFrame] = None: Option[DataFrame]

    def updateDimensionDf() = {
      val dimDf2 = spark.read
        .schema(schemaUntyped2)
        .option("header", "true")
        .csv("src/main/resources/broadcasttest/dimension")

      if (countryDf != None) {
        countryDf.get.unpersist()
      }

      countryDf = Some(
        dimDf2
          .withColumnRenamed("id", "id_2")
          .withColumnRenamed("countrycode", "countrycode_2"))

      countryDf.get.show()
    }

    factDf1.writeStream
      .outputMode("append")
      .foreachBatch { (batchDF: DataFrame, batchId: Long) =>
        batchDF.show(10)

        updateDimensionDf()

        batchDF
          .join(
            countryDf.get,
            expr(
              """
      countrycode_2 = countrycode 
      """
            ),
            "leftOuter"
          )
          .show

      }
      .start()
      .awaitTermination()

  }

}

答案 5 :(得分:0)

我以不同的方式做到了。

我创建了一个广播变量并每 5 分钟在驱动程序的不同线程中更新它。

  var broadcastValue: Broadcast[Set[String]] = spark.sparkContext.broadcast(calculateValue())

  def runScheduledThreadToUpdateBroadcastVariable(): Unit = {
    val updateTask = new Runnable {
      def run() = {
        broadcastValue.unpersist(blocking = false)
        broadcastValue = spark.sparkContext.broadcast(calculateValue())
      }
    }

    val executor = new ScheduledThreadPoolExecutor(1)
    executor.scheduleAtFixedRate(updateTask, 1, 5, TimeUnit.MINUTES)
  }