Kafka+Spark Streaming管理offset

标签: 大数据  流处理  kafka  spark  offset

场景描述:Kafka配合Spark Streaming是大数据领域常见的黄金搭档之一,主要是用于数据实时入库或分析。为了应对可能出现的引起Streaming程序崩溃的异常情况,我们一般都需要手动管理好Kafka的offset,而不是让它自动提交,即需要将enable.auto.commit设为false。只有管理好offset,才能使整个流式系统最大限度地接近exactly once语义。

Kafka+Spark Streaming主要用于实时流处理。到目前为止,在大数据领域中是一种非常常见的架构。Kafka在其中主要起着一个缓冲的作用,所有的实时数据都会经过kafka。所以对kafka offset的管理是其中至关重要的一环。

我们一般都需要手动管理好Kafka的offset,而不是让它自动提交,即需要将enable.auto.commit设为false。

offset的管理方式
一个简单的流程如下:
在这里插入图片描述

  • 在KafkaDirectStream初始化时,取得当前所有partition的存量offset,以让DirectStream能够从正确的位置开始读取数据。
  • 读取消息数据,处理并存储结果。
  • 提交offset,并将其持久化在可靠的外部存储中
  • 图中的“process and store results”及“commit
    offsets”两项,都可以施加更强的限制,比如存储结果时保证幂等性,或者提交offset时采用原子操作。

保存offset的方式

Checkpoint:

Spark Streaming的checkpoints是最基本的存储状态信息的方式,一般是保存在HDFS中。但是最大的问题是如果streaming程序升级的话,checkpoints的数据无法使用,所以几乎没人使用。

offset的三种管理方式:

自动提交offset:
enable.auto.commit=true
一但consumer挂掉,就会导致数据丢失或重复消费。
offset不可控。

Kafka自身的offset管理:(属于At-least-once语义,如果做好了幂等性,可以使用这种方式):在Kafka 0.10+版本中,offset的默认存储由ZooKeeper移动到了一个自带的topic中,名为__consumer_offsets。

Spark Streaming也专门提供了commitAsync() API用于提交offset。

需要将参数修改为enable.auto.commit=false
在我实际测试中发现,这种offset的管理方式,不会丢失数据,但会出现重复消费。停掉streaming应用程序再次启动后,会再次消费停掉前最后的一个批次数据,应该是由于offset是异步提交的方式导致,offset更新不及时引起的。因此需要做好数据的幂等性。(修改源码将异步改为同步,应该是可以做到Exactly-once语义的)

自定义offset:
(推荐,采用这种方式,可以做到At-least-once语义):可以将offset存放在第三方储中,包括RDBMS、Redis、ZK、ES等。若消费数据存储在带事务的组件上,则强烈推荐将offset存储在一起,借助事务实现 Exactly-once 语义。

示例
Kafka自身管理offset:

在Kafka 0.10+版本中,offset的默认存储由ZooKeeper移动到了一个自带的topic中,名__consumer_offsets。所以我们读写offset的对象正是这个topic,Spark Streaming也专门提供了commitAsync() API用于提交offset。实际上,一切都已经封装好了,直接调用相关API即可。

stream.foreachRDD { rdd =>
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
// 确保结果都已经正确且幂等地输出了
stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
}

ZooKeeper

在Spark Streaming连接Kafka应用中使用Zookeeper来存储offsets也是一种比较可靠的方式。

在这个方案中,Spark Streaming任务在启动时会去Zookeeper中读取每个分区的offsets。如果有新的分区出现,那么他的offset将会设置在最开始的位置。在每批数据处理完之后,用户需要可以选择存储已处理数据的一个offset或者最后一个offset。此外,新消费者将使用跟旧的Kafka 消费者API一样的格式将offset保存在ZooKeeper中。因此,任何追踪或监控Zookeeper中Kafka Offset的工具仍然生效的。

一个典型的工具类:

class ZkKafkaOffsetManager(zkUrl: String) {
private val logger = LoggerFactory.getLogger(classOf[ZkKafkaOffsetManager])

private val zkClientAndConn = ZkUtils.createZkClientAndConnection(zkUrl, 30000, 30000);
private val zkUtils = new ZkUtils(zkClientAndConn._1, zkClientAndConn._2, false)

def readOffsets(topics: Seq[String], groupId: String): Map[TopicPartition, Long] = {
	val offsets = mutable.HashMap.empty[TopicPartition, Long]
	val partitionsForTopics = zkUtils.getPartitionsForTopics(topics)

	// /consumers/<groupId>/offsets/<topic>/<partition>
	partitionsForTopics.foreach(partitions => {
		val topic = partitions._1
		val groupTopicDirs = new ZKGroupTopicDirs(groupId, topic)

		partitions._2.foreach(partition => {
			val path = groupTopicDirs.consumerOffsetDir + "/" + partition
			try {
				val data = zkUtils.readData(path)
				if (data != null) {
					offsets.put(new TopicPartition(topic, partition), data._1.toLong)
					logger.info(
						"Read offset - topic={}, partition={}, offset={}, path={}",
						Seq[AnyRef](topic, partition.toString, data._1, path)
					)
				}
			} catch {
				case ex: Exception =>
					offsets.put(new TopicPartition(topic, partition), 0L)
					logger.info(
						"Read offset - not exist: {}, topic={}, partition={}, path={}",
						Seq[AnyRef](ex.getMessage, topic, partition.toString, path)
					)
			}
		})
	})

	offsets.toMap
}

def saveOffsets(offsetRanges: Seq[OffsetRange], groupId: String): Unit = {
	offsetRanges.foreach(range => {
		val groupTopicDirs = new ZKGroupTopicDirs(groupId, range.topic)
		val path = groupTopicDirs.consumerOffsetDir + "/" + range.partition
		zkUtils.updatePersistentPath(path, range.untilOffset.toString)
		logger.info(
			"Save offset - topic={}, partition={}, offset={}, path={}",
			Seq[AnyRef](range.topic, range.partition.toString, range.untilOffset.toString, path)
		)
	})
}
}

这样,offset就会被存储在ZK的/consumers/[groupId]/offsets/[topic]/[partition]路径下。当初始化DirectStream时,调用readOffsets()方法获得offset。当数据处理完成后,调用saveOffsets()方法来更新ZK中的值。

其他介质

Hbase、Redis甚至Mysql也经常被用作进行offset的存储。方式和上面类似,代码可以去网上搜一搜。

需要注意的点

特别需要注意,在转换过程中不能破坏RDD分区与Kafka分区之间的映射关系。亦即像map()/mapPartitions()这样的算子是安全的,而会引起shuffle或者repartition的算子,如reduceByKey()/join()/coalesce()等等都是不安全的。

对Dstream进行窗口操作后就不能手动提交offset。
因为保存offset需要HasOffsetRanges这个类。而HasOffsetRanges是KafkaRDD的一个trait,而CanCommitOffsets是DirectKafkaInputDStream的一个trait。

Scala Trait(特征) 相当于 Java 的接口,实际上它比接口还功能强大。
与接口不同的是,它还可以定义属性和方法的实现。

private[spark] class KafkaRDD[K, V](
sc: SparkContext,
val kafkaParams: ju.Map[String, Object],
val offsetRanges: Array[OffsetRange],
val preferredHosts: ju.Map[TopicPartition, String],
useConsumerCache: Boolean
) extends RDD[ConsumerRecord[K, V]](sc, Nil) with Logging with HasOffsetRanges

private[spark] class DirectKafkaInputDStream[K, V](
_ssc: StreamingContext,
locationStrategy: LocationStrategy,
consumerStrategy: ConsumerStrategy[K, V],
ppc: PerPartitionConfig
) extends InputDStream[ConsumerRecord[K, V]](_ssc) with Logging with CanCommitOffsets {

不能对stream对象做transformation操作之后的结果进行强制转换(会直接报ClassCastException),因为RDD与DStream的类型都改变了。只有RDD或DStream的包含类型为ConsumerRecord才行。

收藏自公众号:大数据技术与架构

版权声明:本文为qq_43081842原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_43081842/article/details/102610458

智能推荐

将 Spark Streaming + Kafka direct 的 offset 保存进入Zookeeper(二)

在上一遍《“Spark Streaming + Kafka direct + checkpoints + 代码改变” 引发的问题》中说到,当时是将 topic 的 partition 的 offset 保存到了 MySQL 数据库中,其存在一个问题,就是无法在现有的监控工具中进行体现(如:Kafka Manager)。那我们现在就来将此offset保存到zookeeper中...

kafka 生产/消费API、offset管理/原理、kafka命令

大数据组件使用 总文章 kafka 生产/消费API、offset管理/原理、kafka命令 kafka 命令、API Kafka 安装、原理、使用 mapreduce 实时消费 kafka 数据   ============================================= ===========================================...

HBase管理offset,解决kafka头越界或尾越界问题(HBase存储offset可以更换为Mysql、Redis、Zookeeper)

HBase管理offset,解决kafka头越界或尾越界问题(HBase存储offset可以更换为Mysql、Redis、Zookeeper 什么是越界? 头越界的原因 尾越界的原因 解决方案流程图 代码实现 调用方式举例 什么是越界? 越界包括头越界或尾越界。指的程序消费的是kafka offset不在kafka的队列里面,可能是数据过期或者kafka数据被清除 ### 头越界的原因 数据过期:...

Spark Streaming 中管理 Kafka Offsets 的几种方式

本文转载自:https://www.jianshu.com/p/ef3f15cf400d(点击下面 阅读原文 即可进入) 英文原文:http://blog.cloudera.com/blog/2017/06/offset-management-for-apache-kafka-with-apache-spark-streaming/ Offset管理概述 Spark Strea...

Spark Streaming状态管理函数updateStateByKey和mapWithState

Spark Streaming状态管理函数updateStateByKey和mapWithState 一、状态管理函数 二、mapWithState 2.1关于mapWithState 2.2mapWithState示例Scala: 2.3mapWithState算子应用示例 2.4mapWithState应用示例 2.5SparkStreaming之mapWithState 三、updateSt...

猜你喜欢

冒泡排序,及改进方式,性能优化400%>>>附图解加源码

首先源码附上,源码中带有注释,看不懂没关系,源码后面附带图解,最后附上代码效率提升图 源码如下: 方案一:其实实现很简单,两层循环,每次内层迭代出最大的一个值,将其放入数组最后一个位置,外层循环的末端便往前移一位。其原理如下图 方案一代码块: 方案二:优化改进 仔细观察上面的图,我们不难发现当迭代到图下这样的情况时,其实已经全排序好了,但是我们还是需要对它进行1,2,3,4,5,6的迭代,这些情况...

css3常用选择器

先介绍一下基本选择器,如下几种 *:通配符。选择页面的所有元素 E:元素选择器。对页面的一些元素进行选择,如p,div,li等 .class:类选择器 #id:id选择器。一个Id在一个页面中只能选择,这是和类选择器的区别 E F:后代选择器。如ul li。选中ul下的li E>F:子元素选择器。如下,可以用 h1 > strong {color:red;}使h1的子元素strong变...

python is not set from command line or npm configuration

问题 猜测原因  python版本冲突 解决方案  去官网找到最新版本python 点击下载,注意记住你下载的目录,下载好以后,打开这个目录下的文件夹,Python310, 整个文件夹复制一下 在项目里运行npm config list --json 找到python的路径   我的是 C:\\Python27\\python.exe 说明项目里用的还是py...

Java8 Stream流操作

前言 我们常常需要将一个容器转化成另一个容器,或是对这个容器中的数据进行批量处理,这时使用Stream流可以大大减少我们的工作量。 1 Stream概述 Java 8 是一个非常成功的版本,这个版本新增的Stream,配合同版本出现的 Lambda ,给我们操作集合(Collection)提供了极大的便利。 那么什么是Stream? Stream将要处理的元素集合看作一种流,在流的过程中,借助St...

带你玩转Visual Studio——带你跳出坑爹的Runtime Library坑

在Windows下进行C++的开发,不可避免的要与Windows的底层库进行交互,然而VS下的一项设置MT、MTd、MD和MDd却经常让人搞迷糊,相信不少人都被他坑过,特别是你工程使用了很多第三库的时候,及容易出现各种链接问题。看一下下面这个错误提示:  LIBCMT.lib(_file.obj) : error LNK2005: ___initstdio already defined...