如何设计一个面向未来的云原生数据库?

2022-11-07 16:10:07 浏览数 (1)

很荣幸我们最新的论文《Manu: A Cloud Native Vector Database Management System》被数据库领域国际顶会 VLDB'22 录用。这两天刚好在大会上分享了论文内容。正好趁热打铁写一篇文章,将梳理后的论文内容分享给大家,聊聊背后的设计与思考。

本文将尝试对 Manu(Milvus 2.0 版本代号)这个“面向向量数据管理而设计的云原生数据库系统”中的关键设计理念和原则进行阐述。如果想要了解更详细的内容,大家可以参考我们已经发布的论文和 Milvus 项目的 GitHub 开源仓库:https://github.com/milvus-io/milvus

项目背景

在最初设计和实现 1.0 版本的 Milvus 时,我们主要的目标是良好地支持向量管理的相关功能,并对向量检索的性能进行优化。随着和用户交流的不断深入,我们发现了实际业务中对于向量数据库的一些普遍需求,而这些需求在 Milvus 早先版本的框架之下是难以得到良好的解决的

我们可以将这些需求归纳为以下几类:持续变化的需求、需要更灵活的一致性策略、需要组件级的弹性扩展能力、以及需要更简单高效的事务处理模型。

需求依旧在持续变化

对于处理向量数据而言,业务中的需求目前仍未完全定型。

业务需求从最早期的主要对向量数据进行 K 近邻搜索,逐渐演变为范围搜索、支持各类自定义距离指标、向量标量数据联合查询以及多模态查询等越来越多样的查询语义等等。

这就要求向量数据库的架构具备足够的灵活性,能够快速、敏捷的完成对新需求的支持。

需要更灵活的一致性策略

以内容推荐场景为例,业务对于时效性有较高的要求。当新内容产生时,我们在几分钟甚至几秒钟内将内容推荐给用户,通常是可以接受的。但如果要隔一天或者更久才将内容推荐出去,则会对推荐效果造成较大影响。在这类场景下,如果“死磕”强一致性将会带来较大的系统开销,但如果仅提供最终一致性,业务效果则难以得到保障

针对这个问题,我们提出了一套解决方案:用户可以根据业务的需求,来指定插入数据在可被查询前所能容忍的最大时延。对应的调整系统在数据处理时的一些机制,来提供对业务的最终效果保障。

需要支持组件级的弹性扩展

向量数据库的各个组件对资源需求的差异很大,在不同的应用场景下,各个组件负载强度也是不同的。比如,向量检索和查询的组件需要大量的计算和内存资源来确保性能,而其他负责数据归档、元数据管理的组件仅需少量资源就可以正常运转。

从应用类型来看的话,推荐类的应用最关键的需求是大规模并发查询的能力,所以通常只有查询组件会承担较高的负载;分析类的应用常常需要导入大量离线数据,此时负载压力就落在数据插入和构建索引这两个相关组件上。

为了提高资源的利用率,需要让各个功能模块具有独立的弹性扩展能力,让系统的资源使用更加贴合应用的实际需求

需要更简单高效的事务处理模型

严格来说,这点不算是需求特征,属于系统设计上可以利用的优化空间。

随着机器学习模型描述能力日益增强,业务通常会倾向将单一实体的多种维度的数据进行融合,以一个统一的向量数据进行表示。比如做用户画像的时候,将个人资料、偏好特征、社交关系等信息进行融合。因此,向量数据库可以仅用单表来进行数据维护,而不需要实现类似传统数据库的 “JOIN 操作”。这样一来,系统仅需支持单表上行级别的 ACID 能力,而不需要支持涉及多表的复杂事务,为系统中组件解耦和性能优化留出了较大的设计空间。

设计目标

作为 Milvus 的第二个大版本,Manu 的定位是一个面向云原生设计的分布式向量数据库系统。

在设计 Manu 的时候,我们综合考虑了上面提到的各种需求,结合分布式系统设计所需要的常见要求,提出了五个大的目标:持续演进能力、可调一致性、良好的弹性、高可用、高性能。

持续演进能力

为了在功能演进的同时,能够将系统的复杂性限制在可控范围之内,我们需要对系统进行很好的解耦,确保系统中的功能组件可以较为独立的增加、减少以及修改。

可调一致性

为了能够让用户指定新插入数据的查询可见延迟,系统需要支持 delta consistency。Delta consistency 要求所有的查询能够至少查询到 delta 时间单位之前的所有相关数据,而 delta 是可以由用户应用根据业务需求指定的。

良好的弹性

为了提高资源使用效率,需要做到组件级的细粒度弹性,同时也要求资源分配策略能够考虑组件对硬件资源需求的差异性。

高可用和高性能

高可用是所有云数据库的基本需求,需要做到系统在少数服务节点或组件的失效情况下,不影响系统其他服务的正常运行,同时能够进行有效的故障恢复。

高性能则是向量数据库场景下“老生常谈”的问题了,在设计过程中需要严格的控制系统框架层面所产生的开销,从而保证良好的性能。

系统架构

Manu 采用了一个四层的架构来实现对读/写、计算/存储以及有状态/无状态组件的分离

如下图所示,系统架构可以分为 access layer,coordinator layer,worker layer 以及 storage layer 四层。此外,Manu 将日志作为系统的主干,用于各组件间的协同和通信。

Access layer

Access layer 访问层由若干个无状态的 proxy 组成

这些 proxy 负责接收用户请求,将处理过的请求转发给系统内的相关组件进行处理,并将处理结果收集整理好之后返回给用户。Proxy 会缓存部分的系统元数据,并基于这些数据对用户请求进行合法性检查。例如,被查询的数据集是否真的存在。

Coordinator layer

Coordinator layer 负责管理系统状态,维护系统元数据以及协调各个功能组件完成系统中的各类任务。

Manu 总共有四种类型的 coordinator。我们将不同功能的 coordinator 都拆分开来设计,这样一来,一方面可以隔离故障,另一方面也方便各个功能组件进行独立的演进。

此外,出于可靠性考虑,每种 coordinator 通常都有多个实例:

  1. Root coordinator 负责处理数据集创建/删除等数据管理类型的请求,并对各个数据集的元数据进行管理。比如,包含的属性信息,每种属性的数据类型。
  2. Data coordinator 负责管理系统中数据的持久化工作,一方面协调各个 data node 处理数据更新请求,另一方面维护各个数据集中数据存储的元数据。例如,每个数据集的数据分片列表以及各分片的存储路径。
  3. Index coordinator 负责管理系统中数据索引的相关工作,一方面协调各个 index node 完成索引任务,另一方面记录各数据集的索引信息,包含:索引类型,相关参数,存储路径等。
  4. Query coordinator 负责管理各个 query node 的状态,并根据负载的变化调整数据和索引在各 query node 上的分配情况。

Worker layer

Worker layer 负责真正的执行系统中的各类任务。

所有的 worker node 都是无状态的,它们仅会读取数据的只读副本并进行相关的操作,并且他们之间不需要相互的通信协作。因此,worker node 的数量可以很方便的根据负载的变化进行调整。Manu 使用不同类型的 work node 来完成不同数据处理任务,这样做使得各个功能组件可以根据负载和 QoS 要求的差异独立进行弹性伸缩。

Storage layer

Storage layer 持久化的存储系统状态数据、元数据、用户数据和相关的索引数据。

Manu 使用高可用的分布式 KV 系统(如 etcd)来记录系统状态和元数据。在更新相关数据时,需要首先将数据写入 KV 系统,然后再将其同步到各相关 coordinator 的缓存中。对于用户数据和索引数据等规模较大的数据,Manu 采用对象存储服务(如 S3)进行管理。对象存储服务的高延迟并不会导致 Manu 的数据处理性能问题,因为 worker node 处理数据前会从对象存储中获取对应数据的只读拷贝并缓存在本地,所以绝大部分的数据处理都是在本地完成的。

日志作为系统主干

为了更好的解耦各个功能组件,使其能够独立的进行资源弹性调整和功能演进,Manu 采用了“日志即数据(log as data)”的设计理念,并将日志作为系统主干用来连接各个组件。在 Manu 中,日志被组织为持久化的可被订阅的信息,而系统中的各个组件则是日志数据的订阅者

Manu 中的日志内容主要有 WAL(write ahead log) 和 binlog 两类。系统中数据的存量部分被组织在 Binlog 当中,而增量部分则在 WAL 中,两者在延迟、容量和成本等方面都起到了互补的作用。

如上图所示,logger 负责将数据写入 WAL,是整个日志系统的数据入口。Data node 订阅了 WAL 的内容并负责对数据进行处理并存入 Binlog 中。而 query node、index node 等其他组件保持了相互独立的关系,仅靠订阅日志来同步数据内容。

除此之外,日志系统还负责了系统内部组件间的部分通信功能,各组件可以借助日志广播系统内部事件。比如,data node 可以告知其他组件有哪些新的数据分片被写入了对象存储系统,index node 可以告知所有 query coordinator 有新的索引构建完成等。各类信息可以根据其功能类别被组织成不同的 channel。每个组件只需要订阅和自己功能相关的 channel 即可,而无需监听所有被广播的日志内容。

工作流程

接下来,我们从数据插入、索引构建和向量查询三个方面,介绍 Manu 系统内部各类任务的工作流程。

数据插入

上图中展示了数据插入相关组件的工作流程。

数据插入请求经 proxy 处理后会被哈希到了不同的分桶当中。系统中通常有多个 logger 按照一致性哈希的方式来处理各个哈希桶中的数据。每个哈希桶中的数据会被写入一个与其唯一对应的 WAL channel 中。当 logger 接受到请求的时候,会为该请求分配一个全局唯一的逻辑序列号(LSN),并将该请求写入对应的 WAL channel 中。该 LSN 由 TSO(time service oracle)产生,各 logger 需要定期向 TSO 获取 LSN 并保存在本地。

为了保证低延迟、细粒度的数据订阅,Manu 在 WAL 中对数据采用行式存储,并由各订阅组件进行流式读取。通常 WAL 可以用类似 Kafka 或者 Pulsar 的消息队列实现。Data node 是订阅了 WAL 的组件之一,它在获取到 WAL 中更新的数据之后,会将其从行式存储转换成列式,并存入 binlog。列式存储将同一列中的数据连续的存储在一起,这种方式对数据压缩和访问都更加友好。例如,index node 需要对某一列向量数据构建索引时,只需从 binlog 中读取该列向量,而无需访问其他列中的数据。

索引构建

Manu 支持批量和流式两种索引构建方式。当用户对某个已经有数据的数据集构建索引的时候,会触发批量索引构建。在这种情况下,index coordinator 将从 data coordinator 获取数据集中所有数据分片的存储路径,并调度各个 index node 为这些数据分片构建索引。如果用户在为数据集构建完索引之后继续向数据集插入新的数据则会触发流式索引构建。

当 data node 将一个新的数据分片写入 binlog 之后,data coordinator 会通知 index coordinator 创建任务为新的数据分片构建索引。不论是以上的哪种方式,index node 构建完索引之后会将其保存到对象存储服务,并将对应的存储路径发送给 index coordinator。而 index coordinator 则会进一步的通知 query coordinator 安排相应的 query node 将索引的副本读取到其本地。

向量查询

为了并行的处理数据查询请求,Manu 将数据集中的数据划分成固定大小的数据分片,并将这些数据分片放置在不同的 query node 上。Proxy 可向 query coordinator 查询数据分片在 query node 上的分布信息,并将其缓存在本地。当收到查询请求时,proxy 会将请求分发到存放了相关数据分片的各个 query node 当中。这些 query node 会依次对本地所有相关的分片进行查询,并将结果合并之后返回给 proxy。Proxy 在收到所有相关 query node 的结果之后则会进一步的将结果整合并返回给客户端。

Query node 中数据的来源主要由三个方面:binlog,索引文件和 WAL。对于存量的数据, query node 会从对象存储服务中读取相应的 binlog 或者索引文件。对于增量部分的数据,query node 会直接从 WAL 中流式获取。如果从 binlog 中获取增量数据,将会导致较大的查询可见延迟,即数据从完成插入到能够被查询的时间间隔会比较大,难以满足对一致性要求较高应用的需求。

前文中提到 Manu 为了能够让用户有更加灵活的一致性选择而实现了 delta consistency。Delta consistency 要求系统保证在收到数据更新(也包括插入和删除)请求后,最多经过 delta 个时间单位更新的内容就可以被查询到。

为了实现 delta consistency,Manu 为所有的插入和查询请求都添加了 LSN ,并在 LSN 中携带了时间戳信息。在执行查询请求时,query node 会检查查询请求的时间戳 Lr 和 query node 处理的最新的更新请求的时间戳 Ls,仅当两个时间的间隔小于 delta 才可以执行查询任务,否则需要先处理 WAL 中记录的数据更新信息。为了避免用户长时间没有数据更新导致 Ls 相对当前的系统时间太小从而阻断查询的执行,Manu 会定期的向 WAL 中插入特定的控制信息强制 query node 更新时间戳。

性能评估

我们在论文中结合实际使用场景对系统进行了综合的性能评估,在这里本文仅列出部分结果。

这张图中,我们比较了 Manu 和其他四个开源的向量检索系统(已匿名)的查询性能。可以看出 Manu 在 SIFT 和 DEEP 两个数据集上向量检索性能相比其他系统均有明显优势

这张图中,我们展示了 Manu 在不同 query node 数量时的查询性能。可以看到在不同数据集以及不同相似度指标下,Manu 的查询性能和 query node 数量都呈现了近似线性的关系

最后这张图中,我们展示了用户在选择不同的一致性要求下 Manu 的查询性能。图中的横坐标即为 delta consistency 中 delta 的值,不同的图例代表了向 WAL 中发送控制信息强制 query node 同步时间的频率。从图中可以观察到随着 delta 的增大,Manu 的查询延迟快速下降。因此用户需要根据自身应用对性能和一致性的要求选择合适的 delta 值

总结

最后,我们来总结一下。

本次论文主要介绍了应用对向量数据库的特殊需求,以及阐述了 Manu 系统的设计理念、关键功能的运转流程。主要设计理念有两个方面:

  1. 将日志作为系统主干连接系统中各个组件,为各组件的独立弹性、功能演进以及资源和故障的隔离提供了较大的便利;
  2. 基于日志和 LSN 实现了 delta consistency,使得用户可以更加自由的在一致性、成本和性能之间进行选择。

我们此次 VLDB 论文中最主要的贡献是介绍了用户对向量数据库的实际需求,并相应的设计了一个云原生向量数据库的基本架构。当然,目前这个框架下仍然存在不少值得探索的问题,例如:

  • 如何对多个模态的向量数据进行联合检索;
  • 如何更好的利用包括本地磁盘、云盘以及其他存储服务在内的云存储服务设计高效的数据检索方案;
  • 如何利用 FPGA、GPU、RDMA、NVM 、RDMA 等新型计算、存储和通信硬件设计极致性能的索引和检索方案等。

写在最后

最早产生写这篇文章的想法还是在一年前的时候,我和公司 CEO 星爵刚在西安参加完 SIGMOD,正准备回上海参加 Milvus 2.0 GA 版本的发布会。谈起这次参会的感想,我和星爵都感受到云原生数据库正逐渐成为了学术界研究新的热点。正巧 Milvus 2.0 就是我们面向向量数据管理而设计的云原生数据库系统,于是我们自然而然的产生了写下这篇文章的想法。

希望我们的工作能够起到一个抛砖引玉的作用,让更多的学者和业界的朋友能够和我们一起对相关的课题进行探索和研究。

此外,这篇论文是由 Zilliz 团队和南方科技大学的数据库团队一起合作完成的,在此感谢唐博老师、晏潇老师和向隆同学在工作中的贡献。

Zilliz 是向量数据库系统领域的开拓者和全球领先者,研发面向 AI 生产系统的向量数据库系统。Zilliz 以发掘非结构化数据价值为使命,致力于打造面向 AI 应用的新一代数据库技术,帮助企业便捷地开发 AI 应用。Zilliz 的产品能显著降低管理 AI 数据基础设施的成本,帮助 AI 技术赋能更多的企业、组织和个人。

0 人点赞