http://spark.apache.org/docs/latest/streaming-programming-guide.html
数据接受流程
启动Spark Streaming(后续缩写为SS)后,SS 会选择一台Executor 启动ReceiverSupervisor,并且标记为Active状态。接着按如下步骤处理:
-
ReceiverSupervisor会启动对应的Receiver(这里是KafkaReceiver)
-
KafkaReceiver 会根据配置启动新的线程接受数据,在该线程中调用
ReceiverSupervisor.store
方法填充数据,注意,这里是一条一条填充的。 -
ReceiverSupervisor
会调用BlockGenerator.addData
进行数据填充。
到目前为止,整个过程不会有太多内存消耗,正常的一个线性调用。所有复杂的数据结构都隐含在 BlockGenerator
中。
BlockGenerator 存储结构
BlockGenerator 会复杂些,这里有几个点,
-
维护了一个缓存
currentBuffer
,就是一个无限长度的ArrayBuffer。currentBuffer
并不会被复用,而是每次都会新建,然后把老的对象直接封装成Block,BlockGenerator会负责保证currentBuffer
只有一个。currentBuffer
填充的速度是可以被限制的,以秒为单位,配置参数为spark.streaming.receiver.maxRate
。这个是Spark内存控制的第一道防线,填充currentBuffer
是阻塞的,消费Kafka的线程直接做填充。 -
维护了一个
blocksForPushing
队列, size 默认为10个(1.5.1版本),可通过spark.streaming.blockQueueSize
进行配置。该队列主要用来实现生产-消费模式。每个元素其实是一个currentBuffer形成的block。 -
blockIntervalTimer 是一个定时器。其实是一个生产者,负责将
currentBuffer
的数据放到blocksForPushing
中。通过参数spark.streaming.blockInterval
设置,默认为200ms。放的方式很简单,直接把currentBuffer做为Block的数据源。这就是为什么currentBuffer不会被复用。 -
blockPushingThread
也是一个定时器,负责将Block从blocksForPushing
取出来,然后交给BlockManagerBasedBlockHandler.storeBlock
方法。10毫秒会取一次,不可配置。到这一步,才真的将数据放到了Spark的BlockManager中。
步骤描述完了,我们看看有哪些值得注意的地方。
currentBuffer
首先自然要说下currentBuffer,如果200ms期间你从Kafka接受的数据足够大,则足以把内存承包了。而且currentBuffer使用的并不是spark的storage内存,而是有限的用于运算存储的内存。 默认应该是 heap*0.4。除了把内存搞爆掉了,还有一个是GC。导致receiver所在的Executor 极容易挂掉,处理速度也巨慢。 如果你在SparkUI发现Receiver挂掉了,考虑有没有可能是这个问题。
blocksForPushing
blocksForPushing
这个是作为currentBuffer
和BlockManager之间的中转站。默认存储的数据最大可以达到 10*currentBuffer
大小。一般不打可能,除非你的 spark.streaming.blockInterval
设置的比10ms 还小,官方推荐最小也要设置成 50ms,你就不要搞对抗了。所以这块不用太担心。
blockPushingThread
blockPushingThread
负责从 blocksForPushing
获取数据,并且写入 BlockManager
。这里很蛋疼的事情是,blockPushingThread
只写他自己所在的Executor的 blockManager
,也就是每个batch周期的数据都会被 一个Executor给扛住了。 这是导致内存被撑爆的最大风险。 也就是说,每个batch周期接受到的数据最好不要超过接受Executor的内存(Storage)的一半。否则有你受的。我发现在数据量很大的情况下,最容易挂掉的就是Receiver所在的Executor了。 建议Spark-Streaming团队最好是能将数据写入到多个BlockManager上。
StorageLevel 的配置问题
另外还有几个值得注意的问题:
-
如果你配置成Memory_Disk ,如果Receiver所在的Executor一旦挂掉,你也歇菜了,整个Spark Streaming作业会失败。失败的原因是一部分block找不到了。
-
如果你配置成Memory_Disk_2,数据会被replication到不同的节点。一般而言不会出现作业失败或者丢数据。但解决不了Receiver也容易挂的问题,当然还是主要还是内存引起的。
-
最好是采用默认设置 MEMORY_AND_DISK_SER_2 比较靠谱些。
-
这里面还有一个风险点就是,如果某个batch processing延迟了,那么对应的BlockManager的数据不会被释放,然后下一个batch的数据还在进,也会加重内存问题。
动态控制消费速率以及相关论文
另外,spark的消费速度可以设置上限以外,亦可以根据processing time 来动态调整。通过 spark.streaming.backpressure.enabled
设置为true 可以打开。算法的论文可参考: Socc 2014: Adaptive Stream Processing using Dynamic Batch Sizing ,还是有用的,我现在也都开启着。
Spark里除了这个 Dynamic
,还有一个就是Dynamic Allocation
,也就是Executor数量会根据资源使用情况,自动伸缩。我其实蛮喜欢Spark这个特色的。具体的可以查找下相关设计文档。
从现在的API来看,是没有提供这种途径的。但是Spark Streaming 提供了同时读多个topic的功能,每个topic是一个InputStream。 我们可以复用这个功能,具体代码如下:
val kafkaDStreams = (1 to kafkaDStreamsNum).map { _ => KafkaUtils.createStream(
ssc,
zookeeper,
groupId,
Map("your topic" -> 1),
if (memoryOnly) StorageLevel.MEMORY_ONLY else StorageLevel.MEMORY_AND_DISK_SER_2)}
val unionDStream = ssc.union(kafkaDStreams)
unionDStream
kafkaDStreamsNum 是你自己定义的,希望有多少个Executor 启动Receiver 去接收kafka数据。我的经验值是 1/4 个Executors 数目。因为数据还要做replication 一般,所以这样内存最大可以占到 1/2 的storage.
另外,务必给你系统设置 spark.streaming.receiver.maxRate
。假设你启动了 N个 Receiver,那么你系统实际会接受到的数据不会超过 N*MaxRate,也就是说,maxRate参数是针对每个 Receiver 设置的。
减少非Storage 内存的占用
也就是我们尽量让数据都占用Spark 的Storage 内存。方法是把spark.streaming.blockInterval
调小点。当然也会造成一个副作用,就是input-block 会多。每个Receiver 产生的的input-block数为: batchInterval* 1000/blockInterval。 这里假设你的batchInterval 是以秒为单位的。 blockInterval 其实我也不知道会有啥影响。其实说白了,就是为了防止GC的压力。实时计算有一个很大问题是GC。
减少单个Executor的内存
一般在Spark Streaming中不建议把 Executor 的内存调的太大。对GC是个压力,大内存一FullGC比较可怕,很可能会拖垮整个计算。 多Executor的容错性也会更好些。