✏️ 作者介绍:
周充,格像科技后端工程师
需求背景
根据格像科技公司的业务需求,我们需要搭建一个近似最近邻(Approximate Nearest Neighbor,即 ANN)搜索引擎,以便将在线向量相似搜索功能应用到公司其他业务中。我们搭建的 ANN 搜索引擎需要满足以下几个要求:
- 具备高可用性。
- 与公司现有的运维、监控和警报系统兼容。
- 可以水平扩展,在流量高峰时保证性能。
- 不同业务的索引资源相互隔离。
- 只需要每日一次更新业务数据即可。
最终我们基于 Milvus 搭建了 ANN 搜索引擎,实现了上述需求。
基础系统
目前公司已有基础系统如下:
- 面向服务的架构(SOA,Service-Oriented Architecture)框架——用于构建微服务架构。功能包括:服务注册,服务发现,访问路由,负载均衡,熔断限流等。主要支持 Java Web 应用,提供功能和 Dubbo 基本一致。
- 运维平台——将 Docker 镜像部署在 k8s 集群中的唯一入口。
- 监控和警报系统——收集各个服务应用中的调用链、日志、指标等信息,集中管理监控和警报。
上述系统环境便于部署无状态的 Java Web 应用,还拥有完整的监控报警覆盖。并且,通过增加 k8s Pod 扩容,能够保障系统的性能和可用性。
为了赋予 ANN 搜索引擎相同的向量相似搜索能力,我们选择在 Milvus 和现有的基础系统之间增加一个中间层,从而将 Milvus 强大的向量相似搜索功能移植到我们的系统之中。
实现方案
3.1 单个节点
我们考虑通过如图所示方式来实现单个节点的功能。我们在一个 Docker 镜像中会启动两个进程——Milvus 进程和 Java SOA 进程。Java SOA 进程本身是一个 Java Web 应用,类似一个代理(proxy),会将相似搜索的请求转发给 Milvus 进程,并返回搜索结果。
虽然 Milvus 本身已经提供了一整套简单直观的 API 来支持线上服务,但是为了将 Milvus 更好地接入我们现有系统体系,我们仍然选择将 Java SOA 作为中间层,从而将成本降至最低。
除此之外,Java SOA 进程还具备管理数据更新的功能。我们业务中的各项数据均可离线更新,而 Java SOA 进程则会在收到数据更新的消息后,从阿里云对象存储(OSS,Object Storage Service) 上下载打包好的数据,并导入 Milvus。
我们选择用 SQLite 作为 Milvus 的元数据(Metadata)管理数据库。Java SOA 从阿里云 OSS 上下载的是一个完整的索引(Index)文件,放到指定目录之后会通过更新 SQLite 的方式将数据导入 Milvus。
3.2 复制节点
为了实现 ANN 搜索引擎系统的高可用性,我们需要更多其他的副本节点来提供相同的向量搜索服务。实现方案如下图所示:
我们的实现方式与其他数据库(如 MySQL、Redis 等)有所不同。不同之处在于我们并非通过数据同步的方式提供多个“复制状态机”,而是仅需通过离线方式保证所有节点在同一时间更新全量数据即可。
写入数据的时间点有两个。首先是在一个新的节点启动时写入数据。这个新的节点会去 OSS 上下载最新打包好的数据,并导入 Milvus 向量相似搜索库中。另一个时间点是在离线任务生成数据后,各节点会收到“消息”通知,去 OSS 上下载最新的数据来进行更新。
根据 CAP 原则 ,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),这三个特性中最多只能同时实现两个,不可能三者兼顾。为实现多个节点的数据一致性,我们采取了以下手段来确保每个客户端的访问在时间维度上是线性一致的。
首先,所有的数据更新并不会直接覆盖原来的数据表(Collection),而是写入一个新的分区(Partition)。客户端在发起向量搜索请求时,会带上最新的分区名称。如果某个节点上的新数据已经完成加载,会返回最新分区中的搜索结果。否则,就返回错误,让客户端重试其他节点,以此保证某一客户端在得知最新的分区名称后,只会访问到最新的数据。
为了系统性能可以在一定程度上牺牲数据一致性。比如,发现陈旧数据也不进行报错,或者客户端在成功访问到最新数据后才会对陈旧数据报错。上述情况都可以通过 Java SOA 进程根据业务配置选择对应的规则实现。
3.3 多个索引
由于线上不同业务使用的索引不同,并且不同业务之间的资源也是相互隔离的,我们需要通过如图所示的多个索引方式来实现构建 ANN 搜索引擎。多个索引方式就是在每个节点上只维护一个数据表中的数据。客户端在访问索引时,会通过元数据服务找到索引名称的 SOA 地址和版本,然后访问对应的节点。
3.4 数据分片
单个节点索引的性能和资源必然存在上限,对于更庞大的索引,我们应该将其分片存储,以分散计算和存储的压力。这一部分的思想和 Mishards 相同。如图所示,我们将数据存储在多个分片节点上,并行获取搜索结果后再聚合得到最终结果。不过这里我们将聚合的计算放在客户端实现,对于 ANN 节点来说不需要做特殊的处理。
总体架构
搭建的 ANN 搜索引擎中包含以下五个角色,我们一一详细介绍五个角色的作用以及角色之间的相互关系:
- ANN Client
ANN Client 是其他服务访问 ANN 搜索引擎的入口。ANN Client 会从元数据管理服务上获取对应的信息,并在搜索向量时路由到对应的节点。
- 服务注册中心
通过 SOA 框架实现。所有的 ANN 节点都会在注册中心上注册自己的版本,并可以通过 SOA 路由访问这些节点。
- 元数据管理服务
用于存储 ANN 集群的元数据,即 ANN 集群上的 SOA 版本和业务数据表名称、分片的对应关系。将此信息持久存储在数据库中,结构如下表所示:
- ANN 集群
ANN 集群由上文提到的节点组成,每个节点只维护一份数据表数据,也存在对应的副本或者其它数据分片。
- 离线数据更新系统
我们使用阿里云的 ODPS 作为数据仓库和离线任务的运行系统,将对应的向量数据从数据仓库中提取出来写入一个离线的 Milvus 节点中,并在它上面构建索引(Index)文件。之后,再把索引文件和对应的元数据打包,上传至阿里云 OSS ,并通知元数据管理服务。之后,ANN 集群中的对应节点会从 OSS 上下载最新的索引文件并更新本地的数据。
五个角色之间的关系如下图所示:
在线部分中,ANN Client 从服务注册中心和元数据(Metadata)管理服务获取到服务和数据的对应关系后,根据业务需要,向对应的 ANN 服务节点发起请求。离线部分则由数据仓库产生每日更新的数据,然后写入一个离线的 Milvus 节点,生成索引文件,打包后上传到阿里云 OSS,并通知 ANN 节点更新数据。
数据的读写流程
5.1 读数据流程
ANN Client 在进行搜索之前,会先从元数据管理服务获取对应分片数据所在的 SOA 地址,即元数据。之后会根据业务需要访问对应的 ANN 节点。如果这个索引是分片的,ANN Client 则会访问索引的全部分片(如下图中的Shard1,Shard2,Shard3),然后汇总结果并返回。
5.2 写数据流程
离线和近实时写入,可以参考上文的离线数据更新系统。
公司的业务暂时没有实时写入数据的需求,而且实时写入数据的方案会面临一些问题。为了保证高可用性,每个分片(Shard)都有多个副本节点,并且可以接受读取请求。因此,写入数据的时候还需要确保实现这几个副本节点之间的数据同步。
Milvus 官方提供的方案是使用网络文件系统。这几个节点在操作系统层面上访问同一个文件,由文件系统保证数据的一致性。但是这个方案需要我们在 K8S 上做一些定制,成本较高。
在理想情况下,我们可能需要实现这些副本节点之间的异步数据同步,例如简单的异步复制或者组成一个 Raft 集群,但是这一方案同样需要大量的额外开发成本。
总结
我们通过开发一个 Java 中间层,将 Milvus 提供的向量相似搜索功能接入目前已有的 SOA 框架中,以实现服务发现、高可用性、水平扩展等功能。然后我们又通过元数据管理服务来组织数据的分片和离线数据更新,从而完成了搭建整个 ANN 搜索引擎所需的链路。
更多 Milvus 用户案例
我的机器人新同事
基于 Milvus 的钓鱼网站检测
相似问答检索——汽车之家的 Milvus 实践
蓝灯鱼 AI 专利检索在 Milvus 的实践
欢迎加入 Milvus 社区
github.com/milvus-io/milvus | 源码
milvus.io | 官网
milvusio.slack.com | Slack 社区
zhihu.com/org/zilliz-11| 知乎
zilliz.blog.csdn.net | CSDN 博客
space.bilibili.com/478166626 | Bilibili