1. 背景
在一些推荐系统、图片检索、文章去重等场景中,对基于特征数据进行 k 近邻检索有着广泛的需求:
- 支持亿级索引的检索,同时要求非常高的检索性能;
- 支持索引的批量实时更新;
- 支持多模型、多版本以灵活开展 ABTest 实验;
- 支持过滤器、过期删除以排除不符合特定条件的数据。
在经过调研后,发现已有的解决方案存在以下问题:
- 在学术界中,已经存在有成熟并开源的 ANN 搜索库,然而这些搜索库仅仅是作为单机引擎存在,而不能作为高性能、可依赖、可拓展的分布式组件为推荐系统提供服务;
- 在业界中,大多数的组件都是基于 ANN 搜索库做一层简单的封装,在可拓展、高可用上的表现达不到在线系统的要求;而对于少数在实现上已经较为成熟的分布式检索系统,在功能上却难以做到紧跟业务发展;
- 而在更新机制上,很多组件都是要么只支持离线更新、要么只支持在线接口更新,无法满足在微信侧小至秒级千数量、大至小时级亿数量的索引更新需求,因此需要可以兼顾近实时更新及离线大批量更新的分布式系统。
基于上述的这些要求以及业内组件的限制,我们借助 WFS 和 Chubby 设计并实现了 SimSvr,它是一个高性能、功能丰富的特征检索组件,具有以下特点:
- 分布式可伸缩的架构,支持亿级以上的索引量,以及索引的并发加速查询,实现了 10ms 以内检索数亿的索引;
- 高性能召回引擎,使用了召回性能极佳的 hnswlib 作为首选召回引擎,大部分请求可在 2ms 内完成检索;
- 集群化管理,集成了完善的数据调度及动态路由功能;
- 多样的更新机制,支持任务式更新及自动更新,同时也支持全量更新与增量更新,跨越秒级千数量到小时级亿数量的索引更新;
- 读写分离的机制,在离线利用庞大的计算资源加速构建索引的同时,不影响在线服务的高性能读;
- 丰富的功能特性,支持轻量 embedding kv 库、单表多索引、多版本索引、过滤器、过期删除等特性。
SimSvr 目前已广泛应用于微信视频号、看一看、搜一搜、微信安全、表情搜索等业务,接下来会阐述 SimSvr 的设计以及如何解决来自于业务的难题。
2. 检索引擎
2.1 引擎的选择
ANN 问题在学术界已被长期研究,并且已有成熟的开源 ANN 搜索库存在,如 nmslib、hnswlib、faiss 等。在 SimSvr 中,性能及集群的存储容量是最主要考量的两个指标,因此选择了以下两个检索引擎:
- 在 ann-benchmarks 中检索性能最好的 hnswlib,能够满足在线服务对召回率及检索耗时的高要求(大于 90% 召回率的情况下,能在 1ms 内完成召回);
- faiss 的 IVFx_HNSWy + PQz 算法,支持将向量压缩 10 ~ 30 倍,能够满足资源有限情况下的高维大数据量的索引要求(亿级索引数据,容纳在内存 64G 的机器上)。
ANN检索引擎效果对比
2.2 巧妙利用资源,提升 50% 的数据容纳量
- hnswlib 是单机检索引擎,在资源使用方面仅考虑了单模型的情况;而 SimSvr 是提供在线服务的组件,一般容纳了多个模型;
- SimSvr 在大部分场景下,拥有读写分离的特点;
基于以上特点,我们在引入 hnswlib 之后,进行了资源整合,使得 SimSvr 单机情况下可以容纳更多的模型索引:
- 极限情况下(以 worker 线程数 80、部署 10 张 2kw 索引量的表为例):
- 现网运营中(以某现网模块(11台实例机器,worker 线程 240)为例):
2.3 点积距离召回率从 62.6% 到 97.8% 的蜕变心路历程
- HNSW 算法在余弦距离表现优秀,但在点乘距离的数据集上存在效果差的情况;
- 点乘距离非度量空间(metric space),不满足三角不等式,距离比较没有传递性;
- 而在论文 Non-metric Similarity Graphs for
Maximum Inner Product Search 中,提到了将点乘距离转换为余弦距离计算的方法,我们将这种方法简称为 ip2cos;
在 ip2cos 距离转换的理论基础上,我们使用看一看视频实时 DSSM 模型进行了实际召回情况的效果对比(64 维、ip 距离、100 万索引数据量,进行 1 万次查询取平均耗时),并见证了 ip2cos 的神奇效果:
2.4 如何使用 faiss 省下 2h 的训练时间并提升 30% 的召回率
- 在
faiss 中增加了 batch kmeans 聚类方法,在保证较好聚类效果的同时大幅加快训练速度。IVF
系类方法训练耗时主要体现在需要从数据中学习 nlist 个聚类中心,对于千万级数据 nlist 的大小在 20 万以上,在 cpu 上使用传统
kmeans 方法训练会非常耗时,下面展示在 128 维、IP 距离、1000 万条数据的情况下 batch kmeans
对训练速度的加速效果:
从结果中可以看到,在相同迭代轮次下,不使用 batch kmeans 的方法训练耗时更长,且没有很好收敛,导致召回率不高。
3. 总体设计
3.1 数据结构 - 为达成一个小目标,需要做出怎样的改变
为了满足单模块多模型的需求,SimSvr 使用了表的概念进行多模型的管理;另外,为支持亿级以上 HNSW 索引的表,并且希望能够并发加速构建索引,我们根据单表的数据情况,将一张表分成了多个 sharding,使得每个 sharding 承担表数据的其中一部分:
tablei 的索引,由 shard0、shard1、…、shardn 构成一份完整的索引数据;而 sect 的数量则决定了表的副本数(可用于伸缩读能力、提供容灾等)。
在 SimSvr 中,我们将一个 shardi_sectj 称之为一个 container,这是 SimSvr 中最小的数据调度和加载单位。
3.2 系统架构 - 如何支撑亿级索引、5毫秒级的检索
SimSvr 架构
- SimSvr 与 FeatureKV 一样,涉及的外部依赖也是三个:
- Chubby:用来保存元数据、路由信息、worker 资源信息等;SimSvr 中的数据协同、分布式任务执行均是依赖于 Chubby;
- USER_FS:业务侧存放原始数据的分布式文件系统,可以是 WFS/HDFS,该文件系统的路径及信息保存在表/任务的元信息中;
- SimSvr_FS:Simsvr 使用的分布式文件系统,用于存放生成的索引文件或者原始的增量数据文件。
- worker
- 负责对外提供检索服务,通过对 Chubby 的轮询检查索引的更新,进而将索引加载至本机以提供服务;
- 每台 worker 负责的数据,由 master 进行调度,worker 根据 master 保存在 Chubby 上的分配信息进行数据的加载/卸载;
- worker 的数据是根据 master 分配得来的,除此之外没有其他状态的差别,因此 worker 是易于扩缩容的。
- master
- 数据调度:通过表的元信息及 worker 状态,将未分配的数据或者失效 worker 上的数据调度给其他有效的 worker;
- 生成路由表:根据 worker 的数据加载情况及状态,生成集群的路由表;
- 感知数据更新:检查表的自动更新目录,若最大数字目录发生了增长,则建一个任务以供 trainer 进行索引的构建;
- master 是一个无状态的服务,通过 Chubby 提供的分布式锁保证数据调度以及路由生成的唯一执行。
- trainer
- 负责构建表的索引及资源回收;
- trainer 单次可构建一张表中一个 sharding 的索引,因此如果表有多个 sharding 时,可通过增加 trainer 的个数实现构建索引的并发加速;
- trainer 是无状态的服务,通常部署在微信 Yard 系统上,充分了利用微信闲置机器上的资源。
- 数据自动更新
- 在建表时,对其指定了一个 fs 的目录,该目录下,是一系列数字递增的目录;
- 当业务侧需要更新索引时,将最新的数据 dump 到更大的数字目录中;
- master 感知最大数字目录的更新,从而更新了元信息;
- trainer 感知元信息的更新并触发建索引;
- worker 加载索引完成索引的更新。
- 数据任务式更新
- 由业务侧主动通过接口的调用,创建一个索引任务;
- 在索引任务中,指定了数据的配置信息(如 fs 信息及路径等);
- trainer 按照表的任务序列,执行任务并构建索引;
- worker 加载索引完成索引的更新。
3.3 数据调度 - 鸡蛋怎么放在多个篮子中
- SimSvr 在每张表创建时就指定了 sharding 数 n 及 sect 数 m,因此这张表拥有了 n * m 个 Conatiner 以供 master 调度;
- master 会根据 worker 的健康情况及资源使用情况进行数据的调度及路由表的生成;
- 路由表带有递增的版本号,可根据版本号感知路由的变化。
- worker 定期轮询 Chubby 获取数据的调度情况及最新的路由表信息;
- client 首次请求时,将随机请求一台 worker 获取最新的路由表信息并将其缓存在本地;
- client 在本地有路由表的情况下,将根据表的数据分布情况,带上版本号并发地向目标 worker 发起请求,最终合并所有 sharding 的结果,将其返回给业务端。
3.4 系统拓展 - 篮子装满了该怎么办
- SimSvr 将表拆分成了更小粒度的数据调度单位,且不要求每台机器上的数据一样,因此可以用拓展机器的方式,将集群的存储容量扩大;
- 对于单表而言,当读能力达到瓶颈时,可以单独扩展此表的读副本数;
4. 近实时增量更新的挑战 - 十秒内完成索引的更新
- 数据一致性与持久化
- 对于大多数的分布式存储组件来说,都是使用 raft 或者 paxos 等一致性协议保证数据一致性并持久化至本机上;
- 对于 SimSvr 来说,每张表会被分为多个 sharding,且 sharding 数不保证为奇数;
- 在 worker 中加入一致性组件及额外的存储引擎,会使得整体的结构变得复杂;
- 最终在考量后,结合业务的批量增量更新的特点,选择了先将数据落地 fs,再由 worker 拉取数据加载的方案;在这种方案下,1000 以内数量的 key 插入,能够在 10s 内完成,达到了业务的要求。
增量持久化
- 增量更新的性能保障
- 由于在线建索引是非常消耗 cpu 资源的过程,因此为了不影响现网的读服务,worker 仅提供少量的 cpu 资源用于增量数据的更新;
- 对于小批量的增量数据,worker 可以直接加载存放在 fs 上的数据并直接进行索引的在线插入;
- 对于大批量的增量数据,为了避免影响读服务及大增量更新慢的问题,SimSvr 将大批量数据在 trainer 进行合并且并发重建索引,最后再由 worker 直接加载建好的索引。
增量更新
5. 丰富的功能特性
5.1 支持额外的特征存储库
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。