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 ...
随机推荐
- Centos6.9下安装OpenOffice 4.1.4
# 对一下时间,时间不准,解压不了yum install -y ntp unzipntpdate -u 202.112.10.36yum install libXext.x86_64 -yyum gr ...
- Python进程间通信:Queue
Python进程间通信Queue 1.Queue使用方法: Queue.qsize():返回当前队列包含的消息数量: Queue.empty():如果队列为空,返回True,反之False : Que ...
- 下载Maven安装包
进入Maven官网的下载页面:http://maven.apache.org/download.cgi,如下图所示: 选择当前最新版本:"apache-maven-3.3.9-bin.zip ...
- onethink 路由规则无效问题解决
修改文件 Application/Common/Conf/config.php 打开注释 //'MODULE_ALLOW_LIST' => array('Home','Admin'), // 1 ...
- LOJ #6277. 数列分块入门 1-分块(区间加法、单点查询)
#6277. 数列分块入门 1 内存限制:256 MiB时间限制:100 ms标准输入输出 题目类型:传统评测方式:文本比较 上传者: hzwer 提交提交记录统计测试数据讨论 2 题目描述 给出 ...
- MD5加密和RSA加密
1.MD5加密 MD5(单向散列算法)的全称是Message-Digest Algorithm 5(信息-摘要算法),MD5算法的使用不需要支付任何版权费用. MD5的功能: ①.输入任意长 ...
- Linux操作命令(二)
本次实验将介绍 Linux 命令中 mkdir.rm.mv.cp.cat.nl 命令的用法. 1.mkdir mkdir命令用来创建指定名称的目录,要求创建目录的用户在当前目录中具有写权限,并且指定的 ...
- Oracle 使用序列、触发器实现自增
之前项目开发多用mysql,对于id自增长设置,只需要简单修改列属性便好.最近改用ORACLE,头大一圈.ORACLE的相关操作,多用脚本.想短平快,难.最终用sql developer通过UI进行修 ...
- Bzoj1015/洛谷P1197 [JSOI2008]星球大战(并查集)
题面 Bzoj 洛谷 题解 考虑离线做法,逆序处理,一个一个星球的加入.用并查集维护一下连通性就好了. 具体来说,先将被消灭的星球储存下来,先将没有被消灭的星球用并查集并在一起,这样做可以路径压缩,然 ...
- eclipse 设置英文
只需要将eclipse解压目录下的eclipse.ini文件中加入下面一句话就可以了(注意这句话应该单独存在一行): -Duser.language=EN 如下图: 配置完成后,重启eclipse ...