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对于事务消息的处理的更多相关文章

  1. RocketMQ源码 — 十一、 RocketMQ事务消息

    分布式事务是一个复杂的问题,rmq实现了事务的最终一致性,rmq保证本地事务成功消息一定会发送成功并被成功消费,如果本地事务失败了,消息不会被发送. rmq事务消息的实现过程为: producer发送 ...

  2. RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)

    在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...

  3. RocketMQ事务消息实现分析

    这周RocketMQ发布了4.3.0版本,New Feature中最受关注的一点就是支持了事务消息: 今天花了点时间看了下具体的实现内容,下面是简单的总结. RocketMQ事务消息概要 通过冯嘉发布 ...

  4. RocketMQ实现事务消息

    在RocketMQ4.3.0版本后,开放了事务消息这一特性,对于分布式事务而言,最常说的还是二阶段提交协议,那么RocketMQ的事务消息又是怎么一回事呢,这里主要带着以下几个问题来探究一下Rocke ...

  5. RocketMQ事务消息回查设计方案

    用户U1从A银行系统转账给B银行系统的用户U2的处理过程如下:第一步:A银行系统生成一条转账消息,以事务消息的方式写入RocketMQ,此时B银行系统不可见这条消息(Prepare阶段) 第二步:写入 ...

  6. RocketMQ事务消息实战

    我们以一个订单流转流程来举例,例如订单子系统创建订单,需要将订单数据下发到其他子系统(与第三方系统对接)这个场景,我们通常会将两个系统进行解耦,不直接使用服务调用的方式进行交互.其业务实现步骤通常为: ...

  7. RocketMQ 事务消息

    RocketMQ 事务消息在实现上充分利用了 RocketMQ 本身机制,在实现零依赖的基础上,同样实现了高性能.可扩展.全异步等一系列特性. 在具体实现上,RocketMQ 通过使用 Half To ...

  8. 分布式开放消息系统RocketMQ的原理与实践(消息的顺序问题、重复问题、可靠消息/事务消息)

    备注:1.如果您此前未接触过RocketMQ,请先阅读附录部分,以便了解RocketMQ的整体架构和相关术语2.文中的MQServer与Broker表示同一概念 分布式消息系统作为实现分布式系统可扩展 ...

  9. rocketmq事务消息

    rocketmq事务消息 参考: https://blog.csdn.net/u011686226/article/details/78106215 https://yq.aliyun.com/art ...

随机推荐

  1. 9. Spark Streaming技术内幕 : Receiver在Driver的精妙实现全生命周期彻底研究和思考

        原创文章,转载请注明:转载自 听风居士博客(http://www.cnblogs.com/zhouyf/)       Spark streaming 程序需要不断接收新数据,然后进行业务逻辑 ...

  2. redis三种连接方式

    安装 tar zxvf redis-2.8.9.tar.gz cd redis-2.8.9 #直接make 编译 make #可使用root用户执行`make install`,将可执行文件拷贝到/u ...

  3. CodeForces 733D Kostya the Sculptor

    排序.把每一个长方体拆成$6$个做,然后排序做即可. #pragma comment(linker, "/STACK:1024000000,1024000000") #includ ...

  4. 使用CSS让元素尺寸缩小时保持宽高比例一致

    CSS中有一个属性padding对元素宽度存在依存关系.如果一个元素的 padding属性以百分比形式表示,padding 的大小是以该元素自身宽度为参照的. 若想要元素尺寸变化时,宽高比例不变,可以 ...

  5. DelegatingFilterProxy干了什么?

    org.springframework.web.filter.DelegatingFilterProxy 一般情况,创建一个Filter是交给自己来实现的.基于servlet规范,在web.xml中配 ...

  6. Poj1182 食物链(并查集/带权并查集)

    题面 Poj 题解 这里采用并查集的补集. \(x\)表示同类集合,\(x+n\)表示敌人集合,\(x+n\times2\)表示敌人的敌人集合. 如果当前给出的是一对同类关系,就判断\(x\)是否吃\ ...

  7. 洛谷——P1165 日志分析

    P1165 日志分析 题目描述 M 海运公司最近要对旗下仓库的货物进出情况进行统计.目前他们所拥有的唯一记录就是一个记录集装箱进出情况的日志.该日志记录了两类操作:第一类操作为集装箱入库操作,以及该次 ...

  8. application.xml

    application.xml Deployment Descriptor Elements The following sections describe the application.xml f ...

  9. Xamarin.Forms教程Android SDK工具下载安装

    Xamarin.Form的Android SDK工具下载安装 本节将讲解如何下载Xamarin.Form的Android SDK工具,并使用其中的工具管理Android SDK,如何创建模拟器等内容. ...

  10. Xamarin中打开别人项目找不到android.jar文件

    Xamarin中打开别人项目找不到android.jar文件 错误信息:Could not find android.jar for API Level 23.打开非本机创建的Xamarin项目,编译 ...