🚀 免费试用 Zilliz Cloud,完全托管的 Milvus,体验 10 倍的性能提升!立即试用>

milvus-logo
LFAI
  • Home
  • Blog
  • Milvus 云可扩展性向量数据库的发展历程

Milvus 云可扩展性向量数据库的发展历程

  • Engineering
December 21, 2021
Jun Gu

本文将分享我们如何设计 Milvus 新数据库集群架构的思考过程。

Milvus 向量数据库的目标

Milvus 向量数据库的想法最初出现在我们脑海中时,我们希望建立一个数据基础设施,帮助人们在其组织中加速人工智能的采用。

为了完成这一使命,我们为 Milvus 项目设定了两个至关重要的目标。

易于使用

AI/ML 是一个新兴领域,新技术层出不穷。大多数开发人员并不完全熟悉快速发展的人工智能技术和工具。开发人员已经耗费了大部分精力去寻找、训练和调整模型。他们很难再花费额外的精力来处理模型生成的大量 Embeddings 向量。更何况,处理大量数据始终是一项极具挑战性的任务。

因此,我们高度重视 "易用性",因为它可以大大降低开发成本。

低运行成本

人工智能在生产中的主要障碍之一是证明投资回报的合理性。我们将有更多机会以较低的运行成本将人工智能应用投入生产。这将有利于提高潜在效益的幅度。

Milvus 2.0 的设计原则

在 Milvus 1.0 中,我们已经朝着这些目标迈出了第一步。但这远远不够,尤其是在可扩展性和可用性方面。于是,我们开始开发 Milvus 2.0,以改进这些方面。我们为新版本制定的原则包括

  • 以高可扩展性和可用性为目标
  • 以成熟的云基础设施和实践为基础
  • 将云计算的性能折损降到最低

换句话说,我们要让 Milvus 数据库集群成为云原生。

数据库集群的演变

向量数据库是数据库的一个新品种,因为它处理的是新型数据(向量)。但它仍与其他数据库面临同样的挑战,并有一些自己的要求。在本文接下来的内容中,我将重点介绍我们从现有数据库集群实现中汲取的经验,以及我们如何设计新的 Milvus 组架构的思考过程。

如果你对 Milvus 组组件的实现细节感兴趣,请随时关注 Milvus 文档。我们将在 Milvus GitHub repo、Milvus 网站和 Milvus 博客中持续发布技术文章。

理想的数据库集群

"小目标,小失误"。

首先,让我们列出理想数据库集群应具备的关键能力。

  1. 并发性和无单点故障:连接到不同群组成员的用户可以同时对同一段数据进行读/写访问。
  2. 一致性:不同的群组成员应看到相同的数据。
  3. 可扩展性:我们可以随时添加或删除群组成员。

老实说,所有这些功能很难同时具备。在现代数据库集群的实现中,人们不得不对其中的一些功能做出妥协。人们并不期待一个完美的数据库集群,只要它能满足用户的使用场景即可。然而,万物共享集群曾经非常接近理想的数据库集群。如果我们想学点什么,就应该从这里开始。

数据库集群的主要考虑因素

与其他现代实现方式相比,万物共享集群的历史更为悠久。Db2 数据共享组和 Oracle RAC 就是典型的万物共享集群。许多人认为万物共享意味着共享磁盘。其实远不止如此。

万物共享群集中只有一种数据库成员。用户可以连接到这些对称成员中的任何一个,访问任何数据。什么是需要共享的 "一切"?

组中的事件顺序

首先,群组事件顺序对于解决不同群组成员并发访问造成的潜在冲突至关重要。我们通常使用数据库日志记录序列号来表示事件序列。同时,日志记录序列号一般由时间戳生成。

因此,对组事件序列的要求等同于对全局计时器的要求。如果我们能为群组提供一个原子钟,那将是一件美妙的事情。然而,Milvus 是一个开源软件项目,这意味着我们应该依靠常见的资源。迄今为止,原子钟仍然是大公司的首选。

我们已经在 Milvus 2.0 数据库集群中实现了时间同步组件。您可以在附录中找到相关链接。

全局锁定

无论是乐观锁还是悲观锁,数据库都有解决并发访问冲突的锁定机制。同样,我们也需要全局锁定来解决不同组成员之间的并发访问冲突。

全局锁定意味着不同的小组成员必须相互对话,协商锁定请求。有几个重要因素会影响全局锁协商过程的效率:

  • 系统间连接的速度
  • 需要参与协商过程的小组成员数量
  • 群组冲突的频率

典型的组规模不超过 100 个。例如,Db2 DSG 为 32 个;Oracle RAC 为 100 个。这些组员将被安置在一个机房内,通过光纤连接,以尽量减少传输延迟。这就是为什么有时称其为集中式群集。由于群集规模的限制,人们会选择高端服务器(大型机或小型机,它们的 CPU、内存、I/O 通道等容量更大)来组成万物共享群集。

这种硬件假设在现代云环境中发生了巨大变化。如今,云数据中心由高密度的服务器机房组成,机房中装满了(成千上万台)带有 TCP/IP 连接的商品 X86 服务器。如果我们依靠这些 X86 服务器来构建数据库集群,那么群组规模应增加到数百(甚至数千)台机器。而在某些业务场景中,我们希望这数百台 X86 机器分布在不同地区。因此,实施全局锁定可能不再值得,因为全局锁定的性能不够好。

在 Milvus 2.0 中,我们不打算实施全局锁定设施。一方面,向量数据没有更新。(因此,我们不必担心在有分片安排的 Milvus 组中,同一数据会出现多写入器冲突的问题。同时,我们可以使用 MVCC(多版本并发控制,一种避免锁定的并发控制方法)来解决读写冲突。

另一方面,向量数据处理比结构化数据处理消耗更多的内存。人们对向量数据库的可扩展性要求更高。

共享内存数据缓存

我们可以将数据库引擎简单分为两部分:存储引擎和计算引擎。存储引擎负责两项关键任务:

  • 将数据写入永久存储,以保证数据的持久性。
  • 将数据从永久存储加载到内存数据缓存(又称缓冲池);这是计算引擎访问数据的唯一地方。

在数据库集群场景中,如果成员 A 更新了成员 B 的缓存数据,该怎么办?成员 B 如何知道其内存数据已过期?经典的万物共享集群有一个缓冲区交叉失效机制来解决这个问题。如果我们在群组成员之间保持较强的一致性,缓冲区交叉失效机制的工作原理与全局锁定类似。如前所述,这在现代云环境中并不现实。因此,我们决定将 milvus 云可扩展组中的一致性级别降低到最终一致性方式。这样,Milvus 2.0 中的缓冲区交叉失效机制就可以成为一个异步过程。

共享存储

共享存储可能是人们在讨论数据库集群时首先会想到的。

在近几年的云存储发展中,存储选项也发生了很大变化。存储连接网络(SAN)曾经是(现在仍然是)万物共享组的存储基础。但在云环境中,没有 SAN。数据库必须使用连接到云虚拟机的本地磁盘。使用本地磁盘会带来跨组成员数据一致性的挑战。我们还必须担心组内成员的高可用性。

随后,Snowflake 为使用云共享存储(S3 存储)的云数据库做了一个很好的模型。它也给 Milvus 2.0 带来了启发。如前所述,我们打算依靠成熟的云基础设施。但在利用云共享存储之前,我们必须考虑几件事。

首先,S3 存储既便宜又可靠,但它并不像数据库那样专为即时 R/W 访问而设计。我们需要创建数据组件(在 Milvus 2.0 中我们称之为数据节点)来连接本地内存/磁盘和 S3 存储。我们可以学习一些示例(如 Alluxio、JuiceFS 等)。我们之所以不能直接集成这些项目,是因为我们关注的数据粒度不同。Alluxio 和 JuiceFS 是为数据集或 POSIX 文件设计的,而我们关注的是数据记录(向量)级别。

当向量数据落户 S3 存储后,元数据的答案就很简单了:将它们存储在 ETCD 中。那么日志数据呢?在经典实施中,日志存储也是基于 SAN 的。一个数据库组成员的日志文件在数据库集群内共享,以便进行故障恢复。因此,在进入云环境之前,这并不是一个问题。

在 Spanner 的论文中,谷歌说明了他们是如何利用 Paxos 共识算法实现全球分布式数据库(组)的。你需要将数据库集群编程为状态机复制组。重做日志通常是将在组内复制的 "状态"。

通过共识算法复制重做日志是一种强大的工具,在某些业务场景中具有很大的优势。但对于 milvus 向量数据库来说,我们并没有发现创建状态机复制组整体的足够动力。我们决定使用云消息队列/平台(Apache Pulsar、Apache Kafka 等)作为日志存储的替代云共享存储。通过将日志存储委托给消息传递平台,我们获得了以下好处。

  • 群组更多地由事件驱动,这意味着许多进程可以是异步的。它提高了可扩展性。
  • 组件的耦合度更高,更容易进行在线滚动升级。提高了可用性和操作符。

我们将在后面的章节中再次讨论这个话题。

至此,我们已经总结了数据库集群的关键注意事项。在跳转到有关 Milvus 2.0 架构的讨论之前,让我先解释一下我们如何在 Milvus 中管理向量。

数据管理和性能可预测性

Milvus 将向量存储在 Collections 中。Collection "是一个逻辑概念,相当于 SQL 数据库中的 "表"。一个 Collections 可以有多个物理文件来保存向量。一个物理文件就是一个 "段"。段 "是一个物理概念,就像 SQL 数据库中的表空间文件。当数据量较小时,我们可以将所有数据保存在一个段/物理文件中。但如今,我们经常要面对大数据。当有多个数据段/物理文件时,我们应该如何将数据分散到不同的数据分区中呢?

虽然数据而不是索引是第一位的,但在大多数情况下,我们必须以索引算法更喜欢的方式存储数据,以便高效地访问数据。SQL 数据库中经常使用的一种策略是按分区 Key 值的范围进行分区。人们通常会创建一个聚类索引来执行分区键。总的来说,这是一种适合 SQL 数据库的方法。数据以良好的形式存储,并针对 I/O(预取)进行了优化。但仍然存在缺陷。

  • 数据倾斜。某些分区的数据可能比其他分区多得多。现实世界的数据分布不像数值范围那么简单。
  • 访问热点。更多的工作量可能会流向某些数据分区。

试想一下,更多的工作量会流向数据更多的分区。出现这些情况时,我们需要重新平衡各分区的数据。(这就是 DBA 繁琐的日常工作)。

The Clustered index for vectors 向量的聚类索引

我们还可以为向量创建聚簇索引(倒列表索引)。但这与 SQL 数据库的情况不同。在 SQL 数据库中,一旦建立了索引,通过索引访问数据就会非常高效,计算量和 I/O 操作符都会减少。但对于向量数据来说,即使有索引,计算量和 I/O 操作符也会多得多。所以,前面提到的缺陷会对向量数据库集群造成更严重的影响。此外,由于数据量和计算复杂度的原因,在不同分段之间重新平衡向量的成本也非常高。

在 Milvus 中,我们采用了按增长分区的策略。当我们向向量 Collections 中注入数据时,Milvus 会将新向量追加到 Collections 中最新的分段中。一旦段的大小足够大(阈值可配置),Milvus 就会关闭该段,并为关闭的段建立索引。与此同时,会创建一个新的数据段来存储即将到来的数据。这种简单的策略对于向量处理来说更加平衡。

向量查询是一个在向量 Collections 中搜索最相似候选对象的过程。这是一个典型的 MapReduce 流程。例如,我们想从一个有十个分段的向量 Collections 中搜索前 20 个相似结果。我们可以在每个分段上搜索前 20 个结果,然后将 20 * 10 的结果合并为最终的 20 个结果。由于每个分段的向量数量相同,索引相似,因此每个分段上的处理时间几乎相同。这为我们带来了性能可预测性的优势,这在规划数据库集群规模时至关重要。

Milvus 2.0 中的新范例

在 Milvus 1.0 中,我们像大多数 SQL 数据库一样实现了读/写分割分片组。这是 Milvus 数据库集群扩展的一次有益尝试。但问题也很明显。

Milvus database 1.0 Milvus 数据库 1.0

在 Milvus 1.0 中,R/W 节点必须完全照顾最新的分段,包括向量追加、在这个未索引的分段中搜索、建立索引等。由于每个 Collections 只有一个写入器,如果数据连续不断地流入系统,写入器就会非常繁忙。R/W 节点和阅读器节点之间的数据共享性能也是一个问题。此外,我们必须依靠 NFS(不稳定)或高级云存储(太贵)来共享数据存储。

这些现有问题在 Milvus 1.0 架构中很难解决。因此,我们在 Milvus 2.0 设计中引入了新的范式来解决这些问题。

Milvus architecture Milvus 架构

角色模型

并发计算系统编程有两种模型。

  • 共享内存意味着并发控制(锁定)和同步处理
  • 角色模型(又名消息传递)意味着消息驱动和异步处理

我们也可以在分布式数据库集群中应用这两种模型。

如前所述,大多数著名的分布式数据库都使用相同的方法:通过共识算法复制重做日志。这是一种同步处理方法,使用共识算法为重做日志记录建立分布式共享存储器。不同的公司和风险资本已经在这项技术上投入了数十亿美元。在我们开始开发 Milvus 2.0 之前,我并不想对此发表评论。许多人认为这项技术是实现分布式数据库系统的唯一途径。这很令人讨厌。如果我不说点什么,人们可能会误解我们在分布式数据库设计上的鲁莽。

近年来,通过共识算法进行的Redo-log复制是最被高估的数据库技术。这其中有两个关键问题。

  • 认为重做日志复制更好的假定是脆弱的。
  • 供应商误导了人们对共识算法能力的期望。

假设我们有两个数据库节点,即源节点和目标节点。一开始,它们都拥有数据的精确副本。我们对源节点进行了一些更改操作(I/U/D SQL 语句),并希望保持目标节点的更新。我们该怎么办?最简单的方法是在目标节点上重放操作符。但这并不是最有效的方法。

考虑到 I/U/D 语句的运行成本,我们可以将其分为执行准备和物理工作两部分。执行准备部分包括 SQL 解析器、SQL 优化器等的工作。无论有多少数据记录会受到影响,这都是固定成本。物理工作部分的成本取决于受影响的数据记录数量,属于浮动成本。重做日志复制背后的理念是节省目标节点上的固定成本;我们只在目标节点上重放重做日志(物理工作)。

节约成本的百分比是重做日志记录数的倒数。如果一个操作只影响一条记录,我应该能从重做日志复制中看到显著的节约。如果是 10,000 条记录呢?那我们就要担心网络的可靠性了。发送一个操作符和发送 10,000 条重做日志记录,哪个更可靠?一百万条记录呢?在支付系统、元数据系统等场景中,重做日志复制的作用非常大。在这些场景中,每次数据库 I/U/D 操作只会影响少量记录(1 或 2 条)。但它很难与批处理作业等 I/O 密集型工作负载配合使用。

供应商总是声称共识算法可以为数据库集群提供强大的一致性。但人们只使用共识算法来复制重做日志记录。不同节点上的重做日志记录是一致的,但这并不意味着其他节点上的数据视图也是一致的。我们必须将重做日志记录合并到实际的表记录中。因此,即使进行了同步处理,我们仍然只能获得数据视图的最终一致性。

我们应该在适当的地方使用共识算法复制重做日志。Milvus 2.0 中使用的元数据系统(ETCD)和消息平台(如 Apache Pulsar)已经实现了共识算法。但正如我之前所说,"对于 Milvus 向量数据库来说,我们并没有找到足够的激励机制来作为一个状态机复制组的整体"。

在 Milvus 2.0 中,我们使用演员模型来组织工人节点。工人节点是孤独的。它们只与消息平台对话,获取指令并发送结果。这听起来很无聊。

"我们的座右铭是什么?""无聊永远是最好的。"--《杀手的保镖》(2017 年

演员模型是异步的。它适用于可扩展性和可用性。由于 Worker 节点之间互不相识,因此如果某些 Worker 节点加入或被移除,不会对其他 Worker 节点造成影响。

可用性和耐用性分离

在 Milvus 2.0 中,我们做的是操作重放而不是日志重放,因为在向量数据库中,操作重放和日志重放没有太大区别。我们既没有更新功能,也没有选择插入功能。而且使用演员模型进行操作符重放也更容易。

因此,多个 Worker 节点可能会根据各自的职责执行来自消息平台的同一操作符。我之前提到过,我们决定使用 S3 云存储作为 Milvus 数据库集群的共享存储层。S3 存储非常可靠。那么不同的工作节点是否有必要向共享存储写出相同的数据呢?

因此,我们为工作节点设计了三种角色。

  • 查询节点根据任务分配维护内存中的数据视图。查询节点的工作包括做向量搜索和保持内存数据更新。但它不需要向 S3 存储写入任何内容。它是该组中对内存最敏感的节点。
  • 数据节点负责将新数据写入 S3 存储。数据节点不需要维护内存中的数据视图,因此数据节点的硬件配置与查询节点截然不同。
  • 当段的大小达到阈值时,索引节点会为数据节点关闭的段建立索引。这是最耗费 CPU 的工作。

这三类节点代表不同类型的工作负载。它们可以独立扩展。我们称之为可用性和耐用性分离,这是从微软苏格拉底云数据库中学到的。

结束,也是开始

本文回顾了 Milvus 向量数据库 2.0 的几个设计决策。 让我们在此快速总结一下这些要点。

  • 我们为 Milvus 集群 2.0 选择了最终一致性。
  • 我们尽可能将成熟的云组件集成到 Milvus 2.0 中。我们对 Milvus 2.0 引入用户生产环境的新组件进行了控制。
  • 通过遵循角色模型以及可用性和耐用性的分离,Milvus 2.0 很容易在云环境中扩展。

到目前为止,我们已经形成了 Milvus 2.0 云可扩展数据库的骨干,但我们的工作积压中还有许多来自 Milvus 社区的需求需要满足。如果你也有同样的使命("构建更多开源基础架构软件,加速人工智能转型"),欢迎加入 Milvus 社区。

Milvus 是 LF AI & Data 基金会的毕业项目。您无需为 Milvus 签署任何 CLA!

附录

Milvus 设计文档

https://github.com/milvus-io/milvus/tree/master/docs/design_docs

C++ 中的 Raft 实现

如果您仍然对共识算法感兴趣,我建议您查看eBay 的开源项目 Gringofts。它是 Raft 共识算法(Paxos 家族的变种)的 C++ 实现。我的朋友 Jacky 和 Elvis(我在摩根士丹利的前同事)为 eBay 在线支付系统构建了该系统,这正是该技术最适合的应用场景之一。

Try Managed Milvus for Free

Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.

Get Started

Like the article? Spread the word

扩展阅读