聊聊 Kafka+Spark Streaming 管理offset的两种方法
标签: # ---- Spark Streaming java spark kafka 大数据
Kafka配合Spark Streaming是大数据领域常见的黄金搭档之一,主要是用于数据实时入库或分析。
为了应对可能出现的引起Streaming程序崩溃的异常情况,我们一般都需要手动管理好Kafka的offset,而不是让它自动提交,即需要将 enable.auto.commit 设为false。只有管理好offset,才能使整个流式系统最大限度地接近exactly once语义。
管理offset的流程
下面这张图能够简要地说明管理offset的大致流程。

offset管理流程
- 在Kafka DirectStream初始化时,取得当前所有partition的存量offset,以让DirectStream能够从正确的位置开始读取数据。
- 读取消息数据,处理并存储结果。
- 提交offset,并将其持久化在可靠的外部存储中。
图中的 “process and store results” 及 “commit offsets” 两项,都可以施加更强的限制,比如存储结果时保证幂等性,或者提交offset时采用原子操作。
图中提出了4种offset存储的选项,分别是HBase、Kafka自身、HDFS和ZooKeeper。综合考虑实现的难易度和效率,我们目前采用过的是Kafka自身与ZooKeeper两种方案。
Kafka自身
在Kafka 0.10+版本中,offset的默认存储由ZooKeeper移动到了一个自带的topic中,名为__consumer_offsets。Spark Streaming也专门提供了commitAsync() API用于提交offset。使用方法如下。
stream.foreachRDD { rdd =>
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
// 确保结果都已经正确且幂等地输出了
stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
}
上面是Spark Streaming官方文档中给出的写法。但在实际上我们总会对DStream进行一些运算,这时我们可以借助DStream的transform()算子。
var offsetRanges: Array[OffsetRange] = Array.empty[OffsetRange]
stream.transform(rdd => {
// 利用transform取得OffsetRanges
offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd
}).mapPartitions(records => {
var result = new ListBuffer[...]()
// 处理流程
result.toList.iterator
}).foreachRDD(rdd => {
if (!rdd.isEmpty()) {
// 数据入库
session.createDataFrame...
}
// 提交offset
stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
})
特别需要注意,在转换过程中不能破坏 RDD 分区与 Kafka 分区之间的映射关系。亦即像map() / mapPartitions()这样的算子是安全的,而会引起shuffle或者repartition的算子,如reduceByKey() / join() / coalesce()等等都是不安全的。
另外需要注意的是,HasOffsetRanges 是 KafkaRDD 的一个 trait,而 CanCommitOffsets是 DirectKafkaInputDStream 的一个trait。从spark-streaming-kafka包的源码中,可以看得一清二楚。
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才行。
ZooKeeper
虽然Kafka将offset从ZooKeeper中移走是考虑到可能的性能问题,但ZooKeeper内部是采用树形node结构存储的,这使得它天生适合存储像offset这样细碎的结构化数据。并且我们的分区数不是很多,batch间隔也相对长(20秒),因此并没有什么瓶颈。
Kafka中还保留了一个已经标记为过时的类 ZKGroupTopicDirs ,其中预先指定了Kafka相关数据的存储路径,借助它,我们可以方便地用ZooKeeper来管理offset。为了方便调用,将存取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中的值。
为什么不用checkpoint
Spark Streaming的checkpoint机制无疑是用起来最简单的,checkpoint数据存储在HDFS中,如果Streaming应用挂掉,可以快速恢复。
但是,如果Streaming程序的代码改变了,重新打包执行就会出现反序列化异常的问题。这是因为checkpoint首次持久化时会将整个jar包序列化,以便重启时恢复。重新打包之后,新旧代码逻辑不同,就会报错或者仍然执行旧版代码。
要解决这个问题,只能将HDFS上的checkpoint文件删掉,但这样也会同时删掉Kafka的offset信息,就毫无意义了。
智能推荐
聊聊 Linux 的匿名管道
相信很多在linux平台工作的童鞋, 都很熟悉管道符 '|', 通过它, 我们能够很灵活的将几种不同的命令协同起来完成一件任务.就好像下面的命令: 不过这次咱们不来说这些用法, 而是来探讨一些更加有意思的, 那就是 管道两边的数据流"实时性" 和 管道使用的小提示. 其实我们在利用管道的时候, 可能会不经意的去想, 我前一个命令的输出, 是全部处理完再通过管道传给第二个命令, ...
聊聊这段时间的感受
转载请注明原创出处,谢谢! HappyFeet的博客 这段时间,经历了许多,离职、面试,还有找房子,感受还是蛮多的。之前就计划找个时间总结一下,只是被我一直拖着,不曾实施。这篇文章也算是落实了我的计划吧,虽然时间往后延了许多。 一、离职 算上实习期间,在公司待了将近四年,也算是老员工,感情也不浅了。走在离开公司的路上,总觉得心里少了点什么,空落落的。不过既然选择了离职,那也是没有回头路的。 原本是...
聊聊Hive的那些事儿
本文仅仅从宏观上去聊聊Hive中的一些常见问题。文末我整理了几篇其他人的文章,个人感觉对自己的学习起到了作用。有兴趣的同学可以点过去阅读。 1. Hive是什么? Hive是基于Hadoop的一个数据仓库工具,用来进行数据提取、转化、加载,这是一种可以存储、查询和分析存储在Hadoop中的大规模数据的机制。hive数据仓库工具能将结构化的数据文件映射为一张数据库表,并提供SQL查询功能,能将SQL...
聊聊 Linux 的内存统计
点击上方“小强的进阶之路”,选择“星标”公众号 优质文章,及时送达 预计阅读时间: 18分钟 写在前面 本文主要分析 Linux 系统内存统计的一些指标以及进程角度内存使用监控的一些方法。 开始阅读这篇文章前,请先简单阅读下面的几篇文章。 《进程眼中的线性地址空间》 《线程眼中的线性地址空间》 《聊聊内存管理》 想必这几篇文章过后,基本概念就不需要再...
聊聊继承的几种方式
这阶段给大家来讲讲javaScript里面的继承,虽然在ES6中有了继承,使用extends关键字就能实现,而这篇要讲的,而是ES6之前的几种实现继承的方式; 讲继承,大家肯定需要先了解的是对象,和原型对象以及原型链,这个知识点我在上一个帖子中已经有过描述,大家可以去看上篇帖子; 那么首先,大家需要知道,什么是继承? 继承概念 我们可以这样去理解,子承父业,父亲拥有的东西,孩子可以去享...
猜你喜欢
冒泡排序,及改进方式,性能优化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...
