跳到主要内容

RabbitMQ,后备存储、数据库和磁盘

·8 分钟阅读
Matthew Sackman

有时,在我们的 邮件列表 和其他地方,会有人提出在 RabbitMQ 中使用不同的后备存储的想法。后备存储是负责将消息写入磁盘的部分(消息可能出于多种原因被写入磁盘),并且经常有人建议看看如果将 RabbitMQ 自己的后备存储替换为另一个存储系统会是什么样子。

这样的更改将允许实现当前不可能实现的功能,例如带外队列浏览或分布式存储,但是像 RabbitMQ 这样的消息代理与通用数据库之间的数据存储和访问模式的性质存在根本差异。实际上,RabbitMQ 特意不将消息存储在这样的数据库中。

首先,我们需要讨论 RabbitMQ 本身对任何后备存储的期望属性。RabbitMQ 在两种情况下将消息写入磁盘:要么消息已以必须写入磁盘的方式发布(例如,以 delivery_mode = 2 发布),要么内存压力导致 RabbitMQ 开始耗尽 RAM,因此它正在将消息推送到磁盘以释放 RAM。在第一种情况下,仅仅因为我们将消息写入磁盘,并不意味着我们会从 RAM 中忘记它:如果内存充足,则没有理由承担后续磁盘读取的成本。

在第二种情况下,这意味着任何始终将所有内容保存在 RAM 中的后备存储都立即不适用:RabbitMQ 将消息写入磁盘是为了释放 RAM,因此,如果“写入磁盘”实际上只是将消息从 RAM 的一个区域移动到另一个区域而不释放 RAM,那么就没有任何收获。使用这样的后备存储可能有效,并且可能实现期望的功能改进,但是这样的更改将对 RabbitMQ 的可伸缩性产生重大影响:它将不再能够吸收超过 RAM 容量的消息,这是导致 RabbitMQ 当前默认后备存储的新持久化器工作的原因之一。

某些数据库或键值存储通过最初写入其整个数据集的快照,然后将增量写入该数据集来写入磁盘内容。过一段时间后,无论是基于时间还是基于增量数量,或增量与快照大小的比率,都会写入新的快照,然后可以丢弃先前的快照及其所有增量。这就是 RabbitMQ 的旧持久化器的工作方式。问题在于,这可能会反复导致大量数据不必要地被重写。想象一下,您有两个队列,其中一个队列完全是静态的:没有人向其发布消息,也没有人从中消费消息,它只是在那里,但是它包含数百万条消息,所有消息都已写入磁盘。另一个队列几乎总是空的,但是移动速度非常快——每秒发布和消费数千条消息。发送到该队列的每条消息都必须写入磁盘,但是它们在写入磁盘后立即被消费。考虑一下这种情况对后备存储的影响:第二个队列将导致快速的增量流发生,但是每当快照被重写时,它也会导致第一个队列的全部内容也被重写,即使该队列的内容没有任何更改。因此,再次说明,以这种方式将消息写入磁盘的后备存储可能不太适合 RabbitMQ 的需求。

因此,合适的后备存储(假设 RabbitMQ 拥有的性能和可伸缩性属性需要保留:这在所有情况下都绝非确定)将能够存储仅受磁盘大小限制而不是 RAM 限制的数据量,并且还具有相当复杂的磁盘数据存储方式,以便未更改的数据不会无限期地重写。

RabbitMQ 默认后备存储还有一些其他方面值得一提。队列本身决定何时以及是否将消息写入磁盘。但是,单个消息可以发送到多个队列,并且显然有利的是确保每条消息仅写入磁盘一次。但是,这里有两个不同的信息:首先,消息内容本身。这在消息已发送到的每个队列中都是相同的,并且应该只写入磁盘一次,而与它进入的队列数量无关;请注意,此后的写入不需要进行值比较:如果后备存储已知消息的 ID,则消息正文将与磁盘上已有的内容匹配——消息内容永远不会被代理更改。第二个信息是消息在每个队列中存在:它在队列中的位置,它的邻居是什么,以及它的队列特定状态是什么。第二个信息允许 RabbitMQ 启动、从磁盘恢复消息和队列,并确保每个队列中的消息顺序与 RabbitMQ 关闭时相同。

因此,RabbitMQ 的默认后备存储由一个节点全局的消息存储组成,该存储仅关注将消息内容写入磁盘;以及每个队列的队列索引,该索引使用非常不同的格式将每个队列的每个消息数据写入磁盘。由于这两个需求非常具体,因此可以应用很多优化(而且我们已经应用了!)。

通用数据库基准测试通常表明,读取性能大大优于写入性能。如果不是这样,那么通常意味着写入实际上没有转到磁盘(使用 fsync),或者存在一个错误,该错误正在严重影响读取性能。实际上,数据库在历史上已经针对读取密集型工作负载进行了优化。这符合它们的一般用例:存在一个缓慢扩展的数据集,必须以各种不同的方式查询。删除往往很少见:如果您考虑一下关系数据库之上的典型网站购物车,那么除非客户删除他们的帐户,否则几乎没有理由发出删除——即使产品已停产,您也可能只是在该产品行上设置一个标志,因为否则您可能会阻止客户查看他们的订单历史记录(假设它是规范化的)。

因此,大多数数据库中的大量数据是相当静态的。这与消息代理中的数据完全相反:对于我们来说,读取数据是最少见的操作,而写入删除数据是常见的情况。理想情况下,如果 RabbitMQ 在充足的内存中运行,则永远不会从磁盘读取任何内容。只会为以必须写入磁盘的方式发布的消息进行写入,即使那样,只要我们可以足够快地将消息发送给消费者,我们就可以通过多种方式优化掉这些写入。我们只在内存压力迫使我们将消息写入磁盘然后从 RAM 中忘记该消息时才读取数据。读取性能当然很重要:我们努力确保 RabbitMQ 尽可能快地摆脱数据(不使用 /dev/null),并且能够快速从磁盘读取消息是其中的一部分。但是,首先避免写入才是目标。

实际上,就消息代理而言,最好将 RAM 视为磁盘的大型写回缓存,然后任务是优化此缓存的管理,以最大限度地消除写入,方法是尽可能延迟写入,希望相应的删除在写入真正转到磁盘之前发生。这与普通数据库明显不同,普通数据库不会尝试从数据生命周期如此短(在消息代理中经常如此)中获益。

所有这些并非旨在阻止为使 RabbitMQ 与替代后备存储一起工作而做出的努力,而只是为了解释为什么我们在为 RabbitMQ 编写新持久化器(首次在 RabbitMQ 2.0.0 版本中发布)时决定自己动手,而不是使用现成的数据库。它解释了为什么直接在普通数据库之上构建高性能消息代理充其量是棘手的,以及为什么消息代理中的数据性质与数据库中的数据性质非常不同。

© . All rights reserved.