Milvus性能优化提速之道:揭秘优化技巧,避开十大误区,确保数据一致性无忧,轻松实现高性能

发布时间 2023-11-15 11:20:55作者: 汀、人工智能

Milvus性能优化提速之道:揭秘优化技巧,避开十大误区,确保数据一致性无忧,轻松实现高性能

Milvus 是全球最快的向量数据库,在最新发布的 Milvus 2.2 benchmark中,Milvus 相比之前的版本,取得了 50% 以上的性能提升。值得一提的是,在 Master branch 的最新分支中,Milvus 的性能又更进一步,在 1M 向量串行执行的场景下取得了 3ms 以下的延迟,整体 QPS 甚至超过了 ElasticSearch 的 10 倍。那么,如何使用 Milvus 才能达到理想的性能呢?本文暂且不提社区大神贡献的黑科技优化,先聊聊使用 Milvus 过程中的一些经验,以及如何进行性能调优。

1.Milvus优化性能技巧

技巧一:合理的预计数据量,表数目大小,QPS 参数等指标

在部署 Milvus 之前,首先需要决定机器的资源、规格、以及一些依赖的资源,以下是你需要考虑的因素:

  1. 有多少张表?

  2. 每张表的数据量有多少?

  3. 每张表的 QPS 需求有多少?

  4. 是否需要存标量字段,如果有字符串,字符串的平均长度是多少?

  5. 是否有删除和流式插入,每天大概有多少比例的数据需要被更新?

基于以上因素,可以遵循以下经验结论:

  1. 节点资源占用可以通过 sizing tool[2] 进行计算,通常情况下 8G 内存可以支持超过 5MB 的 128dim 向量数据和 1MB 的 768dim 数据

  2. 默认情况下,Milvus 会创建 256 个消息队列 topic。如果表数目比较少,可以调整 rootCoord.dmlChannelNum 减少 topic 数目降低消息队列负载

  3. 默认情况下,每个 collection 会使用 2 个消息队列 topic(shard),如果写入非常大或者数据量极大,需要调整 collection 的 shard 数目。建议每个 shard 写入 / 删除不超过 10M/s,单个 shard 的数据量不大于 1B 向量,shard 数目过大也会影响写入性能,因此不建议单表超过 8 个 shard

  4. 根据 benchmark[3] 结果计算需要的 CPU 资源。对于小数据量场景(小于 5mn),使用多副本可以扩展查询性能,但建议副本数目不要超过 10 个。对于中大数据量场景,通常扩容 querynode 就可以自动负载均衡,不需要使用多副本提升 QPS.

  5. 所有的标量字段目前也会加载进内存中,也会消耗内存,请在容量规划时预留原始数据类型两倍以上的内存

  6. Milvus 在存储数据的过程中,存在较多冗余数据(https://github.com/milvus-io/milvus/issues/20453)。考虑到 Minio 的 2,4 纠删码存在两副本冗余,我们建议 Minio 至少包含 6 倍以上的数据的磁盘存储。同时 Pulsar/Kafka 需要包含近五天写入量三倍的存储。合理调整数据的保留时间和 GC 时间可以很大程度上减少磁盘的使用,默认情况下数据会被保留 5 天。个人建议适当缩短数据过期时间,但尽可能保留 1 天以上避免数据丢失或误删除。

  7. Etcd 作为 Milvus 的元信息存储和服务发现节点,请尽可能使用 ssd 磁盘并独立部署。通常 Etcd 的内存使用不会超过 4GB,通过调整参数可以较快地清理 etcd 中的历史版本减少内存使用。

  8. Pulsar/Kafka 作为 Milvus 的日志存储,其依赖的 zookeeper 集群对性能要求也比较高,建议使用 SSD 并独立部署

技巧二:选择合适的索引类型和参数

索引的选择对于向量召回的性能至关重要,Milvus 支持了 **Annoy,Faiss,HNSW,DiskANN **等多种不同的索引,用户可以根据对延迟、内存使用和召回率的需求进行选择。

索引的选择步骤一般如下:

1.2.1 根据是否需要精确结果?

只有 Faiss 的 Flat 索引支持精确结果,但需要注意 Flat 索引检索速度很慢,查询性能通常比其他 Milvus 支持的索引类型低两个数量级以上,因此只适合千万级数据量的小查询(Flat on GPU 已经在路上了,敬请期待)

1.2.2 根据数据量是否能加载进内存进行选择

对于大数据量,内存不足的场景,Milvus 提供两种解决方案:

  1. DiskANN
    • DiskANN 依赖高性能的磁盘索引,借助 NVMe 磁盘缓存全量数据,在内存中只存储了量化后的数据。

    • DiskANN 适用于对于查询 Recall 要求较高,QPS 不高的场景


DiskANN 示意图

  • DiskANN 的关键参数:

    1. search_list: search_list 越大,recall 越高而性能越差。search_list 的大小不应该小于 K。而对于较小的 K,推荐把 search_list 和 K 的比值设置得相对大一些, 这个比值随着 K 增大可以逐渐靠近

    2. IVF_PQ

      • 对于精确度要求不高的场景或者性能要求极高的场景。

      • IVF PQ 的核心是两个算法,IVF + PQ 量化,其中量化可以大幅减少向量的占用内存量。

  • IVF 参数

    • nlist:一般建议 nlist = 4*sqrt(N),对于 Milvus 而言,一个 Segment 默认是 512M 数据,对于 128dim 向量而言,一个 segment 包含 100w 数据,因此最佳 nlist 在 1000 左右。

    • nprobe:nprobe 可以 Search 时调整搜索的数据量,nprobe 越大,recall 越高,但性能越差。具体的 nprobe 需要根据查询的精度要求决定,从 nprobe = 16 开始会是一个不错的尝试。

  • PQ 参数

    • M: 向量做 PQ 的分段数目,一般建议设置为向量维数的 1/4,M 取值越小内存占用越小,查询速度越快,精度也变得更加低。

    • Nbits: 每段量化器占用的 bit 数目,默认为 8,不建议调整。

1.3.1 判断构建索引和内存资源是否充足

性能优先,选择 HNSW 索引

  1. HNSW 索引是目前 Milvus 支持的性能最快的索引,我们的测试报告也是基于 HNSW 作为测试依据。

  2. HNSW 内存的开销较高,通常需要原始向量的 1.5 - 2 倍以上内存。

  • HNSW 参数

    • M:表示在建表期间每个向量的边数目,M 越大,内存消耗越高,在高维度的数据集下查询性能会越好。通常建议设置在 8-32 之间。

    • ef_construction:控制索引时间和索引准确度,ef_construction 越大构建索引越长,但查询精度越高。要注意 ef_construction 提高并不能无限增加索引的质量,常见的 ef_constructio n 参数为 128。

    • ef: 控制搜索精确度和搜索性能,注意 ef 必须大于 K。

资源优先,选择 IVF_FLAT 或者 IVF_SQ8 索引

  • IVF 索引在 Milvus 分片之后也能拿到比较不错的召回率,其内存占用和建索引速度相比 HNSW 都要低很多

  • IVF_SQ8 相比 IVF,将向量数据从 float32 转换为了 int8,可以减少 4 倍的内存用量,但对召回率有较大影响,如果要求 95% 以上的召回精度不建议使用

  • IVF 类索引的参数跟 IVFPQ 类似,这里就不做过多的介绍了。

检索时,Milvus 的查询一致性也会对查询造成较大影响。通常情况,对于一致性要求较高的场景,建议使用最终一致性或者有界一致,默认情况下 Milvus 选择有界一致性,窗口为 3s。

技巧三:合理选择流式插入和批量导入

Mivus 原生支持流批一体,同时支持流式写入和批式写入 (BulkInsert) 两种模式。绝大多数用户在最初接触 Milvus 的时候,都会选择流式写入,这种方式实时性较好,同时也避免了批式写入小文件带来的 Compaction 压力。

如果有大量离线写入的场景,建议使用 BulkInsert,原因是 BulkInsert 不会对查询性能造成太大的影响,并且也大大减少了流式写入对消息队列产生的压力。如何合理选择流式还是批式写入呢:

  1. 单次写入超过 100MB 以上,建议选择批式写入

  2. 希望尽可能减少写入对线上查询的影响,建议选择批式写入

  3. 希望写入实时可见,建议选择流式写入

  4. 单次写入小于 10MB 以下,建议选择流式写入

在选择好写入方式的基础上,还有几个经验需要关注:

  1. 尽可能批量写入,整体吞吐会更高,建议每次写入的大小控制在 10M

  2. 单个 Shard 的流式写入量不建议超过 10M/s

  3. Datanode 多于 Shard 的情况下,部分 DataNode 可能无法获得负载

  4. 导入目前支持的文件大小上限是 1GB,接下来会支持更大的导入文件大小上限

  5. 不建议频繁导入小文件,会给 compaction 带来比较大的压力

技巧四:谨慎使用标量过滤,删除特性等特性

作为数据库,Milvus 支持了删除、标量过滤、TimeTravel 等高级特性。如果不了解底层原理,使用这些高级功能可能会对稳定性和性能造成比较严重的影响,以下是一些使用注意事项:

  1. Milvus 使用的是前过滤,即先做标量过滤生成 Bitset,在向量检索的过程中基于 Bitset 去除掉不满足条件的 entity。对于 HNSW 这一类的图索引而言,标量过滤并不会加速查询,反而可能导致性能变差。特别是对于过滤性很强的条件(比如 PK=1 这种全局唯一的条件),标量过滤甚至会导致单次查询的时间长于爆搜。针对这种情况,用户也可以选择通过后过滤的方式绕过,先基于 Milvus 查出 TopK 的数据,再基于其他数据库进行过滤。

  2. 对于过滤条件相对比较确定的场景,使用 Partition 把数据进行物理分区,在查询的时候指定 Partition 性能更好。

  3. Milvus 的删除是标记删除,在 compaction 时会清理,因此删除的数据依然会占据内存。大量删除也会造成查询性能下降,同时大量 compaction 可能造成建索引压力变大等一系列影响。在需要大量频繁删除的场景,可能需要进行一些 compaction 参数的调整,保证删除的数据能够被及时清理。

  4. Milvus 支持了数据自动过期功能(TTL),可以定时清理过期数据。

  5. 如果需要全量更新一个 Collection 的数据,推荐使用新建表 + 导入数据 + Alias 切换的方案

  6. 制定 Output field 时,如果要获取标量字段,会从对象存储上获取,吞吐和延迟都会受到较大影响。

当然,Milvus 后续的版本会对以上能力做针对性的优化,尤其是删除和标量过滤的场景。

技巧五:部署监控并观察集群情况

可观测性是用户在生产环境落地非常重要的一部分,Milvus 2.2 重新梳理监控指标并且校正了指标的正确性,我们强烈建议你的生产集群部署监控 [4] 并且在上线之前进行性能测试。

除了每个节点的 CPU 使用率,内存使用量信息,以下是一些建议你关注的监控指标:

  • Proxy

    • 查询延迟:milvus_proxy_sq_latency/milvus_proxy_collection_sq_latency

    • 写入 / 删除延迟:milvus_proxy_mutation_latency

    • 写入流量:milvus_proxy_receive_bytes_count

    • 查询返回流量:milvus_proxy_send_bytes_count

  • QueryNode

    • 加载的数据量:milvus_querynode_entity_num

    • 查询请求排队时间:milvus_querynode_sq_queue_latency

    • 单个 Segment 的查询时间:milvus_querynode_sq_segment_latency

  • IndexNode

    • 构建索引的时间:milvus_indexnode_build_index_latency
  • DataNode

    • Flush 花费的时间:milvus_datanode_save_latency

    • Compaction 花费的时间:milvus_datanode_compaction_latency

技巧六:常见的参数优化调整

想要使得 Milvus 跑得更快更稳,针对自己的使用场景、硬件资源情况进行一些定制化的调整自然是不可避免的,你可以从了解以下参数开始:

  1. Segment 大小:Segment 大小越大,查询性能越好,构建索引越慢,负载越不容易均衡。Milvus 默认选择 512M Segment 大小主要是考虑到了内存比较少的机型。对于内存在 8G-16G 的用户,建议 Segment 大小调整到 1024M,16G 以上的机型可以调整到 2G

  2. Segment seal portion: 当 Growing Segment 达到 Segment 大小 * seal portion 后,流式数据就会被转换为批数据。通常情况下建议 Growing segment 的大小控制在 100-200M 左右,调小这个值有助于降低流式写入场景下的查询延迟

  3. DataNode Segment SyncPeriod: Milvus 会定时将数据 Sync 到对象存储,Sync 越频繁故障恢复速度越快,但过于频繁的 sync 会导致 Milvus 生产大量小文件,给对象存储造成较大压力。

  4. Quota 相关的参数:目前支持限制 Milvus 的写入、删除流量、查询的 QPS,以及内存的保护,当触发性能问题时,也要观察是否是因为触发了相应的限流。

小结

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

目前优化方向:

1)接入 Google 的 ScaNN 索引,优化 Milvus 向量执行引擎 Knowhere 的性能

2)10 亿向量的查询 / 加载性能优化

3)支持 RangeSearch

4)改进 Milvus 的标量执行引擎,支持更加复杂的标量数据类型,降低标量的内存开销。

5)改进 Partition 算法,支持动态增加 / 减少 partition 个数

6)支持 Faiss,HNSW,Milvus 1.0 迁移 Milvus 2.0

7) Cpp,Rust,Restful API 的开发和完善

....

2.Milvus 的十大使用误区总结

2.1 GPU 一定比 CPU 快?

在社区里我经常被问到的一个问题是:“Milvus 什么时候可以支持 GPU?”这时候我一般会顺势问一句:“你们为什么需要使用 GPU 呢?”得到的答案经常是 “使用 GPU 肯定比 CPU 算得快!” 诚然,GPU 上的计算单元会比 CPU 多很多,在做并行计算上很有优势。但是 GPU 的显存容量目前是不能和 CPU 的内存相比的,经常会出现向量数据过多,无法全部将其加载到显存的情况。这时候,计算过程中将不可避免地进行内存数据和显存数据的置换,由于数据置换时间的存在,总体的搜索速度也就不是那么快了。可以看到,当数据量不大、可以全部加载到显存的时候,GPU 搜索是有可能比 CPU 更快的,但是在数据量更大、无法全部加载到显存的时候,情况就不一定了。

关于搜索性能,Milvus 团队从 2.1 版本开始就一直在持续地做优化,到今天的 2.2.2 版本,社区的 Benchmark[5] 已经可以在开源的 Sift1M 数据集上达到 1w+QPS,并且 latency 控制在几十毫秒。所以有时候性能不好可能是咱们自己的使用方式不对,没有把 Milvus 的潜能全部释放出来。

2.2 num_entities() 的结果真的准吗?

“为什么我删了向量之后,集合的向量条数还是没变化?num_entities() 的结果怎么不准?” 要解答这个问题,就需要给大家介绍一下 Milvus 里面的删除原理。当我们在调用 delete() 接口的时候,Milvus 内部其实不会真正马上将磁盘上 segment 里的数据做清除,而是通过标记删除的方式,将对应的 entity 打上删除标记,下次搜索的时候直接将其过滤掉。

而 num_entities() 接口,它的准确含义应该为:the number of insert entities,只要通过 insert() 接口插入进来的 entity,它都可以统计到,即使这个 entity 后续被打上了删除的标记。

所以现在的 num_entities() 获得的向量条数会包含删除了的向量,这个是 by design 的,不过后面可以建议在 Milvus 的文档里面加上一些说明。至于真正准确的 count() 接口,可以预料到将是一个比较重的接口,社区可能会在明年提供给大家这个选项。

2.3 Milvus 的主键(primary key)会自动去重?

使用过 Oracle、MySql 等传统关系型数据库的朋友都清楚,当你插入数据的时候,如果主键重复了,是不允许被插入的。因为 Milvus 里面也有主键(primary key)的概念,所以大家会很自然地认为在插入数据的时候,Milvus 也不会允许重复主键的数据被插入,也会自动做主键去重的。但是事实并非如此,由于一些实现成本的原因,主键去重的功能目前还没有在 Milvus 里实现支持,现在是可以插入重复主键的数据。这一点也算是 Milvus 和传统关系型数据库差别比较大的一点,大家需要格外注意。未来,随着 Milvus 发展得越来越成熟,这些功能相信会逐步补齐。

2.4 删完集合之后,数据会立即被清理?

对于不想要的集合,我们通常会选择删除,并希望这个集合的数据也能立即从磁盘上清理掉,释放出磁盘的可用空间。基于这样的期待,很多朋友认为 Milvus 里面,删完集合之后,数据也会立即被清理。实则不然,Milvus 里面有一个叫做时间旅行的功能,为了在大家误删数据的时候能够有后悔药,Milvus 在删完集合之后不会立即将数据从磁盘上清除,而是需要再等待一段时间后才真正清理磁盘上的数据。Milvus 里面控制数据清理时间的是 datacoord 下的 gc 相关的参数,默认是保留一天再清除数据。

dataCoord:
  gc:
    interval: 3600 # gc interval in seconds
    missingTolerance: 86400 # file meta missing tolerance duration in seconds, 60*24
    dropTolerance: 86400 # file belongs to dropped entity tolerance duration in seconds, 60*24


2.5 docker-compose 也可以部署一个真正生产可用的 Milvus 分布式集群

docker-compose 是社区提供的一种部署 Milvus 分布式集群的方式。由于它使用起来很简单,很多小伙伴最开始的部署 Milvus 的方式都会选择 docker-compose。到后来,有些朋友会觉得在生产环境中用 docker-compose 部署一个 Milvus 分布式集群也是可以的。这其实也是一个误区,Milvus 虽然提供了 docker-compose 部署分布式的方案,但是这个方案只是适用于在测试环境中对 Milvus 的功能进行快速验证。真正上生产环境,还是需要使用 k8s 的方式来部署。

主要有这几点原因:一是 docker-compose 不能方便地扩缩容节点,当数据量增加时对集群扩容是一件麻烦的事情;二是 docker-compose 虽然能把 Milvus 里的各个组件都启动起来,但是当其中某个组件挂掉后,它不能像 k8s 那样帮助你自动重启恢复,容易引发故障。所以,使用 k8s 部署 Milvus 才是生产上的最佳实践,还在生产环境里面使用 docker-compose 部署的朋友,最好去升级一下。

2.6 querynode 越多,搜得越快;datanode 越多,插得越快

初看这句话好像说得很正确,但是仔细去分析,会发现它根本经不起推敲。先说 query,当你的数据量很大,现在已有的这些 querynode 节点已经满负荷运载,那么此时增加 querynode 的数量,确实可以分担已有 querynode 的压力,搜索速度确实也会变快。但是当你的数据量并不大,querynode 并没有达到瓶颈的时候呢?比如说你只有 20w 的数据,Milvus 里面只有一个 segment 的时候,此时你增加再多的 querynode 也是用不起来的。在这种情况下,通过调节索引或者增加有负载的 querynode 的 CPU 核数,才有可能使搜索速度更快。

再说到 datanode,因为 datanode 是负责数据插入的,所以有些小伙伴会想当然认为 datanode 的数目越多,插入速度也就越快。其实原理和前面讲的 query 类似,当你的 datanode 数目不断增加,多于创建集合时的 shard 数目时,部分 datanode 可能就无法获得负载。所以,一般当 datanode 的数目等于集合的 shard 数目时,就可以达到最佳的插入性能,datanode 数量过多并不一定能提高插入速度。

2.7 Milvus 可以用做图数据库来帮你找关系

这个也是一些刚接触 Milvus 不久的朋友经常踩到的一个误区。可能是因为 Milvus 最典型的一个应用是以图搜图,所以不少新朋友认为 Milvus 是一个图数据库,可以用来找数据之间的关系。实际上,Milvus 是一个向量数据库,它主要是用来存向量数据并且做向量相关的增删改查。 因为图片可以通过深度学习模型转换成特征向量,这些向量可以存储到 Milvus 里面做检索,所以 Milvus 可以间接地实现图片的相似检索。而图数据库(Graph Database)是指以图表示、存储和查询数据的一类数据库。图数据库里的 “图”,与图片、图形、图表等没有关系,而是基于数学领域的“图论” 概念,通常用来描述某些事物之间的某种特定关系。所以这两者之间差别还是很大的,大家后面可不要再搞混了!

2.8 消息系统是 Milvus 的系统骨架,数据的增删改查操作都需要走消息系统

Milvus 依赖 Pulsar/Kafka 这样的消息系统来作为整个系统的骨架,在 Milvus2.1.0 之前,这句话其实是对的,Milvus 的所有 DML 操作都需要走消息系统。但是,后面我们逐渐发现数据查询操作是一个对延时十分敏感的操作,数据查询走消息系统,整个链路的开销肯定居高难下。这对于 Milvus 这种读操作十分频繁并且性能要求严格的数据库系统来说是不能接受的。所以,在 Milvus2.1.0 之后,社区对读链路做了重构,Proxy 对于搜索请求会直接发送 RPC 请求给 querynode,然后 querynode 计算完结果也直接通过 RPC 请求返回给 Proxy。整个搜索链路不会再经过消息系统,从而极大地提升系统的搜索性能。

2.9 为了保证新插入的数据建上索引并被搜到,每次插入后都要调用 create_index() 和 load() 接口

很多小伙伴为了保险,每次插入完数据后,都会调用一次 create_index() 和 load() 接口,从而来确保新插入的数据能够被建上索引同时还能被加载到内存中。其实大可不必。首先来说 create_index(),这个接口使用的时候,内部有一个限制。只有当一个 segment 被 sealed 了并且该 segment 里的向量条数超过了 1024 条之后,那么调用 create_index 的时候才能顺利建上索引。

当一个 segment 里的向量条数过少时,暴搜已经很快了,再建索引也没有明显的效果。另外,更重要的一点是,当一个 segment 被 sealed 了并且该 segment 里的向量条数超过了 1024 条,只要你曾经给集合建过索引,即使你现在不去显示调用 create_index() 的接口,那么这时候系统也会自动给这个 segment 建索引。load() 这个接口也是类似的,只要你曾经调用过 load(),那么后续你新插入的数据都会自动 load 到内存中的,不用担心搜不到新插入的数据。

2.10 建索引一定比不建索引搜得更快

这句话听起来感觉再正确不过了,在大多数情况下这句话也都是对的,但是凡事都是有例外的。Milvus 在一些标量过滤的场景中,假如过滤之后的结果非常稀疏,符合条件的 entity 只有一两个,那么此时再去走索引(比如,HNSW 图索引)做向量检索的时候,很可能在内部搜索多次都无法找到符合条件的结果,从而需要不断扩大搜索深度。最终整体的搜索性能可能比暴搜都还要慢。

关于 Milvus 的十大误区今天就先盘点到这里,后面发现有新的误区出现,我也会继续更新。最后,在即将到来的新的一年里,祝愿 Milvus 社区发展得越来越好,希望有越来越多的朋友在工作中把 Milvus 用起来!

3. Milvus 一致性等级详解

为什么我刚刚删除的向量还是可以搜到?
这是最近 Milvus 社区里面经常出现的一个问题。要回答这个问题,就不得不提起 Milvus 的一致性等级(Consistency levels)这个概念。对于很多从 Milvus1.0 过来的朋友,一致性等级还是一个比较新鲜的概念。今天咱们就来聊聊 Milvus 里面的几种一致性等级。

首先,什么是一致性?本文提到的一致性,主要是指 CAP 理论中 的 C(Consistency),指在分布式系统中,空间维度上,某一特定时刻,多个实体中不同数据备份之间值的一致性。

了解完一致性的概念之后,我们自然会问第二个问题,Milvus 为什么要引入一致性的概念?在 Milvus1.0 的时候,由于本身只是一个单机版本,数据从插入到落盘都在较短的时间内完成。Milvus1.0 版本中系统默认每隔一秒做一次数据刷盘操作。基本上可以认为,数据插入之后立即可见,搜索读取的就是最新版本的数据。因为只是存在这样一种一致性等级,不会涉及到多种一致性等级之间的区分,所以当时也就没有引入一致性这个概念了。Milvus 2.0 版本是基于消息存储构建的分布式数据库,遵循 PACELC 定理所定义的,必须在一致性、可用性和延迟之间进行取舍。并且不同的一致性保障语义有其独特的应用场景,所以在 Milvus2.0 中引入了一致性的概念。

知道 Milvus 引入一致性概念的缘由,我们再来看今天的主角,一致性等级,就很好理解了。因为一致性保障的语义有多种,为了加以区分,所以就有了一致性等级。在 Milvus 中,根据一致性保障语义从强到弱共分为四个不同的级别,分别是:强一致性 (Strong)、有界一致性 (Bounded Staleness)、会 话 一 致 性 (Session) 、最终一致性(Eventual)。用户可以在创建集合或者执行数据检索请求的时候指定 Milvus 的一致性等级,非常灵活。

下面来对这四种一致性等级一一介绍。

3.1. 强一致性 (Strong)

强一致性是一致性保障语义最强的一致性等级。它可以保证用户读取到的数据就是最新版本的数据,客户端永远不会看到未提交的写入数据或者部分提交的写入数据。但是根据 PACELC 定理,如果把 Milvus 里的 Collection 或者数据检索的请求设置成强一致性等级,那么搜索的 latency 也会是几个一致性等级中最长的。一般在功能测试场景下,用户可以使用强一致性等级来保证测试结果的正确性。

下面这个五线谱的音符动图,比较形象地展示了音符(数据)的强一致性。当数据写入 “West US 2” 区域后,当你从其他区域读取数据时,你就会得到最新版本的值。对应到 Milvus 里,写入时刻的那份数据与用户搜索时候的那份数据永远是保持一致的。

3.2. 有界一致性 (Bounded Staleness)

有界一致性,顾名思义,就是在一段时间窗口内,允许数据存在不一致,但是在这个时间窗口之外,数据是保持总体全局一致的。有界一致性适用于对搜索时延有要求,同时可以接受对少量数据不可见的场景。比如,在一些视频推荐的场景里,少量数据不可见对整体召回率 (Recall) 的影响极小,但对于性能的提升帮助很大。

下面这个五线谱的音符动图,展示了音符(数据)的有界一致性。当数据写入 “West US 2” 区域后,如果你立即从其他区域读取数据,你不会得到最新版本的值。经过一段时间窗口之后,你才能访问到最新版本的值。

3.3. 会话一致性 (Session)

会话一致性,又叫做客户端一致性。设置这种一致性等级之后,在同一个会话内的所有写操作,都可以被这个会话内需要执行读操作的用户立即感知到。说的再通俗一点,当你使用一个客户端写入数据,你自己一定可以立即查到你最新新入的数据。对于同一个会话内,一致性等级要求很高的场景,会话一致性将是很好的选择。

下面这个五线谱的音符动图,展示了音符(数据)的会话一致性。“West US 2 writer”和 “West US 2 reader” 正在使用相同的会话(会话 A),因此它们都同时读取相同的数据。而 “澳大利亚东部” 区域正在使用“会话 B”,因此它会稍后接收数据,但与会话 A 里数据写入的顺序相同。

3.4. 最终一致性(Eventual)

最终一致性,读取没有顺序保证,在没有进一步写入操作的情况下,数据最终会收敛一致。最终一致性是一致性保障语义最弱的一致性等级。但是根据 PACELC 定理,在牺牲一致性的情况下,搜索的 latency 也会是这几种一致性等级中最短的。最终一致性适合对数据一致性没有要求,但是希望搜索性能尽可能快的场景。下图说明了音符(数据)的最终一致性。

最后,回答一下文章开头的那个问题:为什么我刚刚删除的向量还是可以搜到? 自 Milvus2.0GA 版本之后,默认提供的一致性等级是有界一致性。所以当一些朋友在做删除操作的功能性验证时,可能读取数据的那个时间点正好处于有界一致性的 “有界” 时间段内。在这个时间段里,你读取到的数据就有可能不是最新的版本,而是你没有执行删除操作的老版本数据。所以,你搜索到的向量就可能包含你刚刚删除的向量。要解决这个问题也很简单,在你创建集合或者做数据检索请求的时候,将一致性等级设置成强一致性即可。

参考资料

[1]Milvus 2.2 benchmark: https://milvus.io/docs/benchmark.md

[2]sizing tool: https://milvus.io/tools/sizing/

[3]benchmark: https://milvus.io/docs/benchmark.md

[4]部署监控: https://milvus.io/docs/monitor.md

[5]Benchmark: https://milvus.io/docs/benchmark.md