CMU-机器学习数据库研讨会-2023-笔记-全-

CMU 机器学习数据库研讨会 2023 笔记(全)

001:开源向量搜索引擎与向量数据库

概述

在本节课中,我们将学习 Qdrant 开源向量搜索引擎的内部架构与核心技术。我们将了解向量搜索的基本概念、Qdrant 的系统设计,以及它如何解决向量索引、高效搜索和复杂过滤等核心挑战。


向量搜索简介

向量搜索并非一项新技术,它已被许多大公司使用了很长时间。其基本原理是使用一个模型(通常称为编码器,通常是神经网络)将输入数据转换为密集的向量表示。这些向量表示(也称为嵌入)具有一个有趣的特性:在向量空间中彼此接近的一对向量,通常也对应着在某种意义上是相似的对象。对于文本,这可能是语义相似性;对于图像,这可能是视觉相似性。具体捕捉哪种相似性实际上由模型定义。向量之间的距离函数也由模型定义,但在大多数用例中,它只是向量之间的简单点积。

虽然这项技术本身并不新,但新的是这些现成可用模型的普及程度以及围绕它们的工具。现在,任何人都可以下载一个预训练模型并使用它,而无需任何机器学习知识。因此,下一步是将这项技术提升到生产级别,让工程师和软件开发人员也能使用它。目标是为向量搜索世界提供与传统文本搜索引擎或数据库同等的便利性和可靠性。这正是 Qdrant 所做的事情。


Qdrant 架构概览

上一节我们介绍了向量搜索的基本概念,本节中我们来看看 Qdrant 如何实现这一目标。以下是 Qdrant 组件的顶层概览。

此层次结构中的每一层都代表某种特定的隔离级别。

  • 集合:顶层是集合。集合在逻辑上隔离不同类型的数据,类似于关系数据库中的表或文档数据库(如 MongoDB)中的集合。
  • 分片:集合之下是分片。分片隔离数据的子集,保证每个分片只包含不重叠的记录子集。分片可以在节点间移动和复制,以实现高可用性。
  • :最底层的隔离级别是段。段隔离索引和数据存储。每个段都能够在其较小的数据子集上执行与整个集合相同的所有操作。

稍后我将解释为什么需要段以及它们如何工作。

首先,关于顶层架构的更多信息。你可能注意到,Qdrant 的顶层架构相当标准:我们有包含分片的集合,分片可以复制和移动。我们使用 Raft 共识协议来跟踪元数据,例如集合位于何处、节点状态、集合配置等。所有这些元数据都以分布式共识的方式存储。这对于许多分布式系统来说是相当标准的做法。

这是因为 Qdrant 是建立在 BASE 原则之上的系统的典型例子。BASE 代表基本可用、软状态、最终一致性。它通常与更常见于关系数据库的 ACID 原则进行比较。要直观理解 BASE 和 ACID,可以想想 PostgreSQL 和 Elasticsearch。PostgreSQL 是 ACID 数据库的典型例子,它具有非常严格的事务保证,非常关心数据的一致性,但 PostgreSQL 的可扩展性实际上非常有限,基本上受限于单台机器的规模。另一方面,Elasticsearch 是 BASE 系统的典型例子,它具有高度可扩展性,但一致性保证要弱得多。

这实际上也是我更喜欢“向量搜索引擎”这个术语而不是“向量数据库”的原因。对于像 Qdrant 这样的系统,可扩展性和性能在我看来比事务一致性重要得多,因此它应该被视为搜索引擎而非数据库。理想情况下,我认为它甚至不应该用作数据的主存储,特别是考虑到由于模型版本更新而导致的向量全量更新在向量数据库世界中是常见操作。你可能需要清空整个数据库并创建一个新的,仅仅因为你的编码器发生了变化。这在传统数据库中是从未发生过的事情。


段:可变性与性能权衡

上一节我们介绍了 Qdrant 的顶层架构,本节中我们来看看分片内部更具体的设计。分片内部发生的事情更有趣,因为它实际上涉及向量搜索的特性。在顶层,我们看到了一个相当标准的组件——预写日志,它负责确保 Qdrant 在数据提交后通常不会丢失数据。这对于任何数据库来说都是标准组件。不标准的是分片内部数据的第二级隔离——段。

那么,为什么首先需要多个段呢?为什么不能把所有数据都放在一个段里?实际上有几个原因。

第一个原因是可变性。在 Qdrant 中,我们实际上喜欢不可变的数据结构。这种“结构只构建一次,之后永不扩展”的简单假设,为许多不同的优化打开了空间。数据结构可以变得更紧凑;我们不需要在内存中的不同位置之间跳转,因此缓存未命中更少;所有数据统计信息都是预先已知的,因此我们也可以基于此执行各种优化,例如预计算直方图、预计算数据分布等。当然,在这种情况下,我们也可以分配所需的确切内存量,因此我们也不需要担心内存碎片化。加载不可变数据结构也快得多,因为你不需要执行任何反序列化操作,只需从磁盘复制原始内存块,甚至可以进行内存映射,这甚至更快。此外,我们可以使用增量编码、可变字节编码等技术进一步压缩数据。这些优化的综合效果可以使不可变数据结构比可变数据结构的效率高出一个数量级。

第二个原因是延迟与吞吐量之间的权衡。其原因是单个请求的并发性只在某个点之前是高效的。越接近底层索引,并发效率就越低。这使得段成为 Qdrant 中并发的自然单元。例如,如果我们处理一个需要极低延迟或单次请求的应用程序,我们可以通过为每个 CPU 核心分配一个段来优化 CPU 利用率。这样,一个请求就能尽可能多地利用 CPU。另一方面,如果我们有一个处理高吞吐量、需要发出大量并行请求的应用程序,我们可以使用一个单独的大段。在这种情况下,通过让每个请求在专用核心上以只读模式使用整个段,可以最大化整个系统的吞吐量。

以下是关于段管理的更多细节:

我们分片中有很多段,其中一些是不可变的,另一些则用于插入新数据。但是,我们如何为用户维护“整个集合是完全可变的”这一假象呢?Qdrant 用户实际上可以随时插入、删除、更新任何数据。理想情况下,用户甚至不应该知道段的存在,这纯粹是内部机制。为了解决这个问题,我们实际上需要解决两个问题:第一是如何更新不可变数据结构中的数据;第二是如何首先获得可变数据结构。

第一个问题通过简单地采用写时复制机制来解决。每当用户向可变段插入新数据或更改数据时,我们只需将数据片段复制到可变段中,在旧段中将其标记为已删除,一切就正常工作了。

第二个问题稍微复杂一些,因为我们需要在段上执行长时间运行的优化(例如索引构建)。这就是为什么我们需要在优化期间保持该段对用户更新可用。为了做到这一点,我们使用所谓的代理段,这是一种特殊类型的段,它将一个正在优化的段包装在一个接口下。它还持有一个需要应用的修改列表,以解决将数据从旧段复制到新段时发生的冲突。这是一个管理所有插入的特殊数据结构。当优化完成后,它只是转换回常规段,实际上是一对段:优化后的段和一个小的写时复制段,后者成为可变段。


向量索引:核心挑战

上一节我们讨论了段的管理,本节中我们来看看段内部的核心组件——向量索引。段内部有一些抽象组件,我故意不详细描述每个组件在底层是如何工作的,而是将重点放在向量索引上。主要原因是具体实现的选择取决于配置。例如,向量存储,我们在 Qdrant 中至少有三种不同的向量存储实现,很可能在不久的将来我们会添加第四种。Qdrant 能够与任何抽象存储一起工作,它可以是文件,也可以是内存存储,这并不重要。

但重要的是向量索引,让我们终于来谈谈向量搜索的核心组件——索引本身。

向量搜索区别于传统索引(如倒排索引或 B 树)的两个主要特征是:

  1. 近似性:它不保证结果是精确的,甚至不保证对于相同的基础数据,多次运行索引会得到相同的结果,因为结果实际上取决于你将其插入索引的顺序。这是相当根本性的。
  2. 普遍相关性:任何向量都可能是任何搜索请求的结果。换句话说,你集合中的任何文档在某种程度上都与其他文档相似。因此,仅基于向量相似性得分来划清相关文档和不相关文档之间的界限是不可能的。

当然,有很多不同的方法来实现向量索引,但无论你选择哪种方法,它们都必须处理我刚才描述的这些属性。实际上,这打破了许多你在传统数据库中所做的假设,因此我认为它确实需要特殊的处理和围绕它的专用架构。

在 Qdrant 中,我们使用所谓的 HNSW 索引。HNSW 代表分层可导航小世界图。这个名字相当复杂,但我会尝试用一个非常简化的版本来解释它,以提供一些关于它如何工作的直觉。

在内部,HNSW 表现为一个邻近图。这意味着每个向量在图里表示为一个节点,这些节点与一定数量的最近邻居(即数据库中的其他向量)相连。在邻近图中的搜索以贪婪的方式进行,意味着在每一步,我们选择距离目标最近的节点,然后用这个新选择的节点重复搜索步骤。这个过程不断重复,直到无法再改善节点与目标之间的距离。当然,无法保证这种搜索会找到绝对最接近的向量,这就是为什么它被称为近似搜索。但我们可以通过改变搜索的束宽参数来控制精度,并在精度和搜索速度之间进行权衡。


HNSW 的挑战与优化方案

上一节我们介绍了 HNSW 索引的基本原理,本节中我们来看看它带来的挑战以及 Qdrant 的解决方案。HNSW 索引带来了自身的挑战。

首先,构建时间。将新向量插入索引的成本大约是仅搜索索引的两倍,而且它本身也非常消耗 CPU。因此,如果我们想在构建索引的同时不影响其他进程(如搜索),就需要有一个专门用于在后台构建索引的线程池,或者理想情况下,我们甚至可能希望将索引构建过程完全移到另一台机器上。

其次,HNSW 索引不仅消耗大量 CPU,而且具有随机数据访问模式。这意味着在每一步,它都对底层存储的延迟非常敏感。像预取、块读取这样的技术效率不高。这就是为什么它通常需要大量内存,并且不太适合磁盘存储。此外,这种模式不仅是随机的,而且是顺序的——记住,我们在图中从一个节点移动到另一个节点。这意味着我们无法高效地并行化搜索,其性能主要受存储延迟的限制,而不是吞吐量。

为了克服这些挑战,Qdrant 采用了以下方案:解决方案是使用向量的压缩内存表示,并用它来生成候选列表。例如,Qdrant 引擎的最新功能之一是二进制量化。它允许我们将向量压缩到每个维度仅用一个比特表示的水平,这为向量提供了总计 32 倍的压缩。在此基础上,它允许我们使用非常快速的 CPU 指令,基本上允许我们仅用两个 CPU 指令(按位异或和人口计数)来比较向量。这对于像 OpenAI 模型提供的那种大向量(例如,单个向量有 1536 维)尤其有效。

在获得这个候选列表后,我们可以使用原始向量对它们进行重排序,并将最终结果返回给用户。重要的是要知道,与遍历 HNSW 图不同,这个重排序过程实际上可以高效地并行化,因为我们已经知道所有候选向量的偏移量。因此,我们可以在这里利用异步 I/O,甚至可以利用具有巨大延迟的 SSD 或网络挂载磁盘。


结合向量搜索与过滤

另一个与 HNSW 相关的挑战是需要将向量搜索与额外的过滤条件结合起来。例如,你可能想在电商商店中搜索某种商品,并且该商品的价格应低于 100 美元,或者你想在特定位置附近搜索(例如“在我附近找东西”)。这些额外的条件对于现实世界的应用程序来说是必需的,但 HNSW 或任何其他 ANN 算法的原始实现并不具备这些功能。

在一些文献中,你可能会发现有两种方法来解决这个问题:后过滤预过滤

  • 后过滤:建议我们可以先执行常规的向量搜索,然后在结果之上应用过滤条件,排除那些不符合过滤条件的结果。我们可能需要重复这个过程几次,直到获得所需数量的结果。这种方法实现起来相当简单,但不幸的是效率非常低,特别是当过滤条件非常严格,或者过滤条件与向量相似性得分本身相关时。这种方法基本上要么有风险将整个搜索变成线性扫描,要么最终返回不完整的结果。
  • 预过滤:建议先生成一个候选列表,然后基于这个候选列表执行向量搜索。问题是生成候选列表本身可能是一个非常昂贵的操作。在最坏的情况下,它可能需要检查集合中一半向量的条件,这会显著增加搜索延迟。

Qdrant 提出了一种我们称之为原位过滤的方法。这意味着在遍历图的过程中检查过滤条件。这需要我们进行一些自定义实现,对 HNSW 进行自定义调整。因此,我们不再使用原始实现,而是使用 Qdrant 中的自定义实现。通过这种方式,我们可以确保只需要检查执行搜索实际所需的次数的过滤条件。

但这看起来问题解决了?不幸的是,还没有。当过滤条件如此严格以至于图变得不连通时,问题就出现了。这意味着我们无法再在图的入口点和实际包含所需结果的部分之间找到路径。在数学中,有一个专门研究这类问题的领域,称为渗流理论。对于大型随机图,该理论实际上给出了一个令人惊讶的简单方程,定义了需要从图中移除多少节点才能使其不连通。这个方程就是 1 / k,其中 k 是图中每个节点的平均连接数。换句话说,假设每个节点有 10 个连接,那么在移除 90% 的节点后,图将变得不连通。

在实践中,这个图表显示了 HNSW 的精度与被过滤掉的向量比例之间的关系。你可以看到,在某个点之后,精度几乎下降到零。有几项实验表明,这种效应实际上取决于每个节点的连接数,确实非常接近理论。

那么我们能做些什么呢?为了解决这个问题,我们可以利用这样一个事实:我们想要应用于搜索的过滤器实际上并不是随机的。在大多数情况下,这些过滤器基于与向量相关的一些元数据或有效载荷。正如你可能记得的,我们有一个商品旁边的价格,并且我们事先知道价格。我们不仅事先知道价格,而且我们还知道在索引构建阶段哪个价格与哪个向量相关联,因为我们使用不可变的数据结构。

我们能做的是构建额外的链接。我们可以基于现有的有效载荷值和可能的过滤条件来生成这些链接。例如,如果我们有一个关键字字段作为有效载荷,我们可以为这个字段的每个值构建子图,然后将这些子图合并到主图中。这样我们就可以创建这些额外的链接,这些链接将确保当我们应用带有此关键字的过滤条件时,我们的图将始终保持连通。无论搜索条件多么严格,我们都可以保证这一点。

这种方法的好处是它实际上不会增加搜索的复杂度。即使我们增加了图的大小,我们仍然可以只在需要时使用原始链接执行搜索,并仅在原始链接被过滤掉时利用额外链接。因此,搜索的总复杂度不受影响。我们可以根据需要索引尽可能多的额外链接,可以索引许多用于过滤的额外字段。这种方法也兼容在单次搜索中使用多个字段进行过滤,因为当我们这样做时,我们会将所有子图合并到主图中,并且基本上可以在此阶段对链接进行去重。因此,实际上只需要一小部分额外的内存,并且搜索速度不会受到影响。


总结

在本节课中,我们一起学习了 Qdrant 向量搜索引擎的核心架构与技术。

  • 向量搜索方兴未艾:我认为除了文本搜索或聊天机器人的记忆之外,还有许多更有趣的用例即将出现。
  • Qdrant 是搜索引擎:应将其视为搜索引擎。搜索引擎的架构与数据库的架构有根本不同,在设计自己的应用程序时应考虑到这一点。
  • 向量索引需要特殊处理:向量索引是一个非常特殊的组件,即使对于像过滤这样的常规操作,也需要特殊处理。

本次演示未涵盖但可以讨论的主题包括:查询规划和有效载荷索引在 Qdrant 内部的组织方式;每个段的动态搜索限制优化;以及量化技术(目前支持二进制量化、标量量化和乘积量化)。

感谢大家,我们很乐意回答问题。

002:人工智能驱动的数据库优化即服务

在本节课中,我们将学习OtterTune,一个利用机器学习技术自动优化数据库配置的服务。我们将从学术研究项目开始,了解其核心算法,然后探讨其商业化过程中遇到的挑战与解决方案。

概述

数据库调优是一个复杂且耗时的过程,传统上依赖于数据库管理员(DBA)的专业知识。OtterTune旨在通过机器学习自动化这一过程,为用户提供高效、自动化的数据库配置优化服务。本节课将详细介绍OtterTune的工作原理、从学术研究到商业产品的演变,以及在实际应用中面临的挑战和应对策略。

背景:数据库调优的挑战

数据库的性能高度依赖于其配置参数、硬件环境、数据特征和工作负载。找到最优配置通常需要大量手动尝试和专业知识。随着系统复杂性的增加,寻找和雇佣专家DBA变得既困难又昂贵。OtterTune作为一个自动化服务,利用机器学习技术,旨在解决这一难题。

OtterTune学术版本回顾

OtterTune最初是卡内基梅隆大学的一个研究项目。其核心目标是创建一个无需领域知识、可扩展的外部调优服务,能够适用于多种数据库系统。

核心算法流程

OtterTune的机器学习流程主要包含三个部分:工作负载表征、旋钮识别和自动调优组件。前两者作为后台进程运行,后者则负责与数据库交互并生成新配置。

以下是工作流程的简要介绍:

  1. 用户选择目标:用户首先选择希望优化的目标,例如查询延迟、吞吐量或CPU利用率。
  2. 数据收集:部署在用户环境中的代理会连接到数据库,定期收集配置参数、运行时指标等遥测数据。
  3. 分析与建议:OtterTune将收集到的数据发送到调优管理器进行分析,并基于历史数据和机器学习模型,生成一个预计能提升性能的新配置。
  4. 应用配置:新配置被建议或直接应用到数据库,完成一次调优迭代。

关键技术点

  • 工作负载表征:这是一个数据降维方法。系统接收所有性能指标数据,通过因子分析和聚类等技术,找出一组能够区分不同工作负载特征的关键指标子集。
  • 旋钮识别:使用LASSO等特征选择算法,分析历史数据,确定各个配置参数(旋钮)的重要性排序。这有助于将调优重点集中在最重要的参数上,缩小搜索空间。
  • 自动调优组件:采用贝叶斯优化方法。它包含两部分:
    • 工作负载映射:将当前目标工作负载与历史中调优过的工作负载进行相似性比较,找到最相似的一个。
    • 模型训练与配置生成:结合目标工作负载的数据和最相似历史工作负载的数据,训练高斯过程模型。该模型在探索(尝试未知区域)和利用(利用已知好区域)之间进行权衡,以生成下一个待测试的配置。

从学术研究到商业产品的挑战

将OtterTune从实验室推向市场时,我们发现研究中的一些假设在现实世界中并不成立。

主要挑战

  1. 离线调优不现实:研究假设可以在与生产环境隔离的、可重复的环境中调优。现实中,客户的预发布或开发数据库与生产数据库差异巨大,导致调优结果无法直接迁移。因此,许多客户选择直接在生产数据库上进行调优。
  2. 获取高质量训练数据困难:研究中使用的是静态的基准测试数据。真实工作负载是动态变化的,具有周期性(如昼夜模式)和非周期性波动。这导致我们无法像研究中那样仅用5分钟数据做决策,而需要将观察周期延长至24小时,以捕捉完整的工作负载周期。同时,构建具有广泛代表性的模型所需的海量、多样化数据(不同工作负载、硬件、配置)难以获得。
  3. 客户信任与安全至上:商业产品必须绝对保证不会导致客户数据库崩溃或性能严重下降。因此,稳定性比峰值性能更重要。我们需要建立严格的保障措施。
  4. 快速体现价值:客户希望立即看到服务的好处。由于调优周期变长,我们需要通过提供健康检查、索引/查询建议等即时功能来快速交付价值,并管理客户对调优耗时的期望。

商业版OtterTune的解决方案

为了应对上述挑战,商业版OtterTune做出了多项关键改进。

融入领域知识

与学术版本追求“全自动、零知识”不同,商业产品积极融入了数据库领域的专业知识。

  • 约束旋钮范围:系统并非盲目搜索所有可能的参数值,而是基于对MySQL、PostgreSQL等数据库的深入理解,为每个可调参数预设了安全且合理的取值范围。这极大地缩小了搜索空间,提高了调优效率和安全性。
    # 示例:为 `shared_buffers` 参数设置安全范围
    knob_range = {
        'shared_buffers': {
            'min': '128MB',
            'max': '25% of total RAM', # 基于总内存的百分比
            'default': 'dynamic based on instance type'
        }
    }
    
  • 精选调优旋钮:并非所有数据库参数都值得或适合调优。商业版OtterTune通过自动化分析和人工审核,为每种数据库(如RDS PostgreSQL、Aurora MySQL)推荐10-20个最可能影响性能的核心参数,屏蔽了那些需要重启、风险高或影响一致性的参数。
  • 利用最佳实践引导:在调优初期,算法会尝试一些公认的、对大多数数据库有益的配置值(例如来自pgtune的建议),以帮助快速定位到较优的搜索区域。

提供手动控制与保障

为了建立客户信任并满足不同需求,OtterTune提供了灵活的调优模式:

  1. 自动调优模式:OtterTune在安全范围内自动应用配置,无需人工干预。
  2. 人工审核模式:系统生成配置建议后,需经客户团队审核批准才会应用。
  3. 自助调优模式:系统仅提供配置建议和命令行,由客户自行决定是否及如何应用。

数据显示,大部分活跃用户选择了自动化程度较高的前两种模式,体现了对服务的信任。

扩展优化维度与价值体现

除了配置参数调优,商业版OtterTune还提供了更广泛的服务来快速交付价值:

  • 索引优化:分析查询模式,建议创建或删除索引。
  • 查询优化:识别慢查询并提供优化建议。
  • 资源监控与建议:监控数据库资源使用情况(如内存、CPU)。
  • 健康评分:提供一个综合性的“健康分数”,涵盖配置、索引、查询、资源等多个维度,让用户对数据库状态一目了然。

性能表现与未来方向

根据商业运营数据,OtterTune平均能为客户带来显著的性能提升:

  • 对于Amazon RDS,平均提升约35-40%。
  • 对于Amazon Aurora,平均提升可达约50%。

这些提升涵盖了客户选择的不同优化目标(如延迟、吞吐量、成本)。性能提升幅度与客户前期的调优程度有关,从未调优过的数据库通常获益最大。

待解决的问题

尽管已取得成效,但仍有一些开放性问题值得探索:

  • 样本高效的调优技术:如何在数据点有限的情况下更快地收敛到优解。
  • 工作负载合成:如何生成更贴近真实场景的合成工作负载,用于模型训练和测试。
  • 调优启停标准:如何智能判断何时应开始或停止调优,特别是在工作负载发生变化时。

总结

本节课我们一起学习了OtterTune的发展历程。它从一个旨在自动化、通用化的学术研究项目,演变为一个深度融入领域知识、高度重视安全与信任的商业数据库优化服务。商业化的过程揭示了研究假设与工业现实之间的差距,并通过约束参数范围、提供多重保障、扩展服务维度等策略成功应对了挑战。OtterTune的案例表明,将先进的机器学习算法与扎实的领域专业知识相结合,是解决复杂系统优化问题的有效途径。

003:PostgresML的简洁之道

在本节课中,我们将学习PostgresML如何通过将机器学习工作负载深度集成到PostgreSQL数据库中,来构建更高效、可靠和可扩展的系统。我们将探讨其背后的动机、核心架构以及相对于传统微服务架构的优势。


概述

PostgresML是一个深度集成机器学习框架的PostgreSQL扩展。它的核心理念不是通过列式存储或分布式计算来深度改变Postgres内部结构,而是将许多重要的机器学习功能移入数据库。这种方法带来了更高的效率、可靠性和可扩展性。与机器学习领域的许多点解决方案相比,PostgresML基于整个PostgreSQL生态系统和基础构建,因此功能更强大。它是一个开源项目,提供了端到端的开源模型和算法实现,让最终用户对系统有更多控制权。

从简单架构到复杂挑战

上一节我们介绍了PostgresML的基本理念。本节中,我们来看看促使我们思考将机器学习工作负载移入数据库的动机。

一个经典的数据库用例是常见的Web应用架构。应用是无状态的,而数据库负责维护系统中的所有状态并长期持久化。由于这些组件通过互联网连接,系统存在固有的延迟。将状态和无状态分离所引入的延迟,与跨越大陆的网络连接延迟相比非常小。

这种简单的架构非常适合原型设计和最小可行产品。但当考虑系统扩展时,通常会先扩展应用,因为扩展有状态进程很困难。然而,随着应用负载增加,数据库负载也会相应增加。最终,数据库会达到瓶颈。

以下是应对数据库负载增长的常见步骤:

  1. 应用内缓存:开始将更多数据缓存在应用中,以减少数据库查询。
  2. 引入只读副本:对于许多只读、非事务性查询,可以轻松地使用PostgreSQL副本进行处理。
  3. 数据库分片:当单数据库(即使是带副本的)也无法满足需求时,最终需要分片数据库。

数据库分片会带来复杂性,例如应用需要决定数据去向、处理故障转移、管理整个数据库集群等。最终,如果业务成功,你将管理更多的数据库。

微服务架构的得与失

上一节我们看到了随着业务增长,数据库架构如何变得复杂。本节中,我们来看看作为应对方案之一的微服务架构及其带来的新挑战。

在微服务架构中,目标是设计一个永远不需要第二个数据库的服务。通过将应用拆分为越来越小的服务,并在数据库变得太大时将其拆分,来应对扩展问题。然而,这会导致越来越多的网络延迟。

在一个清晰的服务导向架构中,一个完整的Web请求可以由单个服务处理。但最终会出现横切关注点。例如,一个产品搜索系统可能涉及多个机器学习模型和数十个微服务,许多服务之间存在循环依赖和自己的状态管理,最终变得与单体架构有相同的问题。

此外,微服务架构允许每个团队选择最适合自己需求的数据库。这可能导致技术栈的碎片化,增加了运维和问题排查的复杂性。当某个底层存储(如Memcached集群)出现故障时,可能会引发连锁反应,影响整个系统。

机器学习系统的复杂性

上一节我们讨论了微服务架构在数据层带来的复杂性。本节中,我们聚焦于构建和运行机器学习服务时所特有的复杂挑战。

下图展示了构建一个机器学习模型和服务所需的基础设施组件。在每个功能框中,都存在多种竞争性技术选择。数据科学家很可能会做出不同的选择。如果没有一个强大的机器学习平台来解决所有这些问题,每个请求都可能流经一个几乎全新的、不同的技术栈。

这种复杂性会直接导致性能问题。例如,一个搜索请求可能需要依次进行语音识别、同义词检测、查询扩展、初始查询、低库存商品替换查询等步骤,每个步骤都涉及多个模型。当所有微服务都基于Python时,搜索查询的P90响应时间可能高达8秒。这种延迟会导致用户流失和销售损失。

理想的架构演进

上一节我们看到了复杂机器学习系统带来的性能挑战。本节中,我们来看看一种更理想的、可扩展的数据库架构演进路径。

与其陷入微服务和多种数据库的复杂性,不如考虑一种更简洁的架构:使用一个代理(如PgBouncer或PgCat)来管理多个PostgreSQL集群(包括副本和分片)。应用层通过这个代理与数据库交互,代理负责路由、故障恢复等。这样,应用层无需处理复杂的缓存逻辑和分片逻辑,数据库层可以水平扩展。

这种架构的优点是,你不需要从一开始就构建它。可以从简单的单应用单数据库架构开始,随着业务增长,逐步引入代理和分片,而无需让应用变得异常复杂。这种演进方式比管理一堆异构的微服务和数据库要可控得多。

PostgresML的核心思想:推送模型,而非拉取数据

上一节我们探讨了可扩展数据库架构的理想状态。本节中,我们将介绍PostgresML解决机器学习工作负载的核心思想。

在机器学习中,你有两种选择:

  1. 将模型作为无状态服务运行,每次预测时从数据库拉取数据到模型(拉取数据)。
  2. 将模型推送到数据库存储层,在数据所在的位置进行预测(推送模型)。

PostgresML采用第二种方式。这意味着你不再将数据从数据库层拉到应用层,而是直接将数据指针(来自PostgreSQL共享缓冲区)传递给模型,消除了数据移动。你移动的是模型,而不是数据。

从理论上讲,这种方式更优,因为任何好的模型总是小于其训练数据集,也小于用于预测的数据集,并且其更新频率远低于所建模的数据。因此,在PostgresML的处理过程中,涉及的数据传输量(电子移动)要少于微服务架构。

即使在新兴的大语言模型和向量数据库领域,将数据(无论是向量数据还是传统表格数据)与LLM放在同一进程中仍然至关重要。虽然LLM运行较慢,但数据移动仍然是相当大的开销。将LLM加载到数据库中是一次性的数据移动成本,之后可以避免在每次查询时移动大量向量数据。

PostgresML的功能组成

上一节我们介绍了PostgresML“推送模型”的核心思想。本节中,我们具体看看PostgresML提供了哪些功能。

对于经典机器学习,PostgresML主要提供三个核心功能,它们基本上都是用户定义函数(UDF):

  1. 训练模型pgml.train
    • 你可以指定任务类型(如分类、回归)、算法等参数来训练模型。
  2. 部署模型pgml.deploy
    • 战略性地部署训练好的模型,例如指定使用哪个版本来服务预测请求。
  3. 进行预测pgml.predict
    • 利用已部署的模型,基于新数据(来自数据库表或查询参数)进行预测。

对于新式的向量数据库和Transformer模型,PostgresML也提供了相应的功能,例如生成嵌入、进行向量搜索、文本生成等。目前,Transformer相关的功能仍通过Python调用Hugging Face的库实现,而其他核心功能则用Rust编写,以实现高效的零拷贝抽象。

通过这六大类函数,你可以在PostgreSQL内部获得一个非常全面的机器学习工具包,解决大量问题。

PostgresML的内存与并发模型

上一节我们了解了PostgresML提供的功能。本节中,我们深入其内部,看看它如何在PostgreSQL进程内管理内存和处理并发。

PostgreSQL使用共享缓冲区来管理从磁盘到RAM的数据页。PostgresML将模型和特征数据存储在PostgreSQL表中,因此自然地被缓存在共享缓冲区中。

当通过数据库连接调用predictembedtransform等功能时,系统会从共享缓冲区中取出模型权重,并在该连接进程内实例化模型(使用XGBoost、scikit-learn或PyTorch等库)。每个连接都会缓存自己使用的模型副本

这种模型缓存方式与PostgreSQL的连接进程模型配合得很好。因为每个连接都是独立的进程,所以我们可以将模型加载到多个不同的连接中,从而实现并发访问。PgCat这样的连接池工具非常重要,它可以帮助保持连接开放(即使客户端断开),从而保留模型缓存。我们还可以使用PostgreSQL角色来隔离连接、限制特定模型的并发数并实施队列管理。

性能基准

上一节我们探讨了PostgresML的内部工作原理。本节中,我们通过一些基准测试来看看它的实际性能优势。

PostgresML在性能上往往具有“不公平”的优势,因为它避免了网络开销。例如,在嵌入生成方面,它可能比调用OpenAI API快10倍,这主要是因为消除了互联网往返延迟。

值得注意的是,开源模型在许多领域正在赶上甚至超越闭源解决方案。例如,在嵌入模型排行榜上,OpenAI的模型已跌出前列。在文本生成方面,开源模型如Falcon 180B等也构成了强劲竞争。

在向量搜索方面,即使使用查询速度较慢的IVF-Flat索引类型,PostgresML(结合pgvector)也通常比使用独立向量数据库(如Pinecone)的方案更快,因为它消除了两次网络往返延迟。

技术实现与未来展望

上一节我们通过基准测试看到了PostgresML的性能表现。本节中,我们简要了解其技术栈并展望未来的发展方向。

PostgresML使用pgx框架(一个Rust扩展管理框架)进行开发。它利用了许多Rust生态中的库,并正在推动更多机器学习功能在Rust中实现。目前,为了兼容性和提供可靠的参考实现,部分功能(特别是Transformer相关)仍通过Python调用。

未来的主要挑战和发展方向包括:

  1. 列式存储:对于时间序列预测等场景非常重要,需要一个开源实现来集成。
  2. Rust原生LLM:采用Rust实现最新的LLM,以实现完全的内存去重共享,这将是PostgresML 3.0的一个重要里程碑。
  3. 算法覆盖:持续集成机器学习领域不断出现的新算法和模型(如CatBoost),这是一个持续的过程。
  4. GPU与内存共享:在云服务中,实现GPU内存跨连接共享,以降低用户使用LLM的成本。

总结

在本节课中,我们一起学习了PostgresML如何通过将机器学习深度集成到PostgreSQL数据库中,来构建更简洁、高效和可扩展的系统。我们从传统架构的扩展挑战谈起,分析了微服务带来的复杂性,进而引出了“推送模型而非拉取数据”的核心思想。我们详细介绍了PostgresML的功能、内存模型、性能优势以及其技术实现。最后,我们展望了其未来的发展方向。PostgresML代表了一种不同的架构哲学,即在充分利用成熟、强大的数据库生态的基础上,智能地整合先进的计算负载,最终实现“少即是多”的简洁之道。

004:Weaviate 架构深度解析

在本节课中,我们将深入探讨开源向量数据库 Weaviate 的核心架构。我们将从向量数据库的基本概念和动机开始,然后详细解析其内部组件,包括 HNSW 向量索引、产品量化压缩技术以及多租户架构。课程旨在让初学者也能理解这些复杂的技术概念。

概述:为什么需要向量数据库?

传统的关键词搜索系统在处理语义相似性时存在局限。例如,搜索“飞机”可能无法匹配到包含“airplane”或“aeroplane”的文档,尽管它们含义相同。向量数据库通过将文本、图像等数据转换为高维空间中的向量(即“嵌入”),并基于向量间的距离进行相似性搜索,从而克服了这一限制。

这类似于在超市中寻找商品:商品不是按字母顺序排列,而是按类别(如农产品、乳制品)组织,使你能够根据概念相似性快速导航。向量空间也是如此,它将具有相似含义的数据点放置在彼此靠近的位置。

近年来,大型语言模型(如 ChatGPT)的兴起带来了新的应用场景和挑战,例如模型可能产生“幻觉”(提供不正确但看似可信的信息)。解决此问题的一种方法是检索增强生成:首先从可信的知识库(如向量数据库)中检索相关文档,然后将其作为上下文提供给 LLM,从而生成基于事实的答案。Weaviate 在此类架构中扮演着核心的检索角色。

架构总览

Weaviate 的核心架构围绕集合(Collections,旧称 Classes)和分片(Shards)构建。

  • 集合:类似于 SQL 数据库中的表,是用户组织数据的主要逻辑单元(例如,“文章”、“作者”、“事件”集合)。
  • 分片:一个集合可以包含多个分片。分片的目的是将大规模数据集分布到多个节点上,以解决向量搜索对计算和内存的高需求问题。

每个分片内部包含三个主要组件:

  1. 向量索引:默认且最常用的是 HNSW 索引,用于高效执行近似最近邻搜索。
  2. 对象存储:一个键值存储,用于保存与向量关联的原始数据对象(如 JSON 文档、文本块)。这使得 Weaviate 能够端到端地处理搜索请求,无需查询外部存储系统。
  3. 倒排索引:基于 LSM 树构建,并原生支持 Roaring Bitmaps,用于高效执行属性过滤操作(如 AND, OR)。

这种设计使得 Weaviate 不仅是一个向量搜索引擎,也是一个功能齐全的数据库。

HNSW 索引详解

HNSW 代表分层可导航小世界。它是一种基于图的近似最近邻索引,在查询速度和索引构建成本之间做出了权衡:它牺牲了少量精度,换来了极高的查询性能,但构建索引的计算成本相对较高。

图搜索基本原理

HNSW 将数据向量视为图中的节点,并在相似向量之间创建连接(边)。搜索时,算法从一个随机入口点开始,不断评估当前节点的邻居,并移动到距离查询向量更近的邻居节点,直到无法再改进为止。这个过程避免了对图中所有节点进行暴力计算,从而大幅提升搜索效率。

以下是搜索过程的简化步骤:

  1. 从预设的入口点开始。
  2. 评估当前节点的所有邻居节点与查询向量的距离。
  3. 将距离查询向量最近的邻居节点设为新的当前节点。
  4. 记录已访问的节点,避免重复计算。
  5. 重复步骤 2-4,直到无法找到更近的邻居为止。

分层结构

HNSW 的“分层”特性进一步优化了搜索。它构建了多层的图,高层图包含较少的节点和更长的边,用于快速、粗略地定位目标区域;低层图包含更多的节点和更短的边,用于进行精细搜索。查询时,算法从最高层开始,逐层向下导航,类似于跳表索引的搜索过程。

连接修剪

为了保持图的高效性,HNSW 在构建时会进行连接修剪。每个节点有最大连接数限制(参数 M)。当需要添加新连接但连接数已满时,算法会暂时移除所有连接,然后重新连接那些“必不可少”的邻居(即无法通过其他现有节点更高效到达的节点)。这确保了图的稀疏性和导航效率。

参数与数据更新

HNSW 有几个关键参数可调节性能与精度的平衡:

  • ef / efConstruction:控制搜索时或构建时考虑的候选节点数量。增加 ef 能提高召回率(精度),但会降低查询速度。
  • M:每个节点的最大连接数,影响图的密度和搜索路径。

对于数据更新:

  • 插入:HNSW 支持动态插入,新节点通过搜索图找到其位置并连接。
  • 删除:Weaviate 采用“标记删除”加后台图修复的策略。删除时先标记节点,确保查询结果正确;随后在后台逐步重建受影响区域的连接,以避免大规模索引重建。

产品量化:压缩向量

随着嵌入模型维度增长(如 OpenAI 的 text-embedding-ada-002 有 1536 维),存储和内存开销变得巨大。产品量化是一种压缩技术,能显著减少向量占用的内存。

压缩原理

产品量化的核心思想是将高维向量分割成多个子段,并对每个子段进行独立量化。

  1. 聚类:首先,从数据集中采样一部分向量,对每个子段分别运行聚类算法(如 K-Means),得到一组聚类中心(例如 256 个)。
  2. 编码:对于数据集中的每个向量,将其每个子段与对应子段的聚类中心进行比较,并用距离最近的聚类中心的 ID(一个 0-255 的整数,即 1 字节)来代表该子段。
  3. 表示:最终,整个高维向量被表示为一个由这些 ID 组成的短序列(码本)。例如,一个 1536 维的浮点数向量(原始占 6 KB)可能被压缩为仅 256 字节的码本,实现了显著的压缩。

精度与重排序

量化是有损压缩,会引入误差,直接使用压缩向量搜索会降低精度。为了解决这个问题,Weaviate 采用重排序策略:

  1. 使用压缩向量在 HNSW 索引中进行初步搜索,并获取一个较大的候选结果集(例如 top 128)。
  2. 从磁盘加载这 128 个候选向量的原始未压缩版本。
  3. 在这小部分候选集上,使用原始向量重新计算精确距离,并返回最终排名。

这种方法在保持高精度(例如 >95% 召回率)的同时,获得了巨大的内存节省(例如 24 倍压缩),仅付出了少量查询吞吐量的代价。

实践考虑

  • 模型训练:量化模型通常在数据集的一个代表性样本上训练一次即可,对后续的数据分布漂移有一定鲁棒性。
  • 监控:可以通过定期对数据子集进行暴力搜索,比较量化索引的召回率来监控精度是否下降。

总结

本节课我们一起学习了 Weaviate 向量数据库的核心架构。我们从理解向量搜索的动机开始,探讨了其如何解决传统关键词搜索和 LLM 幻觉问题。随后,我们深入剖析了 Weaviate 的三层架构:集合与分片、HNSW 图索引以及产品量化压缩技术。

HNSW 通过构建分层近邻图,实现了快速高效的近似最近邻搜索。产品量化则通过分段聚类和编码,大幅降低了向量的存储开销,并结合重排序技术维持了搜索精度。这些技术共同使 Weaviate 能够处理大规模、高性能的向量检索任务,为构建下一代基于语义理解的智能应用提供了坚实基础。

005:特征存储架构与技术挑战

卡内基梅隆大学的机器学习与数据库系列研讨会正在现场录制。

本项目的资金由谷歌以及像您这样的观众贡献提供。

感谢大家。欢迎来到卡内基梅隆大学数据组的又一次研讨会。今天我们很高兴邀请到Simon Qdar,他是虚拟特征存储公司FeatureForm的联合创始人兼首席执行官。他将为我们详细介绍什么是特征存储,以及如何构建一个可扩展的特征存储。和往常一样,如果您有任何问题,请随时取消静音提问。这将是一场对话,而不是他独自演讲一小时。现在,有请Simon。

谢谢邀请。大家好。

今天我将讨论特征存储。这个概念在MLOps和机器学习基础设施领域被广泛讨论,但“它究竟是什么”以及“如何构建”是根本性问题。我将深入探讨这些问题,分析不同类型的特征存储,并尝试定义特征存储。考虑到听众背景,我会保持技术性,并重点介绍我们遇到并克服的几个技术挑战,特别是FeatureForm的构建方式。它更像一个编排器,而编排器的难点通常不在于解决一个超级难题,而在于处理无数个小问题,如何让所有组件协同工作,并构建一个能适应各种预期用例的架构。

我希望保持互动,会讨论很多不同主题,并在各部分之间留出空间,乐于回答任何问题。我会保持足够的深度以提供信息和背景,同时也会控制深度,让大家可以选择感兴趣的部分深入探讨。

我先简单介绍一下自己。我是Sba,FeatureForm的联合创始人兼CEO。这是我的第二家公司,之前在谷歌工作,背景是软件工程,专注于分布式系统。我的上一家公司构建了一个为约1亿月活用户提供服务的推荐系统,当时构建的ML基础设施后来成为了FeatureForm的基础。

今天的议程如下:

  1. 什么是特征存储?从基础开始。
  2. 三种架构类型。业界解决此问题有三种主要方法,我们将逐一分析,并重点介绍我们采用的方法及其原因。
  3. 深入探讨四个具体技术挑战:流式处理与回填、物化、作业状态与编排、监控与概念漂移。
  4. 最后,鉴于当前领域的关注度,我会简要讨论一下LLMs和RAG,但我知道已经有很多关于向量数据库的讨论,所以这部分会简短,重点放在我们系统的独特之处。

什么是特征存储?

当我说“特征”时,指的并非产品功能,而是模型的输入或信号。例如:用户过去30天最喜欢的歌曲、商店冬季最畅销商品、目录中所有商品的平均价格。这些都是特征的例子。你可以将模型视为一个接收输入信号并生成输出预测的黑盒。

在实践中,数据科学家(作为最终用户)花费大量时间进行特征工程,即不断迭代信号以产生更好的信号来构建更好的模型。他们通常在笔记本中工作,环境通常比较混乱,使用多种基础设施,创建大量数据转换,最终形成这些信号。

这些是常见的工作流程问题,我不会深入探讨,因为它们技术性稍弱,但我想提一下。在构建此类系统时,许多公司过于关注技术细节(如性能),而往往忽略了更高层次的工作流程问题。我们花了很多时间思考什么是正确的API和工作流程,以便让系统对数据科学家无缝工作。

还有一些其他问题可能大家也熟悉。

在深入技术细节之前,最后一个背景点是:实际上,有时需要说服数据科学家同意不应将笔记本直接部署到生产环境,尤其是当整个推荐系统都依赖于某个笔记本时。对于在座各位,这一点可能很清楚:生产环境不应直接部署笔记本。通常,生产环境和离线迭代之间存在一道鸿沟。

许多公司的实际做法是,拿着这些笔记本,几乎从头开始将其重建为生产工作流程。从数据科学家的角度来看,他们提出了所有这些特征,然后会遇到一个巨大的障碍:如何将其投入生产?

将特征投入生产涉及几个部分:

  1. 实验时,通常使用样本、不可扩展的模式(如pandas)、在个人电脑上运行。
  2. 但当要将特征投入生产时,就必须开始处理实际的数据系统:流式数据、批处理数据、按需特征(类似于存储过程)。所有这些需要组合在一起,以极低延迟在生产时构建信号。

我们并不试图构建更好的Redis或Spark,也不认为这是需要解决的问题。我们也不试图让一切都流式化,流处理是这个问题中一个特别困难的部分,稍后会讲到。

最后,我认为“特征存储”这个名称有些用词不当。整个类别以及每个云提供商都有一个“特征存储”。但如果你按照我定义的方式思考“特征”,你可能会认为它只是一个存储特征的地方。实际上,特征存储最终看起来和工作方式(或者说我们的思考方式)更像一个编排器。我们并不认为需要解决的问题是构建一种新型数据库来存储特征。

我们看到并解决的问题,更像是构建一个应用层,置于你的数据基础设施(如Spark计算、S3存储等)之上。该层提供资源的单一事实来源,你可以定义这些资源,方便数据科学家协作,并提供监控、告警和治理功能。

我们需要能够构建每个特征,每个我创建的信号,都必须能同时用于训练和推理,这是一个非常困难的问题。最后,还需要一个良好的声明式API和一些仪表板来理解正在发生的事情并进行监控。

在内部构建FeatureForm时,我们真正想要的是“特征的Terraform”。因此,FeatureForm这个名字的字面意思是“特征的Terraform”。你会看到我们的许多架构选择可能让你联想到Terraform,因为实际上我们所做的非常相似,关键区别在于Terraform启动基础设施,而我们启动的是数据管道。

到目前为止有任何问题吗?这更多是高层概述。看起来没问题。

三种特征存储架构

接下来,我们谈谈业界提出的三种特征存储及其架构。

第一种架构我称之为“字面意义上的特征存储”,因为它就是字面意义上存储特征的地方。如果你使用过特征存储,最可能熟悉这种类型。目前可能使用最广泛的特征存储是开源产品Feast。AWS SageMaker、Vertex、Azure、Databricks等云提供商都有自己的特征存储,它们或多或少都借鉴了Feast,因为Feast是早期特征存储之一,也是最大的开源玩家。

它们采用的方法是让用户构建自己的信号,进行迭代,最后存储在特征存储中。其价值在于所有特征在训练和推理中是统一的。对于推理,你需要以极低延迟提供特征的最新值。例如,为Spotify构建推荐系统时,你可能想知道用户过去30天最喜欢的歌曲类型。因此,你需要维护该值的实时缓存,这就是在线存储的内容。

另一方面是训练部分,有时称为离线存储。这里的难点在于需要维护特征值的历史日志,因为特征值在不断变化,但训练时需要“回退时间”。例如,遍历我的Spotify历史记录,查看我在某个时间点听了红辣椒乐队的某首歌,当时这些特征值是什么。因此,你必须回退时间,构建当时会出现的特征,然后将它们与标签结合起来训练模型。

对于数据库领域的人来说,CDC的概念会很熟悉。实际上,这看起来就像你有一个CDC和一个物化的最新版本。物化版本是在线存储,CDC流是离线存储。对于CDC,你更关注吞吐量和正确性;对于在线存储,你更关注延迟。

这里的问题是迭代。如果你的特征不改变,这种方法效果很好。但实际上,数据科学和机器学习是一个高度迭代的过程。由于特征在这里被视为转换管道产出的工件,而不是与转换紧密绑定,这导致了两侧之间的脱节。

在实践中,如果你看Uber、Airbnb(有Zipline)、Pinterest(有Galax)、Facebook(有FB Learner的部分)等,几乎所有大公司的内部特征存储实际上都更接近我们所说的“物理特征存储”(有些人也称之为特征平台),在那里转换和存储是紧密联系在一起的。

这样做的好处是,当你在迭代转换时,转换与存储深度绑定。因此,与其迭代一个存储的工件,不如在迭代时,该工件会自动更新。它还解决了其他问题,比如流式处理等。

FeatureForm采取的是“虚拟特征存储”架构。“虚拟”一词源于早期与用户交流时,让他们将数据迁移到我们拥有的新平台上是难以逾越的障碍。例如,告诉摩根大通这样的公司,他们需要将所有数据通过我们的物理特征存储进行转换和存储,这很难推销。

我们意识到,实际上大多数公司的基础设施更像数据网格,是跨团队分布的异构基础设施。其次,需要解决的主要问题更多是编排和应用层问题,即在这整套基础设施之上提供一个单一的管理平面,而不是构建一个供应商锁定的系统。因此,我们采用了更像编排器的方法,执行特征存储特有的许多操作,但应用于他们现有的任何基础设施。

这种方法的缺点是,我们需要构建足够通用的接口来工作。无论你使用Kafka、Pub/Sub、Spark、Snowflake还是PostgreSQL,我们都需要能够以统一的方式跨这些系统工作,并保持用户期望的性能水平,至少与不使用FeatureForm时的性能在同一量级。我们必须像一个零成本抽象层。

关于架构,它是可以离线运行的,我们在Kubernetes中原生运行。稍后我会更深入地介绍我们采取的架构。

在深入技术问题之前,我想确保大家对特征存储的生态系统有基本了解。有任何问题吗?如果没有,我们继续。

技术挑战一:流式处理与回填

我们来讨论特征存储公司面临的最具挑战性的问题之一:流式处理与回填。

我们讨论过生产特征。现在深入探讨左上角的流式特征。流式特征有几个独特方面:

  1. 它们通常是预处理的。这意味着有些特征(如用户评论,需要移除脏话)会在推理时处理,我们称之为按需特征,而不是流式特征。流式特征通常是像“用户最喜欢的歌曲”或“最近听过的五首歌”这类。你会有一个数据流,并不断生成特征值。这个值变化很快,以至于按小时或天的批处理无法工作,无法捕捉到该特征的价值(如过去30天的窗口期)。

虽然理论上流式处理是批处理的超集,但实际上并非如此,而且流式处理的工具要复杂得多。

首先,理解“时间点正确性”的概念很重要。时间点正确性的工作原理如下:如果我有一个特征是“用户最近点击的五件商品”,我不希望每次(比如Spotify做推荐时)都去查询历史值并在那个时间点进行查询。这可能会非常慢,特别是如果你在进行某种聚合操作。在实践中,所有这些都会被预处理。

因此,一方面,你需要在推理时维护特征在“现在”这个时间点的值。另一个非常重要的时间点正确性是历史正确性。模型训练的方式是:你有一组标签。例如,一个欺诈交易,标签Y为真(是欺诈交易),用户是用户A。我可能想知道特征X,比如用户过去30天购买商品的平均价值。这个特征值会不断变化,但我需要能够生成该特征在那个时间点会出现的值。这就像是构建一个CDC流,获取特征值在所有变化时间点的变化,并构建这些特征值随时间变化的日志,以便我可以将它们与标签结合起来,构建出训练行,就像它们当时出现的那样。从训练的角度看,你希望模型在训练的前向传递中几乎意识不到它是否正在被训练,因此你需要能够提供特征,就像它们在生产中会出现的那样,以正确进行训练。

历史上,人们会构建两个独立的管道,这很容易出错。例如,他们用Spark构建一个批处理管道来生成这些特征,然后交给ML工程师,由后者将这个批处理作业转换为流式作业。这会产生很多问题。例如,作业可能有一个7天的聚合窗口,但如果你使用Kafka,可能没有这么长的保留期,因此即使在重写特征后,也必须等待至少N天才能有足够的数据开始输出特征。其次,你训练模型的数据集在时间上是冻结的,而新生成的值在不断变化。维护这种正确性、进行监控会带来一系列问题。

总的来说,理想情况(也是特征存储的承诺之一)是统一流式和批处理管道。这样,作为数据科学家,我构建一个特征时,就知道它是可部署的。当我在此框架中定义我的特征时,它将适用于批处理,并会随着新数据的到来自动保持更新。

最后需要补充的是,在实践中,你可能会不断迭代特征,可能在几周内提出10到15种不同类型的特征。在这段时间里,你需要能够测试所有这些特征。几年前Twitter几乎公布了他们的做法:他们会提出15个特征,部署它们(或发送给工程团队部署),然后等待大约60天才能收到训练集,从而训练模型。这就是迭代周期。如果他们不断尝试新东西,60天后才能得到60天前尝试的实验结果。这极大地影响了迭代能力。

那么,我们如何解决这个问题?让我们深入技术细节。像分布式系统中的大多数事情一样,解决方案是日志。原因是日志有一个有趣的特性:如果你有一个日志(可以把它想象成一个栈,有第一个事件,所有东西都有时间戳,它是仅追加操作,历史值不可变,像一个分类账),酷的地方在于你永远无法更改历史值。这意味着你可以在某个时间点冻结日志。如果你只从顶部读取,忽略前面的部分,它看起来就像一个流。因此,它几乎可以同时作为批处理数据集和流来处理,取决于你如何看待它。这就是诀窍所在。听起来简单,但实际如何操作是一个更难的问题。

其思想大致是:当我生成一个新特征(尤其是有状态的特征)时,我会运行一个批处理作业,冻结日志,处理整个日志,然后停止批处理作业,保存状态,从最后一个事件开始运行流式作业,并继续更新我的推理缓存,同时维护一个CDC,记录所有生成的特征值。

历史上,当我构建特征存储时(当时我有拥有基础设施的奢侈条件,这在FeatureForm没有),我们实际上使用了Pulsar。原因是Pulsar有一个巧妙的特点:它将长期存储与消息代理分离。简而言之,如果你设置无限保留期,它会构建我描述的那种日志。随着时间的推移,它会将历史值切分成段,并卸载到S3、GCS或HDFS等存储中。这意味着从Flink作业的角度看,它看起来就像一个具有无限保留期的完美流。

据我了解,Confluent Kafka(Kafka的商业版)现在有无限保留期,但开源Kafka没有。原因在于,理论上你可以将参数设置得足够长,使其几乎是无限的,但实际上Kafka的架构使得消息与代理在同一节点上。由于数据流大小无限增长,即使没有那么多事件通过流,你最终也需要创建大量节点。因此,在实践中,你会反向工程Pulsar原生功能来实现这一点。

诀窍大致是将流视为日志,维护无限保留期,以可以在批处理和流式处理中运行的方式定义转换。如果你这样做,那么你可以冻结日志,运行批处理作业。在Pulsar中,我会获取最后看到的消息ID,然后运行一个大型作业(因为历史数据通常比实时流入的数据多得多,我不希望在进行流式处理时运行庞大的批处理集群)。我会处理日志的冻结部分,然后进行到流式处理的交接。在实际操作中,当我在进行批处理作业时,新事件会进入日志但未被处理。我会维护一个消息ID,将其传递给我的流式作业,流式作业会回溯时间找到那个事件,并从那里开始流式处理。

使用Kafka时,这一点更明显,因为我们不能假装Kafka在构建无限日志。我们采取的做法是:当事件进入Kafka时,我们将它们写入S3,这样我们就有了事件的历史日志。同时,我们使用Flink(也可以是Spark)处理所有传入事件。这是稳态,即特征已经创建,我只需要在新事件到来时保持其更新。

现在,假设作为数据科学家,我有一个新特征的想法:我想知道用户收听歌曲的平均每分钟节拍数。现在,我必须运行一个大型Spark作业,遍历所有用户的所有事件来生成这个特征值,以及在不同时间点的特征值。然后,如前所述,我会进行交接,协调到流式作业,因为如果我将此特征部署到生产环境,我需要在在线存储中维护该特征值。

特征存储问题存在且如此困难的原因,很大程度上源于训练与推理的这种双峰性质,以及对历史特征值的要求。理论上,如果Spark原生能够实现流批统一,让我构建一个作业并在两种模式下运行,那么所有问题都将迎刃而解。虽然这是一个已知的开放性问题,但至今仍未解决。像Apache Beam的“可重启Do函数”等方法试图解决,但最好的方法(也是大多数公司采用的方法)就是构建类似我们这样的方案,这更像是一种变通方案。

问题:在大型部署中,特征存储的规模有多大?它们更像OLTP系统(数据量相对较小,更新和查询频繁),还是规模巨大?

回答:规模可以非常大。回顾架构,推理存储是OLTP系统,而离线存储是OLAP系统。但在实践中,我需要在这两个系统之间保持一致性,并确保可以定义一个同时用于离线训练(本质上是异步长时作业)和在线生产系统(事务性)的转换。所以它兼具两者。批处理训练作业主要在OLAP风格的上下文中运行,处理海量数据集。而流式作业,在实践中,看起来更像典型的生产级事务系统。

追问:在大银行,比如重要的欺诈检测场景中,特征存储是TB级还是GB级?
回答:大于TB级。

追问:因为需要保留大量历史版本以记录过去可能使用的推理机制,数据规模因此增大?
回答:是的。在实践中,这种规模的公司通常会进行采样。他们可能处理所有数据,但不会全部存入存储,或者设置硬性截止窗口(例如,从不训练超过两年的数据),这类似于TTL。即使他们处理了所有数据,实际的训练步骤通常比生成特征本身昂贵得多。因此,即使生成了两年的特征,也可能进行智能采样或其他数据整理,只训练其中可能对模型产生影响的5%的数据。

问题:特征的定义是什么?它可以简单到是一个度量指标,也可以复杂到生成它的整个代码管道。如果是用Python生成的,那还包括了所有依赖库。特征的定义范围有多广?我猜不同的人有不同的理解,从仅仅是一个值,到连接该值的可复现代码实体。

回答:这是一个非常好的问题。我认为这实际上是我们与其他特征存储思考方式的根本区别。其他特征存储将特征视为一个工件,是最终的数据行。每个特征不只是一行,而是历史上每个特征值的CDC记录。我不这么看。我认为更好的抽象是将其定义为一个管道。这里存在一个开放性问题:管道从哪里开始?是从客户端的原始数据流开始,还是从更像数据市场的东西开始?实际上,这取决于公司。例如,LinkedIn有非常干净、完美的数据集供分析和BI团队使用,但专注于ML的数据科学家并不使用这些,他们更倾向于从原始流开始,以便能够创建任何他们想要的特征。因为实际上,指标分析更简单(例如,收入指标,你希望只有一个正确的定义),但ML特征和信号是达到目的的手段,它们不是供人消费的,而是供模型消费的。一个对人类来说很奇怪的特征(例如,只考虑年支付超过1000美元且位于美国、欧洲或印度的用户的收入),对模型来说可能是使其表现良好的正确信号。

你提到的另一点(我未深入探讨但确实是一个核心问题)是:我曾与一家银行交流,问他们生产中有多少特征,答案是数万个。我问这些特征是否都在积极使用,他们回答不知道,但不知道可以关闭哪些,也不想测试关闭哪个会出问题。实际上,如果他们想关闭,会逐一关闭看谁抱怨。这里显然有价值的是血缘关系。但因为我们工作在更高的抽象层次,我们可以决定创建什么、何时创建,决定什么值得缓存和物化,什么值得丢弃。在实践中,我们希望所有转换或多或少是纯函数,这意味着从原始数据开始,FeatureForm中的一切都应该是完全可复现的。这样想,有几件事成为可能:一是我们可以随时关闭或重新启用任何特征;二是对于像“用户过去7天/30天/90天最常做的事”这类特征,如果我们知道只有有限个窗口大小,我们可以非常智能地同时处理多个特征。所有这些都考虑在内。如果我们拥有基础设施,我们可以在这方面做得更多,但我们必须与所有供应商合作,满足他们非常不同、多样的需求。

技术挑战二:物化

接下来讨论另外两个部分,我决定将它们合并。我将讨论这个问题的另一面:到目前为止,我讨论的都是流式处理。对于流式处理,你不断用来自流的新数据填充在线存储(推理存储),同时维护离线存储中的历史特征值日志。

进行批处理时则有些不同。在批处理中,你可能按计划运行。通常发生的情况是,你首先以OLAP风格将表构建到离线存储中,然后希望将其物化到在线存储中。这个物化问题实际上是一个让许多特征存储头疼的难题。

我们之所以觉得困难,特别是如果你不拥有两者(比如虚拟特征存储),我们无法决定用户使用什么离线存储或在线存储。有些离线存储很容易写入某些在线存储,有些则不然。例如,Snowflake(离线存储)到Redis(在线存储)是非常常见的组合(可能仅次于Spark/Databricks到Redis)。没有原生方法将数据从Snowflake复制到Redis。

因此,我们必须填补这个空白。实际上,FeatureForm的核心编排问题之一就是处理许多这类小问题。这些问题累积起来非常烦人。这就是我说的编排系统问题:它不像一个具体的硬技术难题(最接近的可能是流式处理问题),而更多是解决这些缺失的、人们必须自己管理的部分,最终导致一堆疯狂的脚本。

问题:物化具体是什么步骤?是一堆Upsert操作吗?还是像COPY INTO这样的操作?物化看起来像什么?
回答:实际要解决的问题是:我有一个包含一段时间内所有特征值的表。我希望每个用户或实体的每个特征的最新值都在在线存储中,以便我可以进行查找。例如,“Sba的这个特征当前值是什么?”这样,在进行推荐时,我就有了一个预计算特征的缓存可以使用。

不同的公司和产品解决方式不同。Feast的做法是进行一种非常简单的全量复制覆盖。我们的做法是,因为网络数据传输时间通常是最昂贵的操作,而离线存储中的数据处理反而不是主要开销。我们实际上维护上一次快照。我们在离线存储中构建一个视图,表示在线存储中应该有什么。第一次我们逐行复制过去。第二次,我们维护历史值,创建新值,然后计算差异,只复制差异部分,以最小化变更使在线存储达到新状态。

追问:这个差异计算是在服务器端、在线存储上,还是在你的Kubernetes系统中完成的?
回答:我们实际上在Snowflake或Databricks等离线存储中进行计算。然后,在K8s作业中,我们只进行复制。这是一个令人尴尬的并行问题,因为只是插入操作,我可以将其分解成多个作业并行复制。

以这种方式使用Kubernetes是我们的独特之处。我们不仅是Kubernetes原生,而且真正将Kubernetes用作分布式系统的操作系统,可以生成作业,充分利用用户提供给我们的任何基础设施(无论是本地、谷歌云还是混合环境)。

技术挑战三:作业状态与编排

接下来,更广泛地谈谈FeatureForm的架构。它包含许多不同组件:在线服务、离线服务、基础设施提供者、工作节点Pod和作业、协调器、元数据等。这张图甚至已经过时了,自那以后又添加了一些组件。

但核心架构(可以说借鉴了一点Terraform的方式)是:我们将元数据视为事实来源。当你在FeatureForm中定义事物时,你是在创建期望状态。FeatureForm保存该期望状态,所有状态都存在于ETCD中(或者说状态是有状态的,但实践中所有重要状态都可以存储在ETCD中,备份ETCD就相当于备份了FeatureForm集群)。

其架构工作原理是:我们创建这个作为事实来源的元数据,协调器的任务就是获取这个事实来源,查看基础设施提供者上实际存在什么,并尝试使它们同步。如果无法同步(例如作业失败),则通知用户,设置监控和告警。

我认为分布式系统有两种常见方法:一种是将一切视为日志(在数据系统中,日志通常是许多问题的答案);另一种是“不变性是朋友”。因此,创建这种不可变的状态(虽然你可以改变它,但你是从一个不可变状态转移到另一个),使得事情变得简单得多,并且天生具有幂等性。你会看到我在构建系统时不断重复使用这两种技术和策略。

技术挑战四:监控与概念漂移

最后要讨论的是监控概念漂移。概念漂移是指:当我训练特征时,其分布可能看起来像蓝色分布;但当进行推理时,分布可能看起来非常不同。这个概念称为特征漂移。

实际上,维护这种视图,再次体现了将分析型离线存储内容与生产级事务型内容相结合的需求。我们需要事务性地维护“这是当前的事物分布”,同时拥有在更大数据集上的历史视图“这是我们训练时的分布”。然后,我们有一套启发式方法(我没有时间详细介绍所有方法,包括何时使用何种方法)。但我想说,这可能是用户找我们解决的第四个真正困难的技术问题。我们必须自己解决,即维护、构建并提出正确的启发式方法。

对于文本数据,我们实际上构建了自己的模型,使用嵌入等技术来检测漂移。

问题:既然都是启发式方法,你如何确定正确的启发式方法?如何以能够提前发现问题的方式实现?
回答:这确实都是启发式方法。Twitter几年前有一个故事:他们某个季度的收入未达预期,原因是某个特征值设置错误,导致推荐系统质量下降(具体数字我记不清了,但相对较小),最终使他们错过了收入目标。他们不得不在公开财报中说明,因为模型中的特征错误导致收入未达标。这说明了跟踪你所做的事情及其重要性。你会惊讶地发现,从“我们达到收入目标是因为特征存储运行良好”到最终结果之间,往往存在直接的关联。

我还有一些内容,但时间有点超了。我在此暂停,如果还有最后的问题或需要深入探讨的地方,可以提问。

问题:随着更广泛的基础设施试图将许多特征存储功能整合到他们的产品中(每个人都试图提供流式平台、OLAP平台和数据科学平台),独立虚拟特征存储的价值主张是什么?
回答:很好的问题。我认为需要解决两类问题。我在本次演讲中重点讨论了我称之为“处理问题”的部分,这是工程师喜欢解决的问题。还有另一类问题(因为大家可能会觉得无聊,所以我没有讨论),即如何实现治理、正确的工作流程是什么、如何让数据科学家团队协作、版本控制的正确方式等。这些更像是工作流程问题,几乎是API设计问题。

我的一个导师(曾管理过一些非常大的数据库公司)说过,每个数据公司都解决两类问题之一:要么解决困难的技术问题,要么解决工作流程问题。解决困难技术问题的例子是Snowflake,其API大致上已存在很长时间,但他们 arguably 做得比所有人都好。另一类问题是像Terraform和HashiCorp这样的。AWS在技术上可以构建HashiCorp,但他们似乎无法构建强大的Snowflake竞争对手。API设计问题一是极其困难,二是无法靠堆人力解决。五个人的团队可能比一百人的团队设计出更好的API。

我们解决的问题实际上从根本上说是API设计问题和工作流程问题,远多于基础设施问题。因此,我们更有资格解决这个问题,甚至比Databricks更有资格,因为这是我们唯一思考的问题。其次,这类问题通常由拥有标准化工作流程并围绕其构建的开源供应商解决。顺便提一下,FeatureForm是一个开源产品。

问题:你提到了通过特征存储提供数据质量和管理层。你提到了Terraform式的期望状态和漂移监控。状态是如何定义的?你提到了元数据层和期望状态,它在你的特征存储中是如何工作的?
回答:我没有深入这一点,但我可以快速展示一些代码。在实践中,你实际上是构建转换。在这些转换中,你进行定义。例如,在这个例子中,它看起来像批处理的PySpark代码,但你所做的只是用FeatureForm的装饰器来装饰你的Python转换。因此,FeatureForm并不是试图构建一种新的转换语言,我们尽量减少这方面的工作,以便你可以继续使用你喜欢的工具。我们做的是在其之上提供一个框架。所以答案是:用Python定义,最后你调用featureform apply(类似于terraform apply),它就会构建这些东西。你可以与这些数据框交互。如果想了解更多,可以查看我们的开源产品。

问题:关于你提到的更高抽象层次,以及从Snowflake等处进行数据摄取并在其上执行转换,我想了解整体情况:这些转换是每次进行分析时执行吗?具体是如何工作的?
回答:对于流式处理,转换会持续执行。批处理部分执行一次(即回填运行),然后流式处理持续进行。在批处理情况下,你通常按计划运行某些作业。根据原始数据源,有时你希望每次按计划从头运行整个作业。在API中,有一种定义“增量更新”的方式,例如在SQL中添加WHERE timestamp > last_run_timestamp子句。如果你的转换易于定义为增量式,那么你只需在计划运行时处理新增部分。如果无法做到,那么在实践中,你必须重新运行所有数据。这实际上是dbt最近受到一些批评的原因:如果使用不当,可能会产生巨大的Snowflake账单,因为你几乎总是以最昂贵的方式重新运行所有东西。

追问:人们通常有什么样的性能目标?你们是如何实现的?
回答:人们经常问我们FeatureForm的性能如何。我们的目标是成为零成本抽象。在实践中,我们接近这个目标。在服务时我们确实有一些开销,但我们的目标是提供一个框架,最终映射到与你自行运行非常相似的东西,在某些情况下甚至更优化,因为我们了解你最终要做什么(训练或推理)。实际上,大部分繁重工作都已卸载,你所做的主要是元数据和编排,这在实践中并不是计算密集型问题,而更多是状态/元数据密集型问题。

问题:你拥有独特的视角,了解人们使用的基础设施。你列出了许多不同的框架。就Kafka与Pulsar而言,你看到哪个更多?
回答:Kafka多得多。我认为曾经有一段时间Pulsar势头正劲,问题是Kafka能否在功能上赶上Pulsar。我确实认为Pulsar是更好的系统。但问题是,Pulsar能否证明有足够多的团队愿意迁移。我认为这个问题已经有了答案,Kafka已经胜出。在实践中,使Pulsar如此优秀的特性正被反向移植到Kafka中。

追问:那么Flink与Spark呢?你看到哪个更多?
回答:Spark多得多。我认为Databricks在扩展Spark影响力方面做得非常出色。Flink缺乏强大的背后公司支持(虽然可能有些公司在这里演讲过)。曾经有Ververica(被阿里巴巴收购),现在还有Decodable等几家公司。Confluent最近收购了新的Flink公司(我忘了名字)。感觉Flink或Pulsar一直没有像Confluent或Databricks那样的同等体量公司。因此,大公司很难在这方面做出承诺。但我确实看到很多Flink的使用。有趣的是,中国和美国的生态系统也相当不同,中国使用Flink的比例远高于美国。我不确定原因,但阿里巴巴收购了Data Artisans可能是一个因素,但我想知道这是否早于那次收购。

(注:Confluent在2023年1月收购了Flink初创公司Immerok)。

006:深入解析 FeatureBase

在本节课中,我们将学习 FeatureBase 的核心概念,这是一个以位图作为主要存储机制的数据库。我们将探讨其架构、数据存储方式、查询优势以及如何简化数据工作流。


数据库架构概览

FeatureBase 是一个用 Go 语言编写的分布式数据库。其共识机制基于 Raft,数据在节点间分片存储,并支持设置复制因子以实现高可用性。

从内部来看,一个节点主要分为三层:

  • 语言层:提供 SQL 接口,方便用户使用熟悉的查询语言。
  • 计算层:负责执行查询计划。
  • 存储层:包含两种主要的数据存储机制。

核心存储机制:位图与 T 存储

上一节我们介绍了 FeatureBase 的整体架构,本节中我们来看看其核心的存储机制。FeatureBase 主要使用位图进行数据存储,但并非唯一方式。

位图存储(B 存储)

位图擅长编码关系或集合成员信息。在 FeatureBase 中,数据被建模为集合。

示例:假设有一个记录用户访问网页的表。我们可以将“访问的页面”建模为一个字符串集合。每个不同的页面(如“首页”、“定价”)对应一个位列表(bit list),而每个用户则对应位列表中的一个偏移量(offset)。如果用户访问了某个页面,则在该页面对应的位列表中,该用户的偏移量位置被设置为 1。

为了高效存储和计算这些稀疏的位列表,FeatureBase 使用了 Roaring Bitmaps 技术。它将位图数据存储在 B+ 树的叶子节点中,键由位图偏移量的高位比特和其他信息组成。

T 存储

除了位图存储,FeatureBase 还提供传统的行式存储,称为 T 存储。它类似于 PostgreSQL 的存储引擎,将数据以行(tuples)的形式存储在页面中。

数据类型与存储选择

并非所有数据类型都适合用位图存储。以下是 FeatureBase 支持的主要数据类型及其存储倾向:

  • 适合位图存储的类型
    • 整数:以位切片形式存储。例如,一个 64 位整数会被存储为 64 个位列表,每个列表代表一个比特位。这使得范围查询非常高效。
    • 集合:包括 IDSET(数字集合)和 STRINGSET(字符串集合,需字典编码)。
    • 互斥集合:同一时刻只能有一个值的集合。
  • 适合 T 存储的类型
    • 高基数字符串(如用户邮箱)。
    • 浮点数。
    • 向量。

在创建表时,用户可以为每列指定使用 BSTOR(位图存储)还是 TSTOR。查询优化器也会根据查询模式(例如,高基数的 GROUP BY)给出存储建议。


位图存储的查询优势与挑战

了解了数据如何存储后,我们来看看位图存储为查询带来了哪些优势,又面临哪些挑战。

查询优势

位图存储的核心优势在于能极快地处理带有多重过滤条件的查询。

公式结果位图 = 过滤条件1的位图 AND/OR 过滤条件2的位图 ...

  1. 快速范围查询:对于位切片存储的整数(如年龄),查询 WHERE age BETWEEN 18 AND 35 只需读取和计算涉及到的几个比特位对应的位图,数据读取量极小。
  2. 快速过滤组合:多个过滤条件的交集或并集,可以通过位图的按位与(AND)按位或(OR) 操作完成。这些操作可以高度向量化,执行速度极快。

面临的挑战

位图存储并非万能,在某些场景下会面临性能瓶颈:

  1. 点查与重构:查询 SELECT DISTINCT email 或需要返回完整原始值的场景,需要从位图中逐行重构数据,效率较低。这正是 T 存储的用武之地。
  2. 更新开销:更新一个位切片存储的值(如将年龄加 1),可能需要修改多个位图页面,写放大效应明显。FeatureBase 通过批量、预排序的写入来缓解此问题。
  3. 高基数分组:对高基数列进行 GROUP BY 可能导致计算量激增。优化器会尝试先进行过滤以减少数据量,或在必要时建议使用 T 存储。

简化数据工作流:易于使用

FeatureBase 的设计哲学是让数据的流入、处理和流出都尽可能简单。

轻松的数据摄入

FeatureBase 提供了强大的 BULK INSERT 语句,允许用户声明式地将数据从源格式(如 CSV)映射并转换后插入目标表。它支持复杂的执行图,并可分布式执行。

示例:以下 SQL 语句可以从 CSV 读取文本,生成 UUID,并调用 OpenAI 接口生成向量嵌入后入库。

BULK INSERT INTO my_table (uuid, text, vector)
MAP ...
TRANSFORM ...
FROM ...
WITH BATCH_SIZE = 50;

此外,还可以通过管道外部表功能,建立与 Kafka、Redshift 等数据源的持续同步链路,所有操作都通过 SQL 完成。

将计算带至数据

为了应对 ML/AI 工作负载,FeatureBase 致力于将计算推近数据,避免昂贵的数据移动。

  1. 库内模型训练:用户可以直接使用 SQL 语句,基于数据库内的数据训练模型(如线性回归)。WHERE 子句用于快速筛选训练集,训练结果可存入系统表。
    CREATE MODEL my_model
    TRAIN (SELECT * FROM crime_data WHERE ...)
    WITH ALGORITHM = linear_regression;
    
  2. 库内推理:支持通过用户定义函数调用外部推理引擎(如 Python 脚本),或直接集成原生推理库。这使得特征工程、模型应用等步骤可以在数据库内高效完成。
  3. 高级应用:例如在 RAG 场景中,将文档块的关键词提取为字符串集并存入位图,后续的相似性搜索可以通过高效的集合运算(如 Tanimoto 系数)完成,无需向量计算。

总结

本节课中我们一起深入学习了 FeatureBase 位图数据库。

  • 我们首先了解了其分布式架构和 B 存储(位图)、T 存储(行存)混合的存储设计。
  • 接着,探讨了位图存储如何通过集合编码和位切片技术,在多重过滤、范围查询等场景下实现极高性能,同时也分析了其在点查、更新和高基数分组方面的挑战。
  • 然后,我们看到了 FeatureBase 如何通过增强的 SQL 接口、BULK INSERT、管道等功能,极大简化了数据摄入和转换的复杂度。
  • 最后,我们学习了 FeatureBase 如何践行“将计算带至数据”的理念,支持库内的模型训练与推理,为 ML/AI 工作流提供高效支持。

FeatureBase 的核心价值在于为特定的数据模式(宽表、多筛选条件、实时更新查询)提供了与众不同的高性能解决方案。

007:现代列式数据格式

在本节课中,我们将学习一种名为Lance的新型列式数据格式。Lance旨在解决现代机器学习工作负载,特别是处理非结构化数据(如图像、音频、向量嵌入)时,传统格式(如Parquet)所面临的挑战。我们将探讨其设计原理、性能优势以及如何简化机器学习的数据管理流程。


概述

Lance是一个为机器学习时代设计的列式数据格式和表格式。它支持快速扫描和随机访问,尤其擅长处理包含大型二进制对象(如图像、点云)和向量嵌入的数据集。Lance还集成了版本控制、事务和二级索引(如向量搜索索引)等功能,使其成为一个统一的数据管理解决方案。


设计动机与挑战

上一节我们介绍了Lance的概览,本节中我们来看看促使Lance诞生的具体挑战。

随着非结构化数据的普及,数据系统面临新的压力:

  • 数据规模巨大:一张图片可能高达数百KB,远超传统的浮点数。即使行数不多,数据集也容易达到PB级。
  • 数据类型复杂:除了传统表格数据,现在还有向量嵌入、图像、音频、视频、点云等。传统数据库和格式对此缺乏原生支持。
  • 工作负载复杂
    • 机器学习探索与调试:需要基于元数据过滤后,快速随机访问少量特定样本(如图像)。
    • 模型训练:需要高效的全局随机打乱(shuffle)数据。
    • 可复现性:需要强大的数据版本管理和追踪能力,以关联模型检查点与数据状态。
  • 现有方案不足
    • Parquet/ORC:随机访问性能差,尤其不利于读取大型二进制对象。
    • TFRecord等:为训练优化,但缺乏灵活的分析和过滤能力。
    • 向量数据库:许多更像是索引的包装,缺乏完整的数据管理能力(如存储原始数据),且计算与存储未分离,导致扩展成本高昂。

Lance的核心设计

上一节我们了解了现有格式的局限性,本节中我们来看看Lance是如何从底层设计来解决这些问题的。

Lance的设计遵循几个核心原则:不存储比Parquet/ORC更多的数据、提供常量时间的单行查找、将元数据开销分摊到多次查询中,并针对现代存储硬件(如NVMe SSD、对象存储)进行优化。

编码方案

以下是Lance支持的主要编码方式:

  1. 平面编码

    • 适用类型:定长数据类型(数值、向量、张量)。
    • 原理:数据连续存储,通过偏移量计算实现直接访问。支持不同的张量内存布局,便于直接送入GPU处理。
    • 公式/代码表示offset = row_index * element_size
  2. 二进制编码

    • 适用类型:变长数据(字符串、字节、图像、点云)。
    • 与Parquet的关键区别:Parquet将偏移量和数据交错存储,读取单行需要解码整个行组。Lance将偏移量和数据分别集中存储,先读取整个块的偏移量数组到内存,即可实现任意行的常量时间定位和读取。
    • 优势:这是Lance在随机访问大型二进制对象时比Parquet快几个数量级的主要原因。
  3. 高级编码(路线图)

    • 游程编码:针对重复数据压缩,同时支持快速查找。
    • 可变编码:每个数据块可使用不同的编码方式。

设计权衡:二进制编码的布局使得压缩(如Snappy)更困难。但对于以已压缩图像为主的数据集,总体积与压缩Parquet相差不大。Lance通过专注于大对象场景和规划高级编码来减少此差距。


I/O与执行优化

上一节我们探讨了数据在磁盘上的布局,本节中我们来看看Lance如何优化数据的读取过程。

针对大对象和现代存储,Lance的I/O执行层做了两项关键优化:

  1. 延迟物化

    • 问题:传统OLAP计划会先扫描所有列(包括巨大的Lidar点云列),然后过滤,导致读取过量数据。
    • Lance方案:仅先扫描谓词列(过滤条件),进行过滤和排序后,得到最终需要的行ID,再根据这些ID去随机读取投影列(如点云数据)。
    • 前提:这要求底层数据格式支持高效的随机访问,而这正是Lance的优势。
  2. 扁平化I/O树

    • 原理:利用现代NVMe SSD的深度队列和对象存储的高并行能力,尽可能减少I/O操作间的依赖,并发起大量并行I/O请求。
    • 效果:显著提升向量搜索等操作的端到端性能。

表格式与版本控制

上一节我们关注于单文件内的优化,本节中我们来看看Lance如何管理由多个文件组成的数据集。

Lance不仅是一个文件格式(.lance文件),也是一个表格式(Lance Dataset),它组织多个文件并提供数据管理功能。

一个Lance数据集的目录结构如下:

  • data/:子目录,存储实际的.lance数据文件。
  • _latest.manifest:指向最新版本清单文件的指针。
  • _versions/:目录,存储所有历史版本的清单文件。
  • _deletions/:目录,存储软删除信息。

核心特性

  • 版本化与时间旅行:每次数据更新(追加、覆盖、模式演变)都会生成新版本,旧版本数据完好无损。可以轻松查询历史某个时间点的数据状态,这对机器学习实验和回滚至关重要。
  • 模式演变:可以添加或删除列,而无需复制原有数据。新版本的清单会指向新文件和老文件。
  • 事务性:通过清单文件保证数据版本的一致性。


二级索引集成

上一节我们介绍了数据管理的基础,本节中我们来看看Lance如何通过集成索引来加速查询。

Lance将二级索引作为其表格式的一等公民,与数据存储紧密结合。

  1. 向量搜索索引

    • 设计:索引(如IVF、PQ)中存储指向数据集中行ID的指针。查询时,先搜索索引得到相似向量的ID,再根据ID快速读取对应的原始数据(如图片、文本)。
    • 磁盘支持:Lance的向量索引是磁盘优化的,而非完全内存驻留。通过精心设计的数据布局和并行I/O,能在磁盘上实现高速搜索,从而支持超大规模(十亿级)向量集的低成本存储与检索。
    • 代码示例(LanceDB向量搜索)
      import lancedb
      import numpy as np
      db = lancedb.connect("./data")
      table = db.open_table("my_vectors") # 假设有10亿向量
      query_vector = np.random.randn(1536)
      results = table.search(query_vector).limit(10).to_list()
      # 可在毫秒级返回结果
      
  2. 扩展性:框架可扩展至标量索引(加速属性过滤)、全文搜索索引等,实现多召回器统一存储与查询。


路线图与总结

本节课我们一起学习了Lance格式的设计动机、核心编码方案、I/O优化、表格式特性以及集成的二级索引。

当前路线图包括

  1. 完整的统计信息与谓词下推,提升过滤性能。
  2. 对平面编码的NULL值支持。
  3. 实现游程编码、可变编码等高级编码。
  4. 构建标量索引。
  5. 更深的生态系统集成(如Spark原生数据源)。

总结:Lance旨在成为管理多模态机器学习数据的现代基础格式。它通过重新设计数据布局、优化I/O执行、集成版本控制和索引,解决了Parquet在随机访问和大对象处理上的不足,以及向量数据库在数据管理上的缺陷,为AI数据栈提供了一个统一、高效且开源的基础层。


(注:教程根据提供的演讲内容整理,已去除语气词,并按照要求结构化。部分技术细节和问答环节因连贯性和简洁性考虑进行了归纳或省略,但保留了所有核心观点和信息。)

008:自主性能优化

卡内基梅隆大学的“机器学习与数据库”系列研讨会面向现场观众录制。

本项目的资金由谷歌以及像您这样的观众捐款支持。

感谢大家。欢迎各位参加卡内基梅隆大学数据研讨会系列的另一场讲座。今天我们很高兴邀请到Stefano Cereda。他是一位高级数据科学家,就职于Akamas,这是一个自动化性能优化平台。他在米兰理工学院的博士论文就是基于Akamas的研究,后来Akamas发展成了一家初创公司。和往常一样,如果大家在Stefano演讲过程中有任何问题,请自行取消静音并发言。随时都可以提问,这样他就不会在Zoom上独自讲一个小时了。那么,Stefano,现在交给你了。非常感谢你今天来到这里。我还要补充一下,你现在在意大利,对吗?就在米兰附近。你那边比我们早五个小时,所以现在是晚上9点。非常感谢你为我们熬夜。

谢谢,谢谢邀请我今天来到这里,也谢谢非常友好的介绍。

我将向大家介绍Akamas,这是一个用于自主性能优化的软件。

通过本次演讲,我们将首先了解配置调优的含义,即什么是配置,为什么需要调优配置,以及自动调优意味着什么。

然后,我们将探索Akamas的功能、工作原理,以及它与其他自动化调优解决方案的区别。

接下来,我们将使用MySQL进行一些实际示例,以展示Akamas的能力。

那么,让我们从了解配置开始。我们知道现代IT系统具有复杂且多层的结构。

例如,我们可以看看使Cassandra数据库管理系统工作的各个层次。

这意味着它在Java虚拟机(JVM)内部运行。

JVM又可以在Kubernetes或本地容器内执行,或者可以跳过容器,直接在操作系统(Linux或Windows)之上运行。

在最底层,是物理层。这可以是运行数据库的物理机,也可以是虚拟机。如果是虚拟机,显然还会有另一层物理机。

现在,每一层都有一组可调优的配置参数。

这些是您可以在特定软件中设置的选项,它们控制着该软件的行为。

通过控制行为,它们有能力塑造整个系统(即您的数据库)的性能。

例如,我们可以从机器层开始看,这是IT基础设施的基石。因此,我们在这个层面所做的选择确实对数据库性能有着深远的影响。

我们可以选择不同的硬件,或者选择不同的云实例。基本思路是,如果您选择不同的物理磁盘,就会有不同的I/O性能,这将导致数据库运行得更快或更慢,这是一个非常简单的例子。

向上看,我们有内核,内核配置同样影响系统性能。典型的例子包括文件系统或CPU调控器的设置。

再向上看,我们可以看看JVM。JVM设置在系统性能中扮演着至关重要的角色,因为配置不当的JVM会导致垃圾回收时间过长,从而严重影响数据库的性能。

最后,我们有数据库本身。数据库参数用于微调数据库的行为,因此它们显然直接影响系统的性能。

因此,配置调优指的是战略性地选择所有这些参数,在每一层进行选择,以优化您的系统,即您的数据库。

这里的“优化”指的是优化某个指标。这可能是提高数据库的吞吐量、降低响应时间,或者提高系统效率以降低基础设施成本。

然而,在当今复杂的IT环境中,通过手动调优实现最佳性能已成为一项艰巨的挑战。原因有很多。

但理解为什么传统方法不再有效至关重要。首先,系统的层数增加了。我们总是有更多的全局层,并且在每一层内部,可用参数的数量也成倍增加。参数的激增使得手动微调每个参数变得越来越不切实际,基本上是因为我们有太多参数需要控制。

此外,许多参数的影响不明确或缺乏文档。即使有文档,也常常缺乏关于特定设置如何影响系统性能的深入见解。

因此,如果您进行手动配置调优,您只能选择一个值,然后观察其行为。

更令人困惑的是,许多参数提供的默认值并不总是最优的,有时甚至与最优值相差甚远。例如,我们可以看幻灯片中的中间图。这里显示了数据库吞吐量作为缓存大小的函数。您可能认为选择缓存大小的默认值是一个安全的选择。

但反直觉的是,在这个特定示例中,我们发现无论是减小还是增大缓存大小都带来了性能提升。这是一个问题,因为如果您手动进行这个实验,您看不到参数的清晰线性效应,因此您真的需要探索参数的整个定义域,以了解应该如何设置其值。

所以这并不理想。最后,我们面临着发布周期的加速。基本上,每次发布之间的时间间隔越来越短,因此没有时间在每次发布前进行手动调优。这是因为IT团队面临着跟上技术发展的竞赛,发布如此频繁,以至于我们无法再进行手动调优。

在这个不断发展的环境中,对自动化、智能化的配置调优方法的需求变得至关重要。不再可能依赖手动努力。解决方案在于像Akamas这样的工具,它可以为我们适应、优化并持续微调配置。

那么,现在让我们来看看Akamas。显然,Akamas是一个自动调优器,其理念是修改系统配置以满足某些优化目标。这可以是提高服务质量、降低成本、或增加服务弹性,我们可以做许多不同的事情。

我们Akamas的目标是创建一个由机器学习驱动的优化平台,支持尽可能多的应用程序和技术。

因此,Akamas严格来说不是一个数据库调优器。我们不提供查询模式优化等服务,我们并非专门针对数据库。然而,我们确实支持一些数据库,因此我们可以使用Akamas来调优数据库的配置。

此外,我们确实支持这个技术栈的不同层,因此我们也可以使用Akamas来调优操作系统或运行数据库的实例。

显然,Akamas有一些关键能力,使其区别于其他自动调优器。在我看来,最重要的是它是一个全栈自动调优器,因此可以同时调优IT技术栈的不同层。

同时,我们是技术无关的,我们希望尽可能通用,不希望被任何特定技术或平台所限制。

现在,调优技术栈的不同层不仅意味着有更多参数可以调优,从而可能提取更多性能,而且还可以利用层间的一些相互依赖关系。

举个简单的例子,如果您选择一台内存更大的机器,就可以给数据库分配更多内存。这个例子很简单,但还有更复杂的情况,当您调优多层时,确实可以利用这些情况。

第二个最重要的点是,Akamas是一个目标驱动的自动调优器。有些自动调优器有很强的预设观点,它们对特定应用程序应如何运行有自己的想法。而使用Akamas,用户设定目标。同样,目标可以是提高吞吐量、降低响应时间或任何函数。当用户指定某个目标函数时,Akamas将在参数空间中导航,以找到优化该特定指标的配置。

那么,您能谈谈具体是怎么做的吗?假设有人注册并说“我想优化吞吐量”,您会改变要调优的参数吗?还是会改变调优的激进程度?在优化吞吐量、延迟或成本时,有什么变化?

对于目标部分,根据您设定的目标,那将是我们优化过程中监控的内容。基本上,我们有一个机器学习模型,它将您应用的配置映射到一个特定的得分值,比如说。因此,这个值取决于您指定的函数。

然而,对于第二部分,也就是下一个要点——安全性。这里的“安全性”指的是我们对其他性能指标有许多不同的约束,例如服务级别目标或最大基础设施成本。这些约束取决于我们正在调优的应用程序,如果我们对该应用程序有所了解的话。

或者实际上取决于我们如何调优应用程序,但我们稍后会讨论这一点。

这本质上是一个安全特性。我的意思是,在优化过程中,我们尽一切努力检查这些约束,并且在优化结束时,我们确保最终配置符合指定的约束。

最后一点是,Akamas是工作负载感知的。我们持续测量进入系统的工作负载,并将此信息纳入推荐中。当我们在真实环境(生产环境)中进行调优时,这一点尤其重要。在生产环境中,我们暴露在真实的工作负载下,这些负载是嘈杂的且随时间变化,因此我们必须测量并跟踪它。

这实际上引出了介绍部分的最后一点,即Akamas的两种“风味”,或者说两种使用方式。

第一种是我们最初提供的,旨在调优副本系统或副本环境。这里的理念是,Akamas控制配置,同时我们也控制一个负载注入工具。这个工具为系统创建合成工作负载。通过这种方式,我们可以比较不同的配置,因为我们总是针对相同的工作负载进行测试,因此可以轻松比较不同的配置。

在第二种风味中,我们调优真实的生产环境。因此,我们是在真实世界中与真实系统和真实工作负载一起工作。显然,这里不再有负载注入工具,因为我们暴露在真实负载下。

在这种第二种风味中,遵守我们之前讨论的约束变得至关重要。同时,动作也需要更加平滑,因为您不希望在生产环境中进行剧烈的更改。此外,还需要考虑工作负载,因为它是真实的工作负载,我们需要跟踪它。

现在,我们可以看第一个例子,其中我们看到了MySQL的优化,假设我们直接调优系统的副本。

因此,作为一个示例设置,我们使用了MySQL数据库,运行在Ubuntu之上,位于亚马逊EC2云实例内部。

我们使用Prometheus收集性能数据,并使用BenchBase为MySQL创建合成负载。

调优过程的第一步包括将配置应用到系统。

为此,我们提供了一些针对支持技术的配置器。这使得配置过程非常直接,因为您基本上只需告诉Akamas您有一个MySQL数据库和一台Ubuntu机器,需要提供一些凭据,然后Akamas会自动知道哪些参数可用、哪些参数最重要(因为我们支持该技术)、参数的可接受范围是什么,更重要的是,如何将参数应用到系统。

显然,如果某项技术不受Akamas支持,我们希望成为一个通用的自动调优器,因此我们尝试通过提供一些自定义脚本,使集成任何新技术到Akamas变得相当容易。然而,在这个例子中,MySQL和Ubuntu都是受支持的技术,因此无需编写自定义脚本。

值得注意的是,EC2也受Akamas支持。但我们决定不在此示例中包含它。基本上,调优这一层意味着选择使用哪个实例来运行数据库。如果您切换实例选择以迁移数据库,这会使示例更复杂,因此我们决定不这样做。然而,调优实例选择是我们经常做的事情。我们大多数真实的优化都是针对运行在Kubernetes中的Java应用程序,因此如果您有一个容器,将其移动到另一个实例非常容易。

尽管如此,一旦配置被应用,我们需要启动合成工作负载。为此,我们使用BenchBase。我们使用了具有100个仓库和50个终端的TPC-C工作负载,并尝试实现最大可能的吞吐量。

我们让测试运行20分钟,在这20分钟内,我们使用Prometheus收集性能数据。在这个例子中,我们使用了节点导出器(用于实例)和MySQL导出器(用于MySQL)。这只是因为Prometheus非常容易设置,并且是一个很好的示例。但实际上,我们支持许多其他监控解决方案。

在这20分钟结束时,Akamas从BenchBase和Prometheus收集结果。读取BenchBase的输出非常简单,因为有一个很好的CSV文件,您可以直接导入到Akamas中,所以很直接。而对于Prometheus,MySQL和Ubuntu是受支持的技术,因此Akamas再次知道哪些指标对这些层很重要,以及如何查询Prometheus来获取这些指标。

利用这些信息,我们使用机器学习模型来理解配置的行为。基本上,我们得到一个新的建议配置,将其应用到系统,运行新的实验,获取新的性能数据,更新模型,然后得到新的配置。因此,我们基本上是迭代地进行。

然后,我们可以手动停止这个过程(如果我们找到了一个好的配置并对结果满意),或者我们可以让Akamas继续,直到耗尽优化预算。

现在,通过这个例子,我们运行了两项研究,即两个优化研究。第一项研究专注于MySQL的两个非常重要的参数:缓冲池大小和日志容量。通过调优这两个参数,我们将最大吞吐量显著提高了65%,即从每秒130个操作(请求)提高到每秒220个请求。

然而,需要说明的是,实现这个结果相对简单,因为MySQL的默认缓冲池大小是128 MB,对于这个特定的工作负载来说确实不够。因此,实际上,大多数数据库管理员会选择更大的缓冲池大小,并且基本上会获得与研究相同的结果。

现在,显然,使用Akamas,您可以调优MySQL的更多参数。这里我们只调整了这两个参数,以展示我一开始所说的:默认配置对于许多现实工作负载来说确实不是最优的,即使通过非常简单的优化,您也可以实现显著的改进。

这是哪个版本的MySQL?最新的,我不记得是8了。好的,然后它显示实验13,所以您花了13次迭代才达到大约两倍的性能。正确,嗯,实际上,因为我在上周运行了这个实验,所以是MySQL的最新版本。基本上是13次实验,因为那是我停止优化研究的时间。好的,所以最好的结果是那个,但实际上从第四次迭代开始,我们已经提高了60%。基本上是因为缓冲池大小有这个定义域,除了把它调小之外,您所做的任何事情(定义域的大部分都比默认值大)都容易在这里提高吞吐量。明白了,太好了,谢谢。

同样的结果也适用于技术栈的其他层。因此,有许多参数的默认值并非最优,我们应该调优它们。

因此,从这个配置开始,我们运行了另一个实验。所以我们现在从具有大缓冲池大小的MySQL优化配置开始,并添加了来自Ubuntu的27个参数。

通过调整这些参数,我们能够达到255。因此,与220相比,这基本上是15%的改进,但如果与原始性能130相比,则是25%的改进。

因此,基本上,通过调优MySQL和Ubuntu系统所能获得的整个性能改进中,三分之二来自MySQL,三分之一来自操作系统,这仍然是一个实质性的改进。

显然,调优MySQL数据库可以由专家级的人类数据库管理员完成。但调优27个Linux参数则完全是另一回事,它要复杂得多,并且涉及非常不同的技能。这展示了您可以实现的潜在性能增益。

实际上,在优化过程结束时,Akamas会提供一些见解,说明哪些参数对实现优化得分贡献最大。在这个例子中,显然两个MySQL参数是最重要的。但在第三位,我们有CPU调度器的延迟。这真的很有趣,因为这一结果与我们过去运行类似实验时观察到的情况非常不同,当时这个参数并不在最重要的参数之列。

这促使我们深入研究,以了解发生了什么,以及为什么这个参数变得重要了。

抱歉,打断一下,您是如何计算那个相关性的?是像您正在调优的赫林格(Hellinger)距离那样的东西吗?还是预先计算好的?您是如何计算那个的?

这是由机器学习模型计算的,基本上它与参数在模型中的重要性相关。因此,它基于优化过程的结果。

您能分享一下您使用的模型吗?我们使用很多模型。优化的核心是贝叶斯优化过程,因此这与贝叶斯过程核中的自动相关性确定有关。

明白了,太好了。但实际上并不那么直接。我想做的一件事是在优化中引入一个最终步骤,您实际上通过从基线开始,一次将一个参数更改为最优值,来计算相关性,并真正看到哪些参数是最重要的。

好的,回到这个结果的原因。我们观察到Linux内核的5.13版本。更具体地说,是这个提交将调度器的参数移到了这个目录 /kernel/debug/scheduler 中,因此它们被标记为调试参数。现在,这个提交没有改变参数的默认值,但通过更改用于更新参数的路径,它破坏了一些用户空间工具,比如 tuned,这些工具用于更新这些值。

基本上发生的情况是,当您启动Linux机器时,内核设置一个值,然后发行版在启动过程中设置另一个默认值,他们认为这个值更适合您的特定用例。如果您破坏了用户空间工具,发行版就不再更新默认值,因此您就卡在内核的默认值上了。我们在Ubuntu 22.04和RHEL 8.1上观察到了这种行为。这里的“行为”指的是默认值在一个发行版版本和另一个版本之间是不同的。

实际上,这个观察并非我们独有。如果您点击这些链接,您会看到VMware实际上报告了这个错误,并且他们观察到由于调度器参数的这个新默认值,性能下降高达三倍。

因此,我想在这里指出的重点是,像这样的事情经常发生。存在这些未记录的性能回归,系统中某些东西坏了,您有某个外部库依赖于另一个库,有人更改了某些东西,系统中的某些东西就坏了,您基本上就遇到了性能下降。

所以这凸显了拥有像Akamas这样的工具的重要性。首先,因为它们帮助您理解您遇到了问题,因为突然之间系统的性能与过去不同了,所以您看到有问题。但同时,有了Akamas,您真的不需要理解为什么有问题,因为您可以简单地重新调优您的应用程序并解决问题。实际上,在这个例子中,我们必须研究这个,因为我们想理解为什么这个参数变得重要了。但在现实中,我们本可以保留Akamas给出的结果,并获得我们想要的所有性能。

到目前为止,我们的讨论都基于一个假设:我们拥有真实系统的副本,并且在这个副本中,我们可以方便地测试和微调配置。

然而,现实世界真的不同。更准确地说,我们的假设是拥有一个副本环境,它是真实系统的完美复制品。但事实并非如此,原因有二。

有时,副本环境(预生产环境)甚至不存在,根本没有预生产环境。而大多数时候,即使预生产环境可用,它也不是真实系统的完美复制品。因此,显然,如果您有两个不同的环境,它们可能需要不同的最优设置。所以,如果您在预生产环境中运行实验,然后将配置转移到真实环境,最终可能会得到糟糕的性能。

第二个问题在于工作负载。到目前为止,我们使用了合成工作负载,使用BenchBase来测试配置。同样,显然,如果您使用不同的工作负载,最终可能会得到不同的最优配置。

因此,您确实需要创建一个合成工作负载,它是真实工作负载的一个非常好的近似。然而,这同样非常复杂,因为首先真实工作负载是嘈杂的,而且真实工作负载也随时间变化,所以它总是与自身不同,您无法复制它,因为它总是在变化。您只能创建一个足够好的工作负载近似。

但在实际中,复制真实工作条件是一项非常复杂的任务,因为您需要准备必要的测试基础设施和工作场景,这既耗费资源又非常耗时,通常是不可接受的。

为了解决这个问题,我们引入了Akamas的第二种风味,称为Akamas Live,我们在其中调优生产环境。

从Akamas用户的角度来看,我们希望这像创建一个实时优化一样简单,而不是一个优化研究。然而,由于是生产环境,我们必须对算法进行一些调整。

最重要的考虑是增加我们在优化过程中对遵守指定约束的重视程度。因此,我们不希望违反任何安全约束。这对于防止对生产环境的任何干扰至关重要。

第二点,对于数据库来说,很明显,您不会关闭事务提交的FSync,因为您知道,这会使磁盘无法持久化数据。对于您正在调优的其他东西,比如内核参数或JVM,您能举例说明必须设置的其他安全约束吗?

嗯,基本上,为了安全,我们做了很多不同的事情。首先,就像您说的,我们限制参数的定义域以使其更安全。

我们还有一些参数之间的新约束。举个非常简单的例子,如果您在容器内调优JVM,您不会让JVM的堆大小超过容器的内存。

然后,在实时优化的优化过程中,我们进一步限制配置的定义域,使得不同配置之间的变化非常平滑。我们不希望配置发生突变。

还有那些约束,它们基本上是一个性能指标的约束,该指标是参数的未知函数。因此,您可以再次创建一个机器学习模型来将其映射为一个函数,并尝试创建一个预期能满足该约束的配置。

基本上,安全性是您希望距离约束边界有多远。另外,您可以做的一件好事是利用模型的不确定性。因此,如果您的约束模型告诉您某个配置距离边界很远,但模型对这个预测不确定,您就不会尝试该配置,而是采取更安全的做法,收集更多数据。这样优化过程会更慢,但肯定更安全。

听起来您维护了多个贝叶斯优化模型,一个用于目标(如吞吐量或延迟),但也用于内存消耗等其他指标,对吗?是的,这不是一个海洋过程,但理念是……您能说说您使用什么模型吗?啊,然后,很多,我们根据得到的指标动态决定使用哪个模型,是一种集成方法。

好的。那么,您能描述一下谁提出这些约束以及他们如何做到的吗?是Akamas的客户吗?您如何衡量所有约束的完整性?

嗯,对于我们支持的技术,当您选择特定应用程序时,会预定义一些约束。我们还有一些跨层的约束,所以如果您选择,比如说,JVM和Kubernetes容器,您会得到一个将JVM堆与容器内存联系起来的约束。

然后还有一些关于响应时间的约束,响应时间相对于基线不能下降。

还有一些关于资源利用率的约束,这些总是有益的。

除此之外,用户可以指定一些其他约束。实际上,即使是Akamas添加的那些约束,用户也可以删除,如果他们想的话。我不知道他们为什么会这样做,但是……作为用户,您可以做任何您想做的事情。然后在此基础上,我们为研究的创建添加了一些语法糖,一些约束会自动添加。

我希望这回答了任何问题。

好的,那么关于约束就说到这里。对于工作负载,我们说过需要监控工作负载的演变。

实际上,这里有两个技术细节我们需要讨论。基本上,当您设计一个自动调优器时,您正在创建一个选择配置的东西,这个配置应该优化作为参数和工作负载的未知函数的某个指标。同时您还需要保证安全。

因此,您可以控制配置,但无法控制工作负载。所以您需要决定考虑哪种工作负载。基本上,您可能有系统什么都不做的夜晚,或者系统大量工作的白天。

对于这些,您有两个正交的决定要做。您可以选择局部安全,即自动调优器将为您提供一个仅对当前工作负载预期安全的配置;或者选择全局安全,即配置预期对所有可能的工作负载都安全。

同样的决定也可以针对优化做出。您可以决定要一个仅针对当前工作负载优化得分的配置,因此您基本上是在追逐工作负载,自动调优器将不断更改配置以跟上工作负载的演变,您将始终拥有性能最佳的配置。

或者您可以选择平均优化。在这种情况下,您希望创建一个自动调优器,它将找到一个单一的配置,该配置在所有可能的工作条件下都不是最好的,但它是一种平均配置,是您为了在所有工作条件下优化这个得分所能做出的最佳权衡。

现在,当我们开始研究Akamas Live时,我们试图为追逐行为创建一个自动调优器,即不断更改配置。实际上,我们看到我们所有的用户都更喜欢单一、不变的配置的简单性和稳定性,因此我们转向了全局安全和平均优化的场景,这就是我们现在要看到的。

显然,要做到所有这些,您需要在系统中拥有一些组件来跟踪工作负载演变,并进行一些聚类和预测。

好的,有了这些,我们可以转到第二个例子,即MySQL的实时优化。这里我们保持了相同的系统来运行示例。

然而,我们修改了BenchBase的配置。最重要的是,我们不再追求最大吞吐量,而是使用一个我们稍后会看到的工作负载模式,因此工作负载随时间变化。

此外,BenchBase不再连接到Akamas。因此,即使Akamas没有运行,我们也会持续向MySQL发送查询。Akamas和BenchBase之间没有连接。所以显然,这不是一个具有生产工作负载的生产环境,但它是一个很好的近似,因为工作负载随时间变化,并且不与调优工具连接。

至于优化循环,Akamas唯一要做的就是更新配置,这与之前相同。然后我们等待一段时间,收集性能指标。因此,现在与BenchBase没有连接。

当我们应用配置时,根据您如何设置Akamas,我们可以做两件不同的事情。基本上,您可以决定自动批准,即Akamas在没有人工干预的情况下继续实施建议的更改。

或者您可以选择手动批准步骤。在这种情况下,Akamas为用户提供手动审查和修改配置的机会,然后再将其应用到系统。这确保了人类专业知识仍然是决策过程的一个组成部分。

此外,我们还提供了将新配置建议给用户已有的外部配置管理工具的选项。然后我们等待配置部署到系统,然后再测量新的性能。

显然,这种控制水平在生产环境的实时优化中至关重要,因为用户必须保持在循环中,因为配置更改会显著影响系统的性能,尤其是在调优的最初几次迭代中,用户需要建立对Akamas的一些信任,因此他们希望成为循环的一部分,检查Akamas在做什么。

至于工作负载模式,这里我们用蓝色表示我们要求BenchBase达到的吞吐量。显然,它随时间变化。黄色表示基线默认配置的响应时间。

工作负载有一些时间变化。因此,我们在晚上10点(在这里,我不知道您是否能看见我的指针)有一个低工作负载区域。好的,我们从这里开始夜晚,然后上升。

然后工作负载增加,并保持高位直到下午2点。接着在下午4点有一个轻微下降,然后在晚上8点之前又有一个高峰。我们要求的最高吞吐量是135,这与我们在第一个实验中看到的值相同,因此这是系统的饱和点,我们给数据库施加了很大压力。

基本上,这个模式持续24小时。X轴显然是时间。每天我们重复这个模式,并且我们还添加了一些噪声以使其更真实。

因此,利用这个场景,我们模拟了三种优化,这是我们的客户通常使用Akamas的三种方式。

第一种试图通过降低数据库的响应时间来优化性能。

第二种,我们希望提高系统的效率,即减少资源消耗,从而能够缩减基础设施规模。

第三种,我们从一个行为不良的基线开始,该基线无法维持所需的吞吐量,我们使用Akamas来解决这个问题。

让我们从第一种开始。这是我们刚才看到的同一张图,它显示了基线的行为。

从这张图,我们转到这张图,这是同一件事,但我们有更多的响应时间百分位数。另外,我们将响应时间的轴切换为对数坐标,以便能够看到所有百分位数,并更好地理解发生了什么。

实际上,我们想关注这个区域,这是第一个高峰,最高的峰值。正如我们之前所说,对于这个配置,我们接近饱和点,我们看到响应时间很高,尤其是较高的百分位数。而且当吞吐量上升时,它们也在急剧增加。

这里显示的是调优一天(24小时)后某个配置的行为。

实际上,从单个图中比较具有多个工作负载的不同配置非常复杂。因此,这里我决定放大第一个高峰,以便更清楚地看到发生了什么。

我们看到所有百分位数都更低了。而且,如果我们看最深的线,即最大响应时间,我们看到,首先,它比基线低,而且随着吞吐量上升,最大响应时间没有急剧增加。它上升了,但不像基线那样剧烈,这清楚地表明我们远离了系统的饱和点。

在这些表格中,我们基本上再次列出了响应时间,各种百分位数。在第一张表中,我们有最高峰值,即当我们达到135时。基本上是这一分钟的平均值,但确实是峰值时刻。正如我们所见,Akamas降低了所有百分位数的响应时间。

在第二张表中,我们有相同的测量,但是针对夜晚,即所有这10个小时(工作负载的低谷部分)的平均值。同样,在这种情况下,即使系统远离饱和点,响应时间也有显著降低。

现在,这类似于我们看到的第一个示例研究,因为我们从一个MySQL配置开始,该配置确实很小。因此,通过增加缓冲池大小,您可以获得非常好的性能改进。所以这又是一个相当简单的优化,即使这里我们实际上调优了两个MySQL参数和27个Linux参数,总共有29个参数。但看结果,这相当容易。

第二个例子稍微复杂一些,因为现在我们从MySQL和Linux都已调优的配置开始,因此这已经是一个非常好的配置。我们还修改了工作负载模式,不再上升到130,而是上升到100。如果您还记得,调优后的配置可以达到255。所以这是一个不接近饱和的系统,因此存在被浪费的资源。

我们这里的目标是调优MySQL配置和Ubuntu配置,以降低我们系统的资源消耗。

在这两张图中,左边是基线,右边是优化后的配置。同样,从图中比较不同配置很困难,而且因为我做了两张图,坐标轴的比例也有一些差异。

但我们仍然可以通过看蓝线(即吞吐量)看到白天的两个高峰。黑色是内存利用率,两种配置都是90%,所以那里有一些差异。这对于数据库来说是合理的,因为它会尝试使用所有内存,如果数据库不使用,操作系统就会使用。

然后红色是CPU利用率。它基本上与吞吐量模式匹配。但更有趣的是,在基线中,CPU利用率接近58%。而在调优后的配置中,我们降到了75%。

另外,从黄线(即响应时间)来看,基线在第一个高峰期间上升到24毫秒。而对于调优后的配置,我们降到了15毫秒多一点。

因此,基本上,经过一天对29个参数的调优,我们能够以更低的响应时间和低10%的CPU利用率维持相同的吞吐量,这是相当多的。

基本上,这意味着如果您在数据中心运行,这直接转化为更低的功耗和碳足迹。而如果您有云部署,它允许您减少分配给此数据库的资源,从而基本上可以花费更少的钱。

至于第三个例子,我们有一个所谓的弹性场景。这里的目标是修复一个不良配置。因此,基本上从默认配置开始,缓冲池大小很小,并且我使用了更高的工作负载模式。所以显然,该配置无法维持这两个高峰,因为这里对我们可以实现的最大吞吐量有明显的限制。

现在,我用蓝色或黑色垂直线标记了我们开始调优的点。橙色是基线配置可以实现的最大吞吐量。

现在,这不是Akamas Live的典型用例,因为通常您在生产环境中不会有有问题的配置。如果您有这个问题,您会尝试快速解决,而不是用Akamas。因此,在这里,我们希望优化尽可能快。这通常不是我们使用Akamas Live想要做的事情,因为正如我们之前所说,我们希望配置随时间平滑变化,所以这需要一些时间。

尽管如此,我们可以看到,在开始调优的第一个下午,我们已经能够获得更高的吞吐量。然后我们继续,基本上得到了提升。

然而,如果我们看第二天早上,我们看到我们能够实现更高的吞吐量,而且这个吞吐量甚至比下午达到的还要高。这基本上意味着,即使在夜间,Akamas也在更改配置(这里我们每20分钟更改一次配置)。通过测量进入系统的工作负载,我们能够创建一个映射配置、工作负载和性能的模型。

我们甚至能够利用这个工作负载区域(这个低工作负载区域)来获得关于系统在更高吞吐量下行为的见解。因此,第二天,我们基本上有了一个更好的配置。

然后我们继续调优,我们有另一个夜晚,我们做同样的事情。基本上在第三天下午(即经过两天调优后,基本上这里一天,这里第二天),系统表现良好,因为这里不再有吞吐量限制。

实际上,从这些数据来看,听起来您是说您认识到夜间工作负载,即使提交率低得多,但您已经确定工作负载本质上与白天高峰相同,因此您可以利用这些信息。意思是,如果夜间他们开始运行与白天完全不同的报告作业,您会自动说“这是不同的,因此我不想从中学习”,是这样吗?

是的,嗯,在这个例子中,我们始终运行TPC-C工作负载,所以始终是相同类型的东西。我们只是测量数据库的吞吐量。基本上,我们试图做的是说,如果系统在吞吐量很低时,使用这个配置表现更好,我们可以想象这个配置对于更高的吞吐量也会是好的。

在您更现实的特定设置中,您还需要跟踪在系统上运行的操作类型。因此,您有不同种类的吞吐量,比如说。

然后,您基本上从测量所有可能工作负载下的基线开始。您理解工作负载如何影响性能,因为您没有更改配置,所以您只是学习工作负载的影响。然后,即使您使用夜间工作负载进行调优,您也有一个机器学习模型,能够推断出对其他日常工作负载的一些见解。

显然,这只是一个预测,因此这里算法的安全部分变得非常重要,因为您有一个预测,并且需要能够理解该预测的可靠性。因为如果您只看到夜间,并且只在夜间进行大量调优,也许当白天来临时,您会有一个完全错误的配置,因此您真的必须注意这一点。

是的,我理解。我问的是,听起来你们现在在做Akamas Live,你们确实识别出夜间工作负载不同,因此不基于此进行调优,对吗?是的。您能分享一下您是如何做到的吗?嗯……好吧,没关系。基本上,就是用这些机器学习模型。它们映射配置、工作负载和性能,然后我们在此基础上进行一些推理。

好的。另外,我们做的另一件事是,在白天,嗯,对于这个特定例子,在白天,系统真的接近饱和,因为我们有一个行为不良的基线,因此我们真的接近系统的约束。如果我们接近……如果系统接近违反约束,我们不会过多修改配置,因为那真的很冒险。所以在这种特定设置下,我们大部分调优在夜间进行,因为那样更安全。

因此,我们基本上做的是在夜间调优,然后当工作负载回到日常工作时,可能回到基线或非常接近基线,然后我们尝试理解模型是否正确。我们可以利用夜间的结果来获得更好的配置。

好的,太好了,谢谢。

那么,这就是第二个例子的全部内容。基本上,我们看到Akamas是一个通用的优化平台,我们尝试调优尽可能多的应用程序。我们是一个全栈优化平台,因此我们真的希望利用技术栈的每一层。

我们看到了实时优化的一些例子,这是Akamas的最新版本。

在实时优化内部,我们看到Akamas尽可能重视成为一个安全的自动调优器,它不会在生产环境中造成问题。同时,它是工作负载感知的,因此正如我们讨论的,它能够利用夜间时段来获得对日间时段的见解。

就是这样。谢谢。

好的,谢谢。我会关闭我的麦克风。其他人,我们还有时间从观众那里提一两个问题。

好的,也许我可以问一个。Stefano,很棒的演讲。我是Jignesh Patel,Andy的同事。两个问题都相关。您是否在任何时候看到应用机器学习导致工作负载性能以意想不到的方式变化,直到它稳定下来?还是您几乎总是看到性能提升?第二部分是,您知道最难的工作负载是什么吗?有没有一个广泛的分类,您见过哪些超级难调优?是否所有东西似乎都可以用您的技术优化?

那么,到目前为止,我从第二个问题开始回答。到目前为止,我们总是看到可以实现一些性能改进。

关于系统方面。至于最具挑战性的工作负载,最困难的工作负载,真正的问题是当您遇到像最后一个例子中的情况时,基本上您没有明确的方法来理解工作负载是什么,因为这里我们有BenchBase,所以我知道工作负载想要上升。但在现实中,您无法访问这些信息,因此您不知道系统是稍微高于阈值还是远低于阈值,所以您需要查看响应时间并尝试理解。因此,这更多地与系统有多糟糕或离约束有多近有关。

很好,谢谢。

但对于第一个问题,我认为您真的需要考虑您正在调优的技术,因为它们显示配置更改效果所需的时间不同。因此,基本上,当您创建优化研究时,您需要具备这些知识。

009:专为向量数据管理而生的系统

在本节课中,我们将学习 Milvus 向量数据库系统的设计原理与架构。我们将探讨其如何结合向量搜索算法与数据库特性,以应对机器学习时代的海量非结构化数据处理需求。课程将涵盖其分布式架构、写入路径设计,以及如何平衡数据新鲜度与查询效率等核心挑战。

系统架构概述

上一节我们介绍了课程目标,本节中我们来看看 Milvus 的整体架构。Milvus 是一个专为向量数据管理设计的系统,其核心是将向量搜索算法与数据库功能相结合。

Milvus 的初始版本 1.0 采用单节点架构,包含代理、存储、索引和查询四个主要模块,能轻松处理数千万级别的向量。然而,为应对日益增长的数据规模,团队在两年前开始向分布式架构转型,最终形成了当前的 Milvus 2.0 架构。

在 Milvus 2.0 中,四个模块被解耦为独立的分布式服务,并采用主从模式进行管理。系统引入了消息队列(如 Kafka 或 Pulsar)来进一步解耦各模块。数据写入时,首先经过代理节点进入消息队列,数据节点从队列中消费数据,进行分段和存储操作,并将结果写入对象存储。索引节点则从对象存储读取数据,构建向量索引。同时,查询节点从消息队列拉取最新数据以支持实时查询。当执行搜索时,查询节点会从对象存储加载由索引节点构建好的索引到本地内存或磁盘,并结合实时数据提供服务。

这种架构设计有几个关键点值得强调。首先,它实现了存储与计算的分离,提升了系统的灵活性。其次,整个系统采用微服务设计,可由 Kubernetes 进行自动化部署和管理。最后,消息队列的引入帮助解耦了所有无状态组件。

存储与计算分离的深入探讨

上一节我们介绍了 Milvus 的宏观架构,本节中我们深入探讨其存储与计算分离的设计。这是一个数据库领域的经典话题,但由于其复杂性,大多数向量数据库并未很好地实现这一点。Milvus 经过数年努力才在 2.0 版本中实现了分布式,其回报是获得了处理机器学习时代海量数据的强大能力。

以下是存储与计算分离带来的主要优势:

  • 独立扩展:查询、索引构建和存储可以按需独立扩展,以适应不同场景的需求。
  • 资源优化:不同组件对资源类型的需求不同。例如,代理节点和数据节点是 I/O 密集型,查询节点因繁重的向量距离计算而是 CPU 和内存密集型,索引节点也是高 CPU 使用场景。将不同资源分配给不同角色可以显著提高集群效率并降低成本。
  • 高可用性:特定角色的升级、故障恢复或高负载不会影响其他组件的正常运行,从而提高了整个集群的可维护性和鲁棒性。
  • 云原生适配:独立且无状态的索引节点和数据节点可以被池化,这能根据用户的不同使用模式提高资源利用率和索引构建速度。此模式已被基于 Milvus 的 Zilliz Cloud 云服务所采用。

写入路径与索引设计

上一节我们了解了架构的优势,本节中我们聚焦于系统的写入路径,看看 Milvus 如何处理数据。向量搜索算法是向量数据库的核心,其设计决策很大程度上取决于算法本身的特性。与执行确定性搜索的传统数据库不同,向量数据库的核心——向量搜索——是概率性的。这意味着大多数时候,向量数据库不要求绝对精确的 Top-K 最近邻结果,而是可以为了更高的性能牺牲一些精度。

从算法角度看,它们大致可分为三类:

  1. 暴力搜索 (Flat):适用于需要极高精度或实时数据搜索的场景,因为它无需构建时间。
  2. 基于量化的算法 (如 IVF):核心思想是将向量空间分割成多个单元(桶),通过忽略不太可能包含结果的桶来加速搜索。
  3. 基于图的算法 (如 HNSW):如果需要同时兼顾高精度和高速度,图算法是目前的最佳选择。

评估一个索引有多种维度,如构建时间、精度、性能(通常用 QPS 衡量)和资源使用等。这里我们主要关注两个基本指标:构建时间和查询性能。以下是一个简化的对比:

索引类型 构建时间 查询性能 (QPS)
Flat
IVF_FLAT
HNSW

可以看到,我们需要花费更多时间构建索引,以获得更好的查询效率。这可以看作是数据新鲜度查询效率之间的权衡。目前,大多数向量数据库使用 HNSW 作为主要索引,牺牲数据新鲜度以换取效率。

那么,既然单一算法无法同时兼顾两者,是否有可能通过更复杂的系统设计来实现呢?这就是 Milvus 正在做的事情。首先,需要了解 Milvus 内部的数据结构。在每个集合(类似于传统数据库的表)下,数据被进一步分片。每个分片可以同时从消息队列读取数据以加速数据流入。分片内最小的数据单元是。我们会为每个段构建上述提到的一种索引。

段有两种类型:

  • 增长段:直接从消息队列读取数据生成。我们通常使用 Flat 索引来确保插入速度。增长段用于提供实时查询能力并保证数据新鲜度。
  • 封存段:当段内数据增长到一定规模,数据节点会将其密封,使其变为不可变状态,然后交给索引节点去创建更高效的索引(如 HNSW),以提供高效的查询服务。

封存段被索引节点构建索引后,会被加载到查询节点,并替换掉原来的增长段来提供服务。同时,系统会为该分片生成一个新的增长段以继续支持数据的新鲜度。

段大小与合并策略

上一节我们介绍了双段机制,本节中我们探讨由此带来的挑战与解决方案。这种复杂的结构带来了好处,但也带来了许多挑战。例如,我们应如何定义段的大小?如果段非常大,会给分布式调度和故障恢复带来挑战,因为大段在节点间传输会非常昂贵且缓慢。更重要的是,大段会导致查询变慢。

Milvus 的查询涉及三层归约操作:

  1. 第一层在查询节点内部,合并来自同一节点上不同段的 Top-K 结果。
  2. 第二层在分片级别,由于一个分片内的段可能分布在不同查询节点上,需要将结果传输到分片主节点进行再次归约。
  3. 最终在代理节点聚合所有分片的结果,返回给客户端。

对于最常用的 HNSW 索引,更大的段会导致更长的索引构建时间,从而造成增长段的堆积。由于 HNSW 的搜索速度比 Flat 快约 500 倍,堆积的增长段会显著拖慢整个查询过程。

那么,是否应该使用更小的段呢?图表显示,在总数据量恒定的情况下,增加段的数量(即使用更多小段)几乎不会改变 HNSW 的搜索性能,但每个段都需要自己的元数据,过多的小段会极大增加元数据存储的压力。因此,非常小的段也不可行。

既然我们在某些情况下偏好小段(如增长段需用 Flat 索引保证新鲜度),而在索引构建后偏好大段(以获得高效查询),为什么不先构建小索引,然后再将它们合并呢?这正是 Milvus 的压缩机制。在插入过程中,数据节点会主动将多个较小的封存段合并成较大的段,然后传递给索引节点构建索引。最终,查询节点加载这个更大的索引,替换掉被合并的小段索引。

但是,如果索引节点非常繁忙(例如资源有限或插入速度极快)怎么办?我们还有其他选择。除了暴力搜索和图算法,还有 IVF 和量化索引(如 SCANN)作为中间方案。SCANN 的索引构建时间不到 HNSW 的五分之一,但其性能却比暴力搜索快约 200 倍。为了应用这一思路,Milvus 支持使用增长段中初始的一部分数据作为 IVF 聚类算法的样本,构建桶结构。随后插入该增长段的所有数据都会被快速分配到对应的桶中。图表显示,在插入速度上,三种方案(Flat, IVF_FLAT, SCANN)并无显著差异,但 SCANN 能在增长段提供快 200 倍的查询速度,这是一个很好的折中方案。

离线批处理与机器学习支持

上一节我们解决了在线写入的挑战,本节中我们看看 Milvus 如何支持离线批处理场景。在典型的向量数据库应用场景中,非结构化数据通过模型转化为嵌入向量,然后插入 Milvus 以提供搜索能力。然而,随着模型频繁迭代,向量需要重新生成,导致产生大量需要重新导入 Milvus 的离线向量批次。

除了通过流式插入支持在线场景,Milvus 也通过批插入支持离线场景,主要有三种途径:

  • 直接写入对象存储:允许将原始数据直接传输到对象存储(如 S3),绕过从代理到消息队列再到数据节点的复杂插入流程。索引节点可以直接读取这些数据构建索引,这大大提高了数据读取效率。
  • 通过 Spark 导入:用户可以在 Spark 中定义数据预处理任务(如从非结构化数据生成嵌入、数据过滤等),然后将数据导入 Milvus。
  • 导出至 Spark 处理后再导入:数据可以从 Milvus 批量导出到 Spark,经过处理后再导回。这样可以基于数据的全局分布进行优化。例如,系统可以定期导出所有向量,在 Spark 中执行全局的 IVF 索引构建,将每个桶作为一个段,然后导回 Milvus。在搜索时,可以根据查询点与桶的距离跳过大多数段,从而实现性能提升。

现在,我们已经完成了对 Milvus 数据写入路径的介绍,接下来简要回顾一下我们讨论过的特性如何支持机器学习。一个简单的机器学习流水线大致可分为离线数据处理/模型训练,以及在线模型部署与推理。

从离线角度看,一个典型用例是通过相似性搜索进行数据挖掘。以自动驾驶为例,当车辆遇到无法识别的物体(如熊)时,需要从海量驾驶记录中挖掘类似图像以改进模型。数据挖掘和模型训练总是面对非常大的数据集。Milvus 灵活的分布式架构能够很好地处理大规模向量,而其批处理路径提供了快速的插入速度和更便捷的 ETL 流水线,两者结合使 Milvus 具备了处理十亿级别数据的能力。

从在线角度看,除了经典的搜索和推荐,向量数据库在大型语言模型领域也至关重要。在此领域中,智能体是典型场景。智能体是基于大语言模型的 AI 系统,可以通过与 LLM 及第三方接口的多轮对话自动完成复杂任务。在这个系统中,LLM 是健忘的“大脑”,而向量数据库则是“海马体”(负责记忆)。向量数据库的存在使得交互过程从“闭卷考试”变成了“开卷考试”。另一方面,智能体可以浏览领域知识和私有数据提供给 LLM,使回答更准确,也能回忆自身操作历史以更好地理解用户意图,实现个性化。这个场景对实时插入速度、性能和数据库可用性提出了很高要求,而这正是 Milvus 的流式插入场景旨在解决的问题。

未来挑战与思考

上一节我们看到了 Milvus 如何支持机器学习流水线,本节中我们探讨向量数据库未来面临的挑战与发展方向。向量数据库是一个全新的领域,所有解决方案都远未成熟。同时,在 AI 时代,随着机器学习飞速发展,新的挑战不断涌现,我们需要不断思考和改进以跟上发展步伐。

以下是未来需要考虑的一些方向:

  • 语义相似性与数学相似性:随着机器学习技术的发展,模型理解复杂语义的能力不断增强。目前几乎所有向量数据库都基于向量在 L2、内积或余弦空间中的距离来定义相似性,我们称之为数学相似性。而语义相似性代表了两个数据背后真实对象的相似度。当前向量数据库的预设是这两种相似性是等价的。但问题是,它们真的相同吗?学术界已有很多研究试图解决这个问题,例如使用用户自定义的距离计算函数(甚至可以是机器学习模型),或者直接在算法内部使用模型作为索引进行相似性搜索。如何在数据库中实现这些方法,以及是否有更好的方法来解决语义理解问题,都是需要探索的方向。
  • 维度诅咒:随着模型变得更大、更复杂,其生成的嵌入向量的维度也在不断增加。高维度给向量数据库的存储和计算带来了巨大挑战。同时,高维度会导致数据分布极其稀疏,使得语义推断更难用现有的 L2 或内积等度量来衡量。因此,如何在降维和压缩数据的同时,尽可能保留语义信息,是一个非常重要的课题。
  • 基于机器学习的数据库优化:除了数据库追赶以支持机器学习外,机器学习也能帮助增强数据库。基于机器学习的自动调优是最成熟的场景之一。事实上,与传统数据库的确定性搜索方法相比,自动调优在向量数据库领域可以发挥更大作用,因为概率性搜索允许更多的灵活性。除了性能提升,向量数据库还需要在不同的搜索条件下保持相对稳定的精度以支持业务场景,这是另一个需要机器学习介入的领域。以 IVF 索引为例,从性能角度看,除了类似传统数据库的高级可调参数外,在算法侧也有很大的调整空间(如采样数量、桶的数量、搜索涉及的桶数、每个桶使用的压缩类型及程度等)。从可用性角度看,当用户搜索 Top-K 最近邻时,不同的 K 值会影响性能和精度。我们需要一个模型,在用户发起搜索时,综合考虑段的大小、数量、算法类型和 K 值等因素,动态生成参数,以确保精度保持在一个相对稳定的范围内。

总结与核心问题回顾

本节课中,我们一起学习了 Milvus 向量数据库的架构、设计原理及其如何应对机器学习场景下的挑战。最后,让我们回到本课开始时提出的两个问题。

问题一:传统搜索与向量搜索的关系是什么?
首先,向量搜索并非传统搜索的替代品,而是补充。传统搜索更侧重于关键词匹配,而向量搜索更侧重于上下文和语义匹配。因此,许多搜索系统同时包含关键词搜索和语义搜索模块,并对两者的结果进行后处理、合并和重排。我们可以换一个角度思考:将关键词搜索分为两部分——稀疏向量提取和稀疏向量相似性搜索。我们可以简单地使用基于统计的传统方法(如 BM25、TF-IDF)来提取稀疏向量,并使用对稀疏向量的暴力搜索来替代经典的关键词搜索。这种结构带来了更大的灵活性。例如,从稀疏向量提取的角度,也可以使用基于学习的方法来增强对隐藏语义信息的理解。对于稀疏向量搜索,除了使用精确搜索,我们也可以应用一些近似搜索技术来加速过程。我们称这个过程为传统搜索的向量化

问题二:传统数据库与向量数据库的关系是什么?
让我们看一个例子。如果我们需要用一张狗的图片找到三种最相似的狗。传统的向量搜索会返回三张最相似的狗图片,可能是三张雪橇犬的图片。但用户可能想要三种不同的狗。这是一个典型的分组场景。我们可以尝试按狗的名字分组,然后在每组内搜索最相似的图片。如果我们没有“名字”这个列呢?我们需要直接根据图像向量本身进行分组。这意味着,除了确定性匹配,我们还需要具备基于概率相似性对向量进行分组的能力。除了分组,我们还可以有基于此的聚合、连接操作等其他功能。我们可以看到,传统数据库所需的功能同样适用于向量数据库,只是我们需要以概率性的方式而非传统的确定性方式重新实现它们。同样地,就像传统搜索的向量化一样,我们称此为传统数据库的向量化。在机器学习时代,我们将文本向量化为词元,将图像向量化为像素,我们正在向量化一切。

最后留一个思考题:未来,还有哪些事物正在被向量化的路上?

010:面向数据库的领域知识增强人工智能

概述

在本节课中,我们将学习阿里巴巴智能数据库团队如何将精心设计的领域知识干预(Handcrafted Interventions)引入人工智能系统,以增强数据库相关任务(如自然语言转SQL和运维根因分析)的性能与效率。我们将通过两个实际系统(SQL Bridge和Shapley IQ)来具体阐述这一理念。


引言:为何需要“手工”干预?

想象一下,我们讨论的主题是“面向数据库的增强人工智能”。如果我们在前面加上“手工干预的”这个修饰语,标题就变成了“面向数据库的手工干预增强人工智能”。这可能会立即在我们脑海中触发一些警报。

这听起来像是第一波人工智能浪潮的产物。如今,我们几乎已经度过了使用统计学习的第二波浪潮,正处在使用大型深度神经网络的第三波浪潮中。这是否是一种倒退,甚至是一种反模式?

在第一波浪潮中,人们通过演绎原理和总结最佳实践来构建系统。然而,随着神经网络,特别是基于Transformer架构的突破,人们意识到,使用通用模型并减少手工工程,在注入大量数据后,可以产生惊人的涌现能力。例如,同一个模型可以用于完全不同的任务,并且几乎总能给出最佳结果。

以Spider排行榜挑战(将自然语言问题翻译成SQL语句)为例,目前几乎所有排名靠前的解决方案都基于ChatGPT,要么使用某种检索方法来增强,要么使用定义良好的提示词。提示词很棒,但问题在于,有时它可能无法完全遵循你的指令。

对于NL2SQL任务,我个人认为提示词有时过于高层。我们需要一些低层级的控制。这就是今天要传达的新信息:我认为,将精确的、低层级的手工干预插入到系统中是可能的,这样做效率更高,有时甚至更有效,即使是小模型也能变得强大。


第一部分:SQL Bridge - 自然语言转SQL系统

AFDB(AI for DB)是一个很大的话题。今天我将用我们团队开发的两个真实系统来解释为何引入干预是有帮助的。第一个系统是基于NTSE的SQL Bridge,第二个是关于运维根因分析的Shapley IQ。

任务挑战

对于NL2SQL任务,输入肯定包括原始问题,但这还不够。我们还需要模式数据信息,包括表名、列名、其他约束(如类型信息、连接关系、主外键信息等)。有时甚至需要考虑SQL方言,例如SQL Server使用的TOP和MySQL使用的LIMIT

右侧是SQL Bridge生成的一个真实SQL语句示例。我用这个例子来说明推导SQL语句的挑战。问题是:“英语不是官方语言的国家,其平均预期寿命是多少?”这里至少有三个挑战:1)主查询中有一个子查询;2)在子查询中有一个连接关系;3)需要将问题中的短语“官方语言”映射到名为is_official的列。注意,is_official是布尔类型,我们只能为其赋值truefalse。为了使逻辑正确,我们必须为其赋值true。在主查询中,使用了NOT IN操作符。

为何不直接微调大语言模型?

在开发SQL Bridge时,我的同事最常问的一个问题是:既然Spider排行榜上几乎所有顶级解决方案都基于ChatGPT,为什么不直接微调一个大语言模型并优化你的提示词呢?为了回答这个问题,让我尝试用一些例子来比较GPT-4和SQL Bridge。

我想强调的是,以下例子实际上是有意挑选的,对SQL Bridge有利,因此不要过度解读,它可能有偏差。但我的目的是展示,即使是惊人的GPT-4,包括新发布的GPT-4 Turbo,仍然会犯错。

示例一:错误的列选择
问题来自Spider:“找出在1970年生产了一些汽车的制造商名称。”提示词实际上很长,它来自11月2日前排名第一的解决方案。提示词由两部分组成:首先需要提供数据模式信息,然后它使用额外算法从知识库中查找相似问题。这些相似问题包括自然语言问题及其对应的SQL语句。你可以将其视为一种检索增强的解决方案。然而,GPT-4在这里犯了一个错误,生成了一个不存在的列model_id。正确的列应该是model。这看起来是次要的,但即使你纠正了这个错误,还有一个更棘手的问题:modelmake_id之间没有连接关系,无法进行连接。

SQL Bridge始终保证一致性。实际上,这在文献中并不是一个新想法,有很多工作使用了类似的方法。我们的核心引擎是一个基于Transformer的编码器-解码器,它会生成一些隐藏状态。我们使用一个专用模块来衡量这个隐藏状态与表中各列的相似度。换句话说,我们总是选择一个表中存在的列。新颖性实际上在于我们如何使这个过程变得有原则,这将在后面解释。

示例二:多余的连接和聚合问题
这里只有一个连接,但GPT-4生成了三个连接。还有一个更棘手的问题:GPT-4选择了两个列,第一个列被聚合函数修饰(即聚合器只返回单个元素),但第二个列可能返回多行(一个集合)。这里有一个规则:我们需要将这个规则插入到SQL生成过程中。唯一的问题是如何、在何处以及何时插入。我们有一个基于Transformer的引擎,它有自身的运行机制。我们不希望我们的插入操作破坏引擎。

示例三:聚合函数选择错误
GPT-4为短语“有多少人”生成了COUNT(population),这听起来合理,但实际上是错误的。正确答案应该是SUM,因为population是整数类型,对所有这些整数求和来计算总人数更有意义。SQL Bridge之所以在这种情况下恰好给出了正确答案,是因为我们实际上使用了一个专用的分类模块来处理所有操作符。更重要的是,我们可以手动挑选一些手工设计的特征。例如,在这里,我们可以使用类型信息来增强这个非常简单的分类模块。

事实证明,使用专用模块来处理抽象概念的方法非常强大。每当我们遇到难以生成的概念时,我们有两个选择:要么使用一些相似样本重新调整模型(这需要一些手动标注工作),要么我们将这个概念升级为由专用分类模块处理的语法的一部分。

SQL Bridge 的核心设计

听众提问:Sequel Bridge是什么?SQL Bridge只是一个内部产品名称。它是一个桥梁,旨在使数据在数据库和机器学习之间流动。它将成为我们数据库核心引擎的一部分,作为接口为客户提供服务,客户可以直接输入自然语言问题,它应自动生成SQL语句并给出答案。我们的核心引擎基于Transformer架构,但通过语法结构进行了增强。我们使用语法来引导SQL生成过程的结构化。关键是如何使这个过程变得有原则。

与传统方法的关键区别在于:我们意识到大多数传统工作似乎准确率不高,这一定有原因。其次,尽管有很多基于语法的相关文献试图生成抽象语法树中的所有标记,但我认为这首先效率不高,其次这不是正确的表示形式。因此,我们改变了语法,引入了一种新的、相对更高层的语法。正如稍后将看到的,它还允许我们并行解析。

一个关键的观察是:目前几乎所有的工作都从一个隐藏状态生成单个标记。在我们的设计中,一个隐藏状态将被传递到一个多任务学习模块,生成多个标记。因此,我们的解析本质上是并行的。

以下是相关工作的概述。基于语法的工作有很多,我们从中学到了很多。但我想强调,我们有一些我们认为具有优势的独特功能。例如,我们实际上不是考虑SQL语法的一个子集,而是考虑一个超集,并依赖后处理来进一步缩减它。更重要的是,这种语法允许我们进行并行多任务学习。

并行递归下降解析器

高级流程被称为并行递归下降解析器。整个过程被分为多轮。每轮包含三个阶段:

  1. 预处理阶段:仅选择先前子查询中已选出的所有列。这是我们引入长距离依赖的方法。
  2. 核心引擎阶段:依赖于Transformer编码器-解码器。编码器只是一个预训练模型(具体是24层的GanBERTa,它是BERT的一个变体)。新颖性实际上来自解码器的设计。
  3. 后处理阶段:解码器只生成分段信息,由语法来组织这些信息。后处理模块可以检查当前使用的产生式规则,并附加关联操作以插入干预。

那么,为什么需要新语法?SQL不是已经有定义良好的上下文无关语法了吗?原因是这个新语法更简单、更高层。它是一个超集,我们依赖后处理来缩减它。重要的是,这种语法实际上允许我们在训练阶段进行多任务学习,在推理阶段可以并行运行多个推理任务,速度更快。

我们从Query开始,它由一个称为CAT(Colon Action Template)的单元操作组成。一个Query就是一个不包含子查询的SubQuery。如果它包含嵌套子查询,我们将使用一个占位符,该占位符将触发另一轮来生成另一个SubQuery。关键的是,我们引入了一个名为CAT的产生式规则,它将被SELECTWHEREGROUP BYORDER BY顺序使用,但不被FROM使用,因为CAT只选择列,而FROM需要使用表。本质上,对于SQL Bridge,整个SQL生成过程可以看作是一系列CAT的序列。

多任务学习

为什么要引入多任务学习?对于每个CAT,它有7个槽位,属于两类:

  1. 第一类:包括聚合函数、DISTINCT关键字、操作符和排序顺序。关键是要观察到只有固定数量的关键字,因此我们使用一个专用的、非常简单的分类模块(在Transformer层之外)来处理它们。
  2. 第二类:列和值。它们有可变数量的项。我们使用排序算法(具体是Pointwise网络)对它们进行排序,找到最佳匹配。

新颖之处在于:所有这些多任务学习任务都在同一个隐藏空间上进行。编码器-解码器网络实际上包含一个全连接Transformer层,后面跟着这个多任务学习模块。解码器将顺序生成隐藏状态,但每个隐藏状态都将被这个多任务学习模块共享。该模块将同时生成多个标记。然后由语法将它们组织在一起。

架构总览与示例

这是一个说明整个过程的简单示例,它分三轮生成一个谓词,每轮共享相同的隐藏状态。这是架构的全局图。编码器实际上只是一个24层的GanBERTa。因为它是微调的,所以即使我们做了一些修改(例如改变了位置嵌入并增加了一些链接嵌入),但这并不重要。因为编码器本质上只是GanBERTa。所以所有的新颖性实际上都来自解码器的设计。左侧是CAT解码器,它只选择列。右侧是FROM解码器,它选择表。中间是连接网络,它只是一个简单的前馈层,后接softmax来选择集合操作符。

这个例子可以进一步说明整个过程。输入肯定包括原始问题和数据库模式,它们被连接起来并通过分词器。预训练编码器将生成一堆隐藏状态作为参考。然后解码器将消费这些参考隐藏状态。具体对于CAT,它将顺序生成多个隐藏状态。例如,第一个隐藏状态生成后,它将输入到我们的多任务学习模块,该模块自动生成avgLifeExpectancyavg来自分类模块(聚合函数),LifeExpectancy来自排序模块。在下一个隐藏状态,我们生成EOS(句子结束),表示SELECT子句完全完成。在下一个状态,我们生成WHERE,实际上生成了一个NEST_TURN标记,意味着它将触发另一轮整个过程来找到一个子查询。注意,WHEREGROUP BYHAVINGORDER BY每个子句都包含一个EOS,意味着所有这些子句都是空的。现在我们有了外部查询。我们可以收集这两个列并进行连接。重复整个过程,我们生成子查询,然后用这个子查询替换外部查询中的占位符。现在我们得到了完整的查询。

实验评估与讨论

我们在Spider的公共测试集上进行了实验。注意,此表不包含私有测试数据的结果。对于困难和超难问题(意味着它们包含连接和嵌套查询),SQL Bridge有明显的改进。除了公共SQL,我们还请合作者在他们的私有数据上进行了测试。结果如下。

听众提问:私有数据集中的文字是英文还是其他语言?这有影响吗?实际上是中文。我们的合作者用中文测试。GPT-4似乎在中文上表现也不错,但可能不如英文好。这里你可能想知道为什么只测试了GPT-3.5 Turbo。原因是我们确实测试了GPT-4,但不知为何它从未完成。所以他们只向我们报告了这个结果。

听众提问:你们是否计划在BirdBench等更全面的基准上评估?我认为到目前为止,SQL Bridge可能因为训练集不大而表现良好。例如,Spider提供了很多指导。如果我们有更大的数据集,覆盖语法的不同方面和不同领域,可能会更好。我们确实观察到一个现象:一年前我们在Spider和一些其他公开数据集上测试,发现与私有数据存在分布偏移。所以如果有更多数据集,确实会非常有帮助。


第二部分:Shapley IQ - 运维根因分析系统

我选择第二个主题是因为它使用了所谓的因果分析,而干预实际上被广泛用于因果分析。

背景与动机

云数据库通常采用所谓的微服务架构。对于微服务架构,要进行根本原因分析,通常由实体和KPI指标触发。例如,这里只是一个延迟。在底层,我们有某种异常检测算法来触发根因分析。目标通常是量化因果图中所有因素对异常KPI的影响,并找出最可能导致此异常的因素。例如,在这个案例中,红色框(因幻灯片来自真实内部系统,包含中文字符)标识了根本原因。

我们为什么关心根因分析?曾经,我们的云平台系统发生了一次真实故障。突然之间,大量操作停止响应。系统每分钟生成数千条追踪记录,每条追踪包含数百个跨度,更不用说我们有数千台机器,每台机器可以有多个指标,包括CPU、内存和等待线程数。因此,自动化诊断非常重要。

构建因果图

那么,什么是微服务系统中的因果图?为了回答这个问题,首先需要为上下文传播引入一个工作定义,即编织来自各个节点的测量(包括指标和日志),并通过为请求附加唯一ID来收集分布式追踪记录,当该请求遍历微服务系统时。这是一个真实的例子,非常直观且不言自明。

但关键部分是观察到实线代表同步调用,虚线代表异步调用。例如,D1调用数据库,然后是Op P1进行后处理。延迟实际上等于这两个时间段之和。然而,对于Span1,它是D2的父跨度,它们并行运行。延迟实际上由关键路径决定,关键路径定义为Span1D2中延迟的最大值。这就是我们所说的max-plus代数。

事实证明,对于微服务系统,我们可以直接基于RPC调用关系推导出因果图。然而,这并不完整。这里我们列出了在云数据库上观察到的六类问题。除了延迟,我们还有利用率、资源利用率以及一些额外事件(例如软件升级)。因此,需要领域专家将一些额外因素添加到因果图中。因为不同的因果图会导致不同的根本原因。

假设我们决定将CPU利用率加入这个因果图。我们需要一个函数来刻画其与延迟等其他因素的关系。一种方法是使用领域特定模型,例如这里的排队论模型,多项式公式来自排队论。

Shapley IQ 框架

这是我们的Shapley IQ框架。它由前向传播和后向传播组成。前向传播评估各种反事实。什么是反事实?反事实就是一个假设性问题:如果将一个子集的因素从正常变为异常,你会观察到什么?然后,在后向传播中,我们收集所有这些评估并进行总结,以计算每个因素的影响分数。这里我们使用带有新拆分不变性的Shapley值。

对于前向传播,我们需要为每个因素子集定义一个价值函数。价值函数实际上是当该集合中的所有因素从正常变为异常,而所有其他因素保持正常时,KPI的变化。这里有一个例子:假设P1从2秒变为3秒,增加了1秒。使用max-plus代数,我们可以计算其对端到端延迟的影响,实际上也等于1秒。所以价值函数V({P1}) = 1。类似地,我们可以推导出{P1, D1}{D2}{D1, D2}的价值函数。

接下来我们进行后向传播。在这一步,我们需要收集前向传播的所有评估并进行总结。假设我们计算P1的影响,我们需要枚举所有因素的排列。这里有四个因素,总共有24种排列。现在,假设对于每一种排列,我们想象P1坐在一条线上。所有坐在我前面的人,我们称其为集合S。边际变化定义为将我加入这个集合前后的价值函数之差。对于第一行,P1是第一个人,前面没有人,所以边际变化等于V({P1}) - V({})V({P1})根据上一张幻灯片等于1,V({})自然定义为0。所以第一行的边际变化等于1。为每一列重复整个过程,然后取平均值,我们计算出P1的影响。现在我们可以为每个因素重复整个过程。

我想强调的是,这只是一个概念性说明。因为这个概念性说明具有指数级计算复杂度。我们实际上有一个通过利用单调性而高效得多的算法,复杂度为O(N log N)。但为了易于理解,我们暂时使用这个概念性说明。

Shapley值与扩展

事实证明,上一张幻灯片描述的过程恰好给出了Shapley值,它在以下三个著名公理下是唯一的。然而,这还不够。因为传统的Shapley值只描述离散因素,但这里我们有一个跨度,每个跨度代表一个连续的时间段,而一个时间段可以进一步划分为子区间。因此,我们引入了一个称为拆分不变性的属性。此外,我们还需要考虑因果关系。在这条研究线上,Shapley是第一个将Shapley值用于可解释机器学习的工作,但它没有考虑因果关系。后续工作称为非对称Shapley值,尽管它考虑了因果效应,但不满足拆分不变性。

这里有一个例子说明拆分不变性。假设操作C等于D在一个循环中。在右侧,你会看到影响力随着循环次数的增加而不断减小。但直观上,如果整个跨度的总长度保持不变,那么它应该给出相同的影响力。非对称Shapley值与我们的直觉不一致。

性能与总结

我们的系统准确且运行非常快,比基于神经网络的方法快一个数量级。


总结

AI for DB确实是一个非常大的话题。我们的团队主要专注于库内机器学习,包括NL2SQL和可加载函数、库内推理以及运维,包括调度、异常检测、根因分析和参数调优。

我相信,今天讨论的所有细节都会被遗忘,但我希望其精神能够传达。其中一个关键信息是:将低层级控制精确地插入到正确的位置、正确的时刻是可能的(尽管不是必须的),这可以使通用人工智能效率更高,有时甚至更有效。小模型也可以很强大。我个人相信,这种设计哲学具有很大的实用价值,特别是在存在分布偏移的低资源场景中。


问答环节摘要

  • 关于查询复杂度:SQL Bridge可以处理嵌套查询,但深度通常取决于训练数据。如果训练数据中嵌套通常是两到三层,那么模型(即使有语法增强)很少会生成更深的嵌套。但我们可以通过重写规则将嵌套查询转换为连接查询,这可以自动纳入系统。
  • 关于语法覆盖范围:目前的语法是SQL的一个超集,主要优先满足企业客户(如Hologres)的需求,它支持部分关键字。因为是超集,它有很大的灵活性来插入新规则,可以逐步完善以支持完整语法。
  • 关于大语言模型的过度生成:即使我们在提示词中提供了少量示例,GPT-4有时不仅从少量示例中学习,还从其自身先前的经验中学习,可能会生成目标系统不支持的函数,这是一个需要解决的问题。
  • 关于嵌套CASE语句等复杂结构:这是当前正在攻关的关键问题。目前提供的解决方案是基于单个问题。对于跨多个问题的长距离依赖,我们有一个内部演示(非官方产品),它基于大语言模型并使用不同的算法进行总结。
  • 关于剩余的20%错误:错误分析显示,部分错误甚至存在于Spider训练数据集中(逻辑错误)。改进方法有两种:要么提供更多相似样本来增强薄弱环节,要么将一些广泛使用但难以生成的概念升级为我们分类器的一部分。将概念升级到简单而鲁棒的分类模块,对我们来说是一个非常强大的方法。

011:时尚分层可导航小世界索引

卡内基梅隆大学的“机器学习与数据库”系列研讨会是在现场观众面前录制的。本节目由谷歌以及像您这样的观众的贡献提供资金支持。

欢迎回到卡内基梅隆大学的另一场研讨会。今天我们非常高兴地邀请到亚马逊的首席产品经理 Jonathan Katz,他专注于 PostgreSQL 相关工作,是 PostgreSQL 的核心团队成员和主要贡献者,因此他非常了解 PostgreSQL 的内部原理。他今天来到这里,是为了向我们介绍 pgvector 项目,他是该项目的第二大提交者,拥有惊人的提交记录。

概述

在本节课中,我们将学习 pgvector,这是一个用于 PostgreSQL 的向量存储和相似性搜索的开源扩展。我们将探讨为什么向量数据库在当今的生成式 AI 和大语言模型时代变得至关重要,深入了解 pgvector 的工作原理、其支持的索引算法(IVFFlat 和 HNSW),并讨论性能、存储和实际应用中的权衡。课程最后将展望 pgvector 的未来发展路线图。

为什么需要向量数据库?

过去一年,随着生成式 AI 和大语言模型的兴起,世界发生了巨大变化。我们需要存储这些系统的输出并能够快速查询它们。PostgreSQL 的强大之处在于其可扩展性,pgvector 正是通过添加向量功能来扩展 PostgreSQL 的一个例子。

为了理解其重要性,我们首先需要了解为什么数据库需要支持向量。传统的关系数据库处理整数或文本数据,但现代 AI 应用需要处理高维向量数据。

基础模型与检索增强生成

基础模型是经过海量数据训练的大型机器学习系统,能够根据自然语言问题生成自然语言回答。然而,这些模型通常基于公开数据训练,可能无法访问特定于企业或组织的私有数据。

检索增强生成 是一种技术,它允许我们为基础模型提供额外的上下文信息。例如,假设我有一个产品数据库,当用户询问“蓝色大象花瓶多少钱?”时,RAG 系统会从数据库中检索相关信息(如价格),并将其作为上下文提供给基础模型,从而生成准确的答案。

向量嵌入

实现 RAG 的关键在于 向量嵌入。向量是数据的数学表示。通过将文本、图像或视频等信息输入嵌入生成器(通常是基础模型的一部分),我们可以得到一个向量。这个向量以一种通用的方式表示了原始信息,使我们能够将其用于查询或输入到其他模型中。

以下是 RAG 的典型工作流程:

  1. 文档分块与嵌入生成:将原始文档(如 PDF)分割成文本块,然后使用嵌入模型(如 Amazon Titan Embeddings)将每个文本块转换为向量。
  2. 向量存储:将生成的向量及其对应的原始文本块存储到数据库中(例如 PostgreSQL 配合 pgvector)。
  3. 用户查询:当用户提出问题时,同样使用嵌入模型为该问题生成一个查询向量。
  4. 向量相似性搜索:在数据库中对查询向量执行 最近邻搜索,找到与问题最相关的文本块向量。
  5. 答案生成:将检索到的相关文本作为额外上下文,连同原始问题一起提交给大语言模型,生成最终答案。

这个工作流程的强大之处在于,它通过基本的向量数据类型和相似性搜索,为大型语言模型注入了额外的知识。

向量数据带来的挑战

向量虽然概念简单,但在大规模处理时面临诸多挑战:

  1. 生成耗时:为每个文本块生成向量嵌入需要经过机器学习算法处理,这需要时间。无法为每次查询实时生成所有数据的嵌入,因此需要存储。
  2. 存储空间大:现代嵌入向量的维度很高(例如 1536 维)。一个 1536 维的浮点数向量(每个维度 4 字节)约占 6 KB。存储 100 万个这样的向量就需要约 5.7 GB 的原始存储空间,这还不包括索引开销。
  3. 压缩困难:向量由一系列看似随机的浮点数组成,缺乏可压缩的模式。实际上,尝试压缩有时反而会增加存储开销。
  4. 查询计算量大:比较两个向量需要计算它们之间的距离(如欧氏距离、余弦距离)。这需要对每个维度进行计算。对于一个有 N 个向量的数据集,进行精确的最近邻搜索是一个 O(N²) 复杂度的问题,非常耗时。

近似最近邻搜索

为了解决精确搜索的性能问题,研究人员开发了 近似最近邻 算法。与精确搜索必须检查数据集中每个向量不同,ANN 算法只搜索一个缩小的数据集,从而以更快的速度返回“足够好”的结果。

这带来了一个关键的权衡:召回率。召回率衡量的是返回的结果中有多少是真正相关的。例如,如果你想找最近的10家咖啡店,ANN 算法可能只返回了8家最近的咖啡店和2家茶店,那么召回率就是 80%。在 RAG 等应用中,近似结果通常是可接受的,但需要根据应用场景权衡速度与召回率。

除了性能与召回率,在实际应用中还需考虑:

  • 存储:数据是常驻内存还是存储在磁盘上?
  • 成本:为极致性能付出的硬件成本是否值得?
  • 相关性:对于近似搜索,返回结果的相关性可能不如精确搜索稳定,需要纳入考量。

PostgreSQL 作为向量存储

向量本质上是一种数据类型,任何具有存储和处理能力的系统都可以存储向量,PostgreSQL 也不例外。

选择 PostgreSQL 作为向量存储有多个原因:

  • 开源与成熟:PostgreSQL 已有超过 35 年的历史,拥有强大而稳定的社区。
  • 丰富的数据类型和索引支持:PostgreSQL 支持多种数据类型(如范围类型)和高效的索引,简化了应用开发。
  • 强大的可扩展性:PostgreSQL 的核心设计理念就是可扩展性。如果缺少某个功能,可以通过扩展(如 pgvector)来添加,而无需分叉或修改核心代码。

从开发者角度看,使用 pgvector 的优势包括:

  • 无缝集成:可以轻松添加到现有 PostgreSQL 实例中,与现有工具链和驱动程序兼容。某些驱动程序(如 JDBC)还支持二进制向量格式以提高效率。
  • 数据共置:可以将事务性数据与机器学习向量数据存储在同一个数据库中,便于使用 联接 等强大功能。
  • 灵活的架构角色:PostgreSQL 既可以作为核心的向量处理和存储系统,也可以作为上游专用向量处理系统的持久化存储层。

pgvector 简介

pgvector 是一个用于 PostgreSQL 的开源扩展,支持向量存储和搜索。其核心是提供了一个 向量数据类型,并在此基础上实现了高效的索引和搜索能力。

主要特性包括:

  • 两种索引类型:IVFFlat 和 HNSW,用于加速近似最近邻搜索。
  • 搜索支持:既支持精确最近邻搜索,也支持近似最近邻搜索。
  • 元数据存储:向量可以与任何其他元数据(如原始文本块、产品信息)一起存储。
  • 多种距离算子:支持欧氏距离 (L2)、余弦距离 (cosine) 和内积 (inner product) 等多种相似性度量方式。

距离算子说明

  • 欧氏距离:衡量向量之间的直线距离。
  • 余弦距离:衡量向量之间的角度差异。
  • 内积:结合了向量大小和方向的一种度量。

索引与向量归一化

在深入索引之前,需要了解 pgvector 在索引时会对向量进行 归一化。归一化是将向量的幅度(长度)设置为 1 的过程。公式表示为:对于一个向量 v,其归一化向量 u = v / ||v||,其中 ||v||v 的欧氏范数(幅度)。

归一化的重要性在于,对于某些距离计算(如余弦距离),如果向量的幅度为 1,可以消除公式中的除法运算,减少 CPU 计算开销。由于向量比较需要遍历所有维度,任何能减少单次比较开销的优化都能显著提升整体性能。

IVFFlat 索引详解

上一节我们介绍了索引的基本概念和归一化。本节中,我们来看看第一种索引算法:IVFFlat。

IVFFlat 是一种基于聚类的索引方法。

  1. 构建过程
    • 首先,使用 k-means 算法在向量空间中确定一定数量(由 lists 参数指定)的聚类中心。
    • 然后,将每个向量分配到离它最近的聚类中心所在的“列表”中。
  2. 查询过程
    • 给定一个查询向量,先找到离它最近的若干个(由 probes 参数指定)聚类中心。
    • 然后只在这几个中心对应的列表中进行精确搜索,找出最近的向量。

关键参数

  • lists:聚类中心的数量。影响索引构建和插入速度。
  • probes:查询时访问的列表数量。影响查询速度和召回率。

特点与权衡

  • 构建:需要数据集已存在或足够大才能确定好的聚类中心。支持并行构建,速度较快。
  • 插入:插入时间受 lists 数量限制,需要计算新向量到所有中心的距离。
  • 查询probes 值越小,查询越快,但召回率可能越低。数据分布随时间变化后,可能需要重建索引以保持效果。
  • 适用场景:适合需要快速构建索引,且对查询延迟要求不是极致的场景。

pgvector 已支持 IVFFlat 索引的并行构建,可以显著提升大数据集上的索引创建速度。

HNSW 索引详解

接下来,我们探讨第二种索引算法:HNSW,它在查询性能和召回率方面通常表现更优。

HNSW 是一种基于图的索引方法。

  1. 构建过程
    • 算法以层级结构构建一个图。上层是稀疏的图,用于快速导航;下层是稠密的图,包含详细的连接关系。
    • 插入一个新向量时,算法会从上层开始,找到该层的最近邻,然后逐层向下,最终在底层找到其最近邻,并建立连接(边)。边的数量由参数 m 控制。
  2. 查询过程
    • 同样从上层开始,快速定位到大致区域,然后逐层向下搜索,最终在底层找到最近邻。查询时检查的候选向量数量由参数 ef_search 控制。

关键参数

  • m:图中每个节点最大连接数(出边)。影响图的密度和构建时间。
  • ef_construction:构建索引时,为插入每个节点而搜索的候选邻居数量。影响索引质量和构建时间。
  • ef_search:查询时搜索的候选向量数量。必须大于或等于查询中要求的邻居数量 (LIMIT)。影响查询速度和召回率。

特点与权衡

  • 构建:支持增量构建,可以从空表开始。构建时间比 IVFFlat 长,尤其是当 mef_construction 较大时。pgvector 0.6 版本将支持并行构建。
  • 查询:通常能提供比 IVFFlat 更好的查询性能与召回率平衡。通过图的层级结构,可以快速跳过不相关的区域。
  • “默认配置友好”:pgvector 为 HNSW 提供的默认参数 (m=16, ef_construction=64, ef_search=40) 在多数情况下能提供良好的效果。
  • 适用场景:适合对查询延迟和召回率要求高,且可以接受较长索引构建时间的场景。

参数调优建议

  • 想提升召回率时,首先尝试增加 ef_construction,这对构建时间影响相对较小。
  • 如果仍需提升,再考虑增加 m,但这会显著增加构建时间。
  • 更高的 ef_construction 通常允许你在查询时使用更低的 ef_search,从而获得更快的查询速度。

存储考量与性能优化

在讨论了两种核心索引算法后,我们需要关注一个影响性能的基础层面:向量在 PostgreSQL 中的存储方式。

PostgreSQL 使用 TOAST 机制来存储超过页面大小(通常为 8KB)的大字段。一个 1536 维的向量(约 6KB)默认会被 TOAST 存储。

TOAST 对向量查询的影响

  • 热点数据外置:向量是相似性搜索的“热点”数据,但 TOAST 将其存储在独立于主表的空间中。查询时需要额外的跳转。
  • 并行查询计划受影响:PostgreSQL 的查询计划器在估算 TOAST 表数据的扫描成本时可能不准确,导致为包含 TOASTed 向量的全表扫描计划分配更少的并行工作进程,影响扫描性能。

优化策略

  1. 使用 PLAIN 存储:通过 ALTER TABLE ... ALTER COLUMN ... SET STORAGE PLAIN; 将向量列设置为行内存储。但这会将向量维度限制在 2000 以内(因为要保证单行不超过页面大小)。
  2. 调整并行扫描阈值:调低 min_parallel_table_scan_size 参数,可以促使规划器为涉及 TOASTed 向量的扫描分配更多并行工作进程。
  3. 确保足够的内存:将 shared_buffers 设置得足够大,以便将更多的索引和数据页缓存在内存中,这对性能至关重要。

过滤与索引结合

在实际应用中,我们经常需要在向量搜索的基础上增加元数据过滤条件(WHERE 子句)。那么,能否在利用向量索引的同时进行过滤呢?答案是肯定的,有以下几种方法:

以下是几种实现过滤的策略:

  1. 部分索引:在创建索引时使用 WHERE 子句,只为满足特定条件的数据子集建立向量索引。
    CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops) WHERE category_id = 1;
    
  2. 表分区:按照自然的分区键(如类别、时间范围)对表进行分区,然后只在需要的分区上创建向量索引。
  3. 即将到来的 HQANN:这是一种创新的多列索引方法,正在 pgvector 项目中开发。它可以在构建 HNSW 图时,考虑元数据属性(如类别ID),从而在查询时高效地同时利用向量相似性和属性过滤。这有望提供更高的召回率。

硬件选择与性能数据

硬件配置对 pgvector 的性能有显著影响。例如,在 AWS 上,使用更强大的 Graviton3 处理器与 Graviton2 相比,不仅在基础性能上有提升,在处理高 CPU 负载的查询(如提高 ef_search 值)时,性能提升更为明显。

性能数据示例:在一个包含 1000 万条 1536 维向量的数据集上,使用 HNSW 索引,pgvector 能够实现可观的每秒查询数。这表明通过优化的算法(如 HNSW),在 PostgreSQL 中实现低延迟的向量搜索是可行的。

未来发展路线图

pgvector 项目正在快速发展,未来计划包括:

  • HNSW 并行构建:已在 0.6 版本提交,将加快大规模索引的创建速度。
  • HQANN 索引:支持带过滤的高效向量搜索。
  • 更多数据类型:支持 float2 (FP16)、uint8 等更小的浮点或整数类型来存储向量,以减少存储空间和内存带宽占用,但可能会损失一些精度(量化)。
  • 产品量化:一种更高级的向量压缩技术,能大幅减少向量大小,但会对召回率产生一定影响。
  • 并行查询:进一步提升大规模数据集上的查询吞吐量。

总结与建议

本节课中,我们一起学习了 pgvector 扩展,它使 PostgreSQL 成为一个强大的向量数据库。我们探讨了向量数据库的必要性、RAG 工作流程、向量数据面临的挑战,并深入研究了 pgvector 的两种核心索引算法 IVFFlat 和 HNSW。

回顾一下,在选择和使用 pgvector 时,请记住以下要点:

  • 理解召回率与性能的权衡:这是近似最近邻搜索的核心。HNSW 算法在此方面表现优异。
  • 根据需求选择索引:需要快速构建选 IVFFlat;追求高查询性能和召回率选 HNSW。
  • 关注存储和内存:合理配置存储模式 (PLAIN/EXTERNAL) 并确保充足的 shared_buffers
  • 利用过滤技术:使用部分索引、分区或未来的 HQANN 来优化带过滤条件的向量查询。
  • 硬件配置很重要:CPU 和内存资源直接影响性能。
  • 拥抱快速演进:向量数据库领域发展迅速,pgvector 也在持续创新。选择像 PostgreSQL 这样成熟稳定的平台,配合 pgvector 这样活跃的扩展,可以兼顾当前的生产需求与未来的技术演进。

012:面向大型语言模型的检索系统

在本节课中,我们将学习色度向量数据库,这是一个专为大型语言模型设计的端到端检索系统。我们将探讨其设计原理、核心架构、面临的挑战以及未来的发展方向。

概述:大型语言模型与检索增强生成

上一节我们介绍了向量数据库的背景,本节中我们来看看色度数据库如何解决大型语言模型检索中的具体问题。

大型语言模型本质上是参数化系统,它们将知识存储在模型参数中。这带来了几个问题:无法实时更新知识、无法确定性地修改知识、无法提供知识来源的证明,并且容易产生“幻觉”(即生成看似合理但不准确的信息)。

为了解决这些问题,研究者提出了检索增强生成技术。其核心思想是结合参数化记忆(语言模型本身)和非参数化记忆(外部文档数据库)。具体流程如下:

  1. 给定一个查询。
  2. 从一个文档语料库中检索出相关的文档子集。
  3. 将查询和检索到的文档一起输入给语言模型。
  4. 语言模型基于这些上下文信息生成更准确、更具依据的答案。

这种方法的有效性很大程度上依赖于检索步骤的质量。

密集向量相似性搜索

以下是实现高效检索的关键步骤:

  1. 文档处理:将原始文档分割成较小的文本块,以适应嵌入模型的输入窗口。
  2. 嵌入生成:使用一个学习得到的嵌入模型,将每个文本块映射为一个高维向量。
  3. 索引构建:为这些向量构建索引,通常结合向量索引和传统的标量索引(如全文搜索)。
  4. 查询与重排序:对于查询,同样将其转换为向量,在向量索引中进行相似性搜索,并可能使用学习型重排序模型对结果进行精炼。

这个流程包含许多可调节的环节,开发者需要花费大量时间进行调优,这也是色度数据库希望简化的部分。

色度数据库的目标与设计理念

我们的观点是,市场上应该存在一种工具,让开发者无需孤立地思考这些环节,而是能够构建端到端的系统,轻松地完成所有步骤,并随着时间的推移对整个系统进行迭代优化。

色度数据库针对的工作负载具有以下特点:

  • 查询量适中:通常为每秒100到1000次查询。
  • 数据实时性高:数据延迟容忍度在100毫秒级别,需要支持频繁的更新和删除操作。
  • 数据规模中等:单个索引通常包含1百万到1亿个向量。
  • 索引数量巨大:可能为每个用户创建一个索引,导致系统需要管理海量索引。
  • 向量维度高:处理的向量维度通常在768到4096之间,远高于传统向量搜索中的96-128维。
  • 召回率要求高:因为检索结果用于指导语言模型生成,低召回率会导致模型缺乏必要上下文或引入错误信息。

核心索引技术:结合HNSW与优化倒排索引

传统的近似最近邻索引主要有两类:倒排索引邻近图。常见的HNSW算法虽然搜索高效,但作为图结构,其磁盘访问模式随机,通常需要全内存存储,且删除操作会降低图的质量。传统的倒排索引结合乘积量化则存在召回率低、需要过度查询、难以处理实时数据导致聚类中心漂移等问题。

色度数据库的索引结合了这两种方法的优点:

  1. 分层存储:将数据分为实时段和历史段。实时数据索引到内存中的HNSW图;历史数据则压缩到使用优化倒排索引的磁盘存储中。
  2. 优化倒排索引:我们采用了微软SPTAG论文中的多项技巧:
    • 闭包聚类:将边界点分配给多个聚类中心,显著提高召回率。
    • 邻近图规则:避免在重叠聚类中重复存储点。
    • 动态剪枝:根据查询动态决定需要访问的聚类中心数量。
  3. 8D采样:利用高维分析中的Johnson-Lindenstrauss引理,在距离比较时,仅使用向量的一个随机子集进行初步判断,从而减少大量不必要的全维度距离计算。

这种混合架构在保证查询性能的同时,实现了存储与计算层的分离,带来了更好的开发体验和运维灵活性。

分布式系统架构与协议

接下来,我们将从单节点扩展到分布式架构,看看色度如何保证系统的安全性和活性。

单节点色度包含前端服务器、元数据索引和向量索引。在分布式版本中,这些组件被解耦为独立的服务:协调器、前端服务器、索引/查询节点以及一个作为分布式日志的Kafka集群。

读写路径如下:

  • 写路径:写入请求先进入分布式日志,索引节点消费日志并增量构建索引。当数据段达到一定大小时,会刷新到对象存储(如S3)成为历史段。
  • 读路径:前端服务器根据哈希方案确定活跃段和历史段的位置,并将查询路由到相应的节点。

我们设计了关键协议来保证分布式环境下的正确性:

  1. 路由信息传播协议:确保在数据段分裂或节点变更时,查询总能获得完整且最新的数据视图。该协议通过拉取推送协调机制,利用单调递增的纪元号和任期号来同步所有节点状态。
  2. 主题重配置协议:为了支持海量集合并突破Kafka主题数量限制,我们需要将多个集合复用到一个主题上。当系统扩容需要重新分配集合到主题时,该协议能保证消息的全局有序性,防止因重配置导致数据重复或丢失。其核心思想是让生产者在停止向旧配置生产前发送“栅栏”消息,消费者在收到所有相关生产者的“栅栏”消息后,才能安全地提交旧配置下的数据并切换到新配置。

我们的理念是,分布式系统关乎在最小化同步的前提下达成共识。因此,我们使用TLA+等形式化验证工具,在实现之初就确保协议设计的正确性。同时,在单节点层面,我们也广泛采用基于属性的测试和模型检查,以构建坚实的基础。

未来路线图:走向端到端检索系统

向量搜索只是检索系统的一部分。构建基于语言模型的应用程序涉及三大组件:逻辑链数据反馈评估

色度数据库的路线图旨在扩展API,成为一个更完整的端到端系统:

  1. 支持用户自定义函数:允许用户注入自定义的文本分块、嵌入生成、索引和重排序逻辑。
  2. 集成反馈与评估:系统可以直接存储用户对检索结果和生成结果的反馈。这些反馈可用于微调嵌入模型、训练适配器、或构建更好的重排序和过滤机制。
  3. 优化分块策略:探索使用语言模型本身或更高级的方法,在数据注入时自动生成最优的文本分块策略。
  4. 实验不同的重排序模型:允许用户针对特定查询和任务,尝试不同的重排序模型,以获得最相关的结果。

总结

本节课中我们一起学习了色度向量数据库。我们首先分析了大型语言模型在知识检索方面面临的挑战,以及检索增强生成技术的必要性。接着,我们深入探讨了色度数据库的设计目标,即针对实时、多索引、高维向量的特定工作负载。我们详细介绍了其核心的混合索引技术,它巧妙地结合了HNSW和优化倒排索引的优点。然后,我们了解了其分布式架构如何通过精心设计的协议来保证数据一致性和系统可扩展性。最后,我们展望了色度未来将如何超越单纯的向量数据库,演进为一个支持全流程实验、优化和反馈的端到端检索系统。

posted @ 2026-03-26 01:39  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报