消息队列MQ

海外开发者为 AutoMQ 写的精品介绍,太干了!

本文主要是介绍海外开发者为 AutoMQ 写的精品介绍,太干了!,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

原文链接


https://vutr.substack.com/p/how-do-we-run-kafka-100-on-the-object

文章导读

AutoMQ 自2023年底正式官宣推出以后,迅速在 Github 上引起广泛的关注并且多次登上 Github Trending. Kafka 近些年俨然已经成为 Streaming 领域的事实标准,在企业的现代化数据基础设施中扮演重要的角色。因此,大家对 Kafka 的成本、弹性、复杂度等问题也表现出越来越多的关切和诉求。AutoMQ 在遵循云优先设计理念的基础上,构建的基于 WAL 和 S3 的新一代 Kafka 流存储引擎一经发布在国际舞台上就收到开发者的高度认可,具备全球竞争力。今天分享的内容翻译自海外开发者的期刊内容,其中对 AutoMQ 的整体架构和技术优势做了图文并茂、由浅入深的详细解读,希望对各位读者有帮助。

“本周,我有幸深入探索 AutoMQ,这是一款由前阿里巴巴工程师打造的云原生,兼容 Kafka 的流处理系统。在这篇文章中,我们将深入解析 AutoMQ 的一项重要技术特性:完全依赖于对象存储运行 Kafka。”

用户评价

部分海外开发者评论和反馈内容截取:

概述

现代操作系统通常会将未被使用的内存(RAM)部分用作页面缓存。频繁使用的磁盘数据会被加载到这个缓存中,避免过度频繁地直接读取磁盘,以此提升性能。

Apache Kakfa 紧密耦合架构

这种设计将计算和存储紧密地结合在一起,这意味着唯一扩展存储的方式就是增加更多的机器。如果你需要更多的磁盘空间,你就必须增加更多的 CPU 和 RAM,这可能会导致资源的浪费。

Apache Kafka分层存储

由于 Kafka 的高度集成的计算存储设计,Uber 在弹性和资源利用率上遇到了问题。为了避免 Kafka 的这种紧密耦合设计,Uber 提出了 Kafka 分层存储方案(KIP-405)。这个方案的主要思路是,一个代理将具备两个层次的存储:本地和远程。本地存储是代理的本地磁盘,用于接收最新的数据,而远程存储则利用像 HDFS/S3/GCS 这样的服务来持久化历史数据。

在 Kafka 的分层架构中broker并不是100%无状态 虽然把历史数据转储到远程存储可以减少 Kafka broker 的计算和存储层之间的依赖,但是 broker 并不是完全无状态的。AutoMQ 的工程师们开始思考:“是否存在一种方法,可以将所有的 Kafka 数据存储在对象存储中,同时还能保持像在本地硬盘上一样的高性能?”

存储结构

目前,AutoMQ 可以在主流的云服务提供商如 AWS、GCS 和 Azure 上运行,但我将利用我从 AWS 的博客和文档中学习到的知识来描述其架构。AutoMQ 的目标相当直接:通过让 Kafka 将所有消息写入到对象存储,提升 Kafka 的效率和伸缩性,同时不牺牲性能。它实现这个目标的方式是,复用 Apache Kafka 的计算和协议代码,同时引入共享存储架构来替代 Kafka 代理使用的本地磁盘存储。不同于分层存储方法,这种方法需要维护本地和远程存储,AutoMQ 希望让系统完全无状态。从高层次看,AutoMQ 代理会将消息写入内存缓存。在异步将这些消息写入对象存储之前,代理首先需要将数据写入 WAL 存储,以确保数据的持久性。

AutoMQ架构概述

Cache

AutoMQ的缓存类型

AutoMQ 使用一个堆外缓存内存层来处理所有的消息读取和写入,保证实时性能。它管理两种不同的缓存来满足不同的需求:

Log Cache 负责处理写入和热读取(那些需要最新数据的),而系统使用Block Cache处理冷读取(那些访问历史数据的)。

如果数据在日志缓存中不可获取,那么将从块缓存中读取。块缓存通过预读取和批量读取等技术提高了即使对历史数据读取也能命中内存的可能性,这有助于在冷读取操作中保持性能。预取是一种将预期需要的数据提前加载到内存中的技术,以便在需要时已经准备就绪,从而减少等待时间。批量读取则是一种在单次操作中读取多个数据片段的技术,这降低了读取请求的数量并加快了数据检索速度。每种缓存都有不同的数据淘汰策略。日志缓存有一个默认的最大大小(可配置)。

如果达到限制,缓存将使用先进先出(FIFO)策略淘汰数据,以确保其对新数据的可用性。对于其他类型的缓存,AutoMQ 对块缓存采用最近最少使用(LRU)策略来淘汰块数据。内存缓存层提供了最低的读写操作延迟;然而,它受到机器内存量的限制,且不可靠。如果代理服务器崩溃,缓存中的数据将消失。这就是为什么 AutoMQ 需要一种方法来让数据传输更可靠。

预写日志

数据直接通过 IO 从Log Cache写入到原始的 EBS 设备中。

AWS弹性块存储EBS 是一种可以与 EC2 实例连接的持久性块存储设备。AWS EBS 提供了从 SSD 到 HDD 的各种卷类型,用户可以根据自身需求进行选择。EBS 的多重挂载功能可以让用户将一个 EBS 卷连接到多个 EC2 实例。在后续我们探索 AutoMQ 如何在后台从故障中恢复的过程中,将会再次讨论到这个多重挂载功能。EBS 存储扮演着WAL的角色,这是一种只能进行追加操作的磁盘结构,用于在系统崩溃或事务中进行恢复。通常,采用 B-树进行存储管理的数据库都会包含这种数据结构以实现恢复功能;每一次对数据的修改都必须首先经过 WAL,然后才能实际应用到数据上。当机器从崩溃状态中恢复时,它可以读取 WAL,以恢复到之前的状态。

B-Tree 用于实现数据库中的写前日志(WAL)
同样,AutoMQ 将 EBS 设备视为WAL。在将消息写入 S3 之前,broker 必须确保消息已经在WAL中;当 broker 接收到消息时,它会将消息写入内存缓存,并且只有当消息在 EBS 中持久化时才返回 “我已经收到了你的消息” 的回应。AutoMQ 使用 EBS 中的数据来恢复 broker 的故障。我们将在接下来的部分详细介绍这个恢复过程。

在 AutoMQ 中的写前日志(WAL)考虑到 EBS 的高成本,尤其是 IOPS 优化型 SSD 的类型,这一点显得尤为重要。由于 AutoMQ 中的 EBS 设备主要作为一个WAL来确保消息的持久性,系统只需要一小部分 EBS 容量。AutoMQ 的默认WAL大小设置为 10GB。

对象存储

所有的 AutoMQ 数据都存放在对象存储中。客户可以选择使用 AWS S3 或 Google GCS 这类服务来实现这一层的存储需求。云对象存储以其超高的耐用性、可伸缩性和成本效益而受到赞誉。代理将会从日志缓存中异步地将数据写入到对象存储中。在对象存储中,AutoMQ 的数据文件由以下几个部分组成:DataBlock、IndexBlock 和 Footer,分别用于存储实际数据、索引和文件元数据。

数据文件存储在对象存储中

  • DataBlocks 包含了实际的数据。
  • IndexBlock 是一个固定的 36 字节大小的块,由 DataBlockIndex 的项目构成。项目的数量与文件中的 DataBlocks 数量是关联的。每个 DataIndexBlock 中的信息都有助于定位 DataBlock 的位置。
  • Footer 是一个固定的 48 字节的块,其中包含了 IndexBlock 的位置和大小,这使得我们能快速访问到索引数据。

接下来的部分,我们将深入探讨 AutoMQ 的读/写操作;在这个过程中,我们会更深入地了解这个系统是如何在后台运行的。

写入

从用户的视角来看,AutoMQ 的写入流程与 Apache Kafka 非常相似。首先,创建一个包含消息内容和目标主题的记录。然后,消息被序列化并通过网络进行批量发送。关键的差异在于,代理服务器如何处理消息的持久化。在 Kafka 中,代理服务器会将消息写入页面缓存,然后刷新到本地硬盘。它们并没有实现任何内存缓存,所有工作都交给了操作系统。然而在 AutoMQ 中,情况则完全不同。下面我们就来仔细看看消息写入流程:

AutoMQ的整体消息编写过程生产者将消息发送给代理并等待响应。

代理将接收到的消息放入日志缓存,即堆外内存缓存。Java中的堆外内存是在Java堆之外管理的。与JVM处理和垃圾收集的堆内存不同,堆外内存不是自动管理的。开发人员必须手动分配和释放堆外内存,如果处理不当,堆外内存可能更加复杂,容易发生内存泄漏,因为JVM不会自动清理堆外内存。

然后使用直接I/O将消息写入WAL(EBS)设备。一旦消息成功写入EBS,代理就会将成功的响应发送回生产者。(我将在下一节中解释这个过程。)

Direct I/O是一种通过直接从磁盘读取或写入磁盘来绕过操作系统的文件系统缓存的方法,可以减少延迟并提高大数据传输的性能。实现Direct I/O通常需要更复杂的应用程序逻辑,因为开发人员必须管理数据对齐、缓冲区分配和其他底层细节日志缓存中的消息在登陆WAL后异步写入对象存储。

从缓存到WAL的过程

消息通过 SlidingWindow 抽象从日志缓存写入到 WAL 中。SlidingWindow 分配每个记录的写入位置,并管理写入过程。SlidingWindow 具有几个位置:

  • **Start Offset:**这个偏移量标识了滑动窗口的起始位置;在这个偏移量之前,系统已经开始写入记录。
  • **Next Offset:**这是未被写入的下一个位置,新的记录将从这里开始。起始和下一偏移量之间的数据还未被完全写入。
  • **Max Offset:**这是滑动窗口的终止位置;当下一偏移量达到这个点时,它将试图扩展窗口。

为了更好地理解这些概念,我们可以参考一下 AutoMQ 中的一些新的数据结构,这将有助于我们理解写入到 EBS 的过程:

  • **block:**这是最小的 IO 单元,可以包含一个或多个记录,在写入硬盘时会对齐到 4 KiB。
  • writingBlocks: 指的是一组正在被写入的数据块,当这些数据写入完毕后,AutoMQ 会在硬盘中删除这些块。
  • pendingBlocks: 这是等待被写入的数据块,当 IO 线程池空闲时,新的数据块会被放入此处,并在有空间时被转移至正在写入的块。
  • currentBlock: 这是最新从缓存中到达的日志。需要被写入的记录会被放置在这个块中,同时,新的记录也会在这里被分配逻辑偏移。当当前块被填满时,所有的数据块都会被放入待处理的块,系统会创建一个新的当前块。

当所有先决信息准备好后,我们将开始学习如何将数据写入到 EBS 的过程。

整个过程从一个追加请求开始,输入一个记录。这个记录会被添加到currentBlock中,同时会被赋予一个偏移量,然后异步返回给调用者。

如果当前块达到了特定的大小或者时间限制,它会将所有的块移动到pendingBlocks。接下来,AutoMQ 会创建一个新的当前块。

如果writingBlocks的数量小于 IO 线程池的大小,那么一个块会从待处理块移动到正在写入的块进行写入操作。

一旦一个块被写入到硬盘,它就会从正在写入的块中被移除;然后系统会重新启动滑动窗口的起始偏移量。

一个标记会把追加请求标记为已完成。

8 从缓存到对象存储的过程

当日志缓存中的数据积累到一定程度时,AutoMQ 会启动上传到对象存储的操作。日志缓存中的数据会按照 streamId 和 startOffset 进行排序。接着,AutoMQ 会将缓存中的数据分批写入对象存储,每批上传的顺序都是一致的。

如前文所述,对象存储中的数据文件包括数据块(DataBlock)、索引块(IndexBlock)以及页脚(Footer)。

在 AutoMQ 完成数据块的写入后,会利用之前写入的信息构建一个索引块。由于每个数据块在对象内的位置已知,这些数据被用来为每个数据块创建一个数据块索引(DataBlockIndex)。索引块中的数据块索引的数量与数据块的数量一致。

最后,页脚元数据块记录了与索引块数据位置相关的信息。

读取

AutoMQ 的消费者启动消费流程的方式类似于 Apache Kafka。他们通过发出带有所需偏移量位置的异步拉取请求来实现这一过程。在接收到请求后,代理会进行消息检索并将其返回给消费者。消费者会根据当前的偏移量位置和其长度,来计算出下一个偏移量位置,为下一次请求做好准备。

next_offset = current_offset + current_message_length

物理数据读取路径的改变使得情况发生了变化。AutoMQ 尽力从内存中获取尽可能多的数据。起初,Kafka 是从页缓存中读取数据的。如果需要的消息不在页缓存中,操作系统会到磁盘上查找,并将所需的数据填充到页缓存中以应对请求。

AutoMQ的整体消息阅读流程AutoMQ 中的读取操作按照以下路径进行:如果请求需要最近写入的数据,系统会从日志缓存中读取。需要注意的是,只有已经写入 WAL(写前日志)的消息才能满足请求。

如果数据不在日志缓存中,系统会检查块缓存。块缓存是通过从对象存储中加载数据来填充的。如果数据在此处仍未找到,AutoMQ 会尝试预加载它。

预加载允许系统加载预计将很快需要的数据。因为消费者是按照特定的顺序从某个位置开始读取消息,所以预加载数据可以提高缓存命中率,从而提高读取性能。

为了在对象存储中更快找到数据,broker 使用文件的 Footer 来查找 IndexBlock 的位置。IndexBlock 中的数据按照(streamId, startOffset)进行排序,这样就可以通过二分查找快速找到正确的 DataBlock。一旦找到 DataBlock,broker 就可以通过遍历 DataBlock 中的所有记录批次,有效地查找所需的数据。DataBlock 中记录批次的数量会影响到特定偏移量的检索时间。

为了解决这个问题,在上传时,将同一流的所有数据切分成 1MB 的片段,以确保每个 DataBlock 中的记录批次数量不会降低检索速度。

恢复

如前文所述,EBS 存储在 AutoMQ 中扮演着预写日志(Write Ahead Log)的角色,这样有利于更可靠地将消息从内存写入对象存储。假设我们有一个 AutoMQ 集群,其中包含两个代理 A 和 B,每个代理都关联有两个 EBS 存储;让我们看看 AutoMQ 是如何实现可靠消息传递的:

AutoMQ 如何保证消息传输的可靠性?正如前面所提到的,一旦代理确认消息已经存储在 WAL(写前日志,对应 AWS 的 EBS)中,那么这条消息就被认为是已经成功接收的。

但是,如果其中一个代理,比如说Broker A,发生了崩溃,那么这个代理的 EBS 存储设备会发生什么变化?那些还没有写入对象存储的 EBS 数据又会怎样呢?

AutoMQ 会利用 AWS EBS 的多重附加功能来应对这种情况:

  • Broker A 关闭后,EBS 设备 A 会被附加到BrokerB 上。
  • 当BrokerB 拥有两个 EBS 卷时,它可以通过标签来识别哪一个是新附加的。
  • 然后,Broker B 会将 EBS 设备 A 的数据刷新到 S3 中,接着删除该卷。
  • 此外,当将未使用的 EBS 卷附加到Broker B 时,AutoMQ 会利用 NVME 预留来防止对该卷的意外写入。这些策略极大地加速了故障转移的过程。新创建的代理会拥有新的 EBS 存储设备。

元数据管理

“在这篇文章的最后部分,我们将探讨 AutoMQ 是如何管理集群元数据的。它采用了 Kafka 的 KRaft 机制。在编写 Kafka 系列文章时,我并没有深入探讨 KRaft,所以这是一个很好的机会来深入理解这种元数据管理模型。😊”

AutoMQ 使用了基于 Kafka 的 Kraft 模式的最新元数据管理架构。传统的 Kafka 依赖于独立的 ZooKeeper 服务器来进行集群元数据的管理,但是 KRaft 模式消除了对 ZooKeeper 的依赖,简化了 Kafka 的运行并提升了其弹性。

在 KRaft 模式下,Kafka 使用基于 Raft 算法的内部控制器群,这是一组负责维护和确保元数据一致性的 brokers。Raft 共识算法被用于选出领导者并在群体中复制元数据的更改。在 KRaft 模式下的每个 broker 都保留了元数据的本地副本,而控制器群的领导者负责管理更新并将其复制到所有的 brokers,这大大降低了操作的复杂性和可能的故障点。

Zookeeper 模式与 Kraft 模式比较:
AutoMQ 也设有一个控制器裁决组,其作用是选定控制器的领导者。集群的元数据(如主题/分区与数据的对应关系,分区与代理间的映射等)都保存在领导者节点上。只有领导者有权更改这些元数据;如果一个代理需要进行修改,必须先与领导者进行沟通。这些元数据会在各代理间进行复制;元数据的任何更改都由控制器传播给每个代理。

总结

在本篇文章中,我们探讨了 AutoMQ 如何巧妙地利用云服务实现了一项核心目标:**在保持 Kafka 的原始性能和兼容性的同时,能够将所有 Kafka 消息存储在几乎无限容量的对象存储中。**感谢您的阅读,期待在下一篇文章中再次与您相见。

关于我们

我们是来自 Apache RocketMQ 和 Linux LVS 项目的核心团队,曾经见证并应对过消息队列基础设施在大型互联网公司和云计算公司的挑战。现在我们基于对象存储优先、存算分离、多云原生等技术理念,重新设计并实现了 Apache Kafka 和 Apache RocketMQ,带来高达 10 倍的成本优势和百倍的弹性效率提升。

🌟 GitHub 地址:https://github.com/AutoMQ/automq
💻 官网:https://www.automq.com?utm_source=wechat 
👀 B站:AutoMQ官方账号
🔍 视频号:AutoMQ

这篇关于海外开发者为 AutoMQ 写的精品介绍,太干了!的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!