10分钟搞懂!消息队列选型全方位对比

Posted 高可用架构

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10分钟搞懂!消息队列选型全方位对比相关的知识,希望对你有一定的参考价值。

导语 | 消息队列是分布式系统中重要的中间件,在高性能、高可用、低耦合等系统架构中扮演着重要作用。本文对Kafka、Pulsar、RocketMQ、RabbitMQ、NSQ这几个消息队列组件进行了一些调研,并整理了相关资料,为业务对MQ中间件选型提供参考。


一、概述


消息队列是分布式系统中重要的中间件,在高性能、高可用、低耦合等系统架构中扮演着重要作用。分布式系统可以借助消息队列的能力,轻松实现以下功能:


  • 解耦,将一个流程的上游和下游拆开,上游专注生产消息,下游专注处理消息。


  • 广播,一个上游生产的消息轻松被多个下游服务处理。


  • 缓冲,应对流量突然上涨,消息队列可以扮演一个缓冲器的作用,保护下游服务使其可以根据实际的消费能力处理消息。


  • 异步,上游发送消息后可以马上返回,下游可以异步处理消息。


  • 冗余,保留历史消息,处理失败或当出现异常时可以进行重试或者回溯防止丢失。


  • 近几年出现了一些关注度较高的消息队列中间件选型,如Kafka、Pulsar、RocketMQ等,首先从宏观上做一些对比



    结论:


  • 日志处理、大数据处理等场景,高吞吐量、低延迟的特性考虑,Kafka依旧是一个较好的选型。


  • 针对业务交易数据,有延迟消息、队列模式消费、异地容灾,多消息主题等场景,可以选用TDMQ/Pulsar。


  • 其他一些业务自定义的使用场景,由于后台技术栈是Golang,可以考虑采用NSQ进行定制开发或研究学习。


  • 消息中间件性能跟服务端、客户端参数、使用场景等方面上有很大关系,在系统上线前,还需要根据实际应用场景进行压测调优。



  • 二、架构简介


    (一)Kafka


    (来源:https://zhuanlan.zhihu.com/p/38269875)


    一个Kafka集群由多个Broker和一个ZooKeeper集群组成,Broker作为Kafka节点的服务器。同一个消息主题Topic可以由多个分区Partition组成,分区物理存储在Broker上。负载均衡考虑,同一个Topic的多个分区存储在多个不同的Broker上,为了提高可靠性,每个分区在不同的Broker会存在副本。


    ZooKeeper是一个分布式开源的应用程序协调服务,可以实现统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等工作。Kafka里的ZooKeeper主要有一下几个作用:


  • Broker注册,当有Broker故障的时候能及时感知。


  • Topic注册,维护Topic各分区的个副本所在的Broker节点,以及对应leader/follower的角色。


  • Consumer注册,维护消费者组的offset以及消费者与分区的对应关系,实现负载均衡。



  • (二)Pulsar


    (来源:https://cloud.tencent.com/developer/article/1845616)


    Pulsar有三个重要的组件,Broker、BookKeeper和ZooKeeper,Broker是无状态服务,客户端需要连接到Broker上进行消息的传递。BookKeeper与ZooKeeper是有状态服务。BookKeeper的节点叫Bookie,负责存储消息和游标,ZooKeeper存储Broker和Bookie的元数据。Pulsar以这种架构,实现存储和计算分离,Broker负责计算,Bookie负责有状态存储。



    Pulsar的多层架构影响了存储数据的方式。Pulsar将Topic分区划分为分片(Segment),然后将这些分片存储在Apache BookKeeper的存储节点上,以提高性能、可伸缩性和可用性。Pulsar的分布式日志以分片为中心,借助扩展日志存储(通过Apache BookKeeper)实现,内置分层存储支持,因此分片可以均匀地分布在存储节点上。由于与任一给定Topic相关的数据都不会与特定存储节点进行捆绑,因此很容易替换存储节点或缩扩容。另外,集群中最小或最慢的节点也不会成为存储或带宽的短板。



    (三)RocketMQ


    (来源:https://rocketmq.apache.org/docs/rmq-arc/)


    RocketMQ是阿里开源的消息中间件,它是一个开源的分布式消息传递和流式数据平台。总共有四大部分:NameServer,Broker,Producer,Consumer

    NameServer主要用来管理brokers以及路由信息。broker服务器启动时会注册到NameServer上,并且两者之间保持心跳监测机制,以此来保证NameServer知道broker的存活状态。而且,每一台NameServer都存有全部的broker集群信息和生产者/消费者客户端的请求信息。

    Broker负责管理消息存储分发,主从数据同步,为消息建立索引,提供消息查询等能力。



    (四)RabbitMQ


    (来源:https://www.cxymm.net/article/Super_RD/70238869)


    RabbitMQ基于AMQP协议来实现,主要由Exchange和Queue两部分组成,然后通过RoutingKey关联起来,消息投递到Exchange然后通过Queue接收。



    (五)NSQ


    (来源:https://zhuanlan.zhihu.com/p/37081073)


    NSQ主要有nsqlookup、nsqd两部分组成:


  • Nsqlookup为守护进程,负责管理拓扑信息并提供发现服务。客户端通过查询nsqlookupd获取指定Topic所在的nsqd节点。nsqd往nsqlookup上注册和广播自身topic和channel的信息。


  • nsqd在服务端运行的守护进程,负责接收,排队,投递消息给客户端。



  • 二、选型要点



    先来个汇总,接下来会对消息队列中间件的各项功能进行逐个分析。


    (一)功能


  • 消费推拉模式


  • 客户端消费者获取消息的方式,Kafka和RocketMQ是通过长轮询Pull的方式拉取消息,RabbitMQ、Pulsar、NSQ都是通过Push的方式。


    pull类型的消息队列更适合高吞吐量的场景,允许消费者自己进行流量控制,根据消费者实际的消费能力去获取消息。而push类型的消息队列,实时性更好,但需要有一套良好的流控策略(backpressure)当消费者消费能力不足时,减少push的消费数量,避免压垮消费端。


  • 延迟队列


  • 消息延迟投递,当消息产生送达消息队列时,有些业务场景并不希望消费者立刻收到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。延迟队列一般分为两种,基于消息的延迟和基于队列的延迟。基于消息的延迟指为每条消息设置不同的延迟时间,当队列有新消息进入的时候根据延迟时间排序,当然这样会对性能造成较大影响。另一种基于队列的延迟指的是设置不同延迟级别的队列,队列中每个消息的延迟时间都是相同的,这样免去了基于延迟时间排序对性能带来的损耗,通过一定的扫描策略即可投递超时的消息。

    延迟消息的使用场景比如异常检测重试,订单超时取消等,例如:


  • 服务请求异常,需要将异常请求放到单独的队列,隔5分钟后进行重试;


  • 用户购买商品,但一直处于未支付状态,需要定期提醒用户支付,超时则关闭订单;


  • 面试或者会议预约,在面试或者会议开始前半小时,发送通知再次提醒。


  • Kafka不支持延迟消息。Pulsar支持秒级的延迟消息,所有延迟投递的消息会被Delayed Message Tracker记录对应的index,consumer在消费时,会先去Delayed Message Tracker检查,是否有到期需要投递的消息,如果有到期的消息,则从Tracker中拿出对应的index,找到对应的消息进行消费,如果没有到期的消息,则直接消费正常的消息。对于长时间的延迟消息,会被存储在磁盘中,当快到延迟间隔时才被加载到内存里。



    RocketMQ开源版本延迟消息临时存储在一个内部主题中,不支持任意时间精度,支持特定的level,例如定时5s,10s,1m等。


    RabbitMQ需要安装一个rabbitmq_delayed_message_exchange插件。


    NSQ通过内存中的优先级队列来保存延迟消息,支持秒级精度,最多2个小时延迟。


  • 死信队列


  • 由于某些原因消息无法被正确的投递,为了确保消息不会被无故的丢弃,一般将其置于一个特殊角色的队列,这个队列一般称之为死信队列。与此对应的还有一个“回退队列”的概念,试想如果消费者在消费时发生了异常,那么就不会对这一次消费进行确认(Ack), 进而发生回滚消息的操作之后消息始终会放在队列的顶部,然后不断被处理和回滚,导致队列陷入死循环。为了解决这个问题,可以为每个队列设置一个回退队列,它和死信队列都是为异常的处理提供的一种机制保障。实际情况下,回退队列的角色可以由死信队列和重试队列来扮演。


    Kafka没有死信队列,通过Offset的方式记录当前消费的偏移量。

    Pulsar有重试机制,当某些消息第一次被消费者消费后,没有得到正常的回应,则会进入重试Topic中,当重试达到一定次数后,停止重试,投递到死信Topic中。


    RocketMQ通过DLQ来记录所有消费失败的消息。


    RabbitMQ是利用类似于延迟队列的形式实现死信队列。


    NSQ没有死信队列。


  • 优先级队列


  • 优先级队列不同于先进先出队列,优先级高的消息具备优先被消费的特权,这样可以为下游提供不同消息级别的保证。不过这个优先级也是需要有一个前提的:如果消费者的消费速度大于生产者的速度,并且消息中间件服务器(一般简单的称之为Broker)中没有消息堆积,那么对于发送的消息设置优先级也就没有什么实质性的意义了,因为生产者刚发送完一条消息就被消费者消费了,那么就相当于Broker中至多只有一条消息,对于单条消息来说优先级是没有什么意义的。


    Kafka、RocketMQ、Pulsar、NSQ不支持优先级队列,可以通过不同的队列来实现消息优先级。


    RabbitMQ支持优先级消息。


  • 消息回溯


  • 一般消息在消费完成之后就被处理了,之后再也不能消费到该条消息。消息回溯正好相反,是指消息在消费完成之后,还能消费到之前被消费掉的消息。对于消息而言,经常面临的问题是“消息丢失”,至于是真正由于消息中间件的缺陷丢失还是由于使用方的误用而丢失一般很难追查,如果消息中间件本身具备消息回溯功能的话,可以通过回溯消费复现“丢失的”消息进而查出问题的源头之所在。消息回溯的作用远不止与此,比如还有索引恢复、本地缓存重建,有些业务补偿方案也可以采用回溯的方式来实现。


    Kafka支持消息回溯,可以根据时间戳或指定Offset,重置Consumer的Offset使其可以重复消费。


    Pulsar支持按时间对消息进行回溯。


    RocketMQ支持按时间回溯,实现的原理跟Kafka一致。


    RabbitMQ不支持回溯,消息一旦标记确认就会被标记删除。

    NSQ一般消息是不可回溯的,但可以通过nsq_to_file工具,将消息写入到文件,然后从文件里重放消息。


  • 消息持久化

  • 流量削峰是消息中间件的一个非常重要的功能,而这个功能其实得益于其消息堆积能力。从某种意义上来讲,如果一个消息中间件不具备消息堆积的能力,那么就不能把它看做是一个合格的消息中间件。消息堆积分内存式堆积和磁盘式堆积。一般来说,磁盘的容量会比内存的容量要大得多,对于磁盘式的堆积其堆积能力就是整个磁盘的大小。从另外一个角度讲,消息堆积也为消息中间件提供了冗余存储的功能。


    Kafka和RocketMQ直接将消息刷入磁盘文件中进行持久化,所有的消息都存储在磁盘中。只要磁盘容量够,可以做到无限消息堆积。


    RabbitMQ 是典型的内存式堆积,但这并非绝对,在某些条件触发后会有换页动作来将内存中的消息换页到磁盘(换页动作会影响吞吐),或者直接使用惰性队列来将消息直接持久化至磁盘中。


    Pulsar消息是存储在BookKeeper存储集群上,也是磁盘文件。


    NSQ通过nsq_to_file工具,将消息写入到文件。


  • 消息确认机制


  • 消息队列需要管理消费进度,确认消费者是否成功处理消息,使用push的方式的消息队列组件往往是对单条消息进行确认,对于未确认的消息,进行延迟重新投递或者进入死信队列。


    Kafka通过Offset的方式确认消息。

    RocketMQ与Kafka类似也会提交Offset,区别在于消费者对于消费失败的消息,可以标记为消息消费失败,Broker会重试投递,如果累计多次消费失败,会投递到死信队列。


    RabbitMQ和NSQ类似,消费者确认单条消息,否则会重新放回队列中等待下次投递。


    Pulsar使用专门的Cursor管理。累积确认和Kafka效果一样;提供单条或选择性确认。


  • 消息TTL


  • 消息TTL表示一条消息的生存时间,如果消息发出来后,在TTL的时间内没有消费者进行消费,消息队列会将消息删除或者放入死信队列中。


    Kafka根据设置的保留期来删除消息。有可能消息没被消费,过期后被删除。不支持TTL。

    Pulsar支持TTL,如果消息未在配置的TTL时间段内被任何消费者使用,则消息将自动标记为已确认。消息保留期与消息TTL之间的区别在于:消息保留期作用于标记为已确认并设置为已删除的消息,而TTL作用于未ack的消息。上面的图例中说明了Pulsar中的TTL。例如,如果订阅B没有活动消费者,则在配置的TTL时间段过后,消息M10将自动标记为已确认,即使没有消费者实际读取该消息。


    RocketMQ提及到消息TTL的资料比较少,不过看接口似乎是支持的。


    RabbitMQ有两种方式,一个是声明队列的时候在队列属性中设置,整个队列中的消息都有相同的有效期。还可以发送消息的时候给消息设置属性,可以位每条消息都设置不同的TTL。


    NSQ似乎还没支持,有一个Feature Request的Issue处于Open状态。


  • 多租户隔离


  • 多租户是指通过一个软件实例为多个租户提供服务的能力。租户是指对系统有着相同“视图”的一组用户。不支持多租户的系统里边,往往要为不同用户或者不同集群创建多个消息队列实例实现物理隔离,这样会带来较高的运维成本。作为一种企业级的消息系统,Pulsar的多租户能力按照设计可满足下列需求:

     

  • 确保严苛的SLA可顺利满足。


  • 保证不同租户之间的隔离。


  • 针对资源利用率强制实施配额。


  • 提供每租户和系统级的安全性。


  • 确保低成本运维以及尽可能简单的管理。

  • Pulsar通过下列方式满足了上述需求:


  • 通过为每个租户进行身份验证、授权和ACL(访问控制列表)获得所需安全性。


  • 为每个租户强制实施存储配额。


  • 以策略的方式定义所有隔离机制,策略可在运行过程中更改,借此降低运维成本并简化管理工作。


  • 消息顺序性


  • 消息顺序性是指保证消息有序。消息消费顺序跟生产的顺序保持一致。


    Kafka保证了分区内的消息有序。


    Pulsar支持两种消费模式,独占订阅的流模式只保证了消息的顺序性,共享订阅队列模型不保证有序性。


    RocketMQ需要用到锁来保证一个队列同时只有一个消费者线程进行消费,保证消息的有序性。


    RabbitMQ顺序性的条件比较苛刻,需要单线程发送、单线程消费,并且不采用延迟队列、优先级队列等高级功能。


    NSQ是利用了golang自身的case/select实现的消息分发,本身不提供有序性保障,不能够把特性消息和消费者对应起来,无法实现消息的有序性。


  • 消息查询


  • 在实际开发中,经常要查看MQ中消息的内容,比如通过某个MessageKey/ID,查询到MQ的具体消息。或者是对消息进行链路追踪,知道消息从哪里来,发送到哪里去,进而快速对问题进行排查定位。


    Kafka存储层是以分布式提交日志的形式实现,每次写操作都顺序追加到日志的末尾。读也是顺序读。不支持检索功能。


    Pulsar可以通过消息ID,查询到具体某条消息的消息内容、消息参数和消息轨迹。


    RocketMQ支持按Message Key、Unique Key、Message Id对消息进行查询。


    RabbitMQ使用基于索引的存储系统。这些将数据保存在树结构中,以提供确认单个消息所需的快速访问。由于RabbitMQ的消息在确认后会被删除,因此只能查询未确认的消息。


    NSQ自身不支持消息持久化和消息检索,不过可以使用nsq_to_http等工具将消息写入可支持索引的存储里。


  • 消费模式

  • Kafka有两种消费模式,最终都会保证一个分区只有1个消费者在消费:


  • subscribe方式:当主题分区数量变化或者consumer数量变化时,会进行rebalance;注册rebalance监听器,可以手动管理offset不注册监听器,kafka自动管理。


  • assign方式:手动将consumer与partition进行对应,kafka不会进行rebanlance。


  • Pulsar有以下四种消费模式,其中独占模式和灾备模式跟Kafka类似,为流模型,每个分区只有1个消费者消费,能保证消息有序性。共享模式和Key共享模式为队列模型,多个消费者能提高消费速度,但不能保证有序性。



  • Exclusive独占模式(默认模式):一个Subscription只能与一个Consumer关联,只有这个Consumer可以接收到Topic的全部消息,如果该Consumer出现故障了就会停止消费。


  • 灾备模式(Failover):当存在多个consumer时,将会按字典顺序排序,第一个consumer被初始化为唯一接受消息的消费者。当第一个consumer断开时,所有的消息(未被确认和后续进入的)将会被分发给队列中的下一个consumer。


  • 共享模式(Shared):消息通过round robin轮询机制(也可以自定义)分发给不同的消费者,并且每个消息仅会被分发给一个消费者。当消费者断开连接,所有被发送给他,但没有被确认的消息将被重新安排,分发给其它存活的消费者。


  • KEY共享模式(Key_Shared):当存在多个consumer时,将根据消息的 key进行分发,key相同的消息只会被分发到同一个消费者。


  • RocketMQ有两种消费模式,BROADCASTING广播模式,CLUSTERING集群模式。

    广播消费指的是:一条消息被多个consumer消费,即使这些consumer属于同一个ConsumerGroup,消息也会被ConsumerGroup中的每个Consumer都消费一次,广播消费中ConsumerGroup概念可以认为在消息划分方面无意义。


    集群消费模式:一个ConsumerGroup中的Consumer实例平均分摊消费消息。例如某个Topic有9条消息,其中一个ConsumerGroup有3个实例(可能是3个进程,或者3台机器),那么每个实例只消费其中部分,消费完的消息不能被其他实例消费。


    RabbitMQ和NSQ的消费比较类似,都是跟Pulsar共享模式类似的,队列的形式,增加一个消费者组里的消费者数量能提高消费速度。


  • 消息可靠性


  • 消息丢失是使用消息中间件时所不得不面对的一个同点,其背后消息可靠性也是衡量消息中间件好坏的一个关键因素。尤其是在金融支付领域,消息可靠性尤为重要。比如当服务出现故障时,一些对于生产者来说已经生产成功的消息,是否会在高可用切换时丢失。同步刷盘是增强一个组件可靠性的有效方式,消息中间件也不例外,Kafka和RabbitMQ都可以支持同步刷盘,但绝大多数情景下,一个组件的可靠性不应该由同步刷盘这种极其损耗性能的操作来保障,而是采用多副本的机制来保证。


    Kafka可以通过配置request.required.acks参数设置可靠级别,表示一条消息有多少个副本确认接收成功后,才被任务发送成功。


  • request.required.acks=-1 (全量同步确认,强可靠性保证)


  • request.required.acks=1(leader确认收到,默认)


  • request.required.acks=0 (不确认,但是吞吐量大)


  • Pulsar有跟Kafka类似的概念,叫Ack Quorum Size(Qa),Qa是每次写请求发送完毕后需要回复确认的Bookie的个数,其数值越大则需要确认写成功的时间越长,其值上限是副本数Qw。为了一致性,Qa应该是:(Qw+1)/2或者更,即为了确保数据安全性,Qa下限是 (Qw+1)/2。


    RocketMQ与Kafka类似。


    RabbitMQ是主从架构,通过镜像环形队列实现多副本及强一致性语义的。多副本可以保证在master节点宕机异常之后可以提升slave作为新的master而继续提供服务来保障可用性。


    NSQ会通过go-diskqueue组件将消息落盘到本地文件中,通过mem-queue-size参数控制内存中队列大小,如果mem-queue-size=0每条消息都会存储到磁盘里,不用担心节点重启引起的消息丢失。但由于是存储在本地磁盘中,如果节点离线,堆积在节点磁盘里的消息会丢失。



    (二)性能


    Kafka的公司Confluent在2020年8月发了一篇Benchmarking Apache Kafka, Apache Pulsar, and RabbitMQ: Which is the Fastest?文章,并且提出了一个开源的MQ Benchmark框架THE OPENMESSAGING BENCHMARK FRAMEWORK,在这个文档里,对比了Kafka、Pulsar、RabbitMQ的吞吐量、端到端延迟等性能数据。最后得出结论Kafka相对来说性能最好。



    但接下来StreamNative在2020年12月指出了Confluence的基准测试的一些问题,并对Pulsar进行了参数调优之后重新执行了一遍结果,测试报告展示Pulsar能达到跟Kafka同样的吞吐量,在某些场景下,Pulsar的延迟显著低于Kafka。



    而且在性能测试上,有很多客户端、服务端参数设置、机器性能配置等影响,比如消息可靠性级别,压缩算法等,很难做到“完全”控制变量公平的测试。而且OpenMessaging Benchmark的开源Github的Readme上也提到了。


    不过有几个关注点:


  • RabbitMQ的延迟是微秒级的,其他组件的延迟都是毫秒级,RabbitMQ应该是MQ组件里相对来说较低的。


  • Kafka单实例在主题/分区数比较多的情况下,性能会明显降低。


    1. kafka是一个分区一个文件,当topic过多,分区的总量也会增加,kafka中存在过多的文件,当对消息刷盘时,就会出现文件竞争磁盘,出现性能的下降。


    2. 还有Kafka每个消费者加入或退出都会进行重平衡,当分区数比较多时重平衡可能耗时较久,在重平衡的阶段消费者是不能消费消息的。


  • 而Pulsar由于存储与计算分离的架构,使得它可以支持百万级别的Topic数量。


  • Pulsar和Kafka都被广泛用于各个企业,也各有优势,都能通过数量基本相同的硬件处理大流量。部分用户误以为Pulsar使用了很多组件,因此需要很多服务器来实现与Kafka相匹敌的性能。这种想法适用于一些特定硬件配置,但在多数资源配置相同的情况中,Pulsar的优势更加明显,可以用相同的资源实现更好的性能。举例来说,Splunk最近分享了他们选择Pulsar放弃Kafka的原因,其中提到“由于分层架构,Pulsar帮助他们将成本降低了30%-50%,延迟降低了80%-98%,运营成本降低了33%-50%”。Splunk 团队发现Pulsar可以更好地利用磁盘IO,降低CPU利用率,同时更好地控制内存。


    在分布式系统里,单机性能指标虽然也很重要,分布式系统整体的性能以及灵活扩缩容、高可用容灾等能力也会是评估的一个重要参考。MQ中间件具体的性能指标,也需要我们自己根据实际的情况,根据实际购买的集群配置和客户端参数,进行压测调优来评估。



    (三)运维


    在使用过程中难免会出现各种异常情况,比如宕机、网络抖动、扩容等。消息队列具备异地容灾,高可用架构等能力,能避免一些计算节点、网络等基础设施不可用导致的故障。


  • 高可用


  • Kafka通过分区多副本的方式解决高可用问题。


    Pulsar的计算集群Broker是无状态的,可以灵活扩缩容,存储节点Bookie上通过消息分区分片副本的方式,每个分片都有一个或多个副本,保证在某一个Bookie挂掉后,有其他分片可以提供服务。


    RocketMQ和RabbitMQ都是主从架构,当master挂掉后,由原来的从节点继续提供服务。备机提供消费服务,保证消息不丢,但不提供写服务。

    NSQ是类似分布式架构,不过由于消息存储是在节点本地磁盘上,如果一个节点离线,堆积在节点磁盘上的消息会丢失。


  • 跨地域容灾


  • Pulsar原生支持跨地域容灾功能,在这个图中,每当P1、P2和P3的生产者分别向Cluster-A、Cluster-B和Cluster-C中的T1 topic发送消息时,这些消息很快在不同的集群中复制。一旦消息完成复制,消费者C1和C2会从各自的集群消费到这个消息。


    在这个跨地域容灾的设计支撑下,其一,我们可以比较容易的将服务分散到多个机房;其二,可以应对机房级别的故障,即在一个机房不可用的情况下,服务可以转接到其它的机房来继续对外提供服务。


    一句话概括,Pulsar的跨地域复制,其实就是在一个本地集群中创建一个 Producer,把异地的集群作为这个Producer的发送地址,将本地集群的消息发送过去,并且在本地维护一个Cusor来保证消息可靠性和幂等性。



  • 集群扩容


  • 当消息量突然上涨,消息队列集群到达瓶颈的时候,需要对集群进行扩容,扩容一般分为水平扩容和垂直扩容两种方式,水平扩容指的是往往集群中增加节点,垂直扩容指的是把集群中部分节点的配置调高,增加处理能力。


    Kafka集群由于主题分区是物理存储在Broker节点上的,新加入的集群的节点并没有存储分区分片,也就无法提供马上提供服务,因此需要把一些Topic的分区分配到新加入的节点里,这里会涉及到一个分区数据均衡的过程,将某些分区的数据复制到新节点上。这个过程跟分区当前堆积的数据量、Broker性能有关,有可能会出现由于源Broker负载过高,堆积数据过大,导致数据均衡的时间变长。


    Pulsar的无限分布式日志以分片为中心,借助扩展日志存储(通过Apache BookKeeper)实现,内置分层存储支持,因此分片可以均匀地分布在存储节点上。由于与任一给定topic相关的数据都不会与特定存储节点进行捆绑,因此很容易替换存储节点或缩扩容。另外,集群中最小或最慢的节点也不会成为存储或带宽的短板。


    RocketMQ新节点直接加入到集群中,在新的broker创建新topic并且分配队列,或者在已有topic基础上分配队列。与Kafka的区别是,Kafka的分区是在不同的物理机器上,而Rocketmq是逻辑分区,用的队列形式,因此不存在出现数据不均衡的情况。


    RabbitMQ和NSQ类似,由于不涉及过多的消息持久化,直接往集群中增加节点。


  • 使用成本


  • Kafka/Pulsar/RocketMQ/RabbitMQ在腾讯云上都上线了标准产品,可以直接购买创建实例,能大大降低部署运维成本。而NSQ目前暂时还没有上线,需要自行部署。


    CKafka在腾讯云上是以实例的形式售卖,专业版最低配1494元/月,500G SSD,40MB/s,TDMQ Pulsar是以类似无服务的方式按量计费,按调用次数/消息大小/存储大小等计费,调用次数2.00元/百万次。在用量较少的情况下,比如一些小型快速上线的业务,TDMQ Pulsar的成本会比CKafka低很多。


    RocketMQ和RabbitMQ都是最近推出的产品,目前仍在公测阶段,暂时还没有定价。



    三、总结


    Kafka与Pulsar都是腾讯云主打的消息队列中间件,都具有高性能,高可靠,支持多种场景。Kafka推出的时间较早,各种场景比如日志、大数据处理等都有较成熟的解决方案。而Pulsar作为一个新秀,支持的功能比CKafka更丰富,而且跨地域容灾,多租户等功能,解决了很多Kafka设计缺陷和运维成本问题,整体稳定性更强。很多国内外大公司也有很多Pulsar的实践案例。因此,一些传统的日志、大数据处理等场景,对高吞吐量有要求的,对消息可靠性的要求没那么高的,可以选用Kafka,有很多优秀的文档说明怎么参数调优提高性能。而一些对消息可靠性、容灾要求更好,或者有高分区、延迟队列等需求的场景,可以选用Pulsar。


    我们后台的技术栈是基于Golang的,在上文的对比中,还挑了一个基于Golang开发的消息队列NSQ,如果有一些定制化需求或者需要二次开发的,可以选用NSQ。也可以通过阅读NSQ的源码,学习一些优秀高性能消息队列中间件的实现方式,比如里边diskqueue组件,一个基于磁盘的消息队列,在某些场景下可能也可以进行二次利用。


    参考资料:

    1.Kafka vs. Pulsar vs. RabbitMQ: Performance, Architecture, and Features Compared

    2.消息中间件选型分析:从Kafka与RabbitMQ的对比看全局

    3.RabbitMQ的TTL(消息有效期)和DLX(死信交换机/队列)
    4.Apache Pulsar延迟消息投递解析
    5.深入理解RocketMQ延迟消息
    6.三分钟了解RocketMQ与Kafka的异同

    7.个推基于Apache Pulsar的优先级队列方案
    8.消息中间件选型分析:从Kafka与RabbitMQ的对比看全局
    9.RocketMQ中消息的优先级

    10.消息队列--NSQ&Kafka

    11.三分钟了解RocketMQ与Kafka的异同
    12.告别传统金融消息架构:Apache Pulsar在平安证券的实践

    13.【知识积累】MQ消息堆积和TTL过期

    14.Pulsar官方文档-租户
    15.Apache Pulsar的多租户消息系统



     作者简介


    李明宽

    腾讯教育后台开发工程师

    腾讯教育后台开发工程师,毕业于中山大学。目前负责腾讯教育相关产品的后台研发工作。


    参考阅读:

  • Redis 7.0 共享复制缓冲区的设计与实现

  • 还在用ES查日志吗,快看看石墨文档 Clickhouse 日志架构玩法

  • Java内存模型(Java Memory Model,JMM)

  • vivo数据库与存储平台的建设和探索

  • 爱奇艺内容中台之Serverless应用与实践

  • 技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。


    高可用架构
    改变互联网的构建方式

    事务消息大揭秘!RocketMQKafkaPulsar全方位对比

    导语 | 事务是一个程序执行单元,里面的所有操作要么全部执行成功,要么全部执行失败。RocketMQ、Kafka和Pulsar都是当今业界应用十分广泛的开源消息队列(MQ)组件,笔者在工作中遇到关于MQ选型相关的内容,了解到关于“事务消息”这个概念在不同的MQ组件里有不同内涵。故借此文,试着浅析一番这三种消息队列(MQ)的事务消息有何异同,目的是形成关于消息队列事务消息的全景视图,给有类似业务需求的同学提供一些参考和借鉴。

    一、消息队列演化

    消息队列(Message Queue,简称MQ),是指在消息的传输中保存消息的容器或服务,是一种异步的服务间通信方式,适用于无服务器和微服务架构,是分布式系统实现高性能、高可用、可伸缩等高级特效的重要组件。

    常见的主流消息队列有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ、Pulsar等。而在公司内有TubeMQ、Ckafka、TDMQ、CMQ、CDMQ、Hippo等。

    Kafka:Apache Kafka是由Apache软件基金会开发的一个开源消息系统项目,由Scala写成。Kafka最初是由LinkedIn开发,并于2011年初开源。2012年10月从Apache Incubator毕业。该项目的目标是为处理实时数据提供一个统一、高通量、低等待的平台。

    Kafka是一个分布式的、分区的、多复本的日志提交服务。它通过一种独一无二的设计提供了一个消息系统的功能,其整体架构图如下所示。

    RocketMQ:Apache RocketMQ是一个分布式消息和流媒体平台,具有低延迟、强一致、高性能和可靠性、万亿级容量和灵活的可扩展性。它有借鉴Kafka的设计思想,但不是Kafka的拷贝,其整体架构图如下所示。

    Pulsar:Apache Pulsar是Apache软件基金会顶级项目,是下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据复制,具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案,其整体架构图如下所示。

    二、背景知识

    (一)什么是事务?

    • 事务(Trasaction)

    事务是一个程序执行单元,里面的所有操作要么全部执行成功,要么全部执行失败。

    一个事务有个基本特性,也就是我们常说的(ACID)。

    Atomicity(原子性):事务是一个不可分割的整体,事务内所有操作要么全做成功,要么全失败。

    Consistency(一致性):事务执行前后,数据从一个状态到另一个状态必须是一致的(A向B转账,不能出现A扣了钱,B却没收到)。

    Isolation(隔离性):多个并发事务之间相互隔离,不能互相干扰。

    Durablity(持久性):事务完成后,对数据的更改是永久保存的,不能回滚。

    • 分布式事务

    分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。分布式事务通常用于在分布式系统中保证不同节点之间的数据一致性。

    分布式事务的解决方案一般有以下几种:

    XA(2PC/3PC)

    最具有代表性的是由Oracle Tuxedo系统提出的XA分布式事务协议。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA协议通常包含两阶段提交(2PC)三阶段提交(3PC)两种实现。两阶段提交顾名思义就是要进行两个阶段的提交:第一阶段,准备阶段(投票阶段);第二阶段,提交阶段(执行阶段)。实现过程如下所示:

    二阶段提交看似能够提供原子性的操作,但它存在着一些缺陷,三段提交(3PC)是对两段提交(2PC)的一种升级优化,有兴趣的可以深入了解一下,这里不再赘述。

    TCC

    TCC(Try-Confirm-Cancel)是Try、Commit、Cancel三种指令的缩写,又被称补偿事务,其逻辑模式类似于XA两阶段提交,事务处理流程也很相似,但2PC是应用于在DB层面,TCC则可以理解为在应用层面的2PC,是需要我们编写业务逻辑来实现。

    TCC它的核心思想是:“针对每个操作都要注册一个与其对应的确认(Try)和补偿(Cancel)”。

    消息事务

    所谓的消息事务就是基于消息队列的两阶段提交,本质上是对消息队列的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败。

    基于消息队列的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下:

    虽然上面的方案能够完成A和B的操作,但是A和B并不是强一致的,而是最终一致(Eventually consistent)的。而这也是满足BASE理论的要求的。这里引申一下,BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中AP(CAP已经被证实一个分布式系统最多只能同时满足CAP三项中的两项)的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。

    (二)什么是Exactly-once (精确一次)语义?

    在分布式系统中,任何节点都有可能出现异常甚至宕机。在消息队列中也一样,当Producer在生产消息时,可能会发生Broker宕机不可用,或者网络突然中断等异常情况。根据在发生异常时Producer处理消息的方式,系统可以具备以下三种消息语义。

    • At-least-once(至少一次)语义

    Producer通过接收Broker的ACK(消息确认)通知来确保消息成功写入Topic。然而,当Producer接收ACK通知超时,或者收到Broker出错信息时,会尝试重新发送消息。‍‍‍‍‍‍如果Broker正好在成功把消息写入到Topic,但还没有给Producer发送ACK时宕机,Producer重新发送的消息会被再次写入到Topic,最终导致消息被重复分发至Consumer。即:消息不会丢失,但有可能被重复发送

    • At-most-once(最多一次)语义

    当Producer在接收ACK超时,或者收到Broker出错信息时不重发消息,那就有可能导致这条消息丢失,没有写入到Topic中,也不会被Consumer消费到。在某些场景下,为了避免发生重复消费,我们可以容许消息丢失的发生。即:消息可能会丢失,但绝不会被重复发送

    • Exactly-once(精确一次)语义


    Exactly-once语义保证了即使Producer多次发送同一条消息到服务端,服务端也仅仅会记录一次。Exactly-once语义是最可靠的,同时也是最难理解的。Exactly-once语义需要消息队列服务端,消息生产端和消费端应用三者的协同才能实现。比如,当消费端应用成功消费并且ACK了一条消息之后,又把消费位点回滚到之前的一个消息ID,那么从那个消息ID往后的所有消息都会被消费端应用重新消费到。即:消息不会丢失,也不会被重复发送

    三、RocketMQ、Kafka、Pulsar事务消息

    (一)RocketMQ的事务消息

    RocketMQ在4.3.0版中已经支持分布式事务消息,这里RocketMQ采用了2PC的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,流程如下图所示:

    其具体工作流程分为正常事务消息的发送及提交和不正常情况下事务消息的补偿流程:

    1. 在消息队列上开启一个事务主题。

    2. 事务中第一个执行的服务发送一条“半消息”(半消息和普通消息的唯一区别是,在事务提交之前,对于消费者来说,这个消息是不可见的)给消息队列。

    3. 半消息发送成功后,发送半消息的服务就会开始执行本地事务,根据本地事务执行结果来决定事务消息提交或者回滚。

    4. 本地事务成功后会让这个“半消息”变成正常消息,供分布式事务后面的步骤执行自己的本地事务。(这里的事务消息,Producer不会因为Consumer消费失败而做回滚,采用事务消息的应用,其所追求的是高可用和最终一致性,消息消费失败的话,RocketMQ自己会负责重推消息,直到消费成功。)

    补偿流程:RocketMQ提供事务反查来解决异常情况,如果RocketMQ没有收到提交或者回滚的请求,Broker会定时到生产者上去反查本地事务的状态,然后根据生产者本地事务的状态来处理这个“半消息”是提交还是回滚。值得注意的是我们需要根据自己的业务逻辑来实现反查逻辑接口,然后根据返回值Broker决定是提交还是回滚。而且这个反查接口需要是无状态的,请求到任意一个生产者节点都会返回正确的数据。

    其中,补偿流程用于解决消息Commit或者Rollback发生超时或者失败的情况。在RocketMQ事务消息的主要流程中,一阶段的消息如何对用户不可见。其中,事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。那么,如何做到写入消息但是对用户不可见呢?RocketMQ事务消息的做法是:如果消息是“半消息”,将备份原消息的主题与消息消费队列,然后改变主题为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费“半消息”的消息,然后RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。

    讲到这里大家就明白了,这里说的就是上文提到分布式事务中的消息事务,目的是在分布式事务中实现系统的最终一致性。


    (二)Kafka的事务消息

    与RocketMQ的事务消息用途不同,Kafka的事务基本上是配合其幂等机制来实现Exactly-once(见上文)语义的

    开发此功能的原因可以总结如下:

    流处理的需求

    随着流处理的兴起,对具有更强处理保证的流处理应用的需求也在增长。例如,在金融行业,金融机构使用流处理引擎为用户处理借款和信贷。这种类型的用例要求每条消息都只处理一次,无一例外。

    换句话说,如果流处理应用程序消费消息A并将结果作为消息B(B = f(A)),那么恰好一次处理保证意味着当且仅当B被成功生产后A才能被标记为消费,反之亦然。

    事务API使流处理应用程序能够在一个原子操作中使用、处理和生成消息。这意味着,事务中的一批消息可以从许多主题分区接收、生成和确认。一个事务涉及的所有操作都作为整体成功或失败。

    目前,Kafka默认提供的交付可靠性保障是At-least-once。如果消息成功“提交”,但Broker的应答没有成功发送回Producer端(比如网络出现瞬时抖动),那么Producer就无法确定消息是否真的提交成功了。因此,它只能选择重试,这就是Kafka默认提供At-least-once保障的原因,不过这会导致消息重复发送。大部分用户还是希望消息只会被交付一次,这样的话,消息既不会丢失,也不会被重复处理。或者说,即使Producer端重复发送了相同的消息,Broker端也能做到自动去重。在下游Consumer看来,消息依然只有一条。那么问题来了,Kafka是怎么做到精确一次的呢?简单来说,这是通过两种机制:幂等性(Idempotence)和事务(Transaction)。

    • 幂等性Producer

    “幂等”这个词原是数学领域中的概念,指的是某些操作或函数能够被执行多次,但每次得到的结果都是不变的。幂等性有很多好处,其最大的优势在于我们可以安全地重试任何幂等性操作,反正它们也不会破坏我们的系统状态。如果是非幂等性操作,我们还需要担心某些操作执行多次对状态的影响,但对于幂等性操作而言,我们根本无需担心此事。

    在Kafka中,Producer默认不是幂等性的,但我们可以创建幂等性Producer。它其实是0.11.0.0版本引入的新功能。enable.idempotence 被设置成true后,Producer自动升级成幂等性Producer,其他所有的代码逻辑都不需要改变。Kafka自动帮你做消息的重复去重。Kafka为了实现幂等性,它在底层设计架构中引入了ProducerIDSequenceNumber。ProducerID:在每个新的Producer初始化时,会被分配一个唯一的ProducerID,用来标识本次会话。

    SequenceNumber:对于每个ProducerID,Producer发送数据的每个Topic和Partition都对应一个从0开始单调递增的SequenceNumber值。Broker在内存维护(pid,seq)映射,收到消息后检查seq。Producer在收到明确的的消息丢失ack,或者超时后未收到ack,要进行重试。

    • new_seq=old_seq+1: 正常消息;

    • new_seq<=old_seq: 重复消息;

    • new_seq>old_seq+1: 消息丢失;

    另外我们需要了解幂等性Producer的作用范围。首先,它只能保证单分区上的幂等性,即一个幂等性Producer能够保证某个主题的一个分区上不出现重复消息,它无法实现多个分区的幂等性。其次,它只能实现单会话上的幂等性,不能实现跨会话的幂等性。这里的会话,你可以理解为Producer进程的一次运行。当你重启了Producer进程之后,这种幂等性保证就丧失了。如果想实现多分区以及多会话上的消息无重复,应该怎么做呢?答案就是事务(transaction)或者依赖事务型Producer。这也是幂等性Producer和事务型Producer的最大区别。


    • 事务型Producer

    事务型Producer能够保证将消息原子性地写入到多个分区中。这批消息要么全部写入成功,要么全部失败。另外,事务型Producer也不受进程的重启影响。Producer重启后,Kafka依然保证它们发送消息的Exactly-once处理。和普通Producer代码相比,事务型Producer的显著特点是调用了一些事务API,如initTransaction、beginTransaction、commitTransaction和abortTransaction,它们分别对应事务的初始化、事务开始、事务提交以及事务终止。

    Kafka事务消息是由Producer、事务协调器、Broker、组协调器、Consumer等共同参与实现的。

    Producer

    为Producer指定固定的TransactionalId(事务id),可以穿越Producer的多次会(Producer重启/断线重连)中,持续标识Producer的身份。

    每个生产者增加一个epoch。用于标识同一个TransactionalId在一次事务中的epoch,每次初始化事务时会递增,从而让服务端可以知道生产者请求是否旧的请求。使用epoch标识Producer的每一次“重生”,可以防止同一Producer存在多个会话。

    Producer遵从幂等消息的行为,并在发送的BatchRecord中增加事务id和epoch。

    事务协调器(Transaction Coordinator)

    引入事务协调器,类似于消费组负载均衡的协调者,每一个实现事务的生产端都被分配到一个事务协调者。以两阶段提交的方式,实现消息的事务提交。

    事务协调器使用一个特殊的Topic:即事务Topic,事务Topic本身也是持久化的,日志信息记录事务状态信息,由事务协调者写入。

    事务协调器通过RPC调用,协调Broker和Consumer实现事务的两阶段提交。

    每一个Broker都会启动一个事务协调器,使用hash(TransactionalId)确定Producer对应的事务协调器,使得整个集群的负载均衡。

    Broker

    引入控制消息(Control Messages):这些消息是客户端产生的并写入到主题的特殊消息,但对于使用者来说不可见。它们是用来让Broker告知消费者之前拉取的消息是否被原子性提交。

    Broker处理事务协调器的commit/abort控制消息,把控制消息向正常消息一样写入Topic(图中标c的消息,和正常消息交织在一起,用来确认事务提交的日志偏移),并向前推进消息提交偏移hw。

    组协调器

    如果在事务过程中,提交了消费偏移,组协调器在offset log中写入事务消费偏移。当事务提交时,在offset log中写入事务offset确认消息。

    Consumer

    Consumer过滤未提交消息和事务控制消息,使这些消息对用户不可见。

    有两种实现方式:

    • Consumer缓存方式

    设置isolation.level=read_uncommitted,此时topic的所有消息对Consumer都可见。Consumer缓存这些消息,直到收到事务控制消息。若事务commit,则对外发布这些消息;若事务abort,则丢弃这些消息。

    • Broker过滤方式

    设置isolation.level=read_committed,此时topic中未提交的消息对Consumer不可见,只有在事务结束后,消息才对Consumer可见。Broker给Consumer的BatchRecord消息中,会包含以列表,指明哪些是“abort”事务,Consumer丢弃abort事务的消息即可。

    因为事务机制会影响消费者所能看到的消息的范围,它不只是简单依赖高水位来判断。它依靠一个名为LSO(Log Stable Offset)的位移值来判断事务型消费者的可见性。

    (三)Pulsar的事务消息

    Apache Pulsar在2.8.0正式支持了事务相关的功能,Pulsar这里提供的事务区别于RocketMQ中2PC那种事务的实现方式,没有本地事务回查的机制,更类似于Kafka的事务实现机制。Apache Pulsar中的事务主要用来保证类似Pulsar Functions这种流计算场景中Exactly-once语义的实现,这也符合Apache Pulsar本身Event Streaming的定位,即保证端到端(End-to-End)的事务实现的语义。

    在Pulsar中,对于事务语义是这样定义的:允许事件流应用将消费、处理、生产消息整个过程定义为一个原子操作,即生产者或消费者能够处理跨多个主题和分区的消息,并确保这些消息作为一个单元被处理。

    Pulsar事务具有以下语义:

    • 事务中的所有操作都作为一个单元提交。要么提交所有消息,要么都不提交。

    • 每条消息只写入或处理一次,不会丢失数据或重复(即使发生故障)。

    • 如果事务中止,则此事务中的所有写入和确认都将回滚。

    事务中的批量消息可以被以多分区接收、生产和确认。

    • 消费者只能读取已提交(确认)的消息。换句话说,Broker不传递属于打开事务的事务消息或属于中止事务的消息。

    • 跨多个分区的消息写入是原子性的。

    • 跨多个订阅的消息确认是原子性的。订阅下的消费者在确认带有事务ID的消息时,只会成功确认一次消息。

    Pulsar事务消息由以下几个关键点构成:

    • 事务ID

    事务ID(TxnID)标识Pulsar中的唯一事务。事务ID长度是128-bit。最高16位保留给事务协调器的ID,其余位用于每个事务协调器中单调递增的数字。

    • 事务协调器(Transaction Coordinator)

    事务协调器(TC)是运行在Pulsar Broker中的一个模块。

    • 它维护事务的整个生命周期,并防止事务进入错误状态。

    • 它处理事务超时,并确保事务在事务超时后中止。

    • 事务日志

    所有事务元数据都保存在事务日志中。事务日志由Pulsar主题记录。如果事务协调器崩溃,它可以从事务日志恢复事务元数据。

    事务日志存储事务状态,而不是事务中的实际消息(实际消息存储在实际的主题分区中)。

    • 事务缓存

    向事务内的主题分区生成的消息存储在该主题分区的事务缓冲区(TB)中。在提交事务之前,事务缓冲区中的消息对消费者不可见。当事务中止时,事务缓冲区中的消息将被丢弃。

    事务缓冲区将所有正在进行和中止的事务存储在内存中。所有消息都发送到实际的分区Pulsar主题。提交事务后,事务缓冲区中的消息对消费者具体化(可见)。事务中止时,事务缓冲区中的消息将被丢弃。

    • 待确认状态

    挂起确认状态在事务完成之前维护事务中的消息确认。如果消息处于挂起确认状态,则在该消息从挂起确认状态中移除之前,其他事务无法确认该消息。

    挂起的确认状态被保留到挂起的确认日志中(cursor ledger)。新启动的broker可以从挂起的确认日志中恢复状态,以确保状态确认不会丢失。

    处理流程一般分为以下几个步骤:

    1. 开启事务。

    2. 使用事务发布消息。

    3. 使用事务确认消息。

    4. 结束事务。

    Pulsar的事务处理流程与Kafka的事务处理思路大致上保持一致,大家都有一个TC以及对应的一个用于持久化TC所有操作的Topic来记录所有事务状态变更的请求。同样的在事务开始阶段也都有一个专门的Topic来去查询TC对应的Owner Broker的位置在哪里。

    不同的是,第一:Kafka中对于未确认的消息是维护在Broker端的,但是Pulsar的是维护在Client端的,通过Transaction Timeout来决定这个事务是否执行成功,所以有了Transaction Timeout的存在之后,就可以确保Client和Broker侧事务处理的一致性。第二:由于Kafka本身没有单条消息的Ack,所以Kafka的事务处理只能是顺序执行的,当一个事务请求被阻塞之后,会阻塞后续所有的事务请求,但是Pulsar是可以对消息进行单条Ack的,所以在这里每一个事务的Ack动作是独立的,不会出现事务阻塞的情况。

    四、结论

    RocketMQ和Kafka/Pulsar的事务消息实用的场景是不一样的。

    RocketMQ中的事务,它解决的问题是,确保执行本地事务和发消息这两个操作,要么都成功,要么都失败。并且RocketMQ增加了一个事务反查的机制,来尽量提高事务执行的成功率和数据一致性。

    Kafka中的事务,它解决的问题是,确保在一个事务中发送的多条消息,要么都成功,要么都失败。(这里面的多条消息不一定要在同一个主题和分区中,可以是发往多个主题和分区的消息)当然也可以在kafka事务执行过程中开启本地事务来实现类似RocketMQ事务消息的效果,但是Kafka是没有事务消息反查机制的,它是直接抛出异常的,用户可以根据异常来实现自己的重试等方法保证事务正常运行。

    它们的共同点就是:都是通过两阶段提交来实现事务的,事务消息都保存在单独的主题上。不同的地方就是RocketMQ是通过“半消息”来实现的,kafka是直接将消息发送给对应的topic,通过客户端来过滤实现的。而且它们两个使用的场景区别是非常之大的,RockteMQ主要解决的是基于本地事务和消息的数据一致性,而Kafka的事务则是用于实现它的Exactly-once机制,应用于实时流计算的场景中。

    Pulsar的事务消息和Kafka应用场景和语义类似,只是由于底层实现机制有差别,在一些细节上有区别。

    相信看到这里就非常清楚了,对于事务消息如何选型和应用,首先要明白你的业务需求是什么。是要实现分布式事务的最终一致性,还是要实现Exactly-once (精确一次)语义?明白之后需求,选择什么组件就十分明确了。

    参考文章

    1.【万字长文】浅谈Apache Kafka---入门须知

    2. Apache Pulsar技术系列-事务消息

    3. 消息队列(MQ)架构篇之RocketMQ

    4. Apache Pulsar技术系列-Pulsar事务实现机制

    5. 消息队列漫谈:如何使用消息队列实现分布式事务?

     作者简介

    刘若愚

    微信支付后台开发工程师

    微信支付后台开发工程师,硕士毕业于北京大学。深度参与腾讯WXG境外支付团队多个重要业务的研发工作,有丰富的后台开发经验。腾讯技术分享达人,社会招聘伯乐。

     推荐阅读

    Linux入门必看:如何在60秒内分析Linux性能?

    “Docker VS Kubernetes”是共生还是相爱相杀?

    揭秘!是什么能让APP快速精准定位?

    人机共生时代,分布式机器学习是如何加速的?


    以上是关于10分钟搞懂!消息队列选型全方位对比的主要内容,如果未能解决你的问题,请参考以下文章

    MQ 死信队列/延迟队列-( 商品秒杀后30分钟之内付款)

    MQ 死信队列/延迟队列-( 商品秒杀后30分钟之内付款)

    使用RabbitMQ的死信队列实现延迟消息

    使用RabbitMQ的死信队列实现延迟消息

    使用RabbitMQ的死信队列实现延迟消息

    使用RabbitMQ的死信队列实现延迟消息