掘金 后端 ( ) • 2024-06-22 16:48

作为一名程序员,如果你有A和B两个服务,且在A服务发送消息之后,你不希望B服务立即处理它,而是延迟半小时才处理,这就需要实现某种形式的延迟处理策略。

这种延迟处理消息的场景在实际生活中相当常见。例如,当你每天早晨到达办公室并下单外卖时,你可能希望食物能在中午才送达,而不是立即送达。为了实现这一目标,你需要将外卖的订单信息通过某种形式的延时机制,延迟一段时间后再传递给商家进行处理。

A.png

那么问题就来了,有没有优雅的解决方案?

当然有,没有什么是加一层中间层不能解决的,如果有,那就再加一层。

这次我们要加的中间层是消息队列 RocketMQ。

利用 RocketMQ,我们可以实现消息的延迟投递。具体步骤如下:

  1. 配置延迟消息:在 RocketMQ 中,发送消息时可以指定延迟级别。例如,可以设置消息在30分钟后才投递到消费端。

  2. 发送消息:A 服务在需要发送消息时,将消息发送到 RocketMQ,并指定相应的延迟级别。

  3. 消费消息:B 服务从 RocketMQ 消费消息,由于设置了延迟,B 服务会在消息延迟时间到达后才接收到消息并进行处理。

通过这种方式,我们能够优雅地实现消息的延迟处理。RocketMQ 提供了强大的消息队列功能,不仅支持高效的消息传递,还能满足我们对消息延迟处理的需求。

B.png

RocketMQ 是什么?

RocketMQ 是阿里巴巴自主研发的国产消息队列,目前已经成为 Apache 的顶级项目。与其他消息队列类似,它接受来自生产者的消息,并对消息进行分类,每一类称为一个 topic。消费者则根据需要订阅相应的 topic,从中获取消息。

RocketMQ 的优势在于其高效的消息处理能力和灵活的扩展性,使得它在分布式系统中广泛应用。无论是高并发的电商场景,还是复杂的金融交易系统,RocketMQ 都能够稳定高效地支撑业务需求。

1716038925501.jpeg

确实,RocketMQ 和我们在之前文章中提到的 Kafka 都属于消息队列。然而,尽管它们都属于同一类别,但在实际应用中,它们之间还是存在一些显著的差异。

首先,从架构方面来看,RocketMQ 和 Kafka 有着不同的设计理念。RocketMQ 提供更多的功能,包括延迟消息、定时消息、事务消息等,而 Kafka 更注重于简洁和性能,提供了一个高吞吐量的、可扩展的、持久化的、分布式的发布订阅消息系统。

其次,就可靠性而言,RocketMQ 的设计更强调数据的安全性,它通过多种机制,如同步刷盘、同步复制等,来保证消息不会丢失。而 Kafka 则更侧重于系统的弹性和容错性,通过副本机制来实现系统的高可用。

最后,从使用场景来看,RocketMQ 更适用于实时性要求较高、对消息顺序性和延迟性有要求的场景,如金融支付、订单处理等;而 Kafka 则更适合处理大数据、日志收集等海量数据的场景。

总的来说,RocketMQ 和 Kafka 虽然都是消息队列,但在功能、性能、使用场景等方面都有各自的特点和优势,用户可以根据自己的需求来选择适合的工具。

RocketMQ 和 Kafka 的区别

RocketMQ 的架构其实参考了 Kafka 的设计思想,同时又在 Kafka 的基础上做了一些调整。

1716038944866.jpeg

这些调整,用一句话总结就是,"和 Kafka 相比,RocketMQ 在架构上做了减法,在功能上做了加法"。我们来看下这句话的含义。

在架构上做减法

我们来简单回顾下消息队列 Kafka 的架构。
kakfa 也是通过多个 topic 对消息进行分类。

1716039021926.jpeg

  • 为了提升单个 topic 的并发性能,将单个 topic 拆为多个 partition

1716039000949.jpeg

  • 为了提升系统扩展性,将多个 partition 分别部署在不同 broker 上。
  • 为了提升系统的可用性,为 partition 加了多个副本。
  • 为了协调和管理 Kafka 集群的数据信息,引入Zookeeper作为协调节点。

1716039049814.jpeg

确实,Kafka 作为一种强大的消息队列,已经在许多数据处理场景中发挥了重要作用。然而,RocketMQ 在 Kafka 的基础架构之上,仍然展现出了其独特的优势和特性。

简化协调节点

在 Kafka 的架构中,Zookeeper 扮演着关键的角色,它与 broker 进行通信以维护 Kafka 集群的信息。当一个新的 broker 连接到 Zookeeper 时,其他的 broker 会立即感知到它的存在。这样的机制使得在分布式环境中,多个实例能够实时获取并共享同一份信息,这就是所谓的分布式协调服务。

Zookeeper 通过提供这种协调服务,为分布式系统的运行提供了坚实的基础。充分利用 Zookeeper 的特性,可以让分布式系统的节点间的通信和协调变得更加简单、有效,从而提升整个系统的性能和稳定性。

1716039187841.jpeg

Zookeeper 作为一个全能型的分布式协调服务, 不仅可以应用于服务的注册与发现,还可以用于实现分布式锁、配置管理等多种场景。然而,在 Kafka 中,实际上只使用了Zookeeper的一部分功能,这无疑有些大材小用,显得过于繁重。

因此,RocketMQ 选择放弃使用Zookeeper,而是采用了更为轻量级的 nameserver 来管理消息队列的集群信息。在这种架构中,生产者会通过 nameserver 获取到 topic 和 broker 的路由信息,然后再与 broker 进行通信,从而实现服务发现和负载均衡的功能。这种改变使得RocketMQ在保证功能的同时,极大地提高了效率和性能。

1716039215116.jpeg

显然,Kafka 的开发团队后来也意识到了 Zookeeper 的使用过于繁重这一问题。因此,从2.8.0版本开始,Kafka 支持移除 Zookeeper,通过在 broker 之间加入一致性算法 raft,实现相同的效果。这种新的运行模式被称为 KRaft ,或者 Quorum 模式。这一改变无疑是对 Kafka 系统优化的重要步骤,使得 Kafka 在保持其强大功能的同时,也能够实现更高效的运行。

1716039233839.jpeg

简化分区

我们知道,Kafka 会将 topic 拆分为多个 partition,用来提升并发性能

1716039266847.jpeg

在 RocketMQ 中,同样采用了将 topic 分解为多个分区的方式,只不过在这里,它被赋予了一个新的名称,被称为 Queue,也就是我们所熟知的"队列"。这种命名方式既保留了其基本功能的特征,又赋予了它新的含义。

1716039283266.jpeg 在 Kafka 中,每个 partition 都会存储完整的消息内容。然而,在 RocketMQ 中,Queue 只会存储一些基本信息,比如消息的偏移量 offset。而消息的完整内容则会被存储在一个名为 commitlog 的文件中,通过 offset,我们可以精确地找到 commitlog 中的特定消息。

当 Kafka 消费消息时,broker 只需直接从 partition 中读取一次消息即可。这意味着消息只需要读取一次就可以完成消费。

1716039326945.jpeg 而在 RocketMQ 中,broker 则需要先从 Queue 上读取到 offset 的值,再跑到 commitlog 上将完整数据读出来,也就是需要读两次

1716039336550.jpeg

那么,你可能会有疑问,既然 Kafka 的设计看起来更高效,为什么 RocketMQ 不采用同样的设计呢?

这就需要我们深入了解一下 Kafka 的底层存储机制。

Kafka 的底层存储

Kafka 的分区(partition)在底层实际上是由许多段(segment)构成的,可以将每个 segment 视为一个独立的小文件。因此,将消息数据写入到 partition 分区,从本质上来说,就等同于将数据写入到对应的 segment 文件中。

1716039357450.jpeg

我们都知道,在操作系统的机械磁盘中,顺序写入的性能会大大超过随机写入,性能差距甚至可以高达数十倍。为了优化性能,Kafka 选择将数据顺序写入每个小文件。

如果只有一个 segment 文件,那么写入文件的性能会非常出色。然而,随着 topic 数量的增加,其下的 partition 分区也会相应地增加,这就意味着 partition 下的 segment 文件数量也会随之增加。当同时写入多个 topic 下的 partition 时,实际上就是同时写入多个文件。尽管每个文件内部都是顺序写入,但由于这些文件存储在磁盘的不同位置,原先的顺序写入可能会退化为随机写入。这就可能导致写入性能的显著下降。

1716039374651.jpeg

那么,究竟多少个 topic 才算多呢?虽然具体情况需要视实际环境而定,但我并不喜欢模棱两可的回答。根据经验,当每个 topic 有 8 个分区时,如果 topic 数量超过 64 个,Kafka 的性能就会开始显著下降。这个经验值仅供参考。

RocketMQ 的底层存储

为了缓解同时写入多个文件带来的随机写问题,RocketMQ 采用了一种独特的方法:将单个 broker 下的多个 topic 数据全部写入到一个逻辑文件 CommitLog 中。这样一来,就消除了随机写入多个文件的问题,将所有写操作转变为顺序写操作。这种设计大大提升了 RocketMQ 在多 topic 场景下的写入性能。

1716039402130.jpeg

需要注意的是,前面提到的"一个"逻辑文件实际上由多个小文件组成。虽然逻辑上它是一个整体的 CommitLog,但实际上,每个小文件的大小是固定的。当一个文件被写满后,会创建一个新的文件来继续存储新的消息。这种设计不仅提升了写入性能,还方便了旧消息的管理和清理。

简化备份模型

我们知道,Kafka 会将分区(partition)分散到多个代理(broker)中,并为每个分区配置副本,将分区分为leader(主分区)和follower(从分区)。在一个代理中,既可能有某个主题(topic)的主分区,也可能有另一个主题(topic)的从分区。

主从分区之间会建立数据同步机制,本质上就是同步分区底下的段文件(segment)数据。

1716039497979.jpeg

RocketMQ 将所有的主题(topic)数据写入到代理(broker)上的 CommitLog。如果我们按照 Kafka 的方式,为每个分区单独建立同步通信,那么我们就需要将 CommitLog 中的内容进行拆分,这就变成了随机读取,而这并不高效。

因此,RocketMQ 选择以代理为单位来区分主从,使得主从之间可以同步 CommitLog 文件。这种方式不仅保持了系统的高可用性,而且大大简化了备份模型。

1716039512531.jpeg

好了,到这里,我们熟悉的 Kafka 架构,就成了 RocketMQ 的架构。

在功能上做加法

虽然 RocketMQ 的架构比 Kafka 的简单,但功能却比 Kafka 要更丰富,我们来看下。

消息过滤

我们都知道,Kafka支持通过主题(topic)对数据进行分类,例如,订单数据和用户数据可以分别存储在两个不同的主题中。但是,如果我们想在这基础上进一步分类数据,例如,我们想将同样的用户数据根据 VIP 等级再细分,该如何操作呢?

假设我们只需获取 VIP6 的用户数据,在 Kafka 中,我们需要消费包含用户数据的所有消息,然后再从中筛选出 VIP6 的用户数据。这样的操作显然效率辈低,资源浪费大。

然而,RocketMQ 提供了一种更为高效的解决方案,它支持给消息打上标签(tag)。消费者可以根据这些标签来过滤需要的数据。例如,我们可以在某些消息上打上 "VIP6" 的标签,然后消费者就可以直接获取这些带有 "VIP6" 标签的消息,从而大大节省了过滤数据所需的资源消耗。

相当于 RocketMQ 除了支持通过 topic 进行一级分类,还支持通过 tag 进行二级分类。

1716039636534.jpeg

支持事务

我们都知道,Kafka 提供了事务支持,这意味着在某些情况下,生产者可以以事务的方式发送消息。例如,假设生产者需要发送三条消息 A、B 和 C,通过 Kafka 的事务功能,我们可以确保这三条消息要么全部成功发送,要么全都不发送,这样可以保证数据的完整性和一致性。

1716039652367.jpeg

对的,事务不仅仅关乎于消息的发送,更重要的是保证业务逻辑的执行与消息发送的原子性。这正是 Apache RocketMQ 所提供的强大的事务处理能力。

在编写业务代码时,我们时常需要在"执行一些自定义逻辑"和"生产者发消息"这两个操作之间建立事务关系。RocketMQ 支持这样的需求,并能确保这两个操作要么都成功执行,要么都不执行。这种机制可以防止在遇到系统故障时,业务逻辑和消息发送的状态不一致的问题,从而保障数据的完整性和一致性。

1716039663099.jpeg

加入延时队列

对的,如果我们需要消息在被送出后并不立即被消费,而是在经过一段时间后再被消费,这就是所谓的"延时消息"。这就像我们在点外卖时设定的定时送达一样。

如果我们使用 Kafka,实现这样的功能可能会遇到一些困难,因为它并没有提供内置的支持。然而,RocketMQ 天生就支持"延时队列",它能让我们轻松地实现延时消息的功能。这样,我们就可以控制消息在特定的时间点被消费者获取,从而满足更复杂的业务需求。

加入死信队列

在进行消息消费时,有可能会遭遇失败。通常,我们可以设置消息在失败后进行重试。但是,如果重试多次仍然失败,RocketMQ 会将这些消息放入一个专门的队列中,这样我们就可以在稍后进行单独处理。这个专用于储存无法成功处理的消息的队列被称为"死信队列"。

然而,Kafka 并不原生支持这个功能,如果我们需要实现类似的机制,我们就需要自己去设计和编程。这可能会增加我们的开发工作量和复杂性。因此,RocketMQ 在这方面提供的支持能大大简化我们的工作,并提高系统的健壮性。

消息回溯

Kafka 支持通过调整 offset 让消费者从某个特定位置开始消费,而 RocketMQ 除了可以调整 offset,还支持根据时间调整消费位置。

所以,不那么严谨地说,RocketMQ 本质上是在架构上做了减法,在功能上做了加法的 Kafka。这样的总结是不是特别精辟?现在大家是否通了?

不过,还有一个问题。看起来,RocketMQ 在很多方面似乎都比 Kafka 更强大,但 Kafka 依然没有被淘汰,这说明 RocketMQ 在某些方面还是不如 Kafka。那么,是什么呢?答案是性能,严格来说是吞吐量

这就引发了一个有趣的问题,为什么 RocketMQ 参考了 Kafka 的架构,性能却还不如 Kafka?这个问题,我们下期再聊。

总结

相比于 Kafka,RocketMQ 在架构上进行了简化,同时增强了功能。

在架构上,RocketMQ 精简了协调节点和分区以及备份模型,使其更为简洁高效。而在功能上,RocketMQ 提供了更强大的消息过滤、消息回溯和事务处理能力,还新引入了延迟队列和死信队列等特性,使其具备更加丰富的功能。