跳转到内容

1.2 Milvus 架构解析:存算分离的云原生设计

理解架构不是炫技——是调优和排障的前提,你得知道瓶颈在哪个组件


这一节在讲什么?

上一节我们聊了 Milvus 的定位——分布式向量数据库,专为十亿级向量而设计。但"分布式"这三个字背后,Milvus 的架构到底长什么样?为什么它要这样设计?各个组件之间怎么协作?这些问题你可能觉得"我又不是 Milvus 的开发者,为什么要了解架构"——但当你遇到搜索延迟突然飙升、写入吞吐突然下降、或者索引构建卡住不动的时候,如果你不了解架构,就只能像无头苍蝇一样到处试。理解架构,你才能知道问题出在哪个组件、该怎么调。这一节我们就来拆解 Milvus 的架构设计。


存算分离:Milvus 架构的核心理念

Milvus 架构最核心的设计理念是存算分离(Disaggregated Storage)——把数据存储和查询计算分成独立的模块,各自可以独立扩缩容。为什么要这样做?因为向量数据库的存储和计算对资源的需求是完全不同的:

  • 存储需要大容量、持久化、高吞吐的磁盘 I/O——向量数据动辄几百 GB 甚至 TB 级
  • 计算需要大内存、高 CPU——向量搜索需要在内存中计算距离,索引也需要常驻内存

如果把存储和计算耦合在一起(像 pgvector 那样),你就不得不买一台既有大磁盘又有大内存的服务器——这种机器很贵,而且当你只需要更多存储空间时,你也得连带买更多内存,反之亦然。存算分离让你可以独立地扩展存储和计算——存储不够就加对象存储的容量(便宜),计算不够就加 QueryNode(按需),资源利用率更高。

存算分离 vs 存算耦合:

  存算耦合(pgvector):
  ┌──────────────────────────┐
  │  PostgreSQL 服务器        │
  │  ┌────────┐ ┌─────────┐ │
  │  │ 存储   │ │ 计算    │ │  → 必须同时升级,资源浪费
  │  │ (磁盘) │ │ (内存)  │ │
  │  └────────┘ └─────────┘ │
  └──────────────────────────┘

  存算分离(Milvus):
  ┌──────────┐     ┌──────────┐
  │ 对象存储  │     │ QueryNode│  → 独立扩缩容
  │ (MinIO)  │     │ (内存)   │  → 存储不够加磁盘
  │ 便宜     │     │ 计算不够加节点
  └──────────┘     └──────────┘

三大核心组件

Milvus 的架构可以分成三层:协调器层、工作节点层和存储层。每一层都有明确的职责,层与层之间通过消息队列和 RPC 通信。

Milvus 架构全景图:

  ┌─────────────────────────────────────────────────────────┐
  │                    协调器层(大脑)                       │
  │  ┌───────────┐ ┌───────────┐ ┌──────────┐ ┌──────────┐ │
  │  │RootCoord  │ │QueryCoord │ │DataCoord │ │IndexCoord│ │
  │  │(元数据)   │ │(查询调度) │ │(数据调度)│ │(索引调度)│ │
  │  └───────────┘ └───────────┘ └──────────┘ └──────────┘ │
  └────────────────────────┬────────────────────────────────┘

  ┌────────────────────────┼────────────────────────────────┐
  │                    工作节点层(手脚)                      │
  │  ┌───────────┐ ┌───────────┐ ┌──────────┐              │
  │  │QueryNode  │ │DataNode   │ │IndexNode │              │
  │  │(执行搜索) │ │(写入数据) │ │(构建索引)│              │
  │  └───────────┘ └───────────┘ └──────────┘              │
  └────────────────────────┬────────────────────────────────┘

  ┌────────────────────────┼────────────────────────────────┐
  │                    存储层(记忆)                          │
  │  ┌───────────┐ ┌───────────┐ ┌──────────┐              │
  │  │  etcd     │ │  MinIO/S3 │ │Pulsar/   │              │
  │  │(元数据)   │ │(对象存储) │ │Kafka     │              │
  │  │           │ │           │ │(消息队列)│              │
  │  └───────────┘ └───────────┘ └──────────┘              │
  └─────────────────────────────────────────────────────────┘

协调器层(Coordinator):大脑

协调器是 Milvus 的"大脑",负责元数据管理、任务调度和全局协调。它不直接处理数据,而是告诉工作节点"该做什么":

  • RootCoord:管理元数据——Collection 的创建/删除、Schema 的变更、分区的创建。当你在 Python 里调用 create_collection() 时,实际上是 RootCoord 在处理这个请求
  • QueryCoord:管理查询调度——决定哪些 QueryNode 加载哪些数据、如何分配搜索任务、如何做负载均衡。当搜索延迟升高时,QueryCoord 会尝试把数据重新分配到不同的 QueryNode
  • DataCoord:管理数据调度——决定数据写入哪些 Segment、何时触发 Flush 和 Compaction。它维护着"哪些数据在哪个 Segment 里"的全局视图
  • IndexCoord:管理索引调度——决定何时构建索引、分配索引构建任务给 IndexNode。当你调用 create_index() 时,IndexCoord 会选择一个空闲的 IndexNode 来执行

工作节点层(Worker Node):手脚

工作节点是 Milvus 的"手脚",负责实际的数据处理工作:

  • QueryNode:执行搜索和查询。它从对象存储加载数据和索引到内存,然后响应搜索请求。QueryNode 是内存消耗最大的组件——所有需要搜索的数据和索引都必须在 QueryNode 的内存中
  • DataNode:处理数据写入。它从消息队列消费写入请求,把数据攒成 Segment,然后写入对象存储。DataNode 是 CPU 密集型组件——它需要处理数据的序列化、压缩和持久化
  • IndexNode:构建索引。它从对象存储读取原始向量数据,构建索引文件,然后把索引文件写回对象存储。索引构建是 CPU 和内存密集型操作,IndexNode 的资源消耗取决于数据量和索引类型

存储层:记忆

存储层是 Milvus 的"记忆",负责数据的持久化和通信:

  • etcd:存储元数据——Collection 的 Schema、Segment 的位置信息、索引的元数据。etcd 是整个系统的"目录",如果 etcd 挂了,Milvus 就无法找到任何数据
  • MinIO / S3:对象存储——存储实际的向量数据文件和索引文件。这是 Milvus 的"仓库",所有持久化数据都在这里
  • Pulsar / Kafka:消息队列——缓冲写入请求,实现组件间的异步通信。当你的应用调用 insert() 时,数据先写入消息队列,DataNode 再从队列消费

数据流:从写入到搜索的完整路径

理解了组件之后,让我们跟踪一条数据从写入到可搜索的完整路径:

数据从写入到搜索的完整路径:

  1. 应用调用 insert()


  2. 数据写入消息队列(Pulsar/Kafka)


  3. DataNode 从消息队列消费数据


  4. DataNode 把数据攒成 Segment,写入对象存储(MinIO/S3)


  5. DataCoord 更新元数据(etcd),记录新 Segment 的位置


  6. 应用调用 create_index() → IndexCoord 分配任务给 IndexNode


  7. IndexNode 从对象存储读取原始数据,构建索引文件,写回对象存储


  8. 应用调用 load() → QueryCoord 指示 QueryNode 加载数据和索引到内存


  9. 应用调用 search() → QueryNode 在内存中执行搜索,返回结果

这个流程解释了为什么 Milvus 有一些"奇怪"的行为:

  • 插入后搜不到数据:因为数据还在消息队列里,DataNode 还没消费,更别说加载到 QueryNode 了。你需要 flush() 强制数据持久化,或者等待自动同步
  • 建索引后还是暴力搜索:因为索引文件还在对象存储里,你需要 load() 把索引加载到 QueryNode 内存
  • 删除后空间没释放:因为删除是逻辑删除(标记为已删除),需要 compact() 合并 Segment 才能物理清理

比如,下面的程序展示了从创建 Collection 到搜索的完整流程,由于 Milvus 的存算分离架构,每一步都有明确的职责和触发条件:

python
from pymilvus import MilvusClient, DataType

client = MilvusClient(uri="http://localhost:19530")

# 第1步:创建 Collection(RootCoord 处理)
client.create_collection(
    collection_name="documents",
    dimension=768,
    metric_type="COSINE"
)

# 第2步:插入数据(写入消息队列 → DataNode 消费 → 对象存储)
client.insert(
    collection_name="documents",
    data=[
        {"id": 1, "vector": [0.1] * 768, "text": "hello world"},
        {"id": 2, "vector": [0.2] * 768, "text": "goodbye world"},
    ]
)

# 第3步:创建索引(IndexNode 构建 → 写回对象存储)
client.create_index(
    collection_name="documents",
    field_name="vector",
    index_params={"index_type": "HNSW", "metric_type": "COSINE", "params": {"M": 16, "efConstruction": 256}}
)

# 第4步:加载到内存(QueryNode 从对象存储加载索引)
client.load_collection("documents")

# 第5步:搜索(QueryNode 在内存中执行)
results = client.search(
    collection_name="documents",
    data=[[0.15] * 768],
    limit=2,
    output_fields=["text"]
)

为什么需要理解架构

你可能会觉得"我只是用 Milvus 做搜索,为什么要了解这么多架构细节"。原因很简单——调优和排障时你需要知道瓶颈在哪个组件

问题现象可能的瓶颈组件排查方向
搜索延迟高QueryNode检查内存是否足够、索引参数是否合理
插入吞吐低DataNode / 消息队列检查消息队列积压、DataNode 数量
索引构建慢IndexNode检查 CPU 和内存资源、数据量
数据"丢了"DataCoord / QueryNode检查是否 flush、一致性级别
元数据操作慢RootCoord / etcd检查 etcd 性能
整体系统卡协调器检查 Collection/Partition 数量是否过多

比如,当你发现搜索延迟从 10ms 飙升到 500ms,如果你不了解架构,你可能会去调搜索参数(ef、nprobe),但实际上问题可能是 QueryNode 内存不够导致索引被换出了——这时候调搜索参数根本没用,你需要加 QueryNode 或者增加内存。


常见误区:Milvus Standalone 不需要理解架构

有些同学觉得"我用的是 Milvus Standalone(单容器部署),所有组件都在一个容器里,不需要理解架构"。这个想法是错误的——即使所有组件打包在一个容器里,它们的职责和协作方式并没有变。搜索慢了还是得看 QueryNode 的内存,写入慢了还是得看 DataNode 的消费速度,索引构建卡了还是得看 IndexNode 的资源。Standalone 只是把部署变简单了,并没有把架构变简单。


小结

这一节我们拆解了 Milvus 的三层架构:协调器层负责调度和管理,工作节点层负责实际的数据处理,存储层负责持久化和通信。存算分离的设计让 Milvus 可以独立扩展存储和计算,但也带来了更多的组件和更复杂的运维。理解架构的核心价值在于——当系统出问题时,你能快速定位瓶颈在哪个组件,而不是盲目地调参数。下一节我们要聊的是 Milvus 的安装和连接,从最简单的 Milvus Lite 到生产级的分布式集群。

基于 MIT 许可发布