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 ...
随机推荐
- K8S的APISERVER,应用了HTTPS之后,命令行如何访问?
用命令行总是很麻烦,因为要自定义一些证书的位置....... curl https://1.2.3.1:443/api/v1/nodes \ --cacert /etc/kubernetes/pki/ ...
- Go语言的web程序写法
一切来自于扩展... 核心也即处理输入输出... // helloworld project main.go package main import ( "fmt" "h ...
- prerender.io 搜索引擎优化 部署成windows服务 实现开机自动开启服务
一 prerender.io服务端部署 参考官方网站的部署步骤: $ git clone https://github.com/prerender/prerender.git $ cd preren ...
- ios 安卓 video 取消播放自动全屏 属性
x-webkit-airplay="true",x5-playsinline="true",webkit-playsinline="true" ...
- 熟悉并了解uml的使用(一)
本资料对UML各种模型图的构成和功能进行说明,通过本资料的学习达到可以读懂UML模型图的目的.本资料不涉及模型图作成的要点等相关知识. UML简介 UML (Unified Modeling Lang ...
- logrotate日志轮转
1)基本介绍 适合应用服务日志,系统日志按天切割 如果没有日志轮转,日志文件会越来越大 将丢弃系统中最旧的日志文件,以节省空间 logrotate本身不是系统守护进程,它是通过计划任务crond每天执 ...
- 创建 OpenStack云主机 (十五)
创建过程 创建虚拟网络 创建m1.nano规格的主机(相等于定义虚拟机的硬件配置) 生成一个密钥对(openstack的原理是不使用密码连接,而是使用密钥对进行连接) 增加安全组规则(用iptable ...
- vs2005 QT4.7.1编译 详细
http://blog.csdn.net/debugconsole/article/details/8230683 网上一搜有QT+2005编译的很多文章,但是都不详细,很多都编不过,特别的在conf ...
- BZOJ 4873 寿司餐厅(最大权闭合图 网络流)
寿司餐厅 时间限制: 1 Sec 内存限制: 512 MB提交: 6 解决: 3[提交][状态][讨论版] 题目描述 Kiana 最近喜欢到一家非常美味的寿司餐厅用餐.每天晚上,这家餐厅都会按顺序 ...
- Codeforces Round #407 (Div. 2) D. Weird journey(欧拉路)
D. Weird journey time limit per test 2 seconds memory limit per test 256 megabytes input standard in ...