第 1 篇 给出四层地图;第 3 篇 与 第 5 篇 把数据面落到 Channel / Streaming Node。控制面仍缺两块:无状态 Proxy 如何成为唯一对外入口,以及 单活跃 Coordinator 如何持有拓扑、时间戳与调度。多数排障案例的第一个错误假设,就是把这两层想成「一个反向代理 + 一个数据库主节点」——本文要纠正这个假设。
本文依据官方 Architecture
Overview、Streaming/Data
Processing、Timestamp、Time
Synchronization 文档,以及
internal/proxy/timestamp.go 源码,钉住 Access
Layer 与 Coordinator 的职责清单与 TSO
全序机制。不展开一致性四级语义的产品用法全文(第 12
篇),不展开 Data Node 任务内部(第 10 篇)。
本文是「向量检索引擎」系列第 4 篇(共 19 篇)。→ 系列目录
版本锚定:Milvus 2.6.x。
一、最小故事:同一个 Proxy,三条完全不同的路
假设你在维护一个小型 RAG 服务,用 pymilvus 做了三件事:
- 启动时执行一次
create_collection("docs", schema)。 - 服务运行期间持续
insert(vectors)写入新文档的向量。 - 每次用户提问时执行
search(query_vector, top_k=10)。
三条调用都先落在同一个 Proxy 实例上,很容易得到一个错误直觉:「Proxy 收到请求后转给后面唯一的一个大脑处理」。官方文档给出的实际路径表(Architecture Overview · Data Flow and API Categories)否定了这个直觉:
| 类别 | 示例 | 经 Proxy 之后 |
|---|---|---|
| DDL/DCL | createCollection |
→ Coordinator |
| DML | insert / delete /
upsert |
→ Streaming Node |
| DQL | search / query |
→ Streaming Node(再协同 Query Node) |
也就是说,第 1 条请求确实进了「大脑」;第 2、3 条请求根本不经过 Coordinator 的数据路径,而是直接打到负责该 shard 的 Streaming Node。Coordinator 唯一会介入写路径的地方,是稍后第三节会讲的 时间戳分配——但那是一次很小的 RPC,不是数据本身的搬运。
flowchart TB
client["Client SDK"]
proxy["Proxy"]
client --> proxy
subgraph ddlPath ["DDL / DCL path"]
coord["Coordinator<br/>schema, permission, TSO issue"]
end
subgraph dmlPath ["DML path"]
sn1["Streaming Node<br/>assign TSO, append WAL"]
growing["Growing segment"]
end
subgraph dqlPath ["DQL path"]
sn2["Streaming Node<br/>Delegator"]
qn["Query Node<br/>sealed segments"]
end
proxy -->|"createCollection<br/>dropPartition ..."| coord
proxy -->|"insert / delete / upsert"| sn1 --> growing
proxy -->|"search / query"| sn2
sn2 --> qn
记住这张图就抓住了本文的骨架:Access Layer 是单一入口,但入口之后立刻分岔成三条职责完全不同的路。下面把这三条路和其中最容易被误解的 TSO 讲清楚。
二、Access Layer:无状态 Proxy 到底做什么
官方四层文档对 Access Layer 的定义:
- 由一组 无状态 Proxy 组成,是系统前门与用户端点;
- 校验客户端请求,并对返回结果做 reduce / 后处理;
- Proxy 本身无状态,对外可经 Nginx、Kubernetes Ingress、NodePort、LVS 等提供统一入口;
- Milvus 采用 MPP 架构,Proxy 聚合各路中间结果后再交给客户端。
Proxy 因此同时具备三种角色:
| 角色 | 具体动作 | 后果 |
|---|---|---|
| 协议边界 | SDK/REST → 内部 RPC;请求校验 | 错误 schema、越权请求在此拦下 |
| 路由缓存持有者 | 优先用本地 routing cache;缺缓存才问 Coordinator | 减少每次请求都打 Coordinator 的开销 |
| 最终归并器 | 跨 shard 的 Top-\(k\) / query 结果 reduce | search 的最终 CPU/内存压力压在 Proxy 上 |
无状态的直接后果:扩 Proxy
副本不迁移本地段数据;会话亲和性不是正确性前提。代价是:每次
search 的最终 reduce 压在 Proxy CPU/内存上——超大
topk、高并发时要把 Proxy 当成独立容量维度(第
17 篇排障)。
三、Coordinator:集群大脑与它实际管的四件事
官方表述:Coordinator 是 Milvus 的大脑;任一时刻全集群恰好一个活跃 Coordinator,负责维护拓扑、调度各类任务、承诺集群级一致性相关职责。
任务清单(官方列举):
| 职责域 | 内容 | 本系列衔接 |
|---|---|---|
| DDL/DCL/TSO | 创建/删除 Collection、Partition、Index;管理 TSO 与 time ticker | 第 5 篇 Message 顺序;第 12 篇一致性 |
| Streaming 管理 | 绑定 WAL 与 Streaming Node;流服务发现 | 第 5 篇 Streaming Coordinator |
| Query 管理 | Query Node 拓扑与负载均衡;维护 serving query views 指导路由 | 第 7、9、14 篇 |
| 历史数据管理 | 向 Data Node 分发 compaction / 建索引;管理 segment 与 data view 拓扑 | 第 6、10 篇;handoff |
Worker(Streaming / Query / Data)被描述为 dumb executors:听 Coordinator 指令执行;因存算分离而可无状态扩缩(四层文档)。
flowchart LR
proxy["Proxy"]
coord["Coordinator<br/>single active"]
sn["Streaming Node"]
qn["Query Node"]
dn["Data Node"]
proxy -->|"DDL/DCL"| coord
proxy -->|"DML/DQL"| sn
coord --> sn
coord --> qn
coord --> dn
3.1 为什么是「单活跃」大脑
拓扑、TSO、query view、离线任务队列若多主并发写,容易出现双视图。官方选择 单活跃 Coordinator,用元存储(etcd)支撑高可用元数据与服务注册(四层文档 · Meta storage)。故障时切换活跃实例属于运维面;客户端经 Proxy 的 Wait for Ready / 重试与 WAL 迁移窗口类似(第 5、14 篇)。
3.2 「dumb executors」具体意味着什么
官方把 Streaming / Query / Data Node 统称为 dumb executors——这个词容易被读成「简单、不重要」,实际含义更精确:它们不持有集群级真值,只持有自己被分配到的那部分数据与状态。一个 Query Node 重启后,不需要跟别的 Query Node 协商谁是权威版本,它只需要重新问 Coordinator「我现在该加载哪些 segment」,再从对象存储把数据拉回来——集群级的「谁该持有什么」这件事从头到尾只有 Coordinator 说了算。这也是为什么 Worker 层可以做到无状态水平扩缩:状态的权威来源从来不在 Worker 本地磁盘上。
3.3 MixCoord 合并(阅读提示)
2.5→2.6 的官方升级说明明确写出:历史上拆分的 RootCoord / QueryCoord / DataCoord 已合并为单一 MixCoord,元数据管理与任务调度收进一个协调服务,同时新增专职的 Streaming Node 承接原来散落在 Proxy / DataNode / QueryNode 里的实时职责。本系列以 2.6.x 文档中的「Coordinator」职责表为准;读源码或旧博客时看到 RootCoord/QueryCoord/DataCoord 字样,理解为同一角色的历史分包,不要因为名字不同而怀疑架构变了。
四、TSO 与 timetick:全序从哪里来(它不是墙上时钟)
4.1 为什么不能用本地时钟
Time Synchronization 文档用一个例子说明问题:两个用户在不同节点上先后执行 DDL/DML/DQL,如果各自用本地挂钟盖时间戳,会遇到两个麻烦——不同节点的时钟本身不同步;网络延迟会让「更晚发起的操作」反而先落盘。因此 Milvus 引入 Timestamp Oracle(TSO):所有事件必须从 TSO 服务领取时间戳,而不是读本地时钟。
4.2 时间戳的物理结构
TSO 时间戳是一个 uint64:高 46
位是物理时间(UTC 毫秒),低 18
位是逻辑计数——同一毫秒内可以再切出 \(2^{18}\) 个严格递增的序号。TSO
服务由(Mix)Coordinator
提供,客户端可以在一次分配请求里批量领取多个时间戳。
4.3 分配路径:一次 RPC,不是一次网络转发
源码 internal/proxy/timestamp.go 的
timestampAllocator.alloc 显示得很直接:Proxy 向
Coordinator 发一次
AllocTimestampRequest{Count: n},Coordinator
返回起始时间戳与数量,Proxy 在本地把
start, start+1, ..., start+n-1
分给这批操作。这意味着:DML 的数据不经过
Coordinator,但 DML 的时间戳分配要经过
Coordinator——这是 Coordinator
唯一真正介入写路径的地方,也是它作为热点的来源之一(第 17
篇排障时如果看到 Coordinator CPU 随写入 QPS
联动上升,先查这条 RPC,而不是急着去查 Streaming
Node)。
批量 insert 时,Proxy
会一次性申请一批时间戳(count = n),而不是为每一行单独发一次
RPC——这也是为什么「单条反复 insert」比「攒批
insert」更容易把 Coordinator 的 TSO 分配 RPC
数量顶起来:前者每次调用都要走一次
AllocTimestampRequest,后者用一次
RPC换来一整批时间戳。批大小与 RPC 频率的权衡,直接决定
Coordinator 在写密集场景下的负载形态。
4.4 timetick:把「乱序到达」变成「可安全消费」
Time Synchronization 文档进一步说明:同一个 Proxy 发出的 InsertMsg 时间戳一定递增,但不同 Proxy 之间没有这个保证。于是引入 timetick 机制:
- 每个 Proxy 每隔(默认)200 ms 向(Mix)Coordinator 报告它在某个消息流上看到的最大时间戳;
- Coordinator 取该消息流上所有 Proxy 报告值的 最小值,作为 timetick 写回该流;
- 下游组件(Streaming Node 等)读到这个 timetick 后就能确认:所有时间戳小于该 timetick 的消息都已经到齐,可以安全按序消费。
sequenceDiagram
participant P as Proxy
participant C as Coordinator (TSO)
participant S as MsgStream / Streaming Node
P->>C: AllocTimestampRequest(count=n)
C-->>P: start, count
P->>P: assign start..start+n-1 to InsertMsg batch
P->>S: send InsertMsg (with TSO)
loop every 200 ms
P->>C: report max seen timestamp on this stream
end
C->>C: take min(reported) across all proxies
C->>S: insert timetick = min
S->>S: safe to consume all msgs < timetick in order
这张时序图对应第二节故事里最容易被跳过的一步:insert
之所以「立刻能被搜到」而不乱序,靠的不是某个节点本地判断,而是
Coordinator 汇总多个 Proxy 的时间戳水位后广播的
timetick。
4.5 查询侧的时间戳语义
对查询侧,官方引入若干时间戳相关概念(产品层多通过 一致性级别 间接配置,用户通常不手写原始 TSO):
| 概念 | 含义(官方) |
|---|---|
| Guarantee_timestamp | 保证该时刻之前的 DML 更新在本次 search/query 中可见;未配置时默认取请求发起时刻 |
| Service_timestamp | 由查询数据面维护,表示其已完成执行的 DML 时间线进度 |
| Graceful_time | 配置项:可容忍的短暂不可见窗口;与 Guarantee/Service 比较决定是否立即执行或等待 |
当 Service_timestamp(加可选
Graceful_time)尚不满足
Guarantee_timestamp 时,查询侧
推迟
执行,避免在不完整视图上搜(Timestamp 文档 Scenario
描述)。一致性四级如何映射到 GuaranteeTs,见第 12
篇;本篇只钉住:时间语义的权威原料来自 Coordinator
管理的 TSO/timetick 体系,查询可见性用 Guarantee/Service
比较落地。
五、Query view 与路由:视图滞后是什么感觉
Coordinator 的 Query 管理职责包括提供 serving query views 指导路由(Architecture Overview)。结合 Proxy「routing cache,缺则问 Coordinator」:
- handoff / 负载均衡改变「哪个 Query Node 持有哪些 Sealed」;
- Coordinator 更新可服务视图;
- Proxy / Delegator 按视图把历史检索打到正确节点。
若视图滞后,会出现「段已 flush 但查询仍只打 Growing」或「打到未加载节点」类症状——排障时区分 控制面视图(Coordinator 认为谁持有什么)与 Worker 本地加载状态(Query Node 实际加载了什么)是两件事,视图更新与实际加载完成之间总有一个短窗口(第 14、17 篇)。
一个具体场景:某个 Sealed segment 刚完成 handoff,Coordinator 的 query view 已经把它标记为「归属 Query Node B」,但 Query Node B 从对象存储加载该段(及索引)需要一段时间。这段时间里,如果 Proxy 的 routing cache 还没刷新到最新视图,请求可能仍打向旧的持有者,甚至短暂出现「两边都查不到完整数据」的窗口——这不是数据丢失,是 控制面视图更新 与 数据面加载完成 之间天然存在的异步间隙,第 17 篇的排障清单会把这类现象和真正的数据丢失区分开。
六、常见误解
6.1 「Proxy 只是一个反向代理」
不是。Proxy 除了转发,还持有 routing
cache、做请求校验、并且是跨 shard 结果的
最终归并器(MPP reduce)。把 Proxy
当成纯网络层配置容量,会低估它在大
topk、高并发下的 CPU/内存开销。
6.2 「TSO 就是当前墙上时间,可以拿来做跨系统时间对齐」
不是。TSO 的高 46 位确实是毫秒级 UTC 时间,但低 18 位逻辑计数会让同一毫秒内产生多个不同的 TSO 值;TSO 保证的是 单调递增与全序,不是「与真实时钟一一对应」。拿 TSO 直接换算成业务时间戳、再跨系统与别的时钟源比较,是不安全的。
6.3 「单活跃 Coordinator 意味着所有写入吞吐都被它限制」
不完全对。DML/DQL 的数据本身走 Streaming Node / Query Node,不经过 Coordinator;Coordinator 只承担 DDL、TSO 分配 RPC、拓扑与任务调度。真正会被 Coordinator 限制的,是 TSO 分配的 RPC 速率(第 4.3 节)和 元数据/任务队列规模,不是原始数据搬运带宽。误把「单活跃」等同于「单点吞吐瓶颈」,会让排障方向跑偏到 Streaming/Query Node 反而找不到真正的热点。
七、学术谱系与工程间隙
7.1 谱系:从中心化协调者到混合逻辑时钟
| 阶段 | 代表 | 核心思想 | 与 Milvus 的关系 |
|---|---|---|---|
| 经典分布式 DBMS | 单一协调者 / master 管元数据与调度 | 简化一致性,代价是单点 | Coordinator 的直接前身范式 |
| Spanner TrueTime | Corbett et al., Spanner: Google’s Globally-Distributed Database, OSDI 2012 | 用有界时钟误差 + 等待,换取外部一致性,不依赖单点分配器 | 目标相同(全序/因果),手段不同(Milvus 走中心化分配而非误差等待) |
| 混合逻辑时钟(HLC) | Kulkarni et al., Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases, OPODIS 2014 | 物理时间 + 逻辑计数复合成单调时钟,不需要严格时钟同步 | Milvus TSO 的「46 位物理 + 18 位逻辑」正是这一类复合时钟的具体实例 |
| TiDB PD TSO | PingCAP 官方文档 | 同样 46 位物理毫秒 + 18 位逻辑计数,由中心化 Placement Driver 分配 | 与 Milvus TSO 位布局几乎一致,说明这是该类系统里的常见收敛设计,而非 Milvus 独创 |
| Milvus 2.6.x | 本文 | Coordinator 集中分配 TSO;timetick 汇总各 Proxy 水位 | 用中心化分配换简单性,用 timetick 解决多 Proxy 乱序 |
Wang et al. (SIGMOD 2021) 已强调分布式与动态更新;2.x/2.6 把协调职责收束到文档化的 Coordinator 任务表,并把流批 Worker 拆开。TSO 的位格式与 TiDB PD 高度相似,是数据库工程里对 HLC 思想的独立收敛,而不是巧合式抄袭——两者都需要「不依赖严格时钟同步、又要全序、又要能批量分配」的同一组约束。
7.2 争论:中心化 TSO vs 无中心 HLC
- 中心化 TSO 派(Spanner 的 TrueTime 服务、TiDB PD、Milvus Coordinator):所有全序时间戳过一个(或少数几个)权威分配点,实现简单、语义清晰,代价是分配点的可用性与延迟成为集群共同依赖。
- 无中心 HLC 派(如 CockroachDB 的 hybrid-logical-clock 实践):每个节点本地维护物理+逻辑复合时钟,通过消息传递时的时钟同步规则维持因果序,不需要为每次写入单独走一次远程分配 RPC,代价是需要显式处理时钟回退与不确定性窗口。
Milvus 选择前者:TSO 分配是一次可批量、可缓存(Proxy 一次可申请多个)的 RPC,工程上换来的是 timetick 机制的简单性;权衡点在于 Coordinator 的 TSO RPC 吞吐与延迟必须纳入容量规划(7.3 节)。
7.3 工程间隙
- 「无状态 Worker」假设对象存储与 etcd 可用;对象存储高延迟与按请求计费是官方明确指出的代价(四层文档 · Object storage),不是实现细节疏忽。
- 单活跃 Coordinator 简化一致性,却使协调者成为控制面热点与故障域——容量与选举时间需纳入 SLO;TSO 分配 RPC 的延迟会直接叠加到每次 insert 的端到端延迟上,高并发小批量写入(每次只插入几条)比大批量写入更容易放大这部分开销,因为 RPC 次数没有被 batch 摊薄。
- GuaranteeTs 等待会把一致性要求转化为 尾延迟;Strong 与 Eventually 的产品体验差,主要差在等待,而非另一套索引算法。
- timetick 的 200 ms 默认周期意味着「跨 Proxy 的全局最小水位」天然有最多 200 ms 的滞后窗口——这不是 bug,而是用固定周期上报换取协调开销可控的设计取舍。
7.4 开放问题
- 超大规模下单 Coordinator 的 TSO RPC 吞吐、任务队列与 segment 元数据规模上限如何联合建模?
- Proxy 最终 reduce 是否应下沉到两级树形归并以降低单点带宽?
- routing cache 的失效策略如何在正确性与 Coordinator QPS 之间自动调参?
- 若未来引入多活跃 TSO 分配点(类似部分 HLC 实践),Milvus 现有 timetick 汇总协议需要如何改造才能保持全序语义?
八、小结
三句话小结
- Proxy 是无状态入口:DDL 走 Coordinator,DML/DQL 走 Streaming Node,不是「都进大脑」。
- Coordinator 单活跃,集中管 TSO、拓扑与离线调度;TSO 分配 RPC 是它嵌入写路径的关键点。
- TSO + timetick 把多 Proxy 乱序到达变成可按序消费,与 Spanner/TiDB 同一族时间戳思路。
下一篇看持久化形态:对象存储上的 Segment 布局。
把本文三条误解对应到排障动作,方便复用:看到「写入延迟异常」先分清是 Proxy 侧 reduce 慢、TSO 分配 RPC 慢,还是 Streaming Node 本身慢——这三者对应完全不同的扩容/调参方向,而它们恰好就是本文第 6.1、6.3 节纠正的两个误解所指向的组件。
参考资料
- Milvus Documentation v2.6.x, Architecture Overview(四层、API 路径、Coordinator 任务表)。
- Milvus Documentation v2.6.x, Timestamp(Guarantee / Service / Graceful)。
- Milvus Documentation v2.6.x, Time Synchronization(TSO 格式、timetick 机制、200 ms 上报周期)。
- Milvus Documentation v2.6.x, Consistency Level(四级与 GuaranteeTs 映射;第 12 篇展开)。
- milvus-io/milvus,
internal/proxy/timestamp.go,timestampAllocator.alloc/AllocTimestampRequest(TSO 分配 RPC 路径;源码,随 release 演进)。 - Milvus Blog, How to Safely Upgrade from Milvus 2.5.x to Milvus 2.6.x(MixCoord 合并、Streaming Node 引入;B 级)。
- Corbett, James C. et al., Spanner: Google’s Globally-Distributed Database, OSDI 2012。
- Kulkarni, Sandeep S. et al., Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases, OPODIS 2014。
- PingCAP, TimeStamp Oracle (TSO) in
TiDB、
tikv/pdWiki Timestamp Oracle(B 级,位格式对照)。 - Wang, Jianguo et al., Milvus: A Purpose-Built Vector Data Management System, SIGMOD 2021。
- 第 1、3、5 篇、系列 index。
返回 系列目录 | 上一篇:Segment 状态机 | 下一篇:对象存储布局
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【向量检索引擎】Streaming Node 与 Woodpecker WAL:实时可搜的日志层
以一条 insert 从 SDK 到「立刻能被搜到」的最小故事为线索,拆解 Milvus 2.6.x Streaming Service 三件套、Message/TSO 写顺序、Woodpecker 零本地盘 WAL 的 MemoryBuffer/QuorumBuffer 模式,并标明官方吞吐数字的引用边界。
【向量检索引擎】分布式 search 归并:Delegator、多级 reduce 与 GuaranteeTs
按 Milvus 2.6.x Data Processing 与 Architecture 拆解 search 的多级归并树:Proxy → Streaming Node Delegator → Query Node 段级结果;用最小故事、GuaranteeTs 等待时序图与常见误解说明一致性级别如何变成排队等待。
【向量检索引擎】一致性模型:四级 GuaranteeTs 与 PACELC 的延迟账
按官方 Consistency Level 与 Timestamp 文档拆解 Strong/Bounded/Session/Eventually 如何映射到 GuaranteeTs,用最小故事、四级时间轴与 Strong 等待时序图说明「一致性」在 Milvus 里首先是一笔延迟账;对照 Abadi PACELC 定理与 Bailis PBS,说明 Bounded 是定性旋钮而非概率保证。
【向量检索引擎】向量引擎全景:算法、RAG 与专用引擎之间的一层
定位专用向量检索引擎相对 ANN 算法、RAG 应用与湖仓格式的分工;以 Milvus 2.6.x 四层架构与 insert/search 最小故事建立坐标系,并交代从 SIGMOD 2021 到 Streaming 演进的谱系与常见误解。