RAG 进阶之路2:Milvus 架构深度剖析与索引详解

为什么说 Milvus 2.0 是云原生的?它如何通过“存算分离”实现无限扩展?从 Kafka 的多路消费到 HNSW 的参数调优,本文配合详细架构图,深度拆解 Milvus 的内核机制。


在 RAG 系统中,向量数据库(Vector Database)是长时记忆的载体。许多开发者在初次接触时,往往会选择 Faiss 这样的轻量级库。但当数据量突破千万级,或者需要高并发、高可用(HA)支持时,架构完善的数据库系统就成为了必然选择。

Milvus 是目前最主流的云原生向量数据库之一。与传统数据库不同,它采用了存储与计算完全分离的设计。这意味着,如果你还没看懂它的架构,你可能正在浪费大量的服务器资源,或者在面对“为什么刚插的数据搜不到?”这种问题时束手无策。

今天,我们通过一张图,并结合日志流转索引原理,彻底看懂 Milvus 是如何工作的。

一、 一张图看懂 Milvus 架构

Milvus 的架构设计遵循“云原生(Cloud-native)”原则,整体分为四个层次。其中最核心的设计哲学是:日志即数据(Log as Data)

请看下面的架构逻辑图:

graph TD
    %% Define Styles
    classDef storage fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
    classDef compute fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
    classDef coord fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;
    classDef access fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;

    %% Client Layer
    Client([SDK / REST API]) --> LB[Load Balancer]

    %% Layer 1: Access Layer
    subgraph "接入层 (Access Layer)"
        LB --> Proxy1[Proxy Node]
        LB --> Proxy2[Proxy Node]
    end
    class Proxy1,Proxy2 access;

    %% Layer 2: Coordinator Service
    subgraph "协调服务 (Coordinator Service)"
        RootC[Root Coord<br/>(DDL/时间授时)]
        QueryC[Query Coord<br/>(查询调度)]
        DataC[Data Coord<br/>(数据落盘管理)]
        IndexC[Index Coord<br/>(索引构建)]
    end
    class RootC,QueryC,DataC,IndexC coord;

    %% Layer 3: Worker Nodes (Execution)
    subgraph "执行节点 (Worker Nodes)"
        QN[Query Node<br/>(实时+历史搜索)]
        DN[Data Node<br/>(持久化/刷盘)]
        IN[Index Node<br/>(构建索引)]
    end
    class QN,DN,IN compute;

    %% Connections
    Proxy1 -- DML/DDL --> RootC
    Proxy1 -- Insert --> MsgStream
    Proxy1 -- Search --> QN

    QueryC -- 负载均衡 --> QN
    DataC -- Checkpoint --> MsgStream
    IndexC -- 调度任务 --> IN

    %% Layer 4: Storage Layer
    subgraph "存储层 (Shared Storage)"
        Meta[Etcd<br/>(元数据存储)]
        MsgStream[Log Broker<br/>(Pulsar / Kafka)<br/>消息骨干]
        ObjStore[Object Storage<br/>(MinIO / S3)<br/>冷数据仓库]
    end
    class Meta,MsgStream,ObjStore storage;

    %% Data Flow
    MsgStream -->|订阅流式数据 (实时)| QN
    MsgStream -->|订阅流式数据 (归档)| DN
    DN -->|Flush 落盘| ObjStore
    ObjStore -->|Load 历史数据| QN
    ObjStore -->|读取原始向量| IN
    IN -->|写入索引文件| ObjStore

核心层级详解

让我们像剥洋葱一样,由外向内解析这四个层级。

1. 接入层 (Access Layer):系统的“大门”

  • 组件: Proxy
  • 职责: 无状态的 API 前端。
  • 核心机制:
    • 请求验证: 验证你的用户名密码、检查请求格式。
    • 结果聚合 (Global Reduce): 当你发起一次搜索时,可能需要从多个 Query Node 查数据。Proxy 负责把这些节点返回的局部 Top-K 结果收集起来,进行全局排序,剪枝后返回给客户端。
  • 扩展性: 因为它是无状态的,如果并发请求量太大(比如每秒 10 万次查询),你只需要多启动几个 Proxy 容器,配合 Nginx 负载均衡即可。

2. 协调服务 (Coordinator Service):系统的“大脑”

这层不干苦力活,只负责发号施令。

  • Root Coord: 总管。处理创建表(Collection)、删除分区等 DDL 操作。
  • Query Coord: 也是 RAG 开发者最需要关注的。它负责查询节点的负载均衡
    • 场景: 如果某个 Query Node 内存爆了,Query Coord 会把部分数据段(Segment)迁移到其他空闲节点上。
  • Data Coord & Index Coord: 分别管理数据怎么落盘、索引什么时候建。

3. 执行节点 (Worker Nodes):系统的“四肢”

这是真正消耗 CPU 和内存的地方。

  • Query Node (查询节点): 最核心组件。它负责在内存中进行向量检索。它需要同时处理两类数据:
    • 增量数据: 刚插入、还在消息队列里的热数据。
    • 历史数据: 已经存入 S3、建立了索引的冷数据。
  • Data Node: 负责“搬运”。它从消息队列里把流式数据读出来,打包成文件,扔到 S3 里去。
  • Index Node: 负责“压缩”。它读取原始向量,构建 HNSW 或 IVF 索引文件,再写回 S3。

4. 存储层 (Storage):系统的“地基”

Milvus 的精髓在于共享存储(Shared Storage)

  • Meta Store (Etcd): 存储配置和元数据。
  • Log Broker (Pulsar/Kafka): 这是 Milvus 的大动脉
    • Milvus 是基于日志的(Log-based)。所有的插入、删除操作,首先都是写入 Pulsar。
    • 这意味着:只要数据进了 Pulsar,就算整个集群断电,数据也不会丢。
  • Object Storage (MinIO/S3): 数据的最终归宿。
    • 为什么用 S3? 因为向量数据太大了。内存存不下,本地磁盘扩容难。S3 便宜且容量无限。Query Node 需要查历史数据时,直接从 S3 下载加载到内存。

数据的生命周期:从写入到被搜到

理解了架构,我们看看一条数据是如何流转的。

写入流程 (Insert)

  1. 客户端发送 insert 请求给 Proxy
  2. Proxy 将数据打包,写入 Log Broker (Kafka)
    • 注意:此时 Proxy 就可以给客户端返回“成功”了。
  3. Data Node 从 Pulsar 订阅消息,积累到一定程度(比如 512MB),将其 Flush(刷盘)到 MinIO
  4. Index Coord 发现 MinIO里有了新文件,安排 Index Node 为其构建索引。
  1. 客户端发送 search 请求给 Proxy
  2. Proxy 解析路由,请求发给 Query Node
  3. Query Node 执行混合搜索
    • 在内存中的历史数据段(已从 MinIO加载)中搜。
    • 在内存中的增量数据段(直接从 Kafka订阅的实时数据)中搜。
  4. Query Node 将两部分结果合并(Local Reduce),返回给 Proxy。
  5. Proxy 进行全局合并(Global Reduce),返回给客户端。

二、 Log Broker 的双重身份

在 Milvus 架构中,Log Broker (Kafka) 处于绝对的中心位置。Milvus 让 Data Node 和 Query Node 同时订阅 Kafka,也就是 “多路消费(Multi-Consumer)” 机制。

1. Data Node:为了“存” (Persistence)

  • 角色: 它是 Kafka 的归档员。
  • 任务: 订阅数据,缓存在内存,积攒到一定大小(如 512MB)后,打包并 Flush(刷盘) 到对象存储(S3/MinIO)。
  • 结果: 生成了历史数据(Sealed Segment),永久保存。
  • 角色: 它是 Kafka 的实时解说员。
  • 痛点: Data Node 刷盘到 MinIO是有延迟的。如果只读 MinIO,用户刚插的数据要等几十秒才能搜到。
  • 任务: 绕过 MinIO,直接监听 Kafka。 它把从 Kafka 读到的最新消息直接转化为可计算的向量,存入内存的增长段(Growing Segment)
  • 结果: 实现了即时搜索(Real-time Search)
形象比喻:
Kafka 是比赛直播流。Data Node 是录像员,把直播录成光盘(MinIO)存档;Query Node 是现场解说,盯着直播流(Kafka)看,观众问比分(Search),他能立刻回答,不需要等光盘刻录好。

3. 如何保证一致性?(TimeTick 机制)

Query Node 和 Data Node 并行消费,怎么保证步伐一致?
Milvus 引入了 TimeTick(时间滴答)

  1. 授时: Root Coord 周期性向 Kafka 发送带时间戳的消息(如 ts=100)。
  2. 对齐: Query Node 收到 ts=100 时,就知道“100 之前的所有数据都到齐了”。如果用户查询请求要求的时间戳是 98,Query Node 就可以放心地执行搜索;如果是 102,它会阻塞等待下一个滴答。

4. 数据何时删除?(Checkpoint)

Kafka 里的数据不会永久保留。

  • Checkpoint: 当 Data Node 成功把数据存入 MinIO后,会向协调者汇报一个 Checkpoint(检查点)。
  • 清理: 意味着 Checkpoint 之前的数据已经安全了。Milvus 依赖 Kafka 自身的 Retention Policy(保留策略) 自动删除旧日志。
    • Pro Tip: 生产环境中,Kafka 的保留时间(如 24h)务必大于 Milvus 的刷盘间隔,防止数据还没落盘就被 Kafka 删了。

三、索引类型选型:RAG 怎么选?

架构决定了上限,索引决定了速度。在 Milvus 中,你必须为向量字段创建索引才能高效搜索。以下是 RAG 场景的三大主流选择:

索引类型算法原理适用场景内存占用召回率
HNSW基于图结构 (Hierarchical Navigable Small World)通用首选。追求极致的性能和高召回率。高 (原始向量 + 图关系)极高 (99%+)
IVF_SQ8倒排 + 标量量化 (Scalar Quantization)内存受限。将 float (4字节) 压成 int (1字节)。低 (约为原始的 25%)较高 (90%+)
IVF_FLAT倒排 + 原始向量追求 100% 召回,且内存非常充足时。中 (等于原始数据)
DiskANN基于磁盘的图索引超大规模。亿级数据,内存放不下,用 NVMe SSD 扛。极低 (仅缓存导航点)

建议

  • 刚开始做 RAG: 无脑选 HNSW。参数设置 M=16, efConstruction=200,这是一个在速度和精度上非常平衡的配置。
  • 数据量超过 500 万且预算有限: 换成 IVF_SQ8。你会发现内存占用瞬间下降 70%,而检索准确率的肉眼体感差异并不大。

四、 索引类型与参数调优详解

架构决定了系统的上限,而索引(Index)决定了查询的快慢。在 Milvus 中,我们需要区分 “怎么找(图 vs 倒排)”“怎么存(原始 vs 量化)”

1. 怎么找:HNSW vs IVF

索引类型 全称 原理比喻 优缺点
HNSW Hierarchical Navigable Small World 人际关系网。找马斯克不查电话簿,而是通过朋友 A -> 朋友 B -> 马斯克。 性能最强,召回极高。但构建慢,且非常吃内存(要存点与点的边关系)。
IVF Inverted File 图书馆分类。把向量聚类成 1000 个簇。搜的时候只去最近的几个簇里找。 内存占用低,速度快。但召回率不如 HNSW 稳定(边界问题)。

2. 怎么存:Float vs SQ8 (量化)

这是节省成本的关键。

  • Flat (Float32): 原汁原味。1024 维向量占用 1024 * 4 bytes = 4KB。精度最高。
  • SQ8 (Scalar Quantization): 有损压缩。将 4 字节浮点数压缩为 1 字节整数。占用 1024 * 1 byte = 1KB
    • 性价比之选: 内存占用减少 75%。虽然精度有细微损失,但在 RAG 场景下,Top-K 的排序结果几乎不受影响。

3. HNSW 关键参数详解

如果你选择了 HNSW(推荐),必须理解这两个参数:

  • M = 16 (最大连接数)
    • 含义: 每个节点在图里最多能有几个“朋友”。
    • 影响: M 越大,路越通畅,召回越高,但内存消耗急剧增加。16 是一个在性能和内存间完美的平衡点(Goldilocks number)。
  • efConstruction = 200 (构建搜索范围)
    • 含义: 建图时,为了找这 16 个朋友,我要考察多少个候选人。
    • 影响: efConstruction 越大,建索引越慢,但图的质量越高(朋友找得准),以后的搜索召回率越高。设为 200 是为了“一次辛苦,永久受益”。

理解了这些底层逻辑,你就掌握了 RAG 系统的性能命脉。下一篇,我们将离开基础设施,进入 RAG 的“灵魂”——语义模型。我们将探讨 BERT、BGE 以及 Reranker 如何决定你的系统到底“懂不懂”用户的话。