将批处理RDD的结果与Apache Spark中的流式RDD相结合

Bob*_*bby 9 cassandra apache-kafka apache-spark spark-streaming

上下文: 我使用Apache Spark从日志中聚合不同事件类型的运行计数.日志存储在Cassandra中用于历史分析目的,Kafka用于实时分析目的.每个日志都有一个日期和事件类型.为简单起见,我们假设我想跟踪每天单个类型的日志数量.

我们有两个RDD,来自Cassandra的批量数据的RDD和来自Kafka的另一个流式RDD.伪代码:

CassandraJavaRDD<CassandraRow> cassandraRowsRDD = CassandraJavaUtil.javaFunctions(sc).cassandraTable(KEYSPACE, TABLE).select("date", "type");

JavaPairRDD<String, Integer> batchRDD = cassandraRowsRDD.mapToPair(new PairFunction<CassandraRow, String, Integer>() {
    @Override
    public Tuple2<String, Integer> call(CassandraRow row) {
        return new Tuple2<String, Integer>(row.getString("date"), 1);
    }
}).reduceByKey(new Function2<Integer, Integer, Integer>() {
    @Override
    public Integer call(Integer count1, Integer count2) {
        return count1 + count2;
    }
});

save(batchRDD) // Assume this saves the batch RDD somewhere

...

// Assume we read a chunk of logs from the Kafka stream every x seconds.
JavaPairReceiverInputDStream<String, String> kafkaStream =  KafkaUtils.createStream(...);
JavaPairDStream<String, Integer> streamRDD = kafkaStream.flatMapToPair(new PairFlatMapFunction<Tuple2<String, String>, String, Integer>() {
    @Override
    public Iterator<Tuple2<String, Integer> call(Tuple2<String, String> data) {
        String jsonString = data._2;
        JSON jsonObj = JSON.parse(jsonString);
        Date eventDate = ... // get date from json object
        // Assume startTime is broadcast variable that is set to the time when the job started.
        if (eventDate.after(startTime.value())) { 
            ArrayList<Tuple2<String, Integer>> pairs = new ArrayList<Tuple2<String, Integer>>();
            pairs.add(new Tuple2<String, Integer>(jsonObj.get("date"), 1));
            return pairs;
        } else {
            return new ArrayList<Tuple2<String, Integer>>(0); // Return empty list when we ignore some logs
        }
    }
}).reduceByKey(new Function2<Integer, Integer, Integer>() {
    @Override
    public Integer call(Integer count1, Integer count2) {
        return count1 + count2;
    }
}).updateStateByKey(new Function2<List<Integer>, Optional<List<Integer>>, Optional<Integer>>() {
    @Override
    public Optional<Integer> call(List<Integer> counts, Optional<Integer> state) {
        Integer previousValue = state.or(0l);
        Integer currentValue = ... // Sum of counts
        return Optional.of(previousValue + currentValue);
    }
});
save(streamRDD); // Assume this saves the stream RDD somewhere

sc.start();
sc.awaitTermination();
Run Code Online (Sandbox Code Playgroud)

问题: 如何将streamRDD的结果与batchRDD结合起来? 假设batchRDD有以下数据,这项工作是在2014-10-16运行的:

("2014-10-15", 1000000)
("2014-10-16", 2000000)
Run Code Online (Sandbox Code Playgroud)

由于卡桑德拉查询只包括了所有到批量查询的开始时间的数据,我们必须在完成查询,只考虑工作的开始时间后,从日志阅读卡夫卡.我们假设查询需要很长时间.这意味着我需要将历史结果与流式搜索结果相结合.

例如:

    |------------------------|-------------|--------------|--------->
tBatchStart             tStreamStart   streamBatch1  streamBatch2
Run Code Online (Sandbox Code Playgroud)

然后假设在第一个流批处理中我们得到了这些数据:

("2014-10-19", 1000)
Run Code Online (Sandbox Code Playgroud)

然后我想将批处理RDD与此流RDD组合在一起,以便流RDD现在具有以下值:

("2014-10-19", 2001000)
Run Code Online (Sandbox Code Playgroud)

然后假设在第二个流批处理中我们得到了这些数据:

("2014-10-19", 4000)
Run Code Online (Sandbox Code Playgroud)

然后应该更新流RDD以具有值:

("2014-10-19", 2005000)
Run Code Online (Sandbox Code Playgroud)

等等...

可以使用a streamRDD.transformToPair(...)将streamRDD数据与batchRDD数据结合使用join,但如果我们为每个流块执行此操作,那么我们将从batchRDD为每个流块添加计数,使状态值"双重计数",当它应该只添加到第一个流块.

maa*_*asg 5

为了解决这种情况,我将基础rdd与聚合的结果结合起来StateDStream,以保持流数据的总数.这有效地为每个流间隔报告的数据提供基线,而不计算所述基线x次.

我尝试使用示例WordCount的想法,它的工作原理.将其放在REPL上以获取实例:

(nc -lk 9876在单独的shell上使用以提供输入socketTextStream)

import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.StreamingContext._
import org.apache.spark.storage.StorageLevel

@transient val defaults = List("magic" -> 2, "face" -> 5, "dust" -> 7 )
val defaultRdd = sc.parallelize(defaults)

@transient val ssc = new StreamingContext(sc, Seconds(10))
ssc.checkpoint("/tmp/spark")

val lines = ssc.socketTextStream("localhost", 9876, StorageLevel.MEMORY_AND_DISK_SER)
val words = lines.flatMap(_.split(" "))
val wordCount = words.map(x => (x, 1)).reduceByKey(_ + _)
val historicCount = wordCount.updateStateByKey[Int]{(newValues: Seq[Int], runningCount: Option[Int]) => 
    Some(newValues.sum + runningCount.getOrElse(0))
}
val runningTotal = historicCount.transform{ rdd => rdd.union(defaultRdd)}.reduceByKey( _+_ )

wordCount.print()
historicCount.print()
runningTotal.print()
ssc.start()
Run Code Online (Sandbox Code Playgroud)

  • 谢谢.我只想补充一点,而不是在转换中使用`rdd.union(defaultRdd)`我最终使用`rdd.leftOuterJoin(defaultRdd)`,因此`runningTotal`不包括未更改的对.然后我只需要保存它们的值已经改变的对. (2认同)