sparkstreaming关于偏移量的管理

  1. 在 Direct DStream初始化的时候,需要指定一个包含每个topic的每个分区的offset用于让Direct DStream从指定位置读取数据。

    • offsets就是步骤4中所保存的offsets位置
  2. 读取并处理消息
  3. 处理完之后存储结果数据
    • 用虚线圈存储和提交offset只是简单强调用户可能会执行一系列操作来满足他们更加严格的语义要求。这包括幂等操作和通过原子操作的方式存储offset。
  4. 最后,将offsets保存在外部持久化数据库如 HBase, Kafka, HDFS, and ZooKeeper中

SparkStreaming使用checkpoint存在的问题

SparkStreaming在处理kafka中的数据时,存在一个kafka offset的管理问题:

  • 官方的解决方案是checkpoint:

    • checkpoint是对sparkstreaming运行过程中的元数据和

      每次rdds的数据状态保存到一个持久化系统中,当然这里面也包含了offset,一般是HDFS,S3,如果程序挂了,或者集群挂了,下次启动仍然能够从checkpoint中恢复,从而做到生产环境的7*24高可用。如果checkpoint存储做hdfs中,会带来小文件的问题。

但是checkpoint的最大的弊端在于,一旦你的流式程序代码或配置改变了,或者更新迭代新功能了,这个时候,你先停旧的sparkstreaming程序,然后新的程序打包编译后执行运行,会出现两种情况:

  • (1)启动报错,反序列化异常
  • (2)启动正常,但是运行的代码仍然是上一次的程序的代码。

为什么会出现上面的两种情况?

  • 这是因为checkpoint第一次持久化的时候会把整个相关的jar给序列化成一个二进制文件,每次重启都会从里面恢复,但是当你新的

    程序打包之后序列化加载的仍然是旧的序列化文件,这就会导致报错或者依旧执行旧代码。有的同学可能会说,既然如此,直接把上次的checkpoint删除了,不就能启动了吗? 确实是能启动,但是一旦你删除了旧的checkpoint,新启动的程序,只能从kafka的smallest或者largest的偏移量消费,默认是从最新的,如果是最新的,而不是上一次程序停止的那个偏移量

    就会导致有数据丢失,如果是老的,那么就会导致数据重复。不管怎么样搞,都有问题。

    https://spark.apache.org/docs/2.1.0/streaming-programming-guide.html#upgrading-application-code

针对这种问题,spark官网给出了2种解决办法:

(1)旧的不停机,新的程序继续启动,两个程序并存一段时间消费。 评价:仍然有丢重复消费的可能

(2)停机的时候,记录下最后一次的偏移量,然后新恢复的程序读取这个偏移量继续工作,从而达到不丢消息。 评价:官网没有给出具体怎么操作,只是给了个思路:自己存储offsets,

  • Your own data store

For data stores that support transactions, saving offsets in the same transaction as the results can keep the two in sync, even in failure situations. If you’re careful about detecting repeated or skipped offset ranges, rolling back the transaction prevents duplicated or lost messages from affecting results. This gives the equivalent of exactly-once semantics. It is also possible to use this tactic even for outputs that result from aggregations, which are typically hard to make idempotent.

  1. #Java
  2. // Th#e details depend on your data store, but the general idea looks like this
  3. // begin from the the offsets committed to the database
  4. Map<TopicPartition, Long> fromOffsets = new HashMap<>();
  5. for (resultSet : selectOffsetsFromYourDatabase)
  6. fromOffsets.put(new TopicPartition(resultSet.string("topic"), resultSet.int("partition")), resultSet.long("offset"));
  7. }
  8. JavaInputDStream<ConsumerRecord<String, String>> stream = KafkaUtils.createDirectStream(
  9. streamingContext,
  10. LocationStrategies.PreferConsistent(),
  11. ConsumerStrategies.<String, String>Assign(fromOffsets.keySet(), kafkaParams, fromOffsets)
  12. );
  13. stream.foreachRDD(rdd -> {
  14. OffsetRange[] offsetRanges = ((HasOffsetRanges) rdd.rdd()).offsetRanges();
  15. Object results = yourCalculation(rdd);
  16. // begin your transaction
  17. // update results
  18. // update offsets where the end of existing offsets matches the beginning of this batch of offsets
  19. // assert that offsets were updated correctly
  20. // end your transaction
  21. });

思路就在这段伪代码中:数据存储支持事务,在事务中更新结果和偏移量,确认偏移量正确更新。

  1. // begin your transaction
  2. // update results
  3. // update offsets where the end of existing offsets matches the beginning of this batch of offsets
  4. // assert that offsets were updated correctly
  5. // end your transaction

SparkStreaming管理kafka中offsets的几种方式

SparkStreaming管理kafka中offsets,就是将offsets采用某种数据格式存储在某个地方,一般有如下几种方式:

1. 存储在kafka

Apache Spark 2.1.x以及spark-streaming-kafka-0-10使用新的的消费者API即异步提交API。你可以在你确保你处理后的数据已经妥善保存之后使用commitAsync API(异步提交 API)来向Kafka提交offsets。新的消费者API会以消费者组id作为唯一标识来提交offsets

将offsets提交到Kafka中

  1. stream.foreachRDD { rdd =>
  2. val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
  3. // some time later, after outputs have completed
  4. stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
  5. }

注意: commitAsync()是Spark Streaming集成kafka-0-10版本中的,在Spark文档提醒到它仍然是个实验性质的API并且存在修改的可能性

2. 存储在zookeeper

kafka消费者的偏移量本身就是存储在zookeeper中,在sparkstreaming中,需要在启动时,显示的指定从zookeeper中读取偏移量即可,参考代码如下:

step1: 初始化Zookeeper connection来从Zookeeper中获取offsets

  1. val zkClientAndConnection = ZkUtils.createZkClientAndConnection(zkUrl, sessionTimeout, connectionTimeout)
  2. val zkUtils = new ZkUtils(zkClientAndConnection._1, zkClientAndConnection._2, false)
  3. Method for retrieving the last offsets stored in ZooKeeper of the consumer group and topic list.
  4. def readOffsets(topics: Seq[String], groupId:String):
  5. Map[TopicPartition, Long] = {
  6. val topicPartOffsetMap = collection.mutable.HashMap.empty[TopicPartition, Long]
  7. val partitionMap = zkUtils.getPartitionsForTopics(topics)
  8. // /consumers/<groupId>/offsets/<topic>/
  9. partitionMap.foreach(topicPartitions => {
  10. val zkGroupTopicDirs = new ZKGroupTopicDirs(groupId, topicPartitions._1)
  11. topicPartitions._2.foreach(partition => {
  12. val offsetPath = zkGroupTopicDirs.consumerOffsetDir + "/" + partition
  13. try {
  14. val offsetStatTuple = zkUtils.readData(offsetPath)
  15. if (offsetStatTuple != null) {
  16. LOGGER.info("retrieving offset details - topic: {}, partition: {}, offset: {}, node path: {}", Seq[AnyRef](topicPartitions._1, partition.toString, offsetStatTuple._1, offsetPath): _*)
  17. topicPartOffsetMap.put(new TopicPartition(topicPartitions._1, Integer.valueOf(partition)),
  18. offsetStatTuple._1.toLong)
  19. }
  20. } catch {
  21. case e: Exception =>
  22. LOGGER.warn("retrieving offset details - no previous node exists:" + " {}, topic: {}, partition: {}, node path: {}", Seq[AnyRef](e.getMessage, topicPartitions._1, partition.toString, offsetPath): _*)
  23. topicPartOffsetMap.put(new TopicPartition(topicPartitions._1, Integer.valueOf(partition)), 0L)
  24. }
  25. })
  26. })
  27. topicPartOffsetMap.toMap
  28. }

step2: 使用获取到的offsets来初始化Kafka Direct DStream

  1. val inputDStream = KafkaUtils.createDirectStream(ssc, PreferConsistent, ConsumerStrategies.Subscribe[String,String](topics, kafkaParams, fromOffsets))
  • 用于将 可恢复的偏移量 持久化到zookeeper的方法。
  1. #注意: Kafka offset在ZooKeeper中的存储路径为/consumers/[groupId]/offsets/topic/[partitionId], 存储的值为offset
  2. def persistOffsets(offsets: Seq[OffsetRange], groupId: String, storeEndOffset: Boolean): Unit = {
  3. offsets.foreach(or => {
  4. val zkGroupTopicDirs = new ZKGroupTopicDirs(groupId, or.topic);
  5. val acls = new ListBuffer[ACL]()
  6. val acl = new ACL
  7. acl.setId(ANYONE_ID_UNSAFE)
  8. acl.setPerms(PERMISSIONS_ALL)
  9. acls += acl
  10. val offsetPath = zkGroupTopicDirs.consumerOffsetDir + "/" + or.partition;
  11. val offsetVal = if (storeEndOffset) or.untilOffset else or.fromOffset
  12. zkUtils.updatePersistentPath(zkGroupTopicDirs.consumerOffsetDir + "/"
  13. + or.partition, offsetVal + "", JavaConversions.bufferAsJavaList(acls))
  14. LOGGER.debug("persisting offset details - topic: {}, partition: {}, offset: {}, node path: {}", Seq[AnyRef](or.topic, or.partition.toString, offsetVal.toString, offsetPath): _*)
  15. })
  16. }

3. 存储在hbase

  • DDL: 30天过期
  1. create 'stream_kafka_offsets', {NAME=>'offsets', TTL=>2592000}
  • RowKey Layout
  1. row: <TOPIC_NAME>:<GROUP_ID>:<EPOCH_BATCHTIME_MS>
  2. column family: offsets
  3. qualifier: <PARTITION_ID>
  4. value: <OFFSET_ID>

For each batch of messages, saveOffsets() function is used to persist last read offsets for a given kafka topic in HBase.对每一个批次的消息,使用saveOffsets()将从指定topic中读取的offsets保存到HBase中

  1. /*
  2. Save offsets for each batch into HBase
  3. */
  4. def saveOffsets(TOPIC_NAME:String,GROUP_ID:String,offsetRanges:Array[OffsetRange],
  5. hbaseTableName:String,batchTime: org.apache.spark.streaming.Time) ={
  6. val hbaseConf = HBaseConfiguration.create()
  7. hbaseConf.addResource("src/main/resources/hbase-site.xml")
  8. val conn = ConnectionFactory.createConnection(hbaseConf)
  9. val table = conn.getTable(TableName.valueOf(hbaseTableName))
  10. val rowKey = TOPIC_NAME + ":" + GROUP_ID + ":" +String.valueOf(batchTime.milliseconds)
  11. val put = new Put(rowKey.getBytes)
  12. for(offset <- offsetRanges){
  13. put.addColumn(Bytes.toBytes("offsets"),Bytes.toBytes(offset.partition.toString),
  14. Bytes.toBytes(offset.untilOffset.toString))
  15. }
  16. table.put(put)
  17. conn.close()
  18. }

在执行streaming任务之前,首先会使用getLastCommittedOffsets()来从HBase中读取上一次任务结束时所保存的offsets。该方法将采用常用方案来返回kafka topic分区offsets。

情形1:Streaming任务第一次启动,从zookeeper中获取给定topic的分区数,然后将每个分区的offset都设置为0,并返回。

情形2:一个运行了很长时间的streaming任务停止并且给定的topic增加了新的分区,处理方式是从zookeeper中获取给定topic的分区数,对于所有老的分区,offset依然使用HBase中所保存,对于新的分区则将offset设置为0。

情形3:Streaming任务长时间运行后停止并且topic分区没有任何变化,在这个情形下,直接使用HBase中所保存的offset即可。

在Spark Streaming应用启动之后如果topic增加了新的分区,那么应用只能读取到老的分区中的数据,新的是读取不到的。所以如果想读取新的分区中的数据,那么就得重新启动Spark Streaming应用。

  1. /* Returns last committed offsets for all the partitions of a given topic from HBase in
  2. following cases.
  3. */
  4. def getLastCommittedOffsets(TOPIC_NAME:String,GROUP_ID:String,hbaseTableName:String,
  5. zkQuorum:String,zkRootDir:String,sessionTimeout:Int,connectionTimeOut:Int):Map[TopicPartition,Long] ={
  6. val hbaseConf = HBaseConfiguration.create()
  7. val zkUrl = zkQuorum+"/"+zkRootDir
  8. val zkClientAndConnection = ZkUtils.createZkClientAndConnection(zkUrl,
  9. sessionTimeout,connectionTimeOut)
  10. val zkUtils = new ZkUtils(zkClientAndConnection._1, zkClientAndConnection._2,false)
  11. val zKNumberOfPartitionsForTopic = zkUtils.getPartitionsForTopics(Seq(TOPIC_NAME
  12. )).get(TOPIC_NAME).toList.head.size
  13. zkClientAndConnection._1.close()
  14. zkClientAndConnection._2.close()
  15. //Connect to HBase to retrieve last committed offsets
  16. val conn = ConnectionFactory.createConnection(hbaseConf)
  17. val table = conn.getTable(TableName.valueOf(hbaseTableName))
  18. val startRow = TOPIC_NAME + ":" + GROUP_ID + ":" +
  19. String.valueOf(System.currentTimeMillis())
  20. val stopRow = TOPIC_NAME + ":" + GROUP_ID + ":" + 0
  21. val scan = new Scan()
  22. val scanner = table.getScanner(scan.setStartRow(startRow.getBytes).setStopRow(
  23. stopRow.getBytes).setReversed(true))
  24. val result = scanner.next()
  25. var hbaseNumberOfPartitionsForTopic = 0 //Set the number of partitions discovered for a topic in HBase to 0
  26. if (result != null){
  27. //If the result from hbase scanner is not null, set number of partitions from hbase
  28. to the number of cells
  29. hbaseNumberOfPartitionsForTopic = result.listCells().size()
  30. }
  31. val fromOffsets = collection.mutable.Map[TopicPartition,Long]()
  32. if(hbaseNumberOfPartitionsForTopic == 0){
  33. // initialize fromOffsets to beginning
  34. for (partition <- 0 to zKNumberOfPartitionsForTopic-1){
  35. fromOffsets += (new TopicPartition(TOPIC_NAME,partition) -> 0)
  36. }
  37. } else if(zKNumberOfPartitionsForTopic > hbaseNumberOfPartitionsForTopic){
  38. // handle scenario where new partitions have been added to existing kafka topic
  39. for (partition <- 0 to hbaseNumberOfPartitionsForTopic-1){
  40. val fromOffset = Bytes.toString(result.getValue(Bytes.toBytes("offsets"),
  41. Bytes.toBytes(partition.toString)))
  42. fromOffsets += (new TopicPartition(TOPIC_NAME,partition) -> fromOffset.toLong)
  43. }
  44. for (partition <- hbaseNumberOfPartitionsForTopic to zKNumberOfPartitionsForTopic-1){
  45. fromOffsets += (new TopicPartition(TOPIC_NAME,partition) -> 0)
  46. }
  47. } else {
  48. //initialize fromOffsets from last run
  49. for (partition <- 0 to hbaseNumberOfPartitionsForTopic-1 ){
  50. val fromOffset = Bytes.toString(result.getValue(Bytes.toBytes("offsets"),
  51. Bytes.toBytes(partition.toString)))
  52. fromOffsets += (new TopicPartition(TOPIC_NAME,partition) -> fromOffset.toLong)
  53. }
  54. }
  55. scanner.close()
  56. conn.close()
  57. fromOffsets.toMap
  58. }

当我们获取到offsets之后我们就可以创建一个Kafka Direct DStream

  1. val fromOffsets= getLastCommittedOffsets(topic,consumerGroupID,hbaseTableName,zkQuorum,
  2. zkKafkaRootDir,zkSessionTimeOut,zkConnectionTimeOut)
  3. val inputDStream = KafkaUtils.createDirectStream[String,String](ssc,PreferConsistent,
  4. Assign[String, String](fromOffsets.keys,kafkaParams,fromOffsets))

在完成本批次的数据处理之后调用saveOffsets()保存offsets.

  1. /*
  2. For each RDD in a DStream apply a map transformation that processes the message.
  3. */
  4. inputDStream.foreachRDD((rdd,batchTime) => {
  5. val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
  6. offsetRanges.foreach(offset => println(offset.topic,offset.partition, offset.fromOffset,
  7. offset.untilOffset))
  8. val newRDD = rdd.map(message => processMessage(message))
  9. newRDD.count()
  10. saveOffsets(topic,consumerGroupID,offsetRanges,hbaseTableName,batchTime)
  11. })

参考代码:https://github.com/gdtm86/spark-streaming-kafka-cdh511-testing

总结

综上所述,推荐使用zk维护offsets。

参考文献


tips:本文属于自己学习和实践过程的记录,很多图和文字都粘贴自网上文章,没有注明引用请包涵!如有任何问题请留言或邮件通知,我会及时回复。

SparkStreaming使用checkpoint存在的问题及解决方案的更多相关文章

  1. spark-streaming的checkpoint机制源码分析

    转发请注明原创地址 http://www.cnblogs.com/dongxiao-yang/p/7994357.html spark-streaming定时对 DStreamGraph 和 JobS ...

  2. SparkStreaming之checkpoint检查点

    一.简介 流应用程序必须保证7*24全天候运行,因此必须能够适应与程序逻辑无关的故障[例如:系统故障.JVM崩溃等].为了实现这一点,SparkStreaming需要将足够的信息保存到容错存储系统中, ...

  3. SparkStreaming整合Flume的pull报错解决方案

    先说下版本情况: Spark 2.4.3 Scala 2.11.12 Flume-1.6.0 Flume配置文件: simple-agent.sources = netcat-source simpl ...

  4. 在sparkStreaming实时存储时的问题

    1.实时插入mysql时遇到的问题,使用的updateStaeBykey有状态的算子 必须设置checkpoint  如果报错直接删掉checkpoint 在创建的时候自己保存偏移量即可 再次启动时读 ...

  5. FusionInsight大数据开发---SparkStreaming概述

    SparkStreaming概述 SparkStreaming是Spark核心API的一个扩展,它对实时流式数据的处理具有可扩展性.高吞吐量.可容错性等特点. SparkStreaming原理 Spa ...

  6. Spark Streaming 002 统计单词的例子

    1.准备 事先在hdfs上创建两个目录: 保存上传数据的目录:hdfs://alamps:9000/library/SparkStreaming/data checkpoint的目录:hdfs://a ...

  7. SparkStreaming使用mapWithState时,设置timeout()无法生效问题解决方案

    前言 当我在测试SparkStreaming的状态操作mapWithState算子时,当我们设置timeout(3s)的时候,3s过后数据还是不会过期,不对此key进行操作,等到30s左右才会清除过期 ...

  8. SparkStreaming:关于checkpoint的弊端

    当使用sparkstreaming处理流式数据的时候,它的数据源搭档大部分都是Kafka,尤其是在互联网公司颇为常见. 当他们集成的时候我们需要重点考虑就是如果程序发生故障,或者升级重启,或者集群宕机 ...

  9. Key ssd_300_vgg/block3_box/L2Normalization/gamma not found in checkpoint的解决方案

    在Tensorflow下使用SSD模型训练自己的数据集时,经过查找很多博客资料,已经成功训练出来了自己的模型,但就是在测试自己模型效果的时候,出现了如下错误. 2019-10-27 14:47:12. ...

随机推荐

  1. [CF707D]Persistent Bookcase_主席树_bitset

    Persistent Bookcase 题目链接:http://codeforces.com/contest/707/problem/D 注释:略. 题解: 发现虽然$q\le 10^5$但是网格是$ ...

  2. lua介绍及环境搭建(一)

    一.介绍 1.简介 Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能. 其设计目的是为了嵌入应用程序中,从 ...

  3. 2019年9月(System.Data.OracleClient 需要 Oracle 客户端软件 version 8.1.7 或更高版本。)问题解决记录

    System.Data.OracleClient 需要 Oracle 客户端软件 version 8.1.7 或更高版本. 在百度上寻找了很久,都说是权限的问题,可是更改过后一点效果也没有. 实际上真 ...

  4. java中JDBC是什么?

    [学习笔记] JDBC是什么? JDBC即(java database connectivity数据连接).JDBC是Sun公司编的一堆类和方法,都封装在java.sql包中.你可以利用这堆类和方法来 ...

  5. Centos7下关闭Firewalls配置iptables

    在网上搜索了很多这种资料,现在总结一下以备后用. 1.关闭防火墙:sudo systemctl stop firewalld.service 2.关闭开机启动:sudo systemctl disab ...

  6. LC 20 Valid Parentheses

    问题 Given a string containing just the characters '(', ')', '{', '}', '[' and ']', determine if the i ...

  7. thinkphp5日志文件权限的问题

    由于www用户和root用户(比如command的cli进程日志)都有可能对log文件进行读写. 如果是由www用户创建的log文件,不会出任何问题. 但是如果是先由root用户创建的log文件,然后 ...

  8. DevExpress WPF控件记录

    以下是博主用到DevExpress WPF控件时的一些记录笔记: 1.Canvas控件:Canvas控件的背景色一定要设置(background="Transparent"),不然 ...

  9. shell习题第17题:检测磁盘

    [题目要求] 写一个shell脚本,检测所有磁盘分区使用率和inode使用率并记录到以当天日期命名的日志文件里,当发现某个分区容量或者inode使用量大于85%时候,发邮件提醒 [核心要点] df d ...

  10. ACM-ICPC 2017北京

    J. Pangu and Stones 大意: 给定$n$堆石子, $(n\le 100)$, 每次操作任选连续的至少$L$堆至多$R$堆合并, 代价为合并石子的总数, 求合并为$1$堆的最少花费. ...