Milvus 社区发展的速度很快。在今年年初的时候才刚发布了 Milvus2.0GA 版本,到了年末,Milvus 已经发布到了 2.2.2 版本。这期间经历了 2.1.0,2.1.1,2.1.2,2.1.4,2.2.0,2.2.1 这些版本,每一个版本都凝聚了社区几百位贡献者的心血,感谢每一位在背后为 Milvus 默默付出的同学。在这快速发展的一年里,我发现社区里有很多朋友对 Milvus 的认识还存在一些误区。今天这篇文章就来聊聊使用 Milvus 的十大常见误区,快来看看这些误区你以前有踩过吗?
在社区里我经常被问到的一个问题是:“Milvus 什么时候可以支持 GPU?”这时候我一般会顺势问一句:“你们为什么需要使用 GPU 呢?”得到的答案经常是“使用 GPU 肯定比 CPU 算得快!”诚然,GPU 上的计算单元会比 CPU 多很多,在做并行计算上很有优势。但是 GPU 的显存容量目前是不能和 CPU 的内存相比的,经常会出现向量数据过多,无法全部将其加载到显存的情况。这时候,计算过程中将不可避免地进行内存数据和显存数据的置换,由于数据置换时间的存在,总体的搜索速度也就不是那么快了。可以看到,当数据量不大、可以全部加载到显存的时候,GPU 搜索是有可能比 CPU 更快的,但是在数据量更大、无法全部加载到显存的时候,情况就不一定了。
关于搜索性能,Milvus 团队从 2.1 版本开始就一直在持续地做优化,到今天的 2.2.2 版本,社区的 Benchmark[1] 已经可以在开源的 Sift1M 数据集上达到 1w+QPS,并且 latency 控制在几十毫秒。所以有时候性能不好可能是咱们自己的使用方式不对,没有把 Milvus 的潜能全部释放出来。这里我推荐大家日后遇到性能问题的时候,看一下 Milvus maintainer 小凡写的文章《浅谈如何优化 Milvus 性能》,里面讲了很多做性能优化的思路,应该可以帮助到大家!
“为什么我删了向量之后,集合的向量条数还是没变化?num_entities() 的结果怎么不准?” 要解答这个问题,就需要给大家介绍一下 Milvus 里面的删除原理。当我们在调用 delete() 接口的时候,Milvus 内部其实不会真正马上将磁盘上 segment 里的数据做清除,而是通过标记删除的方式,将对应的 entity 打上删除标记,下次搜索的时候直接将其过滤掉。
而 num_entities() 接口,它的准确含义应该为:the number of insert entities,只要通过 insert() 接口插入进来的 entity,它都可以统计到,即使这个 entity 后续被打上了删除的标记。
所以现在的 num_entities() 获得的向量条数会包含删除了的向量,这个是 by design 的,不过后面可以建议在 Milvus 的文档里面加上一些说明。至于真正准确的 count() 接口,可以预料到将是一个比较重的接口,社区可能会在明年提供给大家这个选项。
使用过 Oracle、MySql 等传统关系型数据库的朋友都清楚,当你插入数据的时候,如果主键重复了,是不允许被插入的。因为 Milvus 里面也有主键(primary key)的概念,所以大家会很自然地认为在插入数据的时候,Milvus 也不会允许重复主键的数据被插入,也会自动做主键去重的。但是事实并非如此,由于一些实现成本的原因,主键去重的功能目前还没有在 Milvus 里实现支持,现在是可以插入重复主键的数据。这一点也算是 Milvus 和传统关系型数据库差别比较大的一点,大家需要格外注意。未来,随着 Milvus 发展得越来越成熟,这些功能相信会逐步补齐。
对于不想要的集合,我们通常会选择删除,并希望这个集合的数据也能立即从磁盘上清理掉,释放出磁盘的可用空间。基于这样的期待,很多朋友认为 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
docker-compose 是社区提供的一种部署 Milvus 分布式集群的方式。由于它使用起来很简单,很多小伙伴最开始的部署 Milvus 的方式都会选择 docker-compose。到后来,有些朋友会觉得在生产环境中用 docker-compose 部署一个 Milvus 分布式集群也是可以的。这其实也是一个误区,Milvus 虽然提供了 docker-compose 部署分布式的方案,但是这个方案只是适用于在测试环境中对 Milvus 的功能进行快速验证。真正上生产环境,还是需要使用 k8s 的方式来部署。
主要有这几点原因:一是 docker-compose 不能方便地扩缩容节点,当数据量增加时对集群扩容是一件麻烦的事情;二是 docker-compose 虽然能把 Milvus 里的各个组件都启动起来,但是当其中某个组件挂掉后,它不能像 k8s 那样帮助你自动重启恢复,容易引发故障。所以,使用 k8s 部署 Milvus 才是生产上的最佳实践,还在生产环境里面使用 docker-compose 部署的朋友,最好去升级一下。
初看这句话好像说得很正确,但是仔细去分析,会发现它根本经不起推敲。先说 query,当你的数据量很大,现在已有的这些 querynode 节点已经满负荷运载,那么此时增加 querynode 的数量,确实可以分担已有 querynode 的压力,搜索速度确实也会变快。但是当你的数据量并不大,querynode 并没有达到瓶颈的时候呢?比如说你只有 20w 的数据,Milvus 里面只有一个 segment 的时候,此时你增加再多的 querynode 也是用不起来的。在这种情况下,通过调节索引或者增加有负载的 querynode 的 CPU 核数,才有可能使搜索速度更快。
再说到 datanode,因为 datanode 是负责数据插入的,所以有些小伙伴会想当然认为 datanode 的数目越多,插入速度也就越快。其实原理和前面讲的 query 类似,当你的 datanode 数目不断增加,多于创建集合时的 shard 数目时,部分 datanode 可能就无法获得负载。所以,一般当 datanode 的数目等于集合的 shard 数目时,就可以达到最佳的插入性能,datanode 数量过多并不一定能提高插入速度。
这个也是一些刚接触 Milvus 不久的朋友经常踩到的一个误区。可能是因为 Milvus 最典型的一个应用是以图搜图,所以不少新朋友认为 Milvus 是一个图数据库,可以用来找数据之间的关系。实际上,Milvus 是一个向量数据库,它主要是用来存向量数据并且做向量相关的增删改查。 因为图片可以通过深度学习模型转换成特征向量,这些向量可以存储到 Milvus 里面做检索,所以 Milvus 可以间接地实现图片的相似检索。而图数据库(Graph Database)是指以图表示、存储和查询数据的一类数据库。图数据库里的“图”,与图片、图形、图表等没有关系,而是基于数学领域的“图论”概念,通常用来描述某些事物之间的某种特定关系。所以这两者之间差别还是很大的,大家后面可不要再搞混了!
Milvus 依赖 Pulsar/Kafka 这样的消息系统来作为整个系统的骨架,在 Milvus2.1.0 之前,这句话其实是对的,Milvus 的所有 DML 操作都需要走消息系统。但是,后面我们逐渐发现数据查询操作是一个对延时十分敏感的操作,数据查询走消息系统,整个链路的开销肯定居高难下。这对于 Milvus 这种读操作十分频繁并且性能要求严格的数据库系统来说是不能接受的。所以,在 Milvus2.1.0 之后,社区对读链路做了重构,Proxy 对于搜索请求会直接发送 RPC 请求给 querynode,然后 querynode 计算完结果也直接通过 RPC 请求返回给 Proxy。整个搜索链路不会再经过消息系统,从而极大地提升系统的搜索性能。
很多小伙伴为了保险,每次插入完数据后,都会调用一次 create_index() 和 load() 接口,从而来确保新插入的数据能够被建上索引同时还能被加载到内存中。其实大可不必。首先来说 create_index(),这个接口使用的时候,内部有一个限制。只有当一个 segment 被 sealed 了并且该 segment 里的向量条数超过了 1024 条之后,那么调用create_index 的时候才能顺利建上索引。
当一个 segment 里的向量条数过少时,暴搜已经很快了,再建索引也没有明显的效果。另外,更重要的一点是,当一个 segment 被 sealed 了并且该 segment 里的向量条数超过了 1024 条,只要你曾经给集合建过索引,即使你现在不去显示调用 create_index() 的接口,那么这时候系统也会自动给这个 segment 建索引。load() 这个接口也是类似的,只要你曾经调用过 load(),那么后续你新插入的数据都会自动 load 到内存中的,不用担心搜不到新插入的数据。
这句话听起来感觉再正确不过了,在大多数情况下这句话也都是对的,但是凡事都是有例外的。Milvus 在一些标量过滤的场景中,假如过滤之后的结果非常稀疏,符合条件的 entity 只有一两个,那么此时再去走索引(比如,HNSW图索引)做向量检索的时候,很可能在内部搜索多次都无法找到符合条件的结果,从而需要不断扩大搜索深度。最终整体的搜索性能可能比暴搜都还要慢。
关于 Milvus 的十大误区今天就先盘点到这里,后面发现有新的误区出现,我也会继续更新。最后,在即将到来的新的一年里,祝愿 Milvus 社区发展得越来越好,希望有越来越多的朋友在工作中把 Milvus 用起来!
[1]Benchmark: https://milvus.io/docs/benchmark.md