KafkaConsumer对于事务消息的处理
Kafka添加了事务机制以后,consumer端有个需要解决的问题就是怎么样从收到的消息中滤掉aborted的消息。Kafka通过broker和consumer端的协作,利用一系列优化手段极大地降低了这部分工作的开销。
问题
首先来看一下这部分工作的难点在哪。
对于isolation.level为read_committed的消费者来说,它只想获取committed的消息。但是在服务器端的存储中,committed的消息、aborted的消息、以及正在进行中的事务的消息在Log里是紧挨在一起的,而且这些状态的消息可能源于不同的producerId。所以,如果broker对FetchRequest的处理和加入事务机制前一样,那么consumer就需要做很多地清理工作,而且需要buffer消息直到control marker的到来。那么,就无故浪费了很多流量,而且consumer端的内存管理也很成问题。
解决方法
Kafka大体采用了三个措施一起来解决这个问题。
LSO
Kafka添加了一个很重要概念,叫做LSO,即last stable offset。对于同一个TopicPartition,其offset小于LSO的所有transactional message的状态都已确定,要不就是committed,要不就是aborted。而broker对于read_committed的consumer,只提供offset小于LSO的消息。这样就避免了consumer收到状态不确定的消息,而不得不buffer这些消息。
Aborted Transaction Index
对于每个LogSegment(对应于一个log文件),broker都维护一个aborted transaction index. 这是一个append only的文件,每当有事务被abort时,就会有一个entry被append进去。这个entry的格式是:
TransactionEntry =>
Version => int16
PID => int64
FirstOffset => int64
LastOffset => int64
LastStableOffset => int64
为什么要有这个index?
这涉及到FetchResponse的消息格式的变化,在FetchResponse里包含了其中每个TopicPartition的记录里的aborted transactions的信息,consumer使用这些信息,可以更高效地从FetchResponse里包含的消息里过滤掉被abort的消息。
// FetchResponse v4
FetchResponse => ThrottleTime [TopicName [Partition ErrorCode HighwaterMarkOffset LastStableOffset AbortedTransactions MessageSetSize MessageSet]]
ThrottleTime => int32
TopicName => string
Partition => int32
ErrorCode => int16
HighwaterMarkOffset => int64
LastStableOffset => int64
AbortedTransactions => [PID FirstOffset]
PID => int64
FirstOffset => int64
MessageSetSize => int32
Consumer端根据aborted transactions的消息过滤
(以下对只针对read_committed的consumer)
consumer端会根据fetch response里提供的aborted transactions里过滤掉aborted的消息,只返回给用户committed的消息。
其核心逻辑是这样的:
首先,由于broker只返回LSO之前的消息给consumer,所以consumer拉取的消息只有两种可能的状态:committed和aborted。
活跃的aborted transaction的pid集合
然后, 对于每个在被fetch的消息里包含的TopicPartition, consumer维护一个producerId的集合,这个集合就是当前活跃的aborted transaction所使用的pid。一个aborted transaction是“活跃的”,是说:在过滤过程中,当前的待处理的消息的offset处于这个这个aborted transaction的initial offset和last offset之间。有了这个活跃的aborted transaction对应的PID的集合(以下简称"pid集合"),在过滤消息时,只要看一下这个消息的PID是否在此集合中,如果是,那么消息就肯定是aborted的,如果不是,那就是committed的。
这个pid集合在过滤的过程中,是不断变化的,为了维护这个集合,consumer端还会对于每个在被fetch的消息里包含的TopicPartition 维护一个aborted transaction构成的mini heap, 这个heap是以aborted transaction的intial offset排序的。
public static final class AbortedTransaction {
public final long producerId;
public final long firstOffset;
...
}
private class PartitionRecords {
private final TopicPartition partition;
private final CompletedFetch completedFetch;
private final Iterator<? extends RecordBatch> batches;
private final Set<Long> abortedProducerIds;
private final PriorityQueue<FetchResponse.AbortedTransaction> abortedTransactions;
...
}
//这个heap的初始化过程,可以看出是按offset排序的
private PriorityQueue<FetchResponse.AbortedTransaction> abortedTransactions(FetchResponse.PartitionData partition) {
if (partition.abortedTransactions == null || partition.abortedTransactions.isEmpty())
return null;
PriorityQueue<FetchResponse.AbortedTransaction> abortedTransactions = new PriorityQueue<>(
partition.abortedTransactions.size(),
new Comparator<FetchResponse.AbortedTransaction>() {
@Override
public int compare(FetchResponse.AbortedTransaction o1, FetchResponse.AbortedTransaction o2) {
return Long.compare(o1.firstOffset, o2.firstOffset);
}
}
);
abortedTransactions.addAll(partition.abortedTransactions);
return abortedTransactions;
}
按照Kafka文档里的说法:
- If the message is a transaction control message, and the status is ABORT, then remove the corresponding PID from the set of PIDs with active aborted transactions. If the status is COMMIT, ignore the message.
If the message is a normal message, compare the offset and PID with the head of the aborted transaction minheap. If the PID matches and the offset is greater than or equal to the corresponding initial offset from the aborted transaction entry, remove the head from the minheap and insert the PID into the set of PIDs with aborted transactions.
Check whether the PID is contained in the aborted transaction set. If so, discard the record set; otherwise, add it to the records to be returned to the user.
- 如果收到了一个abort marker(它本身是一个消息,而且单独一个batch),那么就从pid集合里移除这个pid。因为此时这个pid对应的aborted transaction不再是“活跃”的了
- 如果是普通消息,那就根据这个消息和aborted transaction所在的heap,来更新pid集合
- 如果消息的pid跟堆顶的pid一样,而且这个消息的offset >= 堆顶的AbortedTransaction里的offset(这是此pid对应的aborted transaction的initial offset),那么当前这个pid对应的transaction就可以判断为一个活跃的aborted transaction,那就堆顶的这个AbortedTransaction移除,把它的pid放入pid集合里
- 如果不是,就不变更pid集合
- 然后再次判断这个消息的pid是否在pid集合里,如果是的话,就不把这条消息放在返回给用户的消息集里。
但是实际上考虑到batch的问题,情况会比这简单一些。在producer端发送的时候,同一个TopicPartition的不同transaction的消息是不可能在同一个message batch里的, 而且committed的消息和aborted的消息也不可能在同一batch里。因为在不同transaction的消息之间,肯定会有transaction marker, 而transaction marker是单独的一个batch。这就使得,一个batch要不全部被aborted了,要不全部被committed了。所以过滤aborted transaction时就可以一次过滤一个batch,而非一条消息。
相关代码为PartitionRecords#nextFetchedRecord()中:
if (isolationLevel == IsolationLevel.READ_COMMITTED && currentBatch.hasProducerId()) {
// remove from the aborted transaction queue all aborted transactions which have begun
// before the current batch's last offset and add the associated producerIds to the
// aborted producer set
//从aborted transaction里移除那些其inital offset在当前的batch的末尾之前的那些。
//因为这些transaction开始于当前batch之前,而在处理这个batch之前没有结束,所以它要不是活跃的aborted transaction,要不当前的batch就是control batch
//这里需要考虑到aborted transaction可能开始于这次fetch到的所有records之前
consumeAbortedTransactionsUpTo(currentBatch.lastOffset());
long producerId = currentBatch.producerId();
if (containsAbortMarker(currentBatch)) {
abortedProducerIds.remove(producerId); //如果当前batch是abort marker, 那么它对应的transaction就结束了,所以从pid集合里移除它对应的pid。
} else if (isBatchAborted(currentBatch)) { //如果当前batch被abort了,那就跳过它
log.debug("Skipping aborted record batch from partition {} with producerId {} and " +
"offsets {} to {}",
partition, producerId, currentBatch.baseOffset(), currentBatch.lastOffset());
nextFetchOffset = currentBatch.nextOffset();
continue;
}
}
结论
通过对aborted transaction index和LSO的使用,Kafka使得consumer端可以高效地过滤掉aborted transaction里的消息,从而减小了事务机制的性能开销。
KafkaConsumer对于事务消息的处理的更多相关文章
- RocketMQ源码 — 十一、 RocketMQ事务消息
分布式事务是一个复杂的问题,rmq实现了事务的最终一致性,rmq保证本地事务成功消息一定会发送成功并被成功消费,如果本地事务失败了,消息不会被发送. rmq事务消息的实现过程为: producer发送 ...
- RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)
在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...
- RocketMQ事务消息实现分析
这周RocketMQ发布了4.3.0版本,New Feature中最受关注的一点就是支持了事务消息: 今天花了点时间看了下具体的实现内容,下面是简单的总结. RocketMQ事务消息概要 通过冯嘉发布 ...
- RocketMQ实现事务消息
在RocketMQ4.3.0版本后,开放了事务消息这一特性,对于分布式事务而言,最常说的还是二阶段提交协议,那么RocketMQ的事务消息又是怎么一回事呢,这里主要带着以下几个问题来探究一下Rocke ...
- RocketMQ事务消息回查设计方案
用户U1从A银行系统转账给B银行系统的用户U2的处理过程如下:第一步:A银行系统生成一条转账消息,以事务消息的方式写入RocketMQ,此时B银行系统不可见这条消息(Prepare阶段) 第二步:写入 ...
- RocketMQ事务消息实战
我们以一个订单流转流程来举例,例如订单子系统创建订单,需要将订单数据下发到其他子系统(与第三方系统对接)这个场景,我们通常会将两个系统进行解耦,不直接使用服务调用的方式进行交互.其业务实现步骤通常为: ...
- RocketMQ 事务消息
RocketMQ 事务消息在实现上充分利用了 RocketMQ 本身机制,在实现零依赖的基础上,同样实现了高性能.可扩展.全异步等一系列特性. 在具体实现上,RocketMQ 通过使用 Half To ...
- 分布式开放消息系统RocketMQ的原理与实践(消息的顺序问题、重复问题、可靠消息/事务消息)
备注:1.如果您此前未接触过RocketMQ,请先阅读附录部分,以便了解RocketMQ的整体架构和相关术语2.文中的MQServer与Broker表示同一概念 分布式消息系统作为实现分布式系统可扩展 ...
- rocketmq事务消息
rocketmq事务消息 参考: https://blog.csdn.net/u011686226/article/details/78106215 https://yq.aliyun.com/art ...
随机推荐
- Multiply Strings——面试题
Given two numbers represented as strings, return multiplication of the numbers as a string. Note: Th ...
- 跨域请求方式之Jsonp形式
在浏览器端才有跨域安全限制一说,而在服务器端是没有跨域安全限制的. 在两个异构系统(开发语言不同)之间达到资源共享就需要发起一个跨域请求. 而浏览器的同源策略却限制了从一个源头的文档资源或脚本资源与来 ...
- 关于DRY原则
软件工程,模式,语言,设计思想发展到今天,说白了,所有的技巧,思想,原则归根结底都是为了这个DRY 从机器语言开始: 为了DRY,出现了汇编符号来代表指令,使开发人员不用“重复翻阅指令手册” 为了D ...
- 洛谷P3942将军令
啦啦啦,又是五月天的歌------ 题目传送门 那么来分析下题目;给定你一棵树,告诉你一支队伍能管辖的范围,求能覆盖整棵树的最少队伍数. 嘛,如果不会做,第一个想到的肯定是暴搜嘛,但是代码打起来肯定也 ...
- Java面向对象和特征
面向对象: 概念: 面向对象是一种程序设计思想,计算机程序的设计实质上就是将现实中的一些事物的特征抽离出来描述成一些计算机事件的过程,这种抽象的过程中,我们把具体的事物封装成一个一个的整体进行描述,使 ...
- zookeeper,hadoop安装部署其实与防火墙无关
网上查看了很多人关于hadoop,zookeeper的文章,大多都把关闭防火墙作为首要前提,个人觉得这大可不必. 首先你需要知道你部署的是什么东西,它需要哪些端口即可.把相关端口打开就可以了啊.然后把 ...
- scrapy抓取拉勾网职位信息(一)——scrapy初识及lagou爬虫项目建立
本次以scrapy抓取拉勾网职位信息作为scrapy学习的一个实战演练 python版本:3.7.1 框架:scrapy(pip直接安装可能会报错,如果是vc++环境不满足,建议直接安装一个visua ...
- 洛谷——P1190 接水问题
P1190 接水问题 题目描述 学校里有一个水房,水房里一共装有 m 个龙头可供同学们打开水,每个龙头每秒钟的供水量相等,均为 1. 现在有 n 名同学准备接水,他们的初始接水顺序已经确定.将这些同学 ...
- A/B Problem(大数)
描述 做了A+B Problem,A/B Problem不是什么问题了吧! 输入 每组测试样例一行,首先一个号码A,中间一个或多个空格,然后一个符号( / 或者 % ),然后又是空格,后面又是一个号码 ...
- 【BZOJ 1119】 1119: [POI2009]SLO (置换)
1119: [POI2009]SLO Description 对于一个1-N的排列(ai),每次你可以交换两个数ax与ay(x<>y),代价为W(ax)+W(ay) 若干次交换的代价为每次 ...