通过网络接收数据时(比如Kafka,Flume),会将数据反序列化,并存储在Spark的内存中。如果数据接收成为系统的瓶颈,可以考虑并行化数据接收。每个输入DStream都会在某个Worker的Executor上启动一个Receiver,该Receiver接收一个数据流。因此可以通过创建多个输入DStream,并配置它们接收数据源不同的分区数据,达到接收多个数据流的效果。
比如,一个接收两个Kafka Topic的输入DStream,可以拆分成两个输入DStream,每个分别接收一个topic的数据。这样就会创建两个Receiver,从而并行地接收数据,提高吞吐量。多个DStream可以使用union算子进行合并,从而形成一个DStream。后续的算子操作只需要针对合并之后的DSream即可。
代码示例:
int numStreams = 5; List for (int i = 0; i < numStreams; i++) { kafkaStreams.add(KafkaUtils.createStream(...)); } JavaPairDStream unifiedDStream.print(); |
数据接收并行度调优,除了创建更多输入DStream和Receiver以外,还可以调节block interval。通过参数spark.streaming.blockInterval,可以设置block interval,默认是200ms。
对于大多数Receiver而言,在将接收到的数据保存到Spark的BlockManager之前,都会将数据切分成一个一个的block。每个batch的block数量,决定了该batch对应的RDD的partition的数量,以及针对该RDD执行transformation操作时创建的task数量。每个batch对应的task的数量可以大约估算出来,即batch interval / block interval。
比如,batch interval为1s,block interval为100ms,则会创建10个task。如果每个batch的task数量太少,即低于每台机器的CPU Core,说明batch的task数量偏少,导致所有的CPU资源没有被完全利用起来。此时应该为batch增加block的数量,需要减小block interval。
但是,需要注意的是,推荐的block interval的最小值为50ms,如果低于这个值,那么大量的task的启动时间可能会变成性能的一个开销。
使用inputStream.repartition(
如果每秒钟启动的task过多,比如每秒启动50个,100个,那么发送这些task去Worker节点上的Executor的性能开销将会大大增加,可以使用下述操作减少这方面的性能开销:
如果在计算的任何stage中使用的并行task的数量没有足够多,那么集群资源是无法被充分利用的。
举例来说,对于分布式的reduce操作,比如reduceByKey和reduceByKeyAndWindow,默认的并行task的数量是由spark.default.parallelism参数决定的。也可以在reduceByKey等操作中,传入第二个参数,手动指定该操作的并行度,也可以调节全局的spark.default.parallelism参数。
数据序列化造成的系统开销可以由序列化格式的优化来减小。在流式计算的场景下,由两种类型的数据需要优化:
在上述的两个场景中,使用Kyro序列化类库可以减小CPU和内存的性能开销。使用Kyro时,一定要考虑注册自定义的类,并且禁用对应引用的tracking(spark.kyro.referenceTracking)。
在一些特殊的场景中,比如需要为流式应用保持的数据总量并不是很多,也许可以将数据以非序列化的方式进行持久化,从而减少序列化和反序列化的CPOU开销,而且又不会有太昂贵的GC开销。举例来说,如果设置的batch interval,并且没有使用window操作,那么可以通过显式地设置持久化级别,来禁止持久化对数据进行序列化。这样就可以减少用于序列化和反序列化的CPU性能开销,并且不用承担太多的GC开销。
如果想让一个运行在集群上的Spark Streaming应用程序可以稳定,就必须尽可能快地处理接收到的数据。换句话说,batch应该在生成之后,尽可能快地处理掉。对于一个应用来说,可以通过观察Spark UI上的batch处理时间来判断batch interval的设置是否合适。batch处理的时间必须小于等于batch interval的值。
给予流式计算的本质,在固定集群资源条件下,应用能保持的数据接收速率,batch interval的设置会有巨大的影响。例如,在WordCount例子中,对于一个特定的数据接收速率,应用业务可以保证每2秒打印一次单词计数,而不是每500ms。因此batch interval需要设置,让预期的数据接收速率可以在生产环境中保持住。
为应用计算合适的batch大小,比较好的方法是先设置一个很保守的batch interval,比如5s~10s,以很慢的数据接收速率进行测试。要检查应用是否跟得上这个数据速率,可以检查每个batch的处理时间的延迟,如果处理时间与batch interval基本吻合,那么应用就是稳定的。否则,如果batch调度的延迟持续增长,那么久意味着应用无法跟得上这个速率,就是不稳定的。此时可以提升数据处理的速度,或者增加batch interval,以保证应用的稳定。
注意,由于临时性的数据增长导致的暂时的延迟增长是合理的,只要延迟情况可以在短时间内回复即可。
Spark Streaming应用需要的集群内存资源,是由使用的transformation操作类型决定的。举例来说,如果想要使用一个窗口长度为10分钟的window操作,那么集群就必须有足够的内存来保存10分钟内的数据。如果想要使用uodateStateByKey来维护许多key的state,那么内存资源就必须足够大。反过来说,如果想要做一个简单的map-filter-store操作,那么需要使用的内存就很少。
通常来说,通过Receiver接收到的数据,会使用StorageLevel.MEMPRY_AND_DISK_SER_2持久化级别来进行存储,因此无法保存在内存中的数据就会溢写到磁盘上。而溢写到磁盘上,会降低应用的性能。因此,通常的建议是为应用提供它需要的足够的内存资源。
(建议在一个小规模的场景下测试内存的使用量,并进行评估)
内存调优的另一个方面是垃圾回收。对于流式应用来说,如果要获得低延迟,肯定不能有因为JVM垃圾回收导致的长时间延迟。有很多参数可以帮助降低内存使用和GC开销: