milvus
本文最后更新于 162 天前,如有失效请评论区留言。

milvus架构篇

关键字:架构shardvchannelpchannelsegment封闭区段增长区段对象存储数据**日志**快照索引日志快照

milvus的数据结构

collection:table

channel:通道(多querynode并行查询)

segment:物理存储单元

patition:逻辑存储单元

img

官方的

img

结构

milvus官方架构图

img

milvus是一个使用go编写的向量数据库,采用的是一个RPC的架构,一个天然的分布式系统。

  • access layer数据接入层,集群需要负载均衡选取节点(可使用nginx)
  • coordinator service协调服务层,主要用于请求和底层节点(worker node)的交接
    • etc存储元数据
    • root根节点 - 处理ddl和dcl请求,collection/partition/index的操作,需要把这些元数据保存到meta storage
    • query协调器 -
    • 管理query node
    • 负载均衡所有的query节点
    • 对增长段和非增长段进行查询
    • data协调器 -
    • 管理data node
    • segment,collection,partition
    • index协调器 - 管理索引节点(已经弃用 2024/03/20)
  • worker node工作节点,被协调器调度工作
    • 查询节点 - 先从Log队列中获取日志事件
    • 从对象存储中(minio)加载数据
    • 数据节点 - 通过订阅日志时间来完成dml请求
    • 监听增长segment,到达一定就进行封闭为seal segment,若存在index就开始创建索引
    • 处理修改数据的日志操作
    • 负责把增量数据写入增长segment
    • 索引节点
    • 构建索引
  • object storage对象存储层
    • 日志存储 - 在milvus中,日志是以快照的方式存在,其实日志就是数据像redis的rbd文件
    • insertBinlog,deleteBinlog,ddlBinlog
    • 元数据存储 - etcd作为源数据存储,存储元数据快照,同时etcd进行服务注册和健康检查
    • 索引文件存储

img

操作**日志**binlog

消息队列存储处理DML语句是会进入日志消息队列也就是传统增删改查,默认使用rocketmq,但是大数据量情况下建议使用kafka。使用队列通过发布和订阅来完成日志事件。

并且采用的是通道获取日志:虚拟通道和实际通道(类似操作系统中的用户态和内核态)

比如一个insert_log就是一个插入日志,里面对应了我们的patition,collection,segment,数据列信息等等,存到我们的物理segment中,是真正的日志就是数据。

数据处理部分

下面主要分清几个概念:shard分片,vchannel虚拟通道,pchannel物理通道

img

创建collection时指定分片,同时n个分片对应n个虚拟通道,对于插入删除操作会根据主键hash值进行路由到分片。具体参考hashmap里面的put方法。

一个虚拟通道在工作时需要搭配物理通道来存储数据

插入数据时考虑日志代理节点(proxy)是否需要扩容,是否有足够分片来负责均衡

img

从上图,可以清楚地看清对象存储中的collection是以segment为存储单位,一个collection分为多个segment,而且每个segment都有自己的索引。当我们load加载时需要把所有segment(按collection加载)加载到我们的index node中,在内存中处理数据的二进制快照文件,反序列化成数据然后根据索引类型构建索引,最后需要反序列化回到对象存储中,下次修读取进行load时直接从我们的对象存储中加载并且反序列化进行操作。

从上面知道,其实milvus的数据并不会放到内存中,而是把索引加载到内存中而已,然后根据索引去对象存储中加载数据。

而且我们的segment有两种,一种是增长型(新增数据),一种是封闭型(历史数据),一开始所有的segment都是增长型,当某个segment数据量达到一定时,就改成封闭型,不再接受数据,并且开始构建索引,这就是为什么milvus不在一开始构建而在1024段时进行构建的原因,能够避免多次构建segment索引。

创建集合

  1. proxy接收请求,创建collection任务放入队列中,默认proxy队列为1024,若队列满了会报错拒绝服务器,可以在配置文件中调整。
  2. 空闲协程获取任务消费,交付给root协调节点
  3. rootCoord获取到collection和partition等信息,然后为collection分配虚拟通道
  4. 添加物理通道对虚拟通道的映射
  5. rootCoord通知etcd进行collection的metadata记录
  6. 广播创建collection成功的通知给所有节点

img

后面基本都是proxy接收请求,故省略前部分。

删除集合

  1. proxy接收请求
  2. 携程取出,交予root协调节点,并且生产删除日志消息放到我们的消息队列中
  3. query节点异步删除所有的index,segment,释放collection
  4. data节点进行数据删除
  5. 通知etcd注销数据

新增数据

  1. proxy接收
  2. 向root协调节点申请时间戳,向datacoord申请主键
  3. 按照分片通道进行hash路由,生成插入日志并且放入通道
  4. datacoord从通道中获取消息日志,并且根据collection的聚类信息形成新的insert_log
  5. 最后把我们的insert_log写入到存储引擎中

加载数据

  1. 接收请求,由query协调节点
  2. 根据负载均衡把collection对应的shard分配到对应的query node中(hash)
  3. 从存储引擎加载到本地

查询数据

  1. proxy接收请求,把任务放到Dq通道中
  2. 所有QueryNode都订阅这个通道,获取任务向本地的segment进行查询
  3. 最快获取的node进行返回

索引篇

一般的索引不影响结果,只是一种加快查询数据的结果,但是milvus中的索引是有可能会改变查询结果的,主要和他的召回率有关。

评判一个索引的好坏主要有两个因素

  1. 召回率
  2. 速度
  3. 内存占用大小

IVF

聚类倒排桶算法,一种比较常见的索引,是基于聚类桶进行加快索引检索过程的。

img

构建过程

构建先需要kmean聚类,找出聚类中心点,创建倒排索引,这里创建collection的时候需要指定关键参数-nlist就是去指定该聚类的个数,然后倒排索引就最多有nlist个桶。

img

检索过程

检索过程就是需要创建索引指定的向量距离计算算法,用来计算临近向量,一般有L2,IP内积,余弦等等。

关键参数nprobe,搜索聚类个数。

img

通过上面两个过程,可以发现nlist(指定创建倒排聚类个数)和nprobe(检索聚类个数)。

召回也就是能搜索到的数据,然后对所有数据进行score排序(向量距离),最后返回topk。

参数

合理设置nlist和nprobe可以达到一个平衡,当然这需要根据业务场景和数据量来多次测试。

对于nlist如果过大会导致聚类桶过多从而退化成暴力搜索,nprobe接近nlist也是会这种情况。根据我以前的使用我是设置成nlist=1024和nprobe=64.

nprobe越接近nlist召回率越高,当相等召回率100% ,nprobe越小QPS越高。

上述就是IVF的索引结构,但是因为内存和召回的原因,IVF下面又有很多的分化

类型

IVF_FLAT

基于nprobe满足的召回率,,而且该索引并没有进行压缩,目前我们使用的就是该索引

参数和IVF两个基本参数一样

IVF_SQ8

将FLAOT转化成UINT8,由4个字节变成1个字节,空间上缩小了3/4,但是需要牺牲很小一部分精度

参数和IVF两个基本参数一样

IVF_PQ

基于乘积量化,由高维数据变成低纬数据,但是这种的向量距离计算是根据分段索引的,所以精度会损失一部分

参数:

  1. m 将向量压缩成多少个小向量
  2. 压缩比

img

比如现在采用256维度的索引,若m = 4的索引,通常是压缩成4维度作为子向量,就可以达到1 / 4 / 4 = 1/16,缩减16倍

HNSW

一种基于层级图的索引,这种索引具有高召回率,高QPS,但是占用内存大,不适合中小型服务器。目前应该是milvus中第二快的索引。

上层稀疏,下层密集,区分度好

img

搜索过程就是在本层找到最近的节点,然后逼近。

参数:

  1. M 节点最大度
  2. efConstruction 搜索范围

ANNOY

一颗基于树的索引(暂时没有了接太多)

DiskANN

磁盘索引用于大量优化内存空间

检索篇

目前至此的向量检索主要有4种

  1. 欧式距离(L2)
  2. image-20240323105625878
  3. 点乘内积

当对嵌入向量数据归一后,我们的IP和L2计算结果是相等的

3.余弦

工作优化

目前我认为从milvus上的优化主要有

  1. 开启mmap零拷贝,减少内核和用户线程的数据拷贝,最新milvus支持
  2. 利用partition,partition为我们的逻辑分区,用上能大量减少检索向量的数量
  3. 目前是使用IVF_FLAT索引,没有进行向量的压缩,可以通过设置nlist和nprobe达到一个平衡值,之前的nlist和nprobe分别是64和10,官方建议值是nlist = 4 * sqrt(数据量),nprobe可以根据召回设计
  4. 更换索引类型

关于条件检索

关于时间的探讨

目前milvus对条件检索其实不太友好,并没有支持太多的功能,关于带条件的检索和不带条件的检索区别就是带expr就是在走索引之前把所有的向量过滤一遍,但是并不一定带条件比不带条件的要慢;

比如下面举一个例子

我有1000条的数据 expr = "city = '北京' ",topk = 5

带expr的情况:先对1000条数据进行全量比较city条件,花费50ms,然后过滤出来的数据有20条,然后进行索引的检索,搜查桶,如果是nlist = nprobe的话,那么我们就需要对20条数据进行向量距离的计算,就比如L2,20条花费了10ms,那么一共需要60ms的时间

不带expr的情况:直接计算1000条的向量距离,可能需要花费100ms;

这种情况就是不带条件有可能比带条件的花费时间更长的原因;

milvus隐藏段

milvus基于这里同时推出了动态字段和json类型的,其实两者是一样的。动态字段其实也就是milvus中的一个隐藏字段$meta,这就是一个json类型的字段。所以milvus不像es那样能随意实现动态映射的。

目前的痛点:

基于上面回到我们一开始走文件导入是直接文本分段,并且没有使用到隐藏段,所以就无法用到条件的过滤。现在阶段只支持addBatch方式手动添加字段,具体看下面文档。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇