1.使用docker-compose部署Milvus

Milvus是一个高效能的向量数据库,能够对万亿级向量数据进行毫秒级相似度检索。
我部署的是Milvus Standalone ,一种单服务器部署方案,所有组件打包到单个 Docker 镜像中。
构建命令如下:
bash
wget https://github.com/milvus-io/milvus/releases/download/v2.5.14/milvus-standalone-docker-compose.yml -O docker-compose-milvus.yml
bash
sudo docker-compose -f docker-compose-milvus.yml up -d
在运行命令后,所在的目录下自动生成了一个volumes文件夹,所有的数据都持久化存在这里。这样,即使删除了docker容器,数据也不会丢失。
2.数据怎么存?
在volumes文件夹下,有etcd,milvus,minio三个文件夹:
- volumes/milvus/:这是milvus核心的数据目录,负责索引和WAL (Write-Ahead Log, 预写日志)日志。它对应docker容器内的/var/lib/milvus,主要存放向量数据、索引文件和日志等。WAL的作用是数据安全,当有新数据写入时,Milvus会首先将这条操作记录在WAL中,即使系统突然崩溃,重启后也能根据这个日志恢复数据。
- volumes/etcd/:元数据存储服务,负责管理信息。它对应docker容器内的/etcd,负责存储Milvus的所有元数据,例如集合(collection),schema,分区信息等。
- volumes/minio/:对象存储服务,永久保存向量数据文件。它对应docker容器内的.minio_data,向量一旦成功存入,最终都会稳定地存放在这里,重启/删除容器,都不会丢失。
我们看到volumes/milvus/和volumes/minio/其实都存放了向量数据,可以理解为前者是缓冲,后者是最终库。比如我们用delete和insert命令,并且设置flush为false的时候,数据就会先在volumes/milvus/操作,当我们使用flush命令进行数据落盘时,数据就会从volumes/milvus/的记录中整理成规范的binlog文件,存入minio这个仓库里,用于查询和检索。数据成功写入minio后,内部组件会更新存储在etcd中的元数据,记录这些binlog文件在minio中的存储路径。flush结束后,volumes/milvus/中的相关数据也会标记为可删除,后续会被清理。这样就完整执行了一个数据段封存操作。
3.为什么flush很慢?
在开发的过程中,我们会有一个很直观的感受,milvus flush操作好慢!今天我们来看看原因,有没有什么破解之法。
首先,要知道flush确实是一个重量级操作,当我们调用collection.flush(),它涉及到封存、编码、压缩、网络 。容我一一道来:
刚开始我以为,flush不就是将volumes/milvus/里数据打个包,装到volumes/minio/吗?其实不是。Milvus为了确保数据安全、节省成本并支持大规模分布式检索,就没有那么简单。
真实路径是:
内存->封存->编码->压缩->网络(如果是异地部署)->volumes/minio
内存:就是volumes/milvus/里完整的数据,把它读出来。
封存:锁定内存数据,不再接受写入和删除,这一步耗时较短。
编码压缩:volumes/milvus/里的数据打包进binlog中,注意,这里向量仍是浮点型,并没有做向量类型转换,而是将原始向量数据进行序列化压缩,以减少磁盘占用(就像把散乱的积木拆平再装进盒子,或者理解为抽真空减少体积),向量的精度不会变。这一步是CPU密集型任务,数据量越大越耗时。
网络和写入volumes/minio:如果是异地部署,还会有网络传输耗时。如果数据库就在代码运行的服务器上,还要考虑磁盘物理写入速度和内存拷贝带宽。
问题是,在实际项目中,我看到一次向量入库,只有4个chunk的向量,却用了150秒,这是怎么回事?
我使用的是机械硬盘,但I/O再慢,也慢不到150秒,肯定是有别的问题。
4.进一步分析milvus状态
在终端中执行docker stats milvus-standalone,我可以看到当前milvus的状态。

- 首先,CPU占用冲到1565.36%甚至2000%多,然后又回落,这是典型的HNSW索引构建过程,插入向量数据后,触发了索引构建任务,会开启大量并行线程,疯狂消耗CPU资源,构建完成后,CPU被释放,占用率迅速回落。
- MEM USAGE只有2.05%,排除了内存不足导致的异常。
- NET I/O是Milvus起服务以来累计处理的数据量。
- BLOCK I/O是当下读取和写入磁盘的数据量。这次读取2.97GB的数据,写入29.6GB的数据。写入的数据除了原始数据,还加上了HNSW索引数据。
注意: 索引构建不仅吃CPU,还占了很大一部分I/O。如果调用 create_index() 方法时使用默认参数,即索引构建是同步模式,那么flush操作有很大一部分时间在等待索引构建完成。
不过,我只插入了4条数据,却产生了近30GB的磁盘读写?这也太兴师动众了!
当我写入4条数据,其实索引不仅是构建这4条数据,索引节点会将包含我这4条数据的整个数据段加载到内存,并为整个数据段的数据构建索引结构。并且,Milvus有Compaction机制,负责将众多零散的小数据段合并成一个更大的数据段来提升查询性能,Compaction会读取多个旧数据段,在内存中合并,然后写出全新、更大的数据段,最后再删除旧段,这个过程涉及大量I/O 。在服务器上用命令查当前I/O状态:iostat -x 1

发现最后一列%util并没有像想象中飚高,说明瓶颈不在硬件I/O堵塞。而wkB/s会飚高到3万多,刚好符合30GB的量级,结合CPU飚高到1500%,印证了猜测,主要的耗时在索引构建时候的CPU计算,这个计算过于频繁,导致整个入库耗时大。
5.总结问题点和改进方法
综合上面的分析,在我使用milvus时有几个问题点导致耗时长:
(1) 入库同步构建索引,导致入库操作需要等待索引构建完成才返回,索引消耗CPU计算,耗时长。
(2) 每一个markdown的分块都调用一次flush,太频繁,产生大量小文件,拖慢Milvus后台的Compaction效率。
因为这几个问题,所以即使我开了多个进程来并行写Milvus数据,也因为过于频繁用cpu构建索引,导致速度上不去。
改进:
(1)flush和索引构建异步完成。使Milvus入库的完成不依赖索引构建。这样会稍微影响到检索的速度,但加快了入库过程,索引构建完后会恢复毫秒级检索速度。
(2)不要频繁调用flush操作。可以让Milvus自动管理flush,当数据段大小达到阈值时自动触发。或者批量文件导入完成后,仅调用一次flush。