kafka和sparkstreaming是两种适配很好的技术,两者都是分布式系统适用于处理大量数据,两者对于实现数据的零丢失并没有提供现成的解决方案,所以这篇文章就是希望可以帮助你完成这个目标

注:使用Spark Streaming的Direct Stream方式连接kafka,并通过存储偏移量到zookeeper中,来实现数据零丢失,不要使用CheckPoints

问题介绍:Spark Streaming有两种方式连接kafka,官方知道文档 第一种方法,使用receiver,在并行度不是很理想,需要通过创建多个DStream来增加吞吐量。所以,大多数人会更倾向于使用在Spark1.3后出现的Direct Stream方式连接kafka,而放弃Receiver的方式使用Direct Stream方式连接kafka造成的主要问题是当应用崩溃后重启或者我们更新了应用后,如何保证数据不会丢失。也就是说,我们希望可以实现对每条信息的最少一次消费(消息一定不会丢失)而不是最多一次消费(消息会丢失)由此可知,无论我们做什么,我们都可能输出相同的结果很多次。因此,我们的输出存储时要保证幂等性(第二次的写入不会使得只有所改变)或者你应该能够处理下游作业中重复的值那就是说,如果你使用Direct Stream方法,你有两个方法去避免数据丢失

  • 使用CheckPoints
  • 时刻记录已经处理数据的偏移量

当你阅读Spark的官方文档时,你可能认为checkPoints是正确的方法,但是它有一些自身的问题,接下来让我们看一下这个方法

Spark CheckPoints

当你在Spark中使用Direct Stream的方式连接kafka时,Spark使用Kafka的简单消费者API,并且不会更新保存在Zookeeper中的偏移量,也就是说,当应用重启之后,他将从指定topic的消息队列的末尾开始消费,而在程序未运行期间,所生产的消息也不会被消费

如下可见,spark job 会消耗队列中的消息然后停止(已处理的消息标记为“o”,未处理的标记为“O”):

Initial instance stops here
|
v
-------------
|o|o|o|o|O|O|------------------>
-------------

新的spark job实例处理消息标记为“X”,但是有一些未处理的“O”被遗留

Initial instance stopped here
       |
       v
---------------------
|o|o|o|o|O|O|x|x|x|x|------------------>
---------------------
             ^
             |
             New instance starts here

CheckPoints是一种让Spark连接Kafka的过程中去存储它的当前状态并在应用重启之后可以读取存储的信息,然后回复之前的消费状态的方法

CheckPoints方法已经实现,使用时需要指定一个文件系统下的目录去保存Checkpoints要存储的信息,这个文件系统应该被多个节点所共享,如果当前节点挂了,另一个节点应该可以去读取指定目录下存储的CheckPoints信息,HDFS和S3都是很好的选择

第一个你将遇到的问题是你所有的处理操作都应该是序列化的,这是因为Spark不仅存储了其消费的offset,还保存了你序列化的流操作,这种约束是十分痛苦的,尤其是当你的操作依赖的第三方库是没有被编码允许序列化时。

假定你已经将你的所有的类已经序列化,那么你将遇到的第二个问题就是更新你的应用。因为Spark序列化你的操作,如果使用新版本的应用去读取之前版本下的Checkpoints信息,下面的事情将会发生:

  • 你应用程序将启动但将运行旧版本的应用程序(不会让你知道)
  • 或者你将收到反序列化错误,你的应用程序根本无法启动

解决办法是不重用检查点。相反,你必须在杀死第一个实例之前并行启动第一个实例

First instance running alone
     |
     v
-------------
|o|o|o|o|O|O|-------------------------->
-------------

然后让你启动第二个实例,从末尾开始处理作业

First instance running alone
         |
         v
-------------
|o|o|o|o|O|O|-------------------------->
-------------
            ^
            |
            Second instance ready to process from the end of the queue

两个实例并行处理消息(标记为“X”),第一个实例会读取那些“O”信息,第二个实例只接受新生产的消息

First instance still processing new messages
                    |
                    v
---------------------
|o|o|o|o|o|o|x|x|x|x|------------------>
---------------------
                    ^
                    |
                    Second instance processing new messages

然后,您就可以停止第一个实例

---------------------
|o|o|o|o|o|o|x|x|x|x|------------------>
---------------------
                    ^
                    |
                    Second instance processing new messages alone

这整个操作下来,最大的问题就是重复输出的问题,强化了对幂等存储或者处理重复的能力,另一个问题是,在推送升级时必须非常小心,不要过早停止以前版本的应用程序,否则会留下一些未处理的消息

还有一个问题时存储位置的选择,我们一般选择存储在HDFS或S3中,但要注意一个问题,那就是Spark保存这些信息是需要时间的,而spark是微批处理,一个批次的时间大概是60s,要注意时间,避免造成延迟

将偏移量写入Zookeeper

所以现在的问题是存储偏移量的位置。所选择的数据存储区应当是分布式并且具有高可用性,尽管出现故障

Zookeeper 是一个很好的选择,它是一个旨在可靠的存储小值的分布式存储空间,你执行的任何写操作都将在针对Zookeeper集群的事务中完成。由于我们只存储偏移量,因此我们存储的值非常小。

Zookeeper不是一个具有高吞吐量的存储空间。但是由于我们只是每批次去更新一下偏移量,最多每秒一次,所以zookeeper已经可以了

接下来是是实现细节,两个事情需要说明:

  • 我们使用 foreachRDD()的方法并返回初始DStream,以便您可以对其应用任何操作。Spark工作的方式,在完成批处理上的所有操作之前,他不会移动到下一批,这就意味着,如果您的操作需要比批处理处理间隔更长的时间,则下一的偏移量保存不会进行直到上一个批次的完成。你是安全的(但是这样会造成时延)
  • 我们使用和kafka相同的zookeeper客户端。这样,我们就不需要引入另一个依赖
  • 我们将创建一个Zookeeper的客户端实例。因为foreachRDD()方法会在Spark应用程序的Driver端被执行,它不会被序列化然后分发到各个Worker节点
  • 我们写一串字符串,其中包括每个partition的offset。他就像partition1:offset1,partition2:offset2…,意味着spark每个批处理记录一次offset就足够了

当你的应用程序在运行,你应该看到每个批次的日志,就像下面展示的样子

2016-05-04 14:11:29,004 INFO - Saving offsets to Zookeeper
2016-05-04 14:11:29,004 DEBUG - OffsetRange(topic: ‘DETECTION_OUTPUT’, partition: 0, range: [38114984 -> 38114984])
2016-05-04 14:11:29,005 DEBUG - OffsetRange(topic: ‘DETECTION_OUTPUT’, partition: 1, range: [38115057 -> 38115057])
2016-05-04 14:11:29,022 INFO - Done updating offsets in Zookeeper. Took 18 ms
然后,如果你重启应用程序,你将看到如下日志:

2016-05-04 14:12:29,744 INFO - Reading offsets from Zookeeper
2016-05-04 14:12:29,842 DEBUG - Read offset ranges: 0:38114984,1:38115057
2016-05-04 14:12:29,845 INFO - Done reading offsets from Zookeeper. Took 100 ms
到目前为止,这段代码被证明是可靠的,并且我也很高兴去分享它 Github