A more efficient version of the above reduceByKeyAndWindow() where the reduce value of each window is calculated incrementally using the reduce values of the previous window. This is done by reducing the new data that enters the sliding window, and “inverse reducing” the old data that leaves the window. An example would be that of “adding” and “subtracting” counts of keys as the window slides. However, it is applicable only to “invertible reduce functions”, that is, those reduce functions which have a corresponding “inverse reduce” function (taken as parameter invFunc). Like in reduceByKeyAndWindow, the number of reduce tasks is configurable through an optional argument. Note that checkpointing must be enabled for using this operation.
countByValueAndWindow(windowLength,slideInterval, [numTasks]) When called on a DStream of (K, V) pairs, returns a new DStream of (K, Long) pairs where the value of each key is its frequency within a sliding window. Like in reduceByKeyAndWindow, the number of reduce tasks is configurable through an optional argument. 2.5.4 关联操作 最终, 需要说明spark streaming 中支持的关联操作。 2.5.4.1 流与流数据之间joi (stream-stream 关联) 流数据可以很容易与其它流数据关联 val stream1: DStream[String, String] = ... val stream2: DStream[String, String] = ... val joinedStream = stream1.join(stream2) 在每批次数据时, RDD stream1 会和 RDD stream2 关联在一起。 当然也可以使用leftOuterJoin , rightOuterJoin , fullOuterJoin 等。进一步, 也可以和其它窗口流数据进行关联。 val windowedStream1 = stream1.window(Seconds(20)) val windowedStream2 = stream2.window(Minutes(1)) val joinedStream = windowedStream1.join(windowedStream2) 2.5.4.2 流与数据集关联 在前述文中已提过,使用DStream.transform 操作可以实现数据流与数据集关联,此处提及另一个窗口流与数据集关联。 val dataset: RDD[String, String] = ... val windowedStream = stream.window(Seconds(20))... val joinedStream = windowedStream.transform { rdd => rdd.join(dataset) } 事实上, 可以动态调整需要关联的数据集。 此处, transform 会作用于每批次数据流, 并且与此时dataset 所引用的对象进行关联操作。 完整的DStream 变换 API见相应文档: Scala API : http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.streaming.dstream.DStream 和 http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.streaming.dstream.PairDStreamFunctions。 java API : http://spark.apache.org/docs/latest/api/java/index.html?org/apache/spark/streaming/api/java/JavaDStream.html 和 http://spark.apache.org/docs/latest/api/java/index.html?org/apache/spark/streaming/api/java/JavaPairDStream.html, Python API : http://spark.apache.org/docs/latest/api/python/pyspark.streaming.html#pyspark.streaming.DStream 2.6 DStream 上的输出操作 输出操作允许DStream 数据推送到其它外部系统,如数据库或文件系统 。 输出操作允许变换后的数据被外围系统落地,这个操作会触发DStream 变换的执行(类似于RDD的action)。 当前, 只定义了以下输出 Output Operation Meaning print() Prints the first ten elements of every batch of data in a DStream on the driver node running the streaming application. This is useful for development and debugging. Python API This is called pprint() in the Python API. saveAsTextFiles(prefix, [suffix]) Save this DStream's contents as text files. The file name at each batch interval is generated based on prefix and suffix: "prefix-TIME_IN_MS[.suffix]". saveAsObjectFiles(prefix, [suffix]) Save this DStream's contents as SequenceFiles of serialized Java objects. The file name at each batch interval is generated based on prefix and suffix: "prefix-TIME_IN_MS[.suffix]". Python API This is not available in the Python API. saveAsHadoopFiles(prefix, [suffix]) Save this DStream's contents as Hadoop files. The file name at each batch interval is generated based on prefix and suffix: "prefix-TIME_IN_MS[.suffix]". Python API This is not available in the Python API. foreachRDD(func) The most generic output operator that applies a function, func, to each RDD generated from the stream. This function should push the data in each RDD to an external system, such as saving the RDD to files, or writing it over the network to a database. Note that the function func is executed in the driver process running the streaming application, and will usually have RDD actions in it that will force the computation of the streaming RDDs. 2.6.1 使用foreachRDD的模式 dstream.foreachRDD 经常用于推送到外围系统,因此,有必要介绍一下如何正确和高效地使用它, 以下错误要避免 经常将数据写到外围系统 ,需要创建一个连接对像(例TCP 连接到远端服务器) ,并使用此对象次数据发送出去。 出于这种应用,开发人员经常会动不动就在spark driver 侧创建连接对象, 并在worker 上使用它来保存RDD数据 。 例如, dstream.foreachRDD { rdd => val connection = createNewConnection() // executed at the driver rdd.foreach { record => connection.send(record) // executed at the worker } } 以上使用不正确的地方是: 这个连接对象会被序列化后,从driver 发送给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 DStream 的输出操作是lazy地, 就像RDD 在执行action 时 lazy 一样。 特别 , DStream 输出操作内嵌套的RDD action 操作强制要求接收到的数据都被处理。因此,应用中 没有输出操作,或者像 dstream.foreachRDD 这类输出操作内没有嵌套RDD action 操作, 此时DStream 不会执行任何操作。这样系统只是丢弃接收的数据,不做其它动作。 2 默认, 输出操作按应用定义的顺序 ,每次执行一次。 2.7 DataFrame 和 SQL 操作 在spark streaming 程序中可以使用dataframe 和 sql 。 需要先从StreamingContext 中取到SparkContext ,然后再创建一个SparkSession . 因为SparkSession 是单例且lazy 地 ,这样即使drvice 失败了,仍然可以重启 SparkSession 来继续程序。 下例仍然从SparkStreaming中接收数据 ,使用SQL 和Dataframe 来计算单词总数 。这样每个RDD转化为dataframe 。 注册为临时表并执行 SQL 。 /** DataFrame operations inside your streaming program */ val words: DStream[String] = ... words.foreachRDD { rdd => // Get the singleton instance of SparkSession val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate() import spark.implicits._ // Convert RDD[String] to DataFrame val wordsDataFrame = rdd.toDF("word") // Create a temporary view wordsDataFrame.createOrReplaceTempView("words") // Do word count on DataFrame using SQL and print it val wordCountsDataFrame = spark.sql("select word, count(*) as total from words group by word") wordCountsDataFrame.show() } 完整代码见: https://github.com/apache/spark/blob/v2.1.0/examples/src/main/scala/org/apache/spark/examples/streaming/SqlNetworkWordCount.scala 也可以对另一个线程中采集的Streaming data 上运行SQL , 只需要把另一个线程中采集到的数据注册为一个临时表。这样可以实现异步 执行StreamingContext 。 这里需要注意的地方,需要在StreamingContext 中设置(持续记住) 最大数据量执行所需要的时间。 否则,StreamingContext 并不知道异步SQL需要执行多长时间,那么很有可能在执行SQL结束前就把流数据清空。例如, 假设执行一批次最长需要5 分钟, 需要调用 streamingContext.remember(Minutes(5)) 。 Dataframe 和 SQL 部分详见 http://spark.apache.org/docs/latest/sql-programming-guide.html 2.8 MLlib 操作 也可以对streaming data 使用MLlib中提供的机器学习。 首先, 算法库中提供流式机器学习支持(如流式线性回归,流式K均值等), ,这些算法可以同时学习流数据,并将模型应用于流数据。除此而外,对于大型机器学习算法,建议使用离线学习和训练,然后将学习好的 模型应用于流式数据。 详见: http://spark.apache.org/docs/latest/ml-guide.html 2.9 缓存 和持久 与RDD相同,DStream 允许开发使用将流数据持久化在内存。调用persist() API 可以将DStream 每个RDD自动持久化到内存。 这样当流数据需要反复计算时,这样可以明显节省计算时间。 对于窗口计算(如reduceByWindow/ reduceByKeyAndWindow) 及状态更新操作(如 updateStateByKey),这个API 默认持久化到内存。 因此, 窗口DStream 会自动保存在内存中,不需要开发人员手动调用。 对于从网络中接收Input stream (如 kafka , flume , socket 等) , 默认持久化级别是 将流数据至少复制到两个节点,这样可以避免故障时数据丢失。 注意DStream 默认持久化级别是将数据序列化后存储在内存,这点不像RDD 。 这点会在性能 优化章节提及 。 其它存储级别可以参见spark programming guide 。 2.10 checkpoing 流式程序需要持续运行24 * 7 ,因此,不能让应用程序来解决快速故障恢复问题。因此, Spark Streaming 需要定期checkpoint 关键信息到容错存储上, 这样一旦故障发生时可以快速恢复。 有两类数据 checkpoint 方式: 1> 元数据checkpoint 。 将流计算中元数据存储在容灾的存储上,如HDFS。 这样一旦流应用的driver 节点出现故障后,可以快速恢复 driver 节点(后续章节详细讨论)。 元数据包含: i>> 配置 : 创建流应用所需要的配置文件 ii>> DStream 操作 : 流计算中涉及的DStream 操作 iii>> 未完成批次 : 已压入执行队列的批次任务,并且尚未完成。 2> 数据checkpoint 。将计算生成的RDD保存在容灾存储。 这种场景适用于横跨多个批次数据,并且需要前一批次状态信息的变换。 在这类变换中,新生成的RDD是依赖于前述RDD数据 ,这样就会导致每次计算时,需要依赖的批次数据链持续增长。为了避免在故障恢复时无止尽地回溯, 需要将这种有状态依赖的变换周期性保存到容灾存储上,以减少之间的依赖。 简言之,元数据checkpoint主要用于driver 的快速故障恢复,数据的checkpoint 主要用于有状态的变换出错快速恢复。 2.10.1 何时开启checkpoint 当应用程序有以下需求时,需要开启checkpoint i>> 有状态的变换 - 如果程序中使用updateStateByKey 或 reduceByKeyAndWindow ,那么程序中需要周期将RDD数据checkpoint 到容灾存储 ii>> 快速恢复driver 正在运行的程序 - 元数据 checkpoint 用于快速恢复进度信息 注意,不涉及前述流状态的变换,可以不使用checkpoint 。driver 故障恢复只部分适用于 checkpoint (接收到但未处理的数据会丢弃)。一般运行spark streaming 的应用大部分可以接受。未来会支持hadoop之外的容灾存储。 2.10.2 如何配置checkpoint 开启checkpoing 只需要配置一个容灾的文件系统,将信息周期存储起来,可以调用API StreamingContext.checkpoint(checkpointDirectory) 。 这样就可以使用 前述有状态变换了。额外 , 为了保证driver 能从故障中恢复, 你需要重写流应用来实现以下行为: 1> 当应用第一次启动,需要创建一个新的StreamingContext , 创建流数据并启动程序 2> 当应用重启后,需要用checkpoint目录数据重新创建一个StreamingContext 以上行为可以简单使用一个 StreamingContext.getOrCreate ,见下: // Function to create and setup a new StreamingContext def functionToCreateContext () : StreamingContext = { val ssc = new StreamingContext(...) // new context val lines = ssc.socketTextStream(...) // create DStreams ... ssc.checkpoint(checkpointDirectory) // set checkpoint directory ssc } // Get StreamingContext from checkpoint data or create a new one val context = StreamingContext . getOrCreate ( checkpointDirectory , functionToCreateContext _ ) // Do additional setup on context that needs to be done, // irrespective of whether it is being started or restarted context . ... // Start the context context . start () context . awaitTermination () 如果checkpointDirectory 目录存在,可以使用checkpoint数据重新创建context 。 如果checkpointDirectory 目录不存在(如第一次运行), functionToCreateContext 会重新创建一个新的context 并创建DStream 流。 见scala 例子 : https://github.com/apache/spark/tree/master/examples/src/main/scala/org/apache/spark/examples/streaming/RecoverableNetworkWordCount.scala 除了使用getOrCreate 方法外, 还需要保证driver 进程会在发生故障时自动重启。这个需要在应用程序架构来保证,详见 部署: http://spark.apache.org/docs/latest/streaming-programming-guide.html#deploying-applications 章节 注意,checkpoint RDD需要引入容灾存储的开销, 这样会导致处理checkpoint RDD数据的时长变长。因此,checkpoint的时间间隔需要仔细考虑。如果 设置过小,如1 秒, 频繁checkpoint 数据会降低计算的吞吐,相反,如果checkpoint 间隔太长,又会导致lineage 和任务数增长,也存在不利的影响。 当因为有状态变换而使用RDD checkpoint 时,默认的时间间隔控制在批次间隔的倍数,并且至少要大于10秒,这个值可以使用dstream.checkpoint(chekpointInterval)来设置。 一般, checkpoint的间隔在5~ 10秒 间变动都是合适的。 2.11 累加器, 广播变量和checkpoint 累加器和 广播变量 不能通过checkpoint 的方式恢复。如果同时使用,需要创建lazy 单例的累加器和广播变量,以便在driver 故障恢复后可以重新实例化。见下例 : object WordBlacklist { @volatile private var instance: Broadcast[Seq[String]] = null def getInstance(sc: SparkContext): Broadcast[Seq[String]] = { if (instance == null) { synchronized { if (instance == null) { val wordBlacklist = Seq("a", "b", "c") instance = sc.broadcast(wordBlacklist) } } } instance }} object DroppedWordsCounter { @volatile private var instance: LongAccumulator = null def getInstance(sc: SparkContext): LongAccumulator = { if (instance == null) { synchronized { if (instance == null) { instance = sc.longAccumulator("WordsInBlacklistCounter") } } } instance }} wordCounts.foreachRDD { (rdd: RDD[(String, Int)], time: Time) => // Get or register the blacklist Broadcast val blacklist = WordBlacklist.getInstance(rdd.sparkContext) // Get or register the droppedWordsCounter Accumulator val droppedWordsCounter = DroppedWordsCounter.getInstance(rdd.sparkContext) // Use blacklist to drop words and use droppedWordsCounter to count them val counts = rdd.filter { case (word, count) => if (blacklist.value.contains(word)) { droppedWordsCounter.add(count) false } else { true } }.collect().mkString("[", ", ", "]") val output = "Counts at time " + time + " " + counts }) 2.11 部署应用 这章节讨论如何部署spark streaming 应用程序 2.11.1 需求 为了运行spark streaming 应用,需要整理以下信息: 1> 集群管理 - spark 应用程序的基本需求,详见 部署 指南: http://spark.apache.org/docs/latest/cluster-overview.html 2> 打包应用程序 jar - 需要把streaming 应用打包成Jar 包,如果使用spark-submit 启动程序 ,应用不需要spark 和spark streaming 的jar 包。 但是如果使用额外的 数据源 (如kafka , flume ) , 需要把数据源组件所依赖的所有jar 包都部署到环境上。例如, 应用使用KafkaUtils ,就需要 包含spark-streaming-kafka-0-8_2.11 和 所有间接依赖的jar 。 3> executor 配置充足的内存 - 因为接收到的数据存储在内存,因此executor 需要配置走够的内存保存接收的数据 。注意, 如果窗口跨度10分钟, executor 内存需要保存最近10分钟的数据 。 所以,应用使用的内存需要考虑数据的操作。 4> 配置checkpoint - 如果应用需要checkpoint , 需要在hadoop api 兼容的容灾文件系统(如HDFS, S3等)上配置checkpoint目录,并且应用开发中要 利用这个目录用于故障恢复。 详见: http://spark.apache.org/docs/latest/streaming-programming-guide.html#checkpointing 5> 配置自动重启driver - 为了保障driver 故障自动重启, 流应用程序的架构需要监控driver 进程,在故障时适时重启。不同的集群管理可以通过不同的工具实现。 i>> spark standalone - spark 应用程序的driver 可以分配到spark standalone 集群内,即 , 应用程序driver 在 worker 节点运行。 进一步, spark standalone 集群的管理节点可以监控driver , 一旦driver 非正常退出 ,或运行driver 的节点失败, 管理节点会重启应用的driver 。 见: spark standalone 指南中 集群模式和管理模式 : http://spark.apache.org/docs/latest/spark-standalone.html ii>> YARN - yarn 同样支持自动重启应用。 详见 YARN 文档 iii>> Mesos - Marathon 框架用于实现在mesos上自动重启。 6> 配置WAL(write ahead logs) - spark 1.2 之后, 我们引入WAL 来实现容灾存储。如果 开启此选项, 所有接收器收到的数据,优先LOG之前(WAL) 写到checkpoint目录。 这样可以避免driver 恢复期间丢失数据 , 如此保证 0 数据丢失(详细见 : http://spark.apache.org/docs/latest/streaming-programming-guide.html#fault-tolerance-semantics) 。 可以配置spark.streaming.receiver.writeAheadLog.enable为 true (其它配置项见: http://spark.apache.org/docs/latest/configuration.html#spark-streaming) 尽管如此,此功能会牺牲部分接收器的吞吐量。为了提升整体的吞吐,需要使用多个接收器并行工作(见: http://spark.apache.org/docs/latest/streaming-programming-guide.html#level-of-parallelism-in-data-receiving) 。 此外,当开启WAL后,就不需要将spark 接收到的数据复制到其它节点, 因为WAL实际在某种程度上也是复制数据。因此, 需要将存储级别调整为 StorageLevel.MEMORY_AND_DISK_SER . 如果使用S3 来存储WAL, 记得开启spark.streaming.deiver.writeAheadLog.closeFileAfterWrite 和spark.streaming.receiver.writeAheadLog.closeFileAfterWrite , 详见: http://spark.apache.org/docs/latest/configuration.html#spark-streaming 7> 设置最大接收速率 - 如果集群资源不能满足流式应用程序处理数据的速率需要,那只能通过限定接收器的最大速率 records/sec 来平衡。详见: http://spark.apache.org/docs/latest/configuration.html#spark-streaming,接收器速率参数 : spark.streaming.reeceiver.maxRate 和 kafka 的限速率参数 spark.streaming.kafka.maxRatePerPartition 。 spark 1.5 中引入backpressure 特性,spark streaming 会自动计算速率并动态调整,这样就无需手工设置限定速率。 可能通过设置spark.streaming.backpressure.enable 为true 开启此选项。 2.11.1 升级应用代码 如果spark streaming 应用需要升级代码,以下是两个建议: 1> 升级后的spark streaming 应用程序可以原有应用程序并行运行。等到新程序验证无误并准备取代老程序时,此时可以把老程序停掉。 这样只需要把流数据源分别引到两个目的即可。 2> 老应用程序要gracefully 停止(详见streamingContext.stop(...) for gracefully shutdown 参数 ), 这样可以接收到的数据完全处理完。 这样新升级的应用程序可以启动,同时新程序会接上老程序需要处理的数据继续工作。需要注意,此处使用的数据源支持buffer (如kafka , flume) , 这样在老程序停止,新程序还没有启动间隙,数据会在buffer缓存下来。使用新程序从上一次checkpoing点启动 会报错,因为checkpoing点的数据包含 序列化的scala对象,新程序反序列化这些对象会报错。 这种情况,新应用程序可以使用新checkpoint 目录,或者在启动前删除之前checkpoing目录数据。 2.12 监控应用程序 除了spark 监控能力外, 有一些其它方法可以监控spark streaming 。当使用 StreamingContext 时, 在spark web UI 上可以显示streaming TAB 页, 此页可以查看当前正在接收器及相关统计信息(诸如接收器状态,接收到的记录数,接收到错误记录数等), 完成批次(每批次处理时间,队列延时等). 这些信息可以更好的掌握流应用程序的进度。 WEB UI中以下两个指标额外重要: 1) 处理时间 - 每批次数据处理时间 2) 等待时间 - 每批次在队列中等待运行的时间 如果批次的处理时间高于批次的时间间隔,和/或 排队等待时间持续增加, 那么说明系统没法及时处理当前的批次数据,渐渐批次数据会越堆越多,在这种情况, 需要想办法减少每批次处理时间: http://spark.apache.org/docs/latest/streaming-programming-guide.html#reducing-the-batch-processing-times Spark Streaming 应用程序执行的进度也可以通过 StreamingListener 接口来监控, 这个接口可以获取到接收器的状态和处理时间。 注意到这个暂时 处理于开发阶段的API , 日后此接口的功能会进一步扩展。 3 性能优化 优化集群上spark streaming 应用程序需要花费很多心思。 这章节给出部分可以优化spark streaming 应用的参数 。总观你的程序,程序员需要 首先考虑两件事; 1> 合理利用集群资源 ,以保证每批次数据可以及时处理 2> 设置合理的批数据大小,这样才能保证接收器处理数据,与接收新数据基本平衡。 3.1 降低批次数据处理时间 减少spark 处理每批次数据的时间,有多种优化的方法, 详见优化指南: http://spark.apache.org/docs/latest/tuning.html 本章节只列出重要的内容 3.1.1 数据接收的并行级别 从网络中接收数据 (如kafka , flume , socket等)需要反序列化成原始数据,然后存储在spark 内存中处理。如果接收数据成为瓶颈, 就需要考虑提升接收并行度。 注意,每个DStream 源创建一个接收器(运行在一台worker 主机), 这个接收器只接收单一流数据。为了同时接收多个数据流,需要创建多个DStreams 数据源,并且配置每个接收器接收数据流不同的分区。例如, 单个kafka input dstream 接收两个topic 的数据, 可以拆分为两个kafka input dstream, 每个kafka input dstream 只接收一个topic 。 这样虽然需要创建两个接收器,但却保证数据可以同时并行接收, 即提升了网络 的吞吐。 可以把多个input dstream 联合在一起创建成一个dstream , 这样可以让变换同时操作多个input dstream 。 见下: val numStreams = 5 val kafkaStreams = (1 to numStreams).map { i => KafkaUtils.createStream(...) } val unifiedStream = streamingContext.union(kafkaStreams) unifiedStream.print() 另一个优化的参数是接收器的block 间隔(block interval ), 即 spark.streaming.blockInterval 见: http://spark.apache.org/docs/latest/configuration.html#spark-streaming 大多数接收器,会把接收到的数据一起组成一个块数据,然后再存储在spark 内存中。每批次数据的块数,决定了处理接收数据的变换会起多少个task 。 每个接收器每批次生成的任务数, 近似于 batch interval / block interval 。 例如, block interval 是200 ms , 2 秒会创建10个task 。 如果上面这个任务数太小(如相对于单台主机的核心数), 这样主机的CPU没有充分利用。为了提高每批次的任务数,需要缩短block interval . 尽管这样,建议最小block interval 大约在50ms ,这个值低于50 会导致任务频繁起动。 另一种提高接收数据流并发的方法,就是显示重新对输入数据流进行分区 inputStream.repartition 分区数。 这样会对接收到的数据在计算前,重新在多台主机间分布。 3.1.2 数据处理的并行级别 如果每个stage 所运行的并行task数过低,会导致集群资源利用率不高。例如, 在使用reduceByKey 和 reduceByKeyAndWindow 时,每个stage 并行task数由 spark.default.parallelism 配置项控制 ,见: http://spark.apache.org/docs/latest/configuration.html#spark-properties 这时通过PairDStreamFunctions 来设置并行级别,或者重新设置spark.default.parallelism 的默认值,可以修改这个并行task 数。 3.1.3 数据序列化 数据序列化的性能损耗可以优化序列化格式来减少,在流数据中,经常使用以下两种序列化: 1> input data(输入数据) : 默认,接收器收到输入数据后,在executor 内存中存储的级别是 StorageLevel.MEMORY_AND_DISK_SER_2 。 这样,数据会序列化成字节流来降低 GC 消耗, 同时备份到另一台主机降低executor 失败对任务的影响。同时, 数据优先保存在内存中,只有当 内存不够用的情况,才会将数据保存到本地磁盘。这样序列化过程中性能有损耗 -- 接收器需要先反序列化接收到的数据,然后再将数据序列化为 spark 支持的格式。 2> 持久化流操作生成的RDD : streaming 计算生成的RDD持久会在内存。 例如,窗口操作的数据由于要多次计算,会把数据持续化在内存, 但却不像spark core 中默认的 StorageLevel.MEMORY_ONLY, 实际默认存储级别是StorageLevel.MEMORY_ONLY_SER ,这样序列化后的数据存储在 内存中,可以减少GC损耗 。 在上面两种情况,使用kryo 序列化可以减少CPU和内存的消耗。参见spark 优化指南: http://spark.apache.org/docs/latest/tuning.html#data-serialization 对于kryo , 需要考虑注册自定义类,禁止跟踪 对象的引用(object reference tracking ) , 见 kryo 相关的配置项 在特定的例子, 如当流计算操作的数据量并不大时, 使用上述两种序列化是方便的,这样可以降低反序列化时GC消耗。例如, 每批次间隔时间较长(a few seconds), 且不使用窗口操作, 可以显示设置存储级别来禁止持久化数据时序列化过程。 由于序列化时会带来CPU的性能损耗, 减少GC的消耗可以提供性能 。 3.1.4 任务频繁启动消耗 如果每秒启动的任务次数太多(如每秒50个以上), 频繁将任务发送给worker 节点执行的损耗就变得显著,应用程序很难达到秒级延迟。 通过调整以下项可以降低这种损耗 1> 执行模式(execution mode) : 让spark 运行在standalone 或mesos 的粗放化资源管理的模式,会比mesos精细化资源管理的模式更 节省任务启动时间,大约每批次的节省时间相差几百毫秒 。 。详见: http://spark.apache.org/docs/latest/running-on-mesos.html 3.2 恰当的批次间隔 为了使spark streaming 应用运行稳定, 系统需要保证处理数据和接收数据同步。换句话说,处理数据的时间需要能赶上甚至超过源头生成数据 的时间。在streaming web UI 界面上可以持续监控数据处理时间,需要保证这个时间小于批数据采集间隔。 当集群资源一定的条件下,流计算批次的间隔完全由数据生成的速率决定。 例如, 以前面wordcountnetwork 为例, 在特定的数据生成速率下, 系统可以保证按每2秒报告一次结果,而不可能是500毫秒。因此,在生产环境需要设置合适的批次间隔,才能保证跟上数据生成的速率 。 在设置批次时间间隔时,首先配置一个相对保守的时间间隔(如5~10秒),然后逐渐将间隔缩短。为了验证系统能否跟上数据生成的进度,需要 检查一下每批次数据的处理时延(可以查看spark driver 的"Total delay", 或者StreamingListener 接口 )。如果系统的时延持续与批次数据生成 的时间相当,系统可以稳定运行。否则, 如果批次数据计算的延时持续增长,这说明系统没法及时计算生成的数据,这样系统会很不稳定。一旦 有一组保守的配置,可以慢慢提升数据生成的速率 和/或 降低每批次数据量。 注意,数据生成的速率加快后,每批次计算的时延会暂时变长,因此, 需要运行一段时间等计算时延稳定后,观察新的配置组合是否能使系统稳定。 3.2.1 内存优化 内存优化,spark 应用的GC优化详见: http://spark.apache.org/docs/latest/tuning.html#memory-tuning 。 建议详细阅读一下上文中内容,会对优化spark streaming 应用程序有很大的帮助。 spark streaming 应用使用的集群内存,很大因素由数据使用的变换所决定。 例如,如果想使用window 操作计算最后10分钟数据, 这样,要求集群有足够的内存保存近10分钟的数据。 或者如果对大量的keys使用updateStateByKey操作, 这样内存使用就会很高。 相反,如果只想作一些简单的过滤-落地操作,实际使用的内存就很低。 通常,接收器收到的数据存储级别 StorageLevel.MEMORY_AND_DISK_SER_2 , 这样内存放不下时,会将数据写到磁盘。这样会 降低streaming程序的性能 ,因此最好保证集群的内存足够运行程序 。建议先用小量数据计算得到内存使用,以此评估大数据量时 内存真实使用情况。 内存优化的另一方面是GC , 对于流计算的应用程序,要求计算的时延很低,因此JVM GC 引起长时间程序暂停是不希望看到地。 以下有几个参数可以优化内存的使用,减少GC的消耗。 1> DStream 持久化级别 : 在前述数据序列化章节: http://spark.apache.org/docs/latest/streaming-programming-guide.html#data-serialization , 输入数据和RDD默认要序列化为字节,相比于反序列化数据再持久,这样可以降低内存使用和GC 消耗,开启kryo序列化可以进一步降低序列后大小 和内存占用。如果想进一步降低内存占用,需要使用压缩(见spark 配置 spark.rdd.compress),但会消耗CPU的时间。 2> 清除老数据 : 默认, DStream 变换生成的RDD和所有输入数据是自动清除地。 spark 根据变换的时效,spark streaming 决定哪些数据需要清除。 例如, 假设使用窗口操作操作最近10分钟, spark streaming 会保存最后10分钟的数据, 并将之前的数据 清除。 如果想保存更长的时间,需要设置 streamingContext.remember 。 3> CMS GC : 强烈推荐使用并发 标记-清除 地GC, 这样可以保证GC 相关的应用停止时间相对变短。 尽管并发GC 可以降低系统 处理的吞吐, 却可以保证系统处理批数据的连续。 需要确保 CMS GC 在 driver (使用 --driver-java-options 在spark-submit) 和 executor (使用 spark.executor.etraJavaOptions 配置) 4> 其它提示 : 为了进一步降低GC消耗, 需要做以下尝试 使用 OFF_HEAP 存储级别来持久化RDD , 详见 : http://spark.apache.org/docs/latest/programming-guide.html#rdd-persistence 尽量使用更多的executor ,同时保证executor 使用小HEAP size , 这样会降低JVM heap 的GC压力。 3.2.2 重要点: 1> 每个DStream 关联一个接收器。为了在多个接收器上保持读取数据的并发,例如,需要创建多个DStream 。 每个接收器 在一个executor上运行, 占用主机一个CPU核心,需要保证占用一个CPU核心外, 主机仍然有其它核心来处理数据, 例如, spark.cores.max 需要考虑接收器占用的CPU核心。 接收器在executor 间 round robin 方式。 2> 从流数据源接收到的数据 , 接收器会为他们创建一个数据块,每隔 blockInterval 毫秒后,接收器会生成新的数据块。 executor 在每一个批次时间间隔,共要创建N 个数据块,其中 N = batchInterval / blockInterval 。 这些数据块被当前executor 的BlockManager 分发给其它 executor 的BlockManager 。 至此, driver 上运行的网络输入跟踪器 知晓 需要进一步处理的块地址。 3> 在批次时间间隔,driver 会用executor 上的数据块创建RDD。 或者说,这些数据块实际就是RDD的分区。 每个分区对应一个task 。 如果blockInterval == batchInterval , 那么RDD只有一个分区,而且很有可能task 会本地执行。 4> executors(其中一个executor 负责接收块,另一个负责块复制 ) 上执行块的map task , 可以保证executor 上的块与block interval 无法。 除非 使用非本地调度。 blockInterval 时间越长,意味着数据块更大。同时增加spark.locality.wait 以提高本地节点处理块的机率。 这两个参数之间需要平衡,以保证大的数据块可以在本地执行。 5> 除了依赖batchInterval 和blockInterval , 也可以重新分配 输入数据分区 inputDStream.repartition(n)。 此接口会重要随机合并 RDD 中数据, 创建有n个分区的RDD。 为了提升并发度,即使合并需要消耗时间,仍然建议使用repartition 。 driver 的jobsheduler 调度 job , 每个job 会处理 RDD数据。 任意时间点上,只有一个job是活跃地,因此, 如果当前有一个job 在执行时,其它job会排队。 6> 如果有两个DStream, 对应会有两种RDD格式,会生成两个job 并且顺序执行。为了避免顺序执行,需要将两个DStream 联合在一起。 这样可以保证单个 unionRDD 可以满足两种格式的RDD, 同时只生成一个job ,且不影响RDD的分区。 7> 如果批数据处理的时间比batchInterval 还要长,显示接收器的内存会最终填满,接收器会抛异常而终止(很可能是BlockNotFoundException) . 当前还没有办法可以暂停接收器。 使用sparkConf 配置,spark.streaming.receiver.maxRate ,可以限制接收器的速率。 4 故障恢复 在本章节,我们会讨论spark streaming 应用程序的故障恢复相关内容。 4.1 背景 为了理解spark streaming 的故障恢复的语义,需要先了解spark RDD 故障恢复的语义。 1> RDD 是不可变常量, 确定可以反复计算,分布式的数据集。每一个RDD是由一组操作序列生成的,因此,RDD数据的恢复也依赖于 容灾输入数据和这个操作 序列。 2> 如果某个RDD的分区数据由于worker节点失败而丢失, 这个分区数据可以对容灾的输入数据,作用于操作的序列来恢复。 3> 假设所有RDD的变换是确定的, RDD变换得到的结果始终如一,不受spark 集群失败的影响。 spark 支持的容灾文件有HDFS和 S3 . 因此, 由容灾数据所生成的RDD数据 ,一样也具体容灾特性。然而 ,这并不是spark streaming 通用的应用场景, 因为spark streaming 的数据是通过网络采集而来。为了让spark streaming 生成的RDD也具有容灾特性,通常会让 接收到的数据复制到集群worker 节点的多个executor上(默认系数是2)。 这样在发生故障时,就有两种数据需要恢复: 1 接收并复制的数据 - 单个worker 节点失败不会影响数据,因此在另一个worker 节点有一个备份 2 接收到的数据,准备复制 -因为数据还没有来得及复制,因些只能从数据源做数据恢复 进一步,也有两种故障类型需要格外 注意 1 worker 节点失败 - 任意一个运行executor的worker 节点都有可能失败, 这样节点上的所有内存中数据会丢失。如果接收器恰好 在失败的节点上运行, 那么buffer中的数据会丢失。 2 driver 节点失败 - 如果运行spark streaming 应用的driver 节点失败,很显然 SparkContext会丢失, 这样所有executor内存中数据 会丢失。 有了以上知识, 就可以更好的理解 spark streaming 的故障恢复语义。 4.2 定义 流系统的关键指标是: 系统处理一条记录需要多少时间 。 在基本场景下: 系统提供了三种消息传送保障机制: 1> 最多一次 : 每条记录至多处理一次 2> 至少一次 : 每条记录至少处理一次。 本类型虽然不会丢数据,但是会导致数据重复处理 3> 恰好一次 : 每条记录恰好处理一次 - 无数据丢失及数据重复处理, 这是三者中最强的保障机制 4.3 基础语义 在任意一个流处理的平台, 处理数据需要三步走: 1> 接收数据 : 从接收器或其它途径接收数据 2> 变换数据 : 对接收到的数据使用DStream或RDD变换 3> 外推数据 : 最终结果外推给其它系统,如文件系统,数据库或数据盘等 如果流应用程序想获得点到点的恰好只处理一次的保障,这样每一步都需要提供恰好只处理一次的保障。也就是说,每条记录恰好接收 一次,恰好变换一次,恰好外推一次。下面再仔细了解一下spark streaming 的语义 1> 接收数据 : 不同的数据源提供不同的保障机制,在下一章节会详细说明 2> 变换数据 : 所有接收到的数据恰好被处理一次, 归功于RDD提供的保障。 尽管有失败,只要接收到的数据可读,还是可以把最终 结果恢复 3> 外推数据 : 外推操作默认使用至少一次语义,因为它依赖于输入操作的类型(是否支持幂等)和下游系统的语义(是否支持事务)。 但开发人员可以自己实现事务机制来保障恰好一次语义。 本章节后续会详细讲解。 4.4 接收数据语义 不同的数据源提供不同的保障机制,从至少一次到恰好一次,可参考 其它内容 4.4.1 文件数据源 如果输入文件是存储在容灾文件系统,如HDFS, spark streaming 可以从失败中恢复,并还原所有结果。这样可以支持恰好一次 保障机制,意味着即使系统失败,也能保证所有数据恰好只处理一次,不重不漏。 4.4.2 基于接收器的源 基于接收器的输入源,容灾语义依赖于失败的场景 和接收器类型。在前述讨论的,有两类接收器: 1> 可靠接收器 - 可以保证接收到的数据都被复制成功,此时接收器通知可靠数据源发送下一批数据。如果接收器失败,数据源不会 收到通知消息。 因此, 如果接收器重启了,数据源需要重新发送数据,这样才能保证无数据丢失。 2> 不可靠接收器 - 接收器不会向数据源发送确认消息,因此,接收器可能会由于worker 或driver 的失败导致丢数据。 使用不同的接收器,可以获得不同的语义。 可靠接收器: 即使worker 节点失败了,仍然不会丢数据 ;不可靠接收器 : 接收到数据如果 没有复制完成,此时worker 失败会导致数据丢失。如果driver节点失败,除了以上的数据会丢失,在内存中接收到的数据和复制完成的 数据也会丢失。这会景程有状态的变换。 为了避免接收到的历史数据丢失,spark 1.2引入WAL(write ahead logs ), WAL会将接收到的数据保存到容灾存储。当开启WAL和 可靠接收器, 数据不会丢失。这样来看,可以提供恰好只一次的保障机制。 下表总结了故障的语义。 Deployment Scenario Worker Failure Driver Failure Spark 1.1 or earlier, OR Spark 1.2 or later without write ahead logs Buffered data lost with unreliable receivers Zero data loss with reliable receivers At-least once semantics Buffered data lost with unreliable receivers Past data lost with all receivers Undefined semantics Spark 1.2 or later with write ahead logs Zero data loss with reliable receivers At-least once semantics Zero data loss with reliable receivers and files At-least once semantics 4.4.3 kafka direct API spark 1.3 中引入了新的kafka direct API , 新API可以保证所有从kafka 传输的数据,在spark streaming 中支持恰好只一次。除了 这个,如果实现了恰好只一次输出操作, 开发人员可以获得点到点地恰好只一次的保障机制,即从输入数据源,变换到外推全程恰好只 一次。 详见kafka 集成指南: http://spark.apache.org/docs/latest/streaming-kafka-integration.html 。 4.5 输出操作语义 输出操作(类似foreachRDD) 支持至少一次语义,也就是,变换后的数据会写到外部系统 ,假设出现worker 失败的情况,允许 落地至少一次。可以简单地将输出数据写到saveAsXXXFiles文件系统里(因为文件会自动覆盖)。 有两种方式可以实现恰好只一次的语义。 1> 幂等更新 : 幂等更新是指不管输出成功几次,最终都可以得到同一个结果。例如, saveAsXXXFiles 总是生成同一个文件。 2> 事务更新 : 更新操作支持事务,这样可以实现恰好只有一次语义。其中一种实现方式是: 使用批次时间(foreachRDD中支持)和RDD的分区索引来创建标识, 这个标识唯一确定流应用中的一个blob 使用这个标识更新外部系统的blob事务(即,自动实现恰好只一次)。 即, 如果这个标识的数据还没有提交, 应用程序会自动提交标识以及分区数据,否则, 如果已经提交,乎略更新。 dstream.foreachRDD { (rdd, time) => rdd.foreachPartition { partitionIterator => val partitionId = TaskContext.get.partitionId() val uniqueId = generateUniqueId(time.milliseconds, partitionId) // use this uniqueId to transactionally commit the data in partitionIterator } } 5 where to go from here (后续内容,有兴趣请自己查阅原文) Additional guides Kafka Integration GuideKinesis Integration GuideCustom Receiver Guide Third-party DStream data sources can be found in Third Party ProjectsAPI documentation Scala docs StreamingContext and DStreamKafkaUtils, FlumeUtils, KinesisUtils, Java docs JavaStreamingContext, JavaDStream and JavaPairDStreamKafkaUtils, FlumeUtils, KinesisUtils Python docs StreamingContext and DStreamKafkaUtils More examples in Scala and Java and PythonPaper and video describing Spark Streaming.