在前面的章节中,我们介绍了如何开始使用 Milvus,并用它构建应用。从本章到第 7 章,我们将通过探索 Milvus 的外部交互方式和内部工作机制,进一步加深对 Milvus 的理解。
在现代 AI 和数据驱动应用中,高效存储、索引和查询海量向量数据的能力至关重要。Milvus 的架构从一开始就被设计为可以随着需求增长而扩展,无论你是在为推荐引擎处理几百万条 embeddings,还是为大规模图像搜索处理数万亿条向量。本章将深入 Milvus 的可扩展设计,从定义数据如何组织的数据模型,到多层架构和 message queue(MQ,消息队列)。
本章将覆盖以下主题:
- Milvus 数据模型
- Milvus 的分层架构
- MQ:流式数据的生命线
技术要求
本章使用的代码可以在我们的 GitHub 仓库中找到:
https://github.com/PacktPublishing/The-Architecture-Handbook-for-Milvus-Vector-Database
要运行这些代码示例,你需要一个 Python >= 3.8 的环境,可以从以下地址下载:
https://www.python.org/downloads/
并且应使用第 5 章 ch5 目录中的 requirements.txt 文件安装所有依赖:
pip install -r requirements.txt
除了 Python 依赖之外,你还需要一个正在运行的 Milvus 实例来执行本章代码。关于如何启动 Milvus,请参考第 2 章和第 3 章。部分示例可以直接在 Milvus Lite 上运行,而无需运行 Milvus 实例;更多关于 Milvus Standalone 的信息,请查看 Milvus 官方文档:
https://milvus.io/docs
这些示例会在正文中特别注明。
Milvus 数据模型
任何数据库的核心都是其数据模型,它定义数据如何被结构化、存储和访问。Milvus 的数据模型是一种 vector model,专门为管理向量数据而设计,在强制结构的同时也允许动态字段。它由 collections、entities 和 fields 组成。
本节中,我们将通过示例介绍 collection、entity、field 和 schema 的概念。随后,我们将介绍 partitions 和 shards 如何对 collection 进行细分,以支持并行处理和高效检索。先从 collection、entity 和 schema 的关键概念讲起。
Collection、entity 和 schema
Collection、entity 和 schema 分别相当于关系型数据库中的 table、row 和 column。例如,一个名为 movies 的 collection 可以为每部电影存储一个 entity,其中包含 ID、title、genre、release year 和 embedding 等 fields,就像关系型数据库中的 movies 表为每部电影存储一行,并包含对应列。Schema 定义这些 fields 及其数据类型,就像关系型数据库中的表定义一样。
Milvus 中的 collection 是数据的逻辑容器;entities 实际存储、索引和查询的地方都在 collection 中。Milvus 中所有读写操作都针对某个特定 collection 执行。每个 collection 包含 vector embeddings、metadata fields 和 index information。一个 collection 存储遵循已定义 schema 的 entities,也就是 records。
Entity 是 collection 中的一行数据。Field 定义 entity 的属性。每个 collection 都有名称、定义其结构的 schema、shards、partitions 和 segments。它既包含实际数据本身,也包含 metadata 和 schema definition。下面是一个代码示例:
{
"id": 101,
"title": "Milvus Tutorial",
"embedding": [0.12, 0.55, 0.91, ...]
}
这个 entity 包含一个 primary key(PK)ID、一个 metadata title,以及一个 vector embedding,总共三个 fields。
Collection 的 schema 描述 collection 内 entity 的结构和约束。它描述有哪些 fields、它们的类型和角色。创建 collection 时,你必须提供一个 schema,指定 field definitions,也就是 field 的名称、数据类型和其他属性。
例如,考虑一个名为 articles 的 collection。它的 schema 包含三个 fields:id、title 和 embedding。id field 使用 INT64 数据类型,并作为 PK。title field 使用 VARCHAR 数据类型,最大长度为 128 字节。embedding field 使用 FLOAT_VECTOR 数据类型,维度为 768。示例如下:
Collection: articles
Fields:
id(INT64, primary key)
title(VARCHAR, max_length=128)
embedding(FLOAT_VECTOR, dim=768)
Milvus 对 collection 强制执行严格 schema model,同时允许一个特殊动态字段来提供灵活性,该字段以 JSON 数据类型表示。严格 schema model 意味着每个 field 都必须预先在 collection schema 中声明。每个 field 都有固定名称、类型和角色,例如 PK、partition key。插入数据时,entities 必须符合 schema。
在 Milvus 中,collection schema 必须至少包含两个 fields:一个 PK field 和一个 vector field。
接下来,我们使用真实开源数据集 Cohere/wikipedia-2023-11-embed-multilingual-v3 作为示例,在 Milvus 中创建一个 Wikipedia collection:
https://huggingface.co/datasets/Cohere/wikipedia-2023-11-embed-multilingual-v3
该数据集使用 Cohere 的多语言 embedding 模型 embed-multilingual-v3,支持多种语言,每个文档包含 title、body text 和预计算的 1024 维 vector embedding。
为了简化演示,我们聚焦 English("en")子集,并熟悉该数据集:
# Import the load_dataset function from the HuggingFace datasets library
# This function allows us to download and load datasets directly from HuggingFace Hub
from datasets import load_dataset
# Import Polars library
# Polars is a fast DataFrame library (similar to pandas but faster for large data)
# It is often used for high-performance data processing
import polars as pl
# Name of the dataset hosted on HuggingFace Hub
# This dataset contains Wikipedia text embeddings generated using Cohere's multilingual embedding model
DS = " Cohere/wikipedia-2023-11-embed-multilingual-v3"
# Load the dataset
wiki_ds = load_dataset(
DS, # Dataset name on HuggingFace Hub
"en", # Configuration / subset of dataset of dataset (English Wikipedia)
split="train", # Which split of the dataset to load(train/test/validation)
streaming=True # Enables streaming mode so the dataset is not fully downloaded
# instead, it is read lazily row-by-row which saves memory.
)
# Print the schema / structure of the dataset
# This shows the dataset fields such as text, title, embeddings, etc.
print(wiki_ds.features)
执行上述代码后,你会看到类似以下输出。该数据集包含五个属性:_id、url、title、text 和 emb:
{'_id': Value('string'), 'url': Value('string'), 'title': Value('string'), 'text': Value('string'), 'emb': List(Value('float32'))}
接下来,我们以 Wikipedia 数据集为例,在 Milvus 中创建一个名为 coll_wiki 的 collection,该 collection 只包含 id 和 emb 两个 fields。这个示例使用 Milvus Lite 进行演示。只要你安装了带 Milvus Lite 的 PyMilvus,请参见 ch5 目录中的 requirements.txt 文件,就不需要运行 Milvus 实例:
# Import MilvusClient to interact with the Milvus vector database
# Import DataType to define types in the schema
from pymilvus import MilvusClient, DataType
# Create a schema object that will define the structure of the collection
# A schema describes what fields exist in the collection and their data types.
schema = MilvusClient.create_schema()
# Add a field name "id" to the schema
# datatyp=INT64 means the field stores 64-bit integers
# is_primary=True means this field is the primary key (unique identifier for each record)
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
# Add a vector field named "emb"
# datatype=FLOAT_VECTOR means this field will store vector embeddings
# dim = 1024 means each vector contains 1024 floating point values
# This dimension must match the embedding model output size.
schema.add_field(field_name="emb", datatype=DataType.FLOAT_VECTOR, dim=1024)
# Initialize a Milvus client connection
# uri="./milvus.db" creates or connects to a local Milvus Lite database file
# This is useful for local development and testing without running a full Milvus server
client = MilvusClient(uri="./milvus.db")
# Create a new collection named "coll_wiki"
# The collection will follow the schema defined above
# If the collection already exists, Milvus may throw an error depending on configuration.
client.create_collection(collection_name="coll_wiki", schema=schema)
在上述代码中,我们有以下设置:
idfield 被声明为 PK field,设置了is_primary=True,对应数据集中的_id属性。embedfield 被设置为 vector field,数据类型是FLOAT_VECTOR,维度为 1024。FLOAT_VECTOR是 Milvus 支持的向量数据类型之一,在内部使用float32存储每个浮点值。
现在我们已经了解了 collection 和 schema 的基础概念,接下来讨论一个 entity 可能由什么组成。
Entity composition
Entity 是 collection 中的单个数据点。每个 entity 包含以下四类 fields:
Required fields:必需字段是插入 collection 的每个 entity 都必须存在的 fields。这些 fields 在 collection schema 中定义,在插入时不能省略。Milvus entities 需要一个 PK field,并且作为向量数据库,至少需要一个 vector field。如果你提供自己的 PK,Milvus 不保证 PK 值唯一。
Predefined fields:预定义字段是在创建 collection 之前,在 schema 中显式定义的 fields。这些 fields 会和 vectors 一起存储结构化 metadata。由 schema 定义后,Milvus entities 可以包含可选 scalar 数据类型字段,例如 INT64、VARCHAR、JSON、ARRAY、AND 等。
Dynamic fields:动态字段允许存储 schema 中未预定义的额外 fields。由 schema 定义后,Milvus entities 可以包含一个可选 schema-less field 来提供灵活性,该 field 以 JSON 数据类型存储。当部分 entities 需要存储额外数据时,这非常有用。
System-reserved fields:系统保留字段由 Milvus 自动管理,不能作为用户自定义 field names 使用。系统会为所有 collections 添加两个 fields:RowID 和 Timestamp。RowID field 确保唯一性,Timestamp 支持 multi-version concurrency control(MVCC)和查询一致性。与 PK field 不同,RowID field 保证唯一。
MVCC 是一种允许并发数据访问而无需加锁的技术。在 Milvus 中,Timestamp field 充当版本。当查询运行时,它只看到该 timestamp 之前已提交的数据,即使有持续写入,也能确保一致读取。
图 5.1 展示一个 entity 包含什么:
图 5.1:Entity 包含的内容
下面通过一个示例创建包含四类 fields 的 coll_wiki_2 collection,并使用 Wikipedia 数据集插入一个 entity。
沿用前面的代码,我们从数据集中取第一行,并将其插入 collection。我们不会下载完整数据集,所以不用担心数据集太大:
# Import the Milvus client and DataType definitions
# MilvusClient is used to connect and interact with the Milvus database
# DataType helps define the type of each field in the schema
from pymilvus import MilvusClient, DataType
# Create a connection to a local Milvus Lite database
# "./coll_wiki.db" is the local database file where Milvus will store the collection
client = MilvusClient(uri="./coll_wiki.db")
# Create a schema for the collection
# enable_dynamic_field=True allows inserting fields that are not predefined in the schema
# These additional fields will automatically be stored in Milvus' internal $meta field
schema = MilvusClient.create_schema(enable_dynamic_field=True)
# Add a primary key field called "id"
# datatype=VARCHAR means the id will be stored as a string
# is_primary=True indicates this is the unique identifier for each entity
# max_length=128 defines the maximum allowed string lengthschema.add_field(field_name="id", datatype=DataType.VARCHAR, is_primary=True, max_length=128)
# Add a vector field called "emb"
# datatype=FLOAT_VECTOR indicates that this field stores vector embeddings
# dim=1024 means each vector contains 1024 float values
# The dimension must match the embedding model output
schema.add_field(
field_name="emb", datatype=DataType.FLOAT_VECTOR, dim=1024)
# Add a "title" field to store the title of the document/article
# VARCHAR allows storing string text
# max_length=256 defines the maximum character limit
schema.add_field(
field_name="title", datatype=DataType.VARCHAR, max_length=256)
# Add a "url" field to store the source URL of the article
# A very large max_length is used since URLs can be long
schema.add_field(
field_name="url", datatype=DataType.VARCHAR, max_length=65535)
# Add a "text" field to store the full content of the article
# Again using VARCHAR with a large max_length to support long documents
schema.add_field(
field_name="text", datatype=DataType.VARCHAR, max_length=65535)
# Create the collection in Milvus using the defined schema
# "coll_wiki_2" will store Wikipedia embeddings along with metadata
client.create_collection(collection_name="coll_wiki_2", schema=schema)
# Extract the primary key value from the dataframe
# df['_id'][0] gets the first record's id
pk = df['_id'][0]
entity = {
"id": pk, # Primary key
"emb": df['emb'][0], # 1024-dimension embedding vector
"title": df['title'][0], # Article title
"text": df['text'][0], # Article content
"url": df['url'][0], # Source URL
# This field is NOT defined in the schema
# Because dynamic fields are enabled, Milvus will store this inside the internal $meta field
"extra": "This will go into dynamic field".
}
# Insert the entity into the collection
# data must be a list of entities even if inserting a single record
client.insert(collection_name="coll_wiki_2", data=[entity])
当你设置 enable_dynamic_schema = True 时,Milvus 会创建一个名为 $meta 的 JSON 数据类型 field,如示例所示。示例 entity 中的额外信息会被插入这个 dynamic field。
title field 被设置为 VARCHAR 数据类型,并带有 max_length = 256 属性,用于约束可变字符 field 的最大大小。
然后,我们从 coll_wiki_2 collection 中获取插入的行,并输出除 emb field 以外的 entity,因为它太大,不适合显示:
# Retrieve an entity (record) from the collection using the primary key
# "coll_wiki_2" is the collection name from which the record will be fetched
# pk is the primary key value of the entity we want to retrieve
# output_fields specifies which fields we want
res = client.get(
"coll_wiki_2", pk,
output_fields=["id", "title", "text", "url", "extra"])
# Print the retrieved entity
# The result will usually be a list of dictionaries
print(res)
输出如下:
data: ["{'id': '20231101.en_13194570_0', 'text': 'The British Arab Commercial Bank PLC (BACB) is an international wholesale bank incorporated in the United Kingdom that is authorised by the Prudential Regulation Authority (PRA) and regulated by the PRA and the Financial Conduct Authority (FCA). It was founded in 1972 as UBAF Limited, adopted its current name in 1996, and registered as a public limited company in 2009. The bank has clients trading in and out of developing markets in the Middle East and Africa.', 'title': 'British Arab Commercial Bank', 'url': 'https://en.wikipedia.org/wiki/British%20Arab%20Commercial%20Bank', 'extra': 'This will go into dynamic field'}"], extra_info: {}
现在我们已经知道 Milvus 中一个 entity 可能由什么组成,接下来讨论两种为了并行性和可扩展性而物理拆分 collection 的方式:partitions 和 shards。
Partitions 和 shards 是 collection 的两类物理细分方式。它们通常结合使用,也就是说,如果一个 collection 有两个 partitions 和两个 shards,数据会被物理划分为四个部分。图 5.2 中,Collection A 包含两个 shards(Shard 1 和 Shard 2)以及两个 partitions(Partition A 和 Partition B)。每个 shard 持有四个 segments。每个 partition 也持有四个 segments。在每个 shard 和 partition 的组合内部,有两个 segments。
从 shards 视角看,collection 的数据由 Shard 1 和 Shard 2 组成。从 partitions 视角看,它由 Partition A 和 Partition B 组成:
图 5.2:Collection 布局
我们先深入 partitions。
Partition
Partitions 用于将 collection 逻辑划分为更小的数据片段。这有助于组织数据,并通过将查询限制在特定子集来提升搜索效率。Partitions 允许你根据应用相关标准对数据分组,例如按日期、类别或用户组。Collection 中有两种管理 partitions 的方式。
不过,partitions 不应被过度使用;创建过多 partitions 会增加管理开销,并对性能产生负面影响。相反,应围绕有意义的数据分段设计 partitions,并在保持数量相对较少的同时,确保数据在 partitions 间均衡分布。
Partition APIs
第一种将 collection 划分为 partitions 的方式,是使用用户定义 partitions,并通过 partition 相关 APIs 完全控制和管理。Collection 中的 partitions 是共享同一 schema 的小型 collections。你可以像管理 collection 一样管理 partitions,例如创建、删除、加载、搜索、插入等。
下面示例创建两个名为 "p1" 和 "p2" 的 partitions,并使用 Wikipedia 数据集向每个 partition 插入一个 entity。注意,Milvus Lite 不支持 partition-related APIs;运行这段代码前,你需要先部署 Milvus Standalone 或 cluster instance:
# Create two partitions named "p1" and "p2"
client.create_partition(
collection_name="coll_wiki_2", partition_name="p1")
client.create_partition(
collection_name="coll_wiki_2", partition_name="p2")
# Get two rows from Wikipedia dataset
rows = list(dataset.take(2))
# Insert first entity into partition "p1"
entity1 = {
"id": rows[0]['id'],
"emb": rows[0]['emb'],
"title": rows[0]['title'],
"text": rows[0]['text'],
"url": df['url'][1],
"extra": "This entity is in partition p1"}
res1 = client.insert(
collection_name="coll_wiki_2", data=entity1, partition_name="p1")
print(f"Inserted into p1: {res1}")
# Insert second entity into partition "p2"
entity2 = {
"id": rows[1]['id'],
"emb": rows[1]['emb'],
"title": rows[1]['title'],
"text": rows[1]['text'],
"url": df['url'][2],
"extra": "This entity is in partition p2"}
res2 = client.insert(
collection_name="coll_wiki_2", data=entity2, partition_name="p2")
print(f"Inserted into p2: {res2}")
不同 partitions 中的 entities 在物理上是分离的。要向某个 partition 插入数据,需要在 insert API 中提供 partition name。如果 partition name 为空,Milvus 会把 entities 写入 collection 的默认 partition。要从特定 partitions 获取数据,查询时也需要提供 partition names,否则 Milvus 会遍历整个 collection。
通过在这些 APIs 中提供 partition names,你可以将 API 作用范围缩小到 partition 层级,从而节省大量资源并提升性能。
不过,由于物理细分,Milvus 每个 collection 最多只能维护 4096 个 partitions,该值可配置。为解决这些问题,Milvus 引入第二种将 collection 划分为 partitions 的方式,不需要用户维护,同时支持数百万个 partition keys。
Partition key field
第二种对 collection 分区的方式,是使用 partition key field。创建带 schema 的 collection 时,你可以指定某个 field 作为 partition key field,Milvus 会用它自动将数据分发到内部 partitions。
因为 partition key field 会在内部管理 partition placement,你无法再手动创建或删除单个 partitions,也无法在搜索中直接定位某个特定 partition。这些 partitions 的完整生命周期,包括创建、数据路由和删除,都会完全委托给 Milvus。
插入 entities 时,Milvus 会计算每个 entity 的 partition key field value 的哈希,并自动将 entity 路由到合适 partition。Partition key 是一个轻量逻辑概念:多个不同 key values 可能映射到同一个底层物理 partition。
为了具体说明,想象你正在基于 MongoDB embedded_movies 数据集构建一个语义电影搜索引擎,该数据集可通过 Hugging Face datasets library 获取。该数据集包含 plot field、一个预计算 plot_embedding vector,该向量由 OpenAI 的 text-embedding-ada-002 模型生成,维度为 1536,以及一个 genre field。通过将 genre 指定为 partition key,Milvus 会在插入时自动按 genre 对电影分组;因此,搜索与某部 action movie plot 相似的影片时,根本不需要扫描 Drama 或 Comedy partitions:
# create collection with partition-key enabled
schema = client.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("title", DataType.VARCHAR, max_length=512)
schema.add_field("plot", DataType.VARCHAR, max_length=4096)
schema.add_field(
"genre", DataType.VARCHAR, max_length=64, is_partition_key=True)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=1536)
client.create_collection(
collection_name="coll_enables_partitionkey", schema=schema)
接下来,我们深入 shards,这是另一种物理拆分 collection 的方式。
Shards
Shard 也是 collection 的一种物理细分,设计目的在于高效处理 streaming data。与 partitions 配合时,一个 shard 的底层资源是 MQ 中的 virtual channels(VChannels),参见本章后面 “MQ:流式数据的生命线” 一节。Shards 在 collection 创建时定义一次,并且不可变;collection 创建后无法改变 shards 数量。
创建 collection 时,Milvus 会为每个 shard 创建一个 VChannel,并且每个新 collection 默认从一个 shard 开始。创建 collection 时可以覆盖这一默认值。下面示例代码创建一个带四个 shards 的 collection:
client.create_collection(
"coll_enables_four_shards", schema=schema, num_shards=4)
插入 entity 时,Milvus 会对 PK value 做 hash,以决定它进入哪个 shard。对于一个有四个 shards 的 collection,路由规则是 Hash(PK) % 4。PK 为 0、4 或 8 的 entities 都会 hash 到 shard 0。
我们将在本章最后一节详细讨论 MQ 和 VChannels。
下面通过一个具体示例,看看插入四个 entities 时,shard 和 partition key routing 如何协同工作。
首先,定义 schema,并创建一个带 partition key field、两个 shards 和两个 partitions 的 collection:
client = MilvusClient(uri="http://localhost:19530")
schema = MilvusClient.create_schema()
schema.add_field(
field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(
field_name="title", datatype=DataType.VARCHAR, max_length=512)
schema.add_field(
field_name="plot", datatype=DataType.VARCHAR, max_length=4096)
schema.add_field(
field_name="genre", datatype=DataType.VARCHAR,
max_length=64, is_partition_key=True)
schema.add_field(
field_name="embedding", datatype=DataType.FLOAT_VECTOR, dim=1536)
client.create_collection(
collection_name="coll_2_shards_2_partitions",
schema=schema,
num_shards=2, # 2 shards
num_partitions=2 # 2 partitions for partition key
)
在这个 collection 中,genre 是 partition key field,Milvus 会用它将每一行自动路由进某个 partition;两个 shards 表示 Milvus 为这个 collection 使用两个 VChannels。接下来,从 MongoDB embedded_movies 数据集中流式取出前四部 drama 电影:
import itertools
from datasets import load_dataset
dataset = load_dataset(
"MongoDB/embedded_movies", streaming=True, split="train")
drama_rows = (
row for row in dataset
if row.get("plot_embedding") and row.get("genres") and "Drama" in row["genres"]
)
rows = list(itertools.islice(drama_rows, 4))
# Now preview the 4 rows with polars
import polars as pl
preview = pl.DataFrame([
{"id": i, "title": row["title"], "genre": row["genres"][0]}
for i, row in enumerate(rows)
])
print(preview)
你会得到类似以下输出:
id[AC1.1] title genre
i64 str list[str]
-----------------------------------------------------------------------
0 "Beau Geste" ["Action", "Adventure", "Drama"]
1 "Men Without Women" ["Action", "Drama"]
2 "The Crowd Roars" ["Drama", "Action"]
3 "Scarface" ["Action", "Crime", "Drama"]
现在,将全部四行插入 coll_movies collection:
drama_entities = [
{
"id": i,
"title": row["title"],
"plot": row.get("plot", ""),
"genre": "Drama", # just use Drama because the original dataset has a list of genres
"embedding": row["plot_embedding"],
}
for i, row in enumerate(rows)
]
for entity in drama_entities:
print(f"Entity {entity['id']}: genre={entity['genre']} (partition key)")
res = client.insert(collection_name="movies", data=drama_entities)
print(f"Insert result: {res}")
四行都会被路由到同一个 partition,因为它们共享同一个 genre 值 "Drama",因此 partition key 产生相同 hash 值。然而,shard assignment 由 PK 决定。对于两个 shards,规则是 Hash(id) % 2:ID 为 0 和 2 的 entities 落到 Shard 0,ID 为 1 和 3 的 entities 落到 Shard 1。
理解 Milvus 数据模型,是构建高性能向量数据库的基础。你已经学习了 collections、fields 和 schemas 如何提供结构完整性和类型安全,而 partitions 和 shards 如何通过跨节点分发数据来支持大规模扩展。
Shards 的典型数量
在大多数部署中,shards 数量通常在 1 到 16 之间,具体取决于系统规模以及可用 Query 或 Data nodes 数量。较小系统可能使用 1–4 个 shards,而更大的分布式系统可能使用 8–16 个 shards,以支持更高 ingest 和 query 并行度。
Shard 数量通常在 collection 创建时固定,之后无法更改,因此应根据预期数据量和集群容量谨慎选择。设置过多 shards 可能引入不必要开销,而 shards 过少则可能限制并行处理能力并降低性能。
这些知识让你能够设计出不仅语义丰富,而且针对高效存储、索引和查询优化过的 collections。
现在你已经扎实掌握了 Milvus 如何组织数据,我们将探索使这一切运转起来的底层架构。
Milvus 的分层架构
Milvus 是一个开源、云原生、分布式向量数据库。其架构的核心设计理念,是实现高度灵活性和可扩展性。为此,Milvus 严格遵循几个关键原则:数据与控制分离、存储与计算分离、读写操作分离。这种设计允许系统的不同部分根据实际 workload 独立扩展,从而在保证高吞吐和低延迟的同时,显著提高资源利用率和整体系统韧性。
为了将这些设计原则落到实践中,Milvus 构建在一个分工明确的四层架构之上,如图 5.3 所示。这四层分别是:access layer,负责处理客户端交互;coordination layer,作为系统大脑;worker layer,执行具体计算任务;storage layer,确保数据持久化。每一层都封装特定功能,并通过清晰定义的接口与其他层协作,共同形成完整而强大的向量数据库。
图 5.3:Milvus 的分层架构
这种分层设计带来显著工程优势。
第一,它赋予系统极强的弹性扩展能力。当面对高并发连接请求时,可以独立横向扩展 access layer 的 nodes。当计算密集型 query 或 indexing 任务增加时,可以增加 worker layer 的 nodes。这种按需扩展模式避免了传统单体架构中 all-or-nothing scaling 造成的资源浪费。
第二,清晰的层边界天然形成故障隔离。任何单层或单组件故障都会被局部限制,不容易触发整个系统的链式反应,使系统更稳健、更易维护。
最后,这种模块化结构让系统更易演进。每一层都可以独立进行技术升级或组件替换,而不影响其他层,确保 Milvus 可以持续集成最新技术进展。
现在,我们进一步看看每一层扮演的角色:
- Access layer 是系统入口,由一组 stateless proxy nodes 组成,作为所有客户端请求的统一入口点。
- Coordination layer 位于 access layer 之下,是整个 cluster 的指挥中心,负责管理 metadata、分配任务,并协调所有分布式组件活动。
- Worker layer 是系统的动力核心,由各种类型的 compute nodes 组成,执行所有核心数据处理任务,例如数据插入、compaction、index building 和 vector retrieval。
- 最后,所有数据和系统状态都依赖 storage layer 持久化。该层通常由 object storage、metadata storage 和 log broker 等基础设施组成,为上层提供可靠数据基础。
为了更深入理解 Milvus 如何高效处理向量数据,我们将从 access layer 开始,逐一拆解每一层的功能、设计要点和关键角色。
Access layer
Access layer 是 Milvus 架构的网关,也是用户交互的第一入口。它本质上是一个 stateless proxy gateway,核心职责是接收来自各种语言 SDK 客户端的所有请求,并作为 request dispatching 的中心枢纽。它会准确地将请求转发到 Milvus 内部合适服务进行处理,然后聚合结果并返回给用户。由于 access layer 由一组 stateless proxy nodes 组成,它具备优秀的水平扩展能力,只需添加或移除 nodes,就可以优雅处理高并发请求。
Access layer 中的 proxy nodes 会根据请求性质,将请求分为三大类:Data Definition Language(DDL)请求、Data Modification Language(DML)请求和 Data Query Language(DQL)请求。
DDL requests,也称为 Data Control Language(DCL)requests,主要用于定义和管理数据的逻辑结构,例如创建或删除 collection。这些操作直接影响数据组织 metadata,因此对一致性和顺序有极高要求。为了确保操作确定性和结果一致性,access layer 会将收到的 DDL requests 序列化,并按时间顺序排队,然后顺序派发到 coordination layer 执行。这种严格顺序执行机制,可以防止并发操作可能造成的 metadata state conflicts。
DML requests 则关注修改 collection 内数据,覆盖 insert、upsert 和 delete 等常见操作。不同于需要严格排序的 DDL requests,DML operations 通常可以并发处理,以最大化写入吞吐。当 access layer 收到 DML request 时,它会立即将传入的数据变更写入 storage layer 中的 write-ahead log(WAL),以确保数据持久性和可恢复性。一旦数据安全记录到日志中,access layer 就可以向客户端返回确认;而后台 worker nodes 会异步处理这些 logs,并将数据应用到最终存储文件中。
DQL requests 用于从 Milvus 检索数据,主要包括 vector search、scalar query、get by PK 和 hybrid search 等操作。处理这类请求时,access layer 会展示其智能路由能力。它会实时分析后端 query nodes 的当前负载,并选择最适合的 node 处理当前 query request,以实现 load balancing。在分布式执行环境中,一个复杂 query 可能会被分发给多个 worker nodes 并行处理。当所有 nodes 返回局部结果后,access layer 还负责聚合和合并这些碎片化结果,形成最终统一响应,并返回给客户端。
在 Milvus 的 access layer 中使用 Python,允许应用通过 PyMilvus 等客户端库轻松与 Milvus 交互。Python SDK 充当应用和 Milvus server 之间的桥梁,使开发者能够执行创建 collections、插入 vector data、构建 indexes 和执行 similarity searches 等操作。通过简单 Python APIs,请求会经由 gRPC 发送到 Milvus 服务,然后由内部 coordinators 和 worker nodes 处理。Python 在 access layer 中的使用,简化了与 machine learning(ML)pipelines、data processing frameworks 和 Retrieval-Augmented Generation(RAG)systems 的集成,因此成为需要向量存储和检索应用中的常见选择。
接下来,我们将介绍 coordination layer。
Coordination layer
如果说 access layer 是 Milvus 的网关,那么 coordination layer 就是整个 Milvus cluster 的大脑和指挥中心。这是一个 stateful layer,承担广泛且关键的职责。Stateful 意味着 coordination layer 必须维护并持久化关于 cluster 本身的所有关键信息。这些信息,例如 collection schemas、data segments 分布和后台任务进度,共同构成 cluster 的记忆。不同于可以随意替换的 stateless worker nodes,coordination layer 的状态必须在 node 重启或 failover 后完全恢复;否则,整个 cluster 将无法正常工作。正是这种状态维护能力,让 coordination layer 能够作出全局决策并执行精确调度。它不仅处理 access layer 传来的 DDL operations,也集中管理系统中的所有 metadata。同时,coordination layer 还作为 supervisor,持续监控所有 worker nodes 的拓扑和状态,并根据系统负载和数据情况,智能生成并分配后台任务,例如 index building 和 data compaction。此外,resource quota management、user credentials 和 role-based access control(RBAC)等功能,也属于 coordination layer 管辖。
Coordination layer 的核心由三个功能不同的组件组成,称为 coordinators。部署时,这三个 coordinators 既可以作为独立服务运行,也可以合并为一个名为 Mix Coordinator(MixCoord)的 hybrid coordinator service。无论采用哪种部署模式,为保证 metadata 和 system state 的一致性,同一时间每种 coordinator type 只能有一个 active instance 运行。为了实现 high availability(HA),系统允许启动 standby instances。当主 coordinator 因故障或 rolling upgrade 下线时,standby instance 可以立即接管职责,确保服务连续性。
当作为独立服务部署时,三个 coordinators 分别是 Root Coordinator(RootCoord)、Data Coordinator(DataCoord)和 Query Coordinator(QueryCoord)。
RootCoord 负责管理 Milvus 作为分布式数据库运行所需的核心功能和全局状态。它的一个关键任务,是以严格顺序执行全局 DDL requests,例如创建 databases 或 collections,以及管理 security credentials,同时维护相关 metadata。更重要的是,RootCoord 充当整个 cluster 的中央 Time Stamp Oracle(TSO)。通过发放全局唯一且单调递增的 timestamps,它为分布式环境中的数据可见性和事务一致性提供基础保证。
DataCoord 聚焦管理系统的数据修改路径。所有与数据摄取、存储、持久化和修改相关的任务与 metadata 都由它控制。它追踪 data segment 从创建、增长、flush 到最终 compaction 或 indexing 的完整生命周期。当 segment 满足某些条件时,DataCoord 会触发并将对应 compaction 或 index-building tasks 派发给后台 worker nodes。值得注意的是,在早期架构版本中,indexing tasks 的管理由单独的 Index Coordinator(IndexCoord)处理。为简化架构,该组件的功能后来被合并进 DataCoord,使其负责从数据写入到索引构建的完整流程。此外,它还负责维护写路径上所有 worker nodes 的拓扑,并执行 load balancing,确保数据能高效且均衡地写入系统。
QueryCoord 负责管理系统的读路径,其所有工作都围绕优化 query performance 和 availability 展开。它监督 query nodes 上 data segments 的加载和卸载,确保用户查询所需数据预加载到内存中。同时,它负责在多个 query nodes 之间平衡 query load,管理 replicas 以提升查询并发和 fault tolerance(FT),并通过管理 resource groups 隔离不同 tenants 或 applications 的 query workloads,从而确保整个系统查询服务高效、稳定且公平。
这三个 coordinators 并非孤立运行,而是通过协调机制紧密连接,共同确保 cluster 数据一致性和状态平滑流动。它们之间存在信息依赖:RootCoord 是 global metadata 的权威来源,管理 collections 等顶层信息。当 DataCoord 或 QueryCoord 需要这些信息时,会向 RootCoord 请求。类似地,DataCoord 是 data distribution 的权威来源,管理所有 data segments 的详细状态。QueryCoord 会周期性从 DataCoord 获取最新 segment information,并用它指导 query nodes 加载新 segments 或释放旧 segments,确保 queries 始终运行在最新数据视图上。
接下来,我们看看 coordination layer 管理的 worker layer。
Worker layer
Worker layer 是 Milvus 架构中的核心组件,负责执行数据操作和查询中的重计算任务。该层主要由三种不同类型的 stateless nodes 组成:query node、data node 和 index node,它们分别由 QueryCoord 和 DataCoord 统一管理与调度。
Stateless 是 worker layer 的核心设计特性。这意味着 compute nodes 本身不存储持久数据或 metadata;它们更像纯执行单元。这种设计带来巨大灵活性:当系统 workload 增加时,可以按需动态增加 nodes 数量来分担压力;当负载降低时,则可以相应缩容以节省资源。这些 worker nodes 会根据 coordination layer 分配的指令,直接与底层 storage layer 交互,读写数据。它们既能处理实时 streaming data,也能处理历史 batch data,是 Milvus 实现快速扩展和高弹性的关键。下面我们来看看所有关键 worker nodes 的功能。
Query node 是读路径上的执行器,负责处理所有 search 和 query requests。在实践中,QueryCoord 会根据任务需求为不同 query nodes 分配角色。一些 nodes 作为标准 query nodes,专注于查询历史数据。另一些则被指定为更专门的 shard delegator role,每个 data shard 都有自己的专用 delegator。Shard delegator 在多个关键方面扩展了标准 query node 的功能,使其能够高效处理混合实时数据和历史数据的复杂查询场景。它不仅处理实时数据流、为这些新数据构建临时中间索引并管理实时删除,还负责强制执行用户指定的查询一致性级别。最后,它维护并提供属于其 shard 的所有历史 segments 列表,为 query routing 提供精确指导,确保 queries 能高效到达正确数据范围。
Data node 在写路径上承担双重角色。它的主要职责是作为 streaming data 的执行器,处理实时数据修改。它订阅 log broker,消费持久化 DML logs,然后将这些增量数据转换成 Milvus 专有 columnar file format,形成新的 segments,最终写入 object storage。除了处理实时数据流,data node 还承担重要后台维护任务,其中最关键的是 compaction。随着数据持续写入,系统会生成大量小 segment files。为了防止过度文件碎片造成性能下降和 metadata 管理开销增加,DataCoord 会周期性触发 compaction tasks。接收任务的 data node 会从 object storage 读取多个小 segments,将它们合并为一个或少数几个更大 segments,并在过程中物理移除已经标记删除的数据。这个过程不仅优化数据布局、提升后续查询效率,也有效回收存储空间。
Index node 是专门负责构建 indexes 的 compute node。当 data segment 被写入 object storage 并满足 index building 条件时,DataCoord 会将 index-building task 派发给 index node。收到任务后,index node 会从 object storage 获取对应 data segment,然后在其上执行用户指定的 vector index algorithm,例如 HNSW 或 IVF_FLAT。这个过程通常是计算密集型的。Index 构建完成后,index node 会将生成的 index files 写回 object storage,供后续 query nodes 加载和使用,从而大幅加速 vector retrieval。
值得一提的是,为进一步简化架构和优化资源调度,即将发布的 Milvus 2.6 已经合并 data node 和 index node 的功能。这一步演进旨在将数据写入和索引构建这两类紧密相关任务统一到同一种 node 中,从而减少 node 类型数量,并提高资源利用率和运维效率。
理解了负责各种计算任务的 worker layer 之后,我们转向 storage layer,它为整个系统提供数据持久化基础。
Storage layer
Storage layer 是 Milvus 架构中的持久化基石,为系统中所有数据和 metadata 提供安全可靠的归宿。该层的核心设计理念是 pluggability,也就是可插拔性。这意味着 Milvus 并不捆绑某个特定存储实现。相反,它允许用户根据自身业务需求、技术栈偏好或成本考量,在多种成熟第三方外部服务中自由选择,构建最适合的存储方案。
Storage layer 处理的数据资产可以明确分为三类,每类都有专门组件支持:
第一类是系统操作的 streaming messages,也就是 transaction log,其中包括近期 DML 和 DDL operations,以 log message 形式存储在高可用 log broker 中。
第二类是系统的 household register,也就是 master record,包括所有关键 system-level metadata,存储在专用 metastore 中。
第三类,也是数据量最大的一类,是所有已处理和固化的数据归档,包括不可变 vector 和 scalar data files、用于加速检索而构建的 index files,以及相关统计信息。这些大文件最终都归档在可无限扩展的 object storage 中。
Meta Store 默认实现为 etcd,充当 Milvus cluster 的 registration center 和 configuration center。etcd 是高度一致的 key-value store,广泛用于分布式系统。它不仅负责持久化所有 Milvus 核心 metadata,例如 collection schema definitions、partition statuses 和 segment information,还为系统中所有组件提供关键 service registration and discovery 功能。Cluster 中的每个 node 启动时都会向 etcd 注册自身信息,并依赖 etcd 发现其他 nodes 的存在和地址,从而形成一个凝聚协作整体。
Log broker 是 Milvus 实时数据处理能力的生命线。它是 Milvus 依赖的第三方服务器之一。在 Milvus 中,它首先作为 WAL,确保数据可靠性。来自 SDK client 的任何 insert、upsert 或 delete requests,都必须先将对应 log message 成功写入 log broker,然后 proxy 才会返回请求。这确保成功的 DML request 会持久化在 log broker 中,永不丢失。此外,log broker 也是支撑 Milvus 精细 distributed timestamp model 和读写分离架构的底层支柱。
Milvus 会针对不同部署方式采用不同服务器作为默认 log broker:
- 对于 standalone mode,内置 log broker 是 RocksMQ,一个基于 RocksDB 的轻量级 embedded MQ,免除了用户部署和维护复杂消息中间件的负担。
- 对于 cluster mode,默认部署使用 Apache Pulsar 作为 log broker。Pulsar 是一个云原生分布式消息和流式平台,以多租户、分层存储和灵活消息模型等强大企业级特性闻名。
- 此外,对于已经拥有成熟 Kafka 技术栈的团队,Milvus 也支持使用 Apache Kafka 作为 log broker,提供更丰富的生态集成选择。
Object storage 是 Milvus 云原生设计的基石。其默认实现是 MinIO,同时也无缝支持 Amazon S3 和 Google Cloud Storage 等主流公有云对象存储服务。系统中的所有大体量 batch data,例如 data nodes 消费 logs 后生成的 columnar data files,以及 index nodes 计算后生成的 index files,最终都存储在这里。这种彻底分离计算与存储的设计模式,是 Milvus 实现极致弹性的关键。由于数据的 source of truth(SoT)位于 object storage,上层 stateless worker nodes 可以根据负载需求自由创建和销毁,而无需担心数据丢失。例如,当 query node 需要处理查询时,它可以按需从 object storage 将所需数据和 indexes 加载到自身内存中,甚至在任务完成后释放这些资源,使整个过程高度高效且灵活。
到这里,我们已经完整拆解了 Milvus 的四层架构。接下来,我们将聚焦数据流过程,并深入了解 Milvus 如何巧妙利用 MQ 驱动其复杂的内部数据处理和分发工作流。
MQ:流式数据的生命线
在 Milvus 中,并非所有数据都是静态的。近期、实时数据操作的连续流,也就是所有 DML requests 和部分 DDL requests,共同构成所谓 streaming data。为了处理这种持续涌入,Milvus 设计了以 MQ 为核心的专门基础设施。MQ 不仅是实现高吞吐分布式写入的关键,也是确保数据持久性、可扩展性,并支持各种查询一致性级别的基石。Milvus 常用的 MQ 系统包括 Apache Pulsar、Kafka 或 RocksDB(用于 standalone deployments)。这一设计使 Milvus 能够高效处理大规模数据摄取和分布式查询处理。
图 5.4:MQ 架构
Milvus 中的 MQ 架构也采用精细的分层设计,如图 5.4 所示,它建立在基础 log broker 之上。在 log broker 提供的原生 topic resources 之上,Milvus 构建了自己的 physical channels 或 PChannels 层。在 Kafka 或 Pulsar 等消息系统中,topic 是一个命名 channel 或 category,用于组织 messages。Producers 将 messages 写入特定 topics,consumers 订阅这些 topics 读取 messages。在 Milvus 中,每个 PChannel 与底层 topic 一一对应,从而保证它们之间完整物理数据隔离。不过,由于 Pulsar 或 Kafka 等系统中的 topics 相对消耗资源,一个 cluster 中可创建的 PChannels 数量客观上有限。
为克服这一限制,同时为大量 collections 提供隔离 channels,Milvus 引入了更轻量的抽象层:VChannel。多个 VChannels 可以共享一个 PChannel。作为轻量逻辑资源,理论上几乎可以创建无限数量的 VChannels。Milvus 巧妙地将 VChannel 机制绑定到其 sharding 机制上:每个 collection 的每个 shard 都会分配给一个专用 VChannel。这意味着,所有发往特定 shard 的 streaming data 都保证写入同一个 VChannel。由于下游 data nodes 和 query nodes 可以独立订阅并处理特定 VChannels 中的数据,因此这种将 shards 绑定到 VChannels 的设计,构成了 Milvus 实现 shard-level load balancing 和 elastic scalability 的基础。
理解了 MQ 的分层结构后,我们进一步看 MQ 在 Milvus 生态中的两个核心角色:
Streaming data 的持久化:MQ 是 streaming data 持久性的保证。在 Milvus 中,一次写入操作成功的标志,是对应 log message 成功生产到 MQ。在这一职责中,MQ 充当 WAL,我们将在后续章节详细探讨这个概念。它确保任何数据变更都被持久记录,即使发生系统故障,也可以通过 replay 这些 logs 快速恢复数据,从而保证数据正确性并防止数据丢失。
支持多种一致性级别:MQ 是实现多种数据一致性级别的基础。它支撑 Milvus 支持多种 isolation levels 的能力,从 eventual consistency 到 strong consistency。对于最严格的 strong consistency,MQ 确保数据一旦确认写入,就会立即对所有后续查询可见,无论使用哪个 SDK client 或 proxy node。
支撑这种强大多一致性级别能力的基础,是全局事件排序能力,它由 MQ 和 Milvus 自身的时间同步机制共同实现。下一节我们将对此展开。
全局事件排序
每一条流经 MQ 的 record,无论我们称它为 log 还是 message,都代表系统中已经发生的一个 event。为了实现 strong consistency 并保证 events 的全局顺序,Milvus 必须解决两个根本问题:
第一,标记每个 event 发生的精确时间。
第二,确保任何服务都能以相同且一致的顺序读取这些 messages。虽然底层 log broker 在一定程度上可以保证单个 producer 的 message order,但在多个 proxy nodes 并发写入的分布式场景下,这一保证并不成立。
在 Milvus 架构中,access layer 使用多个 stateless proxy nodes 的设计,虽然提供 HA 和负载分发,但也引入了一个根本挑战:如何为来自不同 proxies 的所有 requests 建立一个全局一致的时间顺序。这个挑战主要表现为两个方面:
第一是物理时钟不一致问题。在分布式系统中,不同机器上的物理时钟永远无法完美同步;它们之间总会存在轻微差异,这被称为 clock skew。如果 Milvus 依赖各个 proxy nodes 的本地服务器时钟来判断 events 顺序,这种 skew 就会成为数据不一致的根源,最终导致严重数据完整性问题。
第二是网络和处理延迟导致的乱序问题。即使 events 有清晰逻辑顺序,由于 proxy 处理速度和网络不确定性的差异,它们物理到达 MQ 的顺序可能与逻辑发生顺序完全不同。这种时间与顺序错位,使基于时间的一致性控制极其困难。
我们用一个具体场景理解这些问题的严重性。假设 Client A 在时间 t1 通过 Proxy A 发送一个 insert request;随后,Client B 在时间 t2(其中 t2 > t1)通过 Proxy B 发送一个 delete request,目标正是 Client A 插入的数据。逻辑上,delete operation 应该在 insert operation 之后执行。然而,由于网络延迟、proxy 处理时间或内部调度差异,Proxy B 的 delete operation 很可能先于 Proxy A 的 insert operation 被处理,从而尝试删除一条尚不存在的记录,这显然违背了逻辑意图。
类似地,当 Client C 通过 Proxy A 成功插入数据后,立即通过 Proxy B 执行 query 来检索该数据时,该 query 可能命中一个尚未因 cluster 内数据传播延迟而更新到新插入数据的 query node,导致结果集不完整。
为在分布式环境中彻底解决这些时序和一致性问题,Milvus 引入两项关键技术:timestamp 和 Timetick。
我们先看 Milvus 如何使用 timestamp 机制解决物理时钟不一致问题。
Milvus 中的 Timestamp
为从根本上解决分布式环境中的时钟不一致问题,Milvus 采用一种 centralized、hybrid timestamp scheme,结合物理时间和逻辑时间。这个方案的优雅之处在于,它不再要求 cluster 中不同机器的物理时钟完美同步。相反,通过巧妙组合物理时间和逻辑计数器,它为整个分布式系统提供连续、单调递增且可靠的事件时间来源。
如图 5.5 所示,timestamp 是一个 64-bit unsigned integer。这 64 bits 被分为两部分:高 46 bits 存储毫秒级物理时间,来自 timestamp-generating node 的系统时间;低 18 bits 作为 logical counter。
图 5.5:Timestamp
整个 Milvus cluster 中所有 timestamps 的生成,都由 RootCoord 集中管理;在这一角色中,RootCoord 充当全局 TSO。当组件需要 timestamp 时,它会向 TSO 发送请求。随后,TSO 获取自身当前物理时间,并将其与内部 logical counter 组合,生成唯一 timestamp。如果多个请求在同一毫秒内到达,TSO 会递增 logical counter,以确保每个 timestamp 的唯一性。
通过这种集中式设计,Milvus 将整个 cluster 的 event time 与任何单一 compute node 的物理时钟解耦,从而缓解物理 clock skew 带来的风险,并为所有 events 提供稳定、可靠、全局有序的时间基准。
解决了 event 何时发生的问题后,我们将继续探索 Milvus 如何使用 Timetick 机制解决 events 的“有序读取”问题。
Timetick 机制
为了支持高吞吐 DML requests,access layer 中的 proxy nodes 会并发处理它们,并将产生的 log messages 写入 MQ。然而,如前所述,由于网络延迟和处理速度差异,一个 event 的发生时间、执行时间以及最终物理写入 MQ 的时间可能都不同。这会导致队列中的 messages 顺序混乱,如图 5.6 所示:
图 5.6:MQ 内部的 Messages
在上图中,来自 Proxy B 的 delete message 具有较晚 timestamp T3,但它在队列中的物理位置却出现在 timestamp 更早的 insert message T2 之前。如果下游 consumer,例如 data node,简单按照物理顺序处理 messages,就会发生逻辑错误。为处理这种潜在乱序,并建立全局一致事件消费顺序,Milvus 使用 Timetick 机制。它可以理解为在数据流中设置全局 checkpoints 或 watermarks 的机制。具体来说,RootCoord 会周期性生成一种特殊 message,即 Timetick message,并将其广播到 MQ 的所有 physical channels。
每个 Timetick message 携带一个 timestamp T,而这个 timestamp 的含义至关重要:它表示一个全局共识,即所有 timestamp 小于或等于该 Timetick timestamp 的 requests,已经被所有 proxy nodes 完整处理并发送到 MQ。
那么,这个基于全局共识的 Timetick timestamp 是如何生成的?它来自一个优雅的同步过程。Cluster 中每个 proxy node 会周期性向 RootCoord 报告其最近处理的 request timestamp。RootCoord 收集所有 proxy nodes 的报告后,选择所有报告中的最小 timestamp。这个全局最小值代表整个系统当前处理进度的瓶颈,也就是所有 proxy nodes 共同到达的安全时间点。随后,RootCoord 使用这个最小值作为下一条 Timetick message 的 timestamp。通过这种方式,Timetick 机制确保它发布的 timestamp 是一个绝对可靠的全局 watermark。
因此,一个包含 Timetick messages 的数据流概念视图可能如图 5.7 所示:
图 5.7:带 Timeticks 的 MQ 内部 Messages
如图 5.7 所示,一些具有更晚 timestamps 的 events,例如 T3,在物理上可能仍然出现在 timestamp 更早的 Timetick message,例如 TT Msg T2,之前。然而 Timetick 机制的核心保证是:当 Milvus 中的 consumer node,例如 data node 或 query node,处理 timestamp 为 T2 的 Timetick message 时,它接收到一个确定性信号:所有 timestamp 小于或等于 T2 的 event messages,都保证已经到达队列。此时,consumer 可以安全地对其内部 buffer 中所有 timestamp 不晚于 T2 的 messages 执行全局排序,然后按照正确逻辑顺序处理它们。
借助这一共识机制,Milvus 中的下游服务能够以 Timeticks 定义的离散节拍处理数据。在每个 beat 内,也就是两个连续 Timetick messages 之间的时间窗口内,service node 会消费并 buffer 所有到达 messages。当下一次 Timetick beat 信号到达时,该 node 会完整、有序地处理上一时间窗口中 buffer 的所有 messages。通过这种方式,Timetick 机制巧妙地把物理上乱序的数据流,转换为逻辑上有序、按批处理的数据流,从而在保持高吞吐的同时实现强数据处理一致性。简单来说,Timetick 机制充当同步信号,使所有 Milvus 组件对齐在同一时间线上,确保分布式向量搜索操作中的查询结果正确和数据可见性一致。
小结
Milvus 的强大能力来自其核心架构组件之间的协同工作。本章探索了 Milvus 的能力和韧性如何直接来源于它分解式的四层架构设计。我们看到 stateless access layer 如何作为统一网关,coordination layer 如何充当中央大脑,worker nodes 如何执行重计算任务,可插拔 storage layer 如何提供持久化基础。把整个架构连接在一起的是 MQ,也就是系统的中枢神经系统。MQ 作为持久 log stream,使这些层能够通过异步通信完全解耦,从而获得巨大的韧性和可扩展性。最重要的是,通过精细的 timestamp 和 Timetick 机制,MQ 保证数据可靠性,并为所有 events 建立一致、全局的顺序,使 Milvus 成为强大、可扩展、云原生的向量数据库。
下一章中,我们将聚焦最基础的操作之一:写入数据。我们会追踪一次 insert request 的旅程,从客户端出发,穿过本章讨论的各层和 channels,准确观察 Milvus 如何为未来查询准备数据。