译者 | 陈林
审校 | 孙淑娟 梁策
Milvus向量型数据库的目标
当我们第一次出现Milvus向量型数据库的想法时,我们希望构建的是一个数据基础设施,从而加速人工智能在人们组织架构中的使用。
为了完成这一使命,我们为Milvus项目设定了两个关键目标。
易用性
人工智能/机器学习是一个新兴领域,新技术不断涌现。大多数开发人员对于高速发展的AI技术和工具并不熟悉。开发人员花费了大量的精力来寻找、训练和调整模型,基本没有额外的精力来处理模型生成的大量嵌入向量,更不用说处理海量数据一直都是一项相当有挑战的任务。
因此,我们给“易用性”定了相当高的优先级,因为它可以显著地降低开发成本。
低运行成本
- AI投入实际生产应用的最大阻碍之一就是合理衡量投资回报比。有了更低的运行成本,我们将AI应用程序投入生产中将有更大的可能性,这将有利于提高边际的潜在收益。
Milvus 2.0的设计原则
我们在 Milvus 1.0 中朝着这些目标迈出了第一步,但这还远远不够,尤其是在可扩展性和可用性方面。然后我们开始研发Milvus 2.0来完善这些点。我们为2.0新版本制定的原则包括:
- 以高可扩展性和可用性为目标
- 基于成熟的云基础架构和实践
- 云端的最小性能妥协性
也就是说,我们想让Milvus数据库集群成为云原生。
Milvus数据库集群的演进
向量数据库是一种新型数据库,因为它处理的向量数据是一种新的数据类型。它面临与其他数据库相同的挑战,但也具有自身的一些场景和需求。在本文剩下的部分,我将重点介绍我们从现有的数据库集群实现中能学到什么,以及我们在设计新的Milvus Group架构时的思考旅程。
如果你对Milvus Group组件的实现细节感兴趣,请继续关注 Milvus文档。我们将在Milvus GitHub仓库、Milvus官网和Milvus博客上持续发布技术文章。
理想的数据库集群
让我们首先罗列出一个理想的数据库集群应该具备的关键能力。
- 支持并发且无单点故障:连接到不同组成员的用户可以同时对同一条数据进行读/写访问。
- 一致性:不同的组成员看到的数据应该是相同的。
- 可扩展性:我们可以随时随地添加或删除组成员。
说实在的,现有数据库是无法同时提供和保障这些能力的。在现代数据库集群的实现中,人们不得不对其中的部分功能妥协。我们并不期望一个完美的数据库集群,只要能够适用和满足用户的业务场景就行了。然而,共享 “一切” 的集群一度非常接近理想的数据库集群。如果我们想学习一些经验,我们应该以它为基础开始。
数据库集群的主要考虑因素
与其他数据库实现相比,shared-everything的数据库集群具有更久远的历史。DB2数据共享组和Oracle RAC是典型的shared-everything集群。许多人认为shared-everything意味着共享磁盘,其实远不止于此。
shared-everything的数据库集群在组中只有一种数据库成员。用户可以连接到这些对等成员中的任何一个实例来访问任何数据。完成这项操作需要共享的 “一切” 是什么?
组内事件序列
首先,组内事件序列对于解决不同组成员由于并发访问引起的潜在冲突至关重要。我们通常使用数据库日志记录序号来表示事件的顺序。同时,日志记录序号一般是由时间戳生成的。
因此,对组内事件顺序的要求等价于对全局定时器的需要。如果我们可以为组配备一个原子钟,那就太棒了。然而,Milvus是一个开源软件项目,这意味着我们应该依赖常用的资源。迄今为止,原子钟仍然是大公司的首选。
我们在Milvus 2.0数据库集群中实现了时间同步组件。您可以在附录中找到链接。
全局锁
数据库有一个锁定机制来解决并发访问冲突,无论是乐观锁还是悲观锁。同样,我们需要使用全局锁定来解决不同组成员之间同时访问的冲突。
全局锁定意味着不同的组成员必须相互交谈以协商锁定请求。 影响这个全局锁定协商过程的效率主要有几个重要的因素:
- 系统间连接的速度
- 需要参与协商过程的组成员数量
- 组内发生冲突的频率
通常的组大小不超过100。例如,DB2 DSG为32;Oracle RAC为100。这些组成员将被放置在一个通过光纤连接的服务器机房中,以最大限度地减少传输延迟。这就是为什么它有时被称为集中式集群。 由于组大小的限制,人们会选择高端服务器(大型机或小型机,在 CPU、内存、I/O 通道等方面具有更多容量)来组成共享一切集群。
这种硬件假设在现代云环境中发生了巨大变化。现如今,云数据中心都是由高密度服务器机房组成,集群配置了(数千台) TCP/IP 网络连通的商用 X86 服务器。如果我们依靠这些 X86 服务器来构建数据库集群,那么组大小应该会增加到数百(甚至数千)台机器。而在一些业务场景中,我们希望这数百台X86机器分布在不同的区域。因此实现全局锁定的价值和意义就不大了,因为全局锁定性能不够好。
在Milvus 2.0中,我们不会实现全局锁定功能。一方面,向量数据不会有更新(用户倾向于删除后插入而不是更新)。所以我们不需要担心基于分片编排的Milvus组内的同一条数据的由于多次写入造成冲突。同时,我们可以使用MVCC(多版本并发控制,一种避免锁的并发控制方法)来解决读写冲突。
另一方面,向量数据处理比结构化数据处理消耗更多的内存占用。 人们一直在寻找具有高扩展性的向量数据库。
共享内存数据缓存
我们可以简单地将数据库引擎分为两部分,即存储引擎和计算引擎。存储引擎负责两项关键任务:
- 将数据写入持久化存储永久存储。
- 将数据从持久化存储加载到内存数据缓存(AKA缓冲池);这是计算引擎访问数据的唯一地方。
在数据库集群场景中,如果成员A更新了成员B中缓存的数据怎么办?成员B如何知道其内存数据已过期?对于经典的 shared-everything集群,有一种缓冲区交叉失效机制来解决这个问题。 如果我们在组成员之间维持强一致性,则缓冲区交叉失效机制将类似于全局锁。如上所述,它在现有的云环境中并不实用。所以我们决定将Milvus弹性云分组的一致性级别降低为最终一致性的方式。这样,Milvus 2.0中的缓冲区交叉失效机制就可以是一个异步过程。
共享存储
共享存储可能是人们在讨论数据库集群时会想到的第一件事。
近年来,随着云存储的发展,存储可选项也发生了显著的变化。 存储连接网络 (SAN) 一直是shared-everything的存储基础。但是在云环境中并没有SAN,数据库必须使用本地磁盘连接到云虚拟机。使用本地磁盘对于跨组成员的数据一致性带来了新的挑战,而且我们还不得不考虑组成员的高可用。
Snowflake数据仓库为打算使用云共享存储(S3存储)的云数据库树立了一个很好的榜样,它也启发了Milvus 2.0。如上所述,我们打算基于成熟的云基础设施实现Milvus 2.0,但在我们能够利用云共享存储之前,我们必须考虑以下问题。
首先,S3存储便宜且可靠,但它不是为像数据库那样的实时读写的场景而设计的。我们需要创建数据组件(我们在Milvus 2.0中称为数据节点)来桥接本地内存/磁盘和S3存储。市面上有一些示例可以参考,如Alluxio、JuiceFS等。无法直接集成这些项目的原因是我们考虑到不同的数据粒度。Alluxio和JuiceFS是为数据集或POSIX文件设计的,而我们专注于数据记录(向量)级别。
当向量数据在S3存储的问题被解决时,元数据的存储很简单:将它们存储在etcd。那么日志数据呢?在传统的实现中,日志存储也是基于SAN。一个数据库组成员的日志文件在数据库集群内共享,用于故障恢复。在进入云环境之前这不是问题。
在Spanner论文中,Google展示了他们是如何使用Paxos共识算法实现全局分布式数据库(组)的。你需要基于状态机复制组实现数据库集群,重做日志(redo log)通常就是用于整个组复制的那个“状态”。
共识算法的重做日志(redo log)复制是一种强大的工具,在某些业务场景中具有相当大的优势。但是,对于Milvus向量数据库,我们没有找到足够的措施创建一个完整的状态机复制组。我们决定使用云消息队列/平台(Apache Pulsar、Apache Kafka等)用于日志存储,作为共享云存储的替代品。通过将日志存储委托给消息传递平台,我们获得了以下好处:
- 更加的事件驱动化,整个处理过程可以是异步的,从而提高了可扩展性。
- 组件耦合更松散,使得在线滚动升级和发布更容易,可用性和可操作性显著提高。
我们将在后面的部分重新讨论这个话题。
到目前为止,我们已经总结了设计一款数据库集群要考虑的关键因素。在开始讨论Milvus 2.0架构之前,让我先说明一下我们是如何在Milvus中管理向量的。
数据管理和性能可预测性
Milvus将向量存储在集合中。“集合”是一个逻辑概念,相当于SQL数据库中的“表”。一个“集合”可以有多个物理文件来保存向量,一个物理文件是一个“段”。“段”是一个物理概念,类似于SQL数据库中的表空间文件。当数据量较小时,我们可以将所有内容保存在单个段/物理文件中。但是,现如今不断面临着海量数据的存储,当有多个段/物理文件时,应该如何将数据分散到不同的数据分区中?
尽管数据先于索引到来,但我们必须以更适合索引算法的方式存储数据,以便在大多数情况下高效地访问数据。SQL数据库中常用的策略是以分区键值的范围进行分区。通常情况下,就是创建一个聚集索引来强制分区键。这对于SQL数据库来说是一种不错的方法,一方面数据存储良好,另一方面针对 I/O(预取)进行了优化,但仍然存在以下缺陷:
- 数据倾斜:某些分区可能比其他分区存储了更多的数据,实际应用中数据的分布并不像数值范围那么简单。
- 访问热点:更多工作负载可能会流向其中几个数据分区。
当越来越多的工作负载流向数据倾斜度高的分区时,我们需要对各个分区的数据进行重新平衡。
向量的聚集索引
我们还可以为向量创建聚集索引(倒排列表索引),这与SQL 数据库的索引不同。给SQL数据库建立索引后,通过索引访问数据非常高效,计算量少,I/O操作少。然而对于向量数据来说,即使有索引,计算和I/O操作也不会因此减少。因此,在向量数据库集群中,数据倾斜和热点数据集中的影响更为明显。此外,由于数据量和计算复杂性因素,在不同分段对向量进行重新平衡的成本非常高。
在Milvus中,我们采用分区自动增长的策略。当我们将数据存入向量集合时,Milvus会将新向量追加到集合中的最新段。一旦这个段的大小达到某个阈值(阈值可配置),Milvus将关闭该段并为关闭的段建立索引。同时,它还将创建一个新段来存储新的数据。这种简单的策略对于向量处理来说平衡性更友好。
向量查询指的是在向量集合中查询与目标条件匹配度最相近的结果。它是一个典型的Map Reduce过程。例如,如果想从包含10个分段的向量集合中搜索前20个相似的结果,我们可以搜索每个段的前20个,然后将20 * 10个查询合并,筛选出其中20个结果返回。 由于每个段具有相同数量的向量和相似的索引,因此每个段的处理时间几乎相同。这样的好处是带来了性能可预测性的优势,它在规划数据库集群的规模时至关重要。
Milvus 2.0的新范式
在Milvus 1.0中,我们实现了与大多数SQL数据库一样的读写分离分片组。这种实现对Milvus数据库集群来说是种不错的尝试,但带来的问题也是显而易见的。
Milvus 1.0:分片集群
在Milvus 1.0中,写节点必须全程维护最新的段,其中就包括在未索引的段中追加向量、搜索向量,以及建立索引等操作。由于每个集合只有一个写节点,如果数据在源源不断地写入系统,写入节点将会成为瓶颈。写节点和其他读节点之间的数据共享性能也是一个问题。此外,我们必须依赖 NFS(不稳定)或者商用云存储(太贵)来作为共享数据存储。
以上问题在Milvus 1.0架构中很难解决。 因此,我们在Milvus 2.0设计中引入了新的范式。
Milvus 2.0:弹性云向量数据库
Actor模型
现在有两种常用的并发计算的模型编程。
- 共享内存:对应的是并发控制(锁定)和同步处理。
- Actor模型(AKA 消息传递):对应的是消息驱动和异步处理。
我们也可以在分布式数据库集群中应用这两种模型。
近年来,基于Redo-log复制的算法一直是最被高估的数据库技术,它存在两个关键问题。
- Redo-log复制更好的假设是脆弱的。
- 供应商误导了人们对共识算法能力的期望。
假设我们有两个数据库节点,一个是主节点,另一个是从节点。 从一开始,两个节点的数据是完全一致的。我们在主节点上有一些修改操作(新增/更新/删除的SQL语句),我们希望保持从节点同步更新。我们应该做什么?最简单的方法是在从节点上重放更新操作,但这不是最高效的手段。
考虑新增/更新/删除语句的运行成本:我们可以将其分为执行准备和实际执行部分。执行准备部分包括SQL解析器、SQL优化器等工作,不管影响到了多少条数据记录,成本都是固定的。实际执行部分的成本取决于有多少数据记录会受到影响,它的成本是浮动的。 redo-log复制背后的想法是为了节约从节点上的固定成本,即只在从节点上重放redo-log即可。
成本节省率和redo-log记录数成反比。如果一次更新操作只影响一条记录,redo-log复制其实有很大的提升空间。如果是10000条记录呢?当然,我们还应该关心网络可靠性。哪个更可靠,发送一个操作还是10000条redo-log记录?一百万条记录又怎么样? redo-log复制最适合的场景是支付系统、元数据系统等。在这些场景中,每个数据库新增/更新/删除操作只影响少量的记录(1或2),它不适用于I/O密集型类的场景,例如任务批处理。
部分供应商总是声称共识算法可以为数据库集群提供强一致性。虽然redo-log记录在不同节点上是一致的,但这并等价于数据视图也是一致的。没有将redo-log记录合并到数据库表之前,即使使用这种同步处理,我们也只能在数据上保证最终一致性。
我们应该在合适的场景下使用redo-log复制一致性算法。 Milvus 2.0中使用的元数据系统(etcd)和消息中间件平台(例如 Apache Pulsar)已经实现了一致性算法。但正如我之前所说的,“对于Milvus向量数据库,目前还没有办法让它成为一个完整的状态机复制组。
在Milvus 2.0中,我们使用Actor模型来编排工作节点。工作节点是单独隔离的,它们只与消息中间件平台通信,获取命令并发送结果。
Actor模型是异步的,它适用于对扩展性和可用性要求高的场景。由于工作节点之间是互相隔离的,加入或移除部分工作节点对其他工作节点没有影响。
可用性和持久性的分离
在 Milvus 2.0 中,我们实现的是操作重放而不是日志重放。因为在向量数据库中,操作重放和日志重放没有太大区别。我们没有更新功能,也没有查询插入功能,并且使用Actor模型进行操作回放要简单得多。
多个工作节点可能会根据各自的职责从消息中间件平台执行相同的操作。之前提到我们决定使用S3云存储作为Milvus数据库集群的共享存储层。S3存储非常可靠,那么不同的工作节点是否需要将相同的数据写入共享存储呢?
因此,我们把工作节点分类为以下三个角色:
- 查询节点:它维护一个内存数据视图。查询节点的工作包括提供向量搜索和对内存中数据的更新。但它不需要向S3存储写入任何内容。它是组中对内存最敏感的节点。
- 数据节点:负责将新数据写入S3存储。数据节点不需要维护内存中的数据视图,因此数据节点的硬件配置与查询节点有很大的不同。
- 索引节点:当段的大小达到阈值时,索引节点为数据节点已关闭的段建立索引。这是该组中对CPU密集性要求最高的节点。
这三种类型的节点分担了不同类型的工作负载。它们可以独立扩展。这里的可用性和持久性分离是从微软Socrates云数据库中学习衍生而来的。
总结和展望
本文回顾了Milvus向量数据库2.0版本的几个设计决策。让我们在这里快速总结一下这些要点。
- Milvus集群2.0版本选择了最终一致性。
- 我们尽可能将成熟的云组件集成到Milvus 2.0中,并控制了因适用Milvus 2.0引入到用户生产环境中的新组件。
- Milvus 2.0遵循Actor模型,对可用性和持久性实现了分离,在云环境中易于扩展。
到目前为止,Milvus 2.0弹性云数据库的骨架已经定型,目前还有许多来自Milvus社区的需求需要满足。如果您有相同的使命(“构建更多开源基础设施软件,加速AI转型”),欢迎加入Milvus社区。
Milvus是LF AI & Data基金会的孵化的项目。你无需为Milvus 签署任何CLA!
附录
Milvus设计文档
https://github.com/milvus-io/milvus/tree/master/docs/design_docs
Raft的C++实现
如果你对共识算法感兴趣,建议你查看eBay的开源项目 Gringofts。它是Raft共识算法(Paxos系列的一个变体)的C++实现。它是我的朋友Jacky和Elvis(我在摩根士丹利的前同事)为eBay 在线支付系统构建的,这也正是它最适合的场景之一。
译者介绍
陈林,51CTO社区编辑,某零售银行龙头DevOps持续集成平台技术负责人,主导核心业务方案设计落地,推动产品架构持续演进。负责安全工具接入、扫描中台建设、构建加速、SCM性能优化、镜像治理等模块。参与微服务治理、多活构建调度架构、异构存储集群集成、缓存和分布式限流等架构优化。热爱开源技术和写文章,追求技术广度和领域深度。
原文标题:Evolution of Milvus Cloud-Scalable Vector Database,作者:Jun Gu