Объединить результаты пакетного RDD с потоковым RDD в Apache Spark
контекст: Я использую Apache Spark для агрегирования количества различных типов событий из журналов. Журналы хранятся как в Кассандре для целей исторического анализа, так и в Кафке для целей анализа в реальном времени. В каждом журнале есть дата и тип события. Для простоты предположим, что я хотел отслеживать количество журналов одного типа за каждый день.
у нас есть два RDDs, RDD пакетных данных от Cassandra и другой потоковой 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();
вопрос:
как объединить результаты streamRDD с batchRDD?
Допустим, что batchRDD
имеет следующие данные, и это задание было выполнено в 2014-10-16:
("2014-10-15", 1000000)
("2014-10-16", 2000000)
поскольку запрос Cassandra включал только все данные до времени начала пакетного запроса, мы должны прочитать из Кафки, когда запрос будет завершен, только учитывая журналы после времени начала задания. Мы предполагаем, что запрос занимает много времени. Это означает, что мне нужно объединить исторические результаты с потоковыми результатами.
для примера:
|------------------------|-------------|--------------|--------->
tBatchStart tStreamStart streamBatch1 streamBatch2
тогда предположим, что в первом пакете потока мы получили эти данные:
("2014-10-19", 1000)
затем я хочу объединить пакет RDD с этим потоком RDD, чтобы поток RDD теперь имел значение:
("2014-10-19", 2001000)
предположим, что во втором пакете потока, мы получили такие данные:
("2014-10-19", 4000)
затем поток RDD должен быть обновлен, чтобы иметь значение:
("2014-10-19", 2005000)
и так далее...
можно использовать streamRDD.transformToPair(...)
для объединения данных streamRDD с данными batchRDD с помощью join
, но если мы сделаем это для каждого фрагмента потока, то мы будем добавлять счетчик из batchRDD для каждого фрагмента потока, делая значение состояния "double counted", когда оно должно быть добавлено только к первому фрагменту потока.
2 ответов
для решения этого случая я бы объединил базовый rdd с результатом агрегированного StateDStream
что держит итогам потоковых данных. Это эффективно обеспечивает базовый уровень для данных, сообщаемых на каждом интервале потоковой передачи, без подсчета указанных базовых x раз.
я попробовал эту идею, используя образец WordCount, и он работает. Отбросьте это на REPL для живого примера:
(использовать nc -lk 9876
на отдельной оболочке, чтобы обеспечить ввод в 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()
вы могли бы дать updateStateByKey
попробуй:
def main(args: Array[String]) {
val updateFunc = (values: Seq[Int], state: Option[Int]) => {
val currentCount = values.foldLeft(0)(_ + _)
val previousCount = state.getOrElse(0)
Some(currentCount + previousCount)
}
// stream
val ssc = new StreamingContext("local[2]", "NetworkWordCount", Seconds(1))
ssc.checkpoint(".")
val lines = ssc.socketTextStream("127.0.0.1", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val stateWordCounts = pairs.updateStateByKey[Int](updateFunc)
stateWordCounts.print()
ssc.start()
ssc.awaitTermination()
}