土法炼钢兴趣小组的算法知识备份

【向量检索引擎】一致性模型:四级 GuaranteeTs 与 PACELC 的延迟账

文章导航

分类入口
databasestorage
标签入口
#milvus#consistency#guarantee-timestamp#pacelc#bounded-staleness#tso#vector-engine

目录

假设一个客服知识库场景:客服刚把一条错误答案标记为”已撤回”(一次 delete),三十毫秒后同一个客服在同一个会话里追问,触发一次 search。如果这次 search 命中的执行节点还没来得及处理那条 delete,撤回的错误答案会重新出现在检索结果里,客服可能当场把它读给用户听。反过来,如果每一次 search 都要等到”集群里所有节点都追上最新写入”才放行,撤回生效很快,但代价是每次查询都可能因为别的租户的写入峰值而排队。“实时可搜”常被简化理解成”insert 返回成功后下一次 search 一定看到”,但这句话在存算分离架构下并不成立——Milvus 用可调一致性级别把它拆成一句更诚实的话:你愿意为多新的数据视图等待多久第 4、9 篇 已引入 GuaranteeTs;本文按官方文档把四级语义、等待条件与代价交代清楚。

本文是「向量检索引擎」系列第 12 篇(共 19 篇)。→ 系列目录

版本锚定:Milvus 2.6.x Consistency LevelTimestamp;数据面路径对齐 Data Processing(Growing / Sealed)。


一、问题从哪来:PACELC,不是”要不要一致”

存算分离下,执行节点可能尚未看见全部最新流式更新;若不加约束,直接在不完整视图上 search 会丢未提交/未追上的点。这不是 Milvus 独有的困境,而是所有复制数据的分布式系统共享的结构性权衡。Abadi 在 Consistency Tradeoffs in Modern Distributed Database System Design(IEEE Computer, 2012)里把 CAP 定理之外的这层权衡显式化为 PACELC:如果发生网络分区(P),系统要在可用性(A)与一致性(C)之间取舍;否则(E,正常运行时),系统要在延迟(L)与一致性(C)之间取舍。Abadi 的论文用这套框架给 Dynamo/Cassandra(PA/EL)、PNUTS(PC/EL)、VoltDB/Megastore(PC/EC)、MongoDB(PA/EC)各自定位——绝大多数系统在”正常运行时”这一栏(ELC)上是固定的架构选择,不是每次请求可变的参数。

Milvus 官方 Consistency Level 文档直接引用 PACELC 定理来解释四级设计的动机:高一致性意味着更高准确性也意味着更高搜索延迟,低一致性带来更快的搜索速度但有一定的数据可见性损失。它与 Abadi 分类的系统不同之处在于:Milvus 把 ELC 权衡从”系统级固定架构选择”下放成了”单次 search / query 可覆盖的参数”——同一个 Collection 里,一次功能测试的 search 可以用 Strong,一次高峰期的推荐召回可以用 Eventually,二者共享同一份底层数据和执行节点。这个”per-request 可调”的设计本身没有对应的独立学术定位——PACELC 论证的是系统架构层面的一次取舍,Milvus 的做法更接近把这次取舍参数化并交给调用方,是工程上对 PACELC 权衡的一种再包装,而不是对该权衡的绕开。


二、四级一致性与 GuaranteeTs

支持级别:Strong、Bounded、Session、Eventually默认 Bounded

级别 GuaranteeTs 如何定(官方直觉) 体验
Strong 等于最新系统时间戳 等到 ServiceTime 追上再搜;延迟↑,新鲜度↑
Bounded(默认) 早于最新的时间点,容忍有界陈旧 延迟与新鲜度折中
Session 该客户端最近一次写操作的时间戳 “我刚写的”优先可见,不代表其它客户端的写也立即可见
Eventually 取一个很小的值,跳过一致性检查 立即在已有视图上返回

用一条时间轴把四级的 GuaranteeTs 摆在同一个刻度上,比记住四条定义更容易在排障时对号入座:

flowchart LR
  t0["t = t0(历史数据,已稳定)"] --> t1["t = t1(write A 提交到 WAL)"]
  t1 --> t2["t = t2(write B 刚提交,此刻发起 search)"]
  subgraph G["四级一致性对应的 GuaranteeTs 取值"]
    strong["Strong: GuaranteeTs = t2"]
    bounded["Bounded(默认): GuaranteeTs 落在 [t0, t2) 的某个容忍窗口内"]
    session["Session: GuaranteeTs = 发起方自己最近一次写的时间戳(可能是 t1 或更早)"]
    eventually["Eventually: GuaranteeTs ≈ 0,跳过比较"]
  end
  t2 -.->|"必须等 ServiceTime 追上 t2"| strong
  t1 -.->|"容忍窗口内即可"| bounded
  t1 -.->|"只保证看到自己写的那部分"| session
  t0 -.->|"立即在旧视图上执行"| eventually

可在创建 Collection 时设默认级别,也可在单次 search / query 覆盖(官方 API 示例)。用户通常不手写原始 TSO;选级别即可,由系统映射 GuaranteeTs。


三、等待条件与 Strong 一致性的完整时序

Timestamp 文档给出的比较对象:Service_timestamp(执行侧已完成的 DML 水位)、Guarantee_timestamp(本次查询要求的水位)、Graceful_time(集群配置的容忍窗口,是一段时长,不是时间点)。

条件 行为
\(S \ge G\) 可立即执行
\(S + g \ge G\)\(g\) 为 Graceful_time) 可在容忍窗口内执行
仍不满足 推迟 search/query,直到水位追上

用一组具体数字把四级映射到这张判定表,能看出它们其实共享同一套比较逻辑,只是 \(G\) 的取值方式不同。假设某次查询发起时刻,全局最新时间戳是 \(t=1000\)(单位为逻辑时间刻度),执行节点当前 Service_timestamp\(S=940\),容忍窗口 \(g=80\)

一致性级别 \(G\) 的取值 判定 结果
Strong \(G=1000\) \(S=940 < G=1000\),且 \(S+g=1020\ge G\) 若严格按 \(S\ge G\) 判定则需等待 \(S\) 追到 1000;Strong 语义上不使用 Graceful_time 容忍,必须等 \(S\ge G\)
Bounded(默认) \(G\) 略小于 1000,例如 \(920\)(示意值,非官方精确算法) \(S=940 \ge G=920\) 立即执行
Session \(G=\) 该客户端上次写入时间戳,例如 \(900\) \(S=940 \ge G=900\) 立即执行
Eventually \(G\approx 0\) 恒成立 立即执行

这组数字想说明的是:四级一致性不是四套不同的比较算法,而是同一个 \(S \ge G\)(或 \(S+g\ge G\))判定式下,\(G\) 的四种不同生成方式——Strong 把 \(G\) 钉在”此刻的绝对最新”,其它三级都在用不同规则让 \(G\) 更容易被 \(S\) 满足,从而减少等待。表中 Bounded 的具体数值是示意,用来说明”\(G\) 比最新时间戳小一些”这句官方定性描述在判定式里如何生效,不代表官方给出的精确折算公式。

把开篇客服撤回的故事画成时序图:客户端发起 delete,随后同一会话发起 Strong 一致性 search,Streaming Node 必须等到自己的 Service_timestamp 追上这次删除的时间戳才放行。

sequenceDiagram
  participant C as Client(客服会话)
  participant Px as Proxy
  participant SN as Streaming Node(Delegator)
  C->>Px: delete(pk, filter)
  Px->>Px: TSO 分配时间戳 t_del,写入 WAL
  Note over C,Px: 30ms 后,同一会话追问
  C->>Px: search(consistency_level=Strong)
  Px->>Px: 解出 Guarantee_timestamp = 当前最新时间戳(>= t_del)
  Px->>SN: search(plan, Guarantee_timestamp)
  loop 直到水位满足
    SN->>SN: 比较 Service_timestamp (+ Graceful_time) 与 Guarantee_timestamp
    alt Service_timestamp >= Guarantee_timestamp
      SN->>SN: 放行,进入段级 search(已应用删除 bitset)
    else 尚未追上
      SN->>SN: 推迟,等待 WAL 消费追赶
    end
  end
  SN-->>Px: 不含撤回答案的结果
  Px-->>C: 最终结果

因此 Strong 相对 Eventually 的主要代价往往是尾延迟(等待),不是另一套 ANN 算法——图中卡住的环节是”比较水位、决定放行还是等待”,不是 Knowhere 的向量距离计算。若这次删除还在 WAL 提交与 Growing 追加的路径上排队(写入峰值期),这次 search 就会跟着卡住。


四、Session 与 Bounded 的精确边界

Session 保证的是”该客户端能看到自己写入的效果”,对应经典的 read-your-writes 语义(Terry, Demers, Petersen, Spreitzer, Theimer, Welch, Session Guarantees for Weakly Consistent Replicated Data, PDIS 1994,定义了会话一致性的四种保证,read-your-writes 是其中一种)。官方文档明确 Session 是”该客户端插入到达的时间点为 GuaranteeTs”——这不是”全集群所有客户端任意写入立即彼此可见”,那更接近 Strong。开篇故事里,如果客服 A 撤回答案、客服 B 在另一个会话里立刻查询,Session 级别不保证 B 能看到 A 的撤回——只有 A 自己的下一次查询才有这层保证。

4.1 Terry et al. 的四种会话保证,Milvus 只实现了其中一种

Terry et al. 1994 年的论文不是只定义了”read-your-writes”,而是给出了一组四种独立的会话保证,工程上经常被笼统地简化成”session consistency”一个词,但四种保证各自约束的读写关系不同:

保证 论文定义(简述) Milvus Session 是否实现
Read Your Writes(RYW) 一次读操作能看到该会话此前所有写操作的效果 ——官方 Session 定义直接对应这一条
Monotonic Reads 该会话内连续的读操作,看到的数据只会越新不会倒退 官方文档未显式声明;若同一 collection 存在多个执行节点各自追赶 GuaranteeTs 的速度不同,两次连续的 Session 级 search 理论上也可能落在不同节点,是否单调未见官方保证
Writes Follow Reads 该会话一次写操作发生在它读到的所有更新之后(写不会”追不上”已读的更新) 官方文档未提及;Milvus 的写路径通过 TSO 分配时间戳,时间戳本身单调,但这条保证关心的是”因果顺序”而非”时间戳大小”,官方未给出因果层面的说明
Monotonic Writes 该会话内的写操作按发起顺序生效,不会乱序提交 同一 shard 内的 DML 由持有 WAL 的单一 Streaming Node 顺序处理(第 5 篇),符合直觉,但官方 Consistency Level 文档没有把这条作为 Session 级别的显式承诺

把这张表摆出来的意义是:“Session 一致性”这个名字容易让人以为四种保证都齐了,但 Milvus 官方只显式承诺了 Read Your Writes 这一条。剩下三条即便工程实现上大概率成立(例如同 shard 内 DML 顺序处理天然满足 Monotonic Writes),也没有被写进产品语义承诺——如果业务逻辑依赖”两次连续读一定不会倒退”这类更强的假设,需要自己验证,不能只看 Session 这个名字就当作免费获得。

4.2 Bounded:从定性描述到一个具体窗口的例子

Bounded 允许有界陈旧,但官方给出的只是定性描述:“GuaranteeTs 设得比最新时间戳小一些,容忍一个窗口内的陈旧”,没有给出这个窗口有多大、多少比例的请求会落在窗口内的概率化保证。Bailis, Venkataraman, Franklin, Hellerstein, Stoica 在 Probabilistically Bounded Staleness for Practical Partial Quorums(VLDB 2012)里给的是相反方向的工作:对 quorum 复制系统,给出”\(t\) 毫秒后读到最新写入的概率是多少”的可计算模型。Milvus 的 Bounded 是一个运维可调的定性旋钮,Bailis et al. 的 PBS 是一个理论上可计算的概率模型——两者解决同一个问题(有界陈旧),但一个给旋钮,一个给公式,不能拿 PBS 的公式去预测 Milvus Bounded 窗口下的具体命中率。

把这个”旋钮”具体化成一个可推演的时间线,比记住”容忍一个窗口”这句话更容易在运维时对号入座。假设某个 Collection 配置的容忍窗口对应的 Graceful_time 是一个固定时长(记为 \(g\)),一次写入在 \(t_1\) 时刻提交,一次 Bounded 级别的 search\(t_1 + 3g\) 时刻发起:

\[ \text{Guarantee\_timestamp} = t_{\text{now}} - g \quad(\text{官方描述的定性关系,具体折算方式未公开}) \]

如果发起查询时 \(t_{\text{now}} = t_1 + 3g\),则 \(\text{Guarantee\_timestamp} = t_1 + 2g\),早于当前最新时间戳;只要执行节点的 Service_timestamp 已经追到 \(t_1 + 2g\)(这几乎总是成立,除非该节点严重落后),查询立即放行,读到的数据可能比”绝对最新”落后最多 \(g\)。反过来,如果查询紧贴写入发起(\(t_{\text{now}} = t_1\)),\(\text{Guarantee\_timestamp} = t_1 - g\),早于写入本身的时刻,查询同样立即放行——但这次很可能读不到 \(t_1\) 这次写入的效果,因为查询要求的水位本来就在写入之前。这个推演想说明的是:Bounded 的”陈旧窗口”不是”写入后固定等 \(g\) 才可见”,而是”查询愿意接受比当前时刻旧 \(g\) 的视图”——旧到什么程度取决于查询发起的时刻与写入时刻的相对位置,不是一个对每次写入都固定的等待时长。官方文档没有公开 Graceful_time 具体如何折算进 Guarantee_timestamp 的计算公式,上面的推演是按官方定性描述做的示意,不代表官方承诺的精确算法。


五、多 shard 场景下,一次查询的 GuaranteeTs 要满足几个节点

前几节都在单个 Streaming Node 的视角下讨论”水位追上”,但一次真实的 search 通常会按 Channel/shard 拆成多路并发请求(第 9 篇的多级归并树)。这里有一个容易被忽略的细节:Guarantee_timestamp 是这次查询整体的要求,但”是否放行”要在每一路 shard 各自的 Streaming/Query Node 上分别判断,不是由某个中心节点统一裁决一次。

flowchart TB
  Px["Proxy:解出 Guarantee_timestamp = G"]
  Px --> SN1["Shard 0 Streaming Node<br/>比较本地 Service_timestamp"]
  Px --> SN2["Shard 1 Streaming Node<br/>比较本地 Service_timestamp"]
  Px --> SN3["Shard 2 Streaming Node<br/>比较本地 Service_timestamp"]
  SN1 -->|"已追上 G"| R1["立即返回 shard 0 局部结果"]
  SN2 -->|"仍落后 G"| W2["推迟,等待追赶"]
  SN3 -->|"已追上 G"| R3["立即返回 shard 2 局部结果"]
  W2 -.->|"追上后"| R2["返回 shard 1 局部结果"]
  R1 --> Merge["Proxy 归并三路结果"]
  R3 --> Merge
  R2 --> Merge

这张图想说明的是:只要有一路 shard 落后,整次查询的可观测延迟就是”最慢那一路追赶的时间”,不是各 shard 延迟的平均值。这解释了为什么”某个 shard 因为写入倾斜、消费 WAL 慢半步”这类局部问题,会拖累使用 Strong 或严格 Bounded 一致性的整次查询,即便其它 shard 早就绪——第 9 篇讨论的”多级归并树”在一致性维度上还叠加了一层”多路水位对齐”,两层放大机制会同时出现在同一次慢查询里。排障时如果只看 Proxy 侧的平均延迟指标,容易漏掉”某一个 shard 长期落后”这类局部问题,需要下钻到分 shard 的 Service_timestamp 差值。


六、对照 2.6 数据面:精确理解”刚写入就能搜”

2.6 Data Processing:WAL 提交后进入 Growing,由 Streaming Node 路径参与实时查询;Sealed 在 Query Node。Consistency 文档的示意图仍用较笼统的 QueryNode 收流叙述——阅读时映射为:

  1. 写入成功 = 相关 Message 已按 TSO 提交到 WAL(第 5 篇);
  2. 对某次 search 可见 = 该次请求的 GuaranteeTs 所要求的更新,已进入执行路径的可见水位(Growing 与/或已 handoff 的 Sealed);
  3. 索引最优路径就绪 = Data Node 建索 + Query Node 加载完成(第 10 篇)——这是性能问题,不是一致性级别字面承诺的内容。

三者互相独立:一致性级别只回答第 2 步,不回答第 3 步。一次 Strong 一致性的查询完全可能”看到”了刚写入的数据(第 2 步满足),但因为这批数据还在 Growing 段里跑暴力或未优化的临时索引路径(第 7 篇),延迟仍然偏高——这不是一致性级别的问题,是索引生命周期的问题,混在一起排障容易找错方向。


七、常见误解

误解一:一致性级别只影响能不能读到最新数据,不影响延迟。 第三节的时序图已经说明:Strong 意味着执行侧要等到 Service_timestamp 追上请求发起时刻的最新写入水位,这段等待是真实可观测的排队时间。反过来,Eventually 的低延迟不是”性能优化”换来的,是”放弃等待”换来的——PACELC 框架下这是同一枚硬币的两面,不是一个更”先进”、一个更”过时”。

误解二:Bounded 是”不完全一致”,Strong 才是唯一正确选择,能用就应该用。 第四节已经区分:Bounded 的陈旧窗口是定性可调的,不是”错误”或”降级”,而是产品默认值——多数业务能接受秒级陈旧,换来的是查询延迟不被写入峰值拖累。把 Strong 当成默认正确项,会让高并发写入场景下的查询排队现象被误判成”系统故障”。

误解三:选了 Session 级别,就能保证团队里任何两个成员的操作立刻互相可见。 第四节已经引用官方定义澄清:Session 只保证发起写入的那个客户端自己的后续查询能看到自己的写入,不覆盖 Terry et al. 论文里定义的跨会话因果一致性。多客户端协作场景(如开篇的多客服会话)如果需要”任意客户端都能看到彼此的最新操作”,语义上更接近 Strong,而不是 Session。

误解四:Session 级别既然叫”会话一致性”,就自动获得 Terry et al. 论文定义的全部四种会话保证。 4.1 节已经把四种保证摆成表格对照:Milvus 官方只显式承诺了 Read Your Writes 这一条,Monotonic Reads、Writes Follow Reads、Monotonic Writes 三条即便工程实现上大概率成立,也没有写进产品语义。业务如果依赖”连续读一定不倒退”这类更强的假设,不能只凭 Session 这个名字就假定已经获得保证。

误解五:多 shard 场景下,Strong 一致性查询的延迟等于各 shard 等待时间的平均值,个别 shard 慢一点问题不大。 第五节的图已经说明:Proxy 侧要等所有 shard 都放行才能归并结果,可观测延迟由最慢的一路决定,不是平均值。一个长期落后的 shard 会持续拖慢整个 collection 的 Strong 查询,即便其它 shard 表现正常,这类问题不下钻到分 shard 指标很难发现。


八、与 RAG 工程的选型直觉

RAG 场景 更常见选择 依据
文档入库后立刻问答同一会话 Session 或 Strong 4.1 节确认 Session 至少满足 RYW,覆盖”自己刚写的能看到”这一诉求
批量索引、可接受秒级陈旧 Bounded(默认) 4.2 节的窗口推演:陈旧程度取决于查询与写入的时间差,批量场景天然有时间差
离线评测、只读近似视图 Eventually 可能够用 不涉及”刚写的能不能看到”,评测关心的是稳定的近似分布
多租户 SaaS,各租户 SLA 不同 per-request 覆盖默认级别 第二节已强调 Milvus 把 ELC 权衡下放成 per-request 参数,租户可各自选

权限过滤很严时,陈旧视图可能导致偶发读到已撤销文档的向量——这正是开篇故事的通用版本。产品上常与第 11 篇的过滤、应用层 ACL 一并设计,而不是只调 ef_search。往前看一步:第 13 篇会说明 upsert 的 merge 模式内部会先发起一次强一致性查询取回旧值再合并——一致性级别不只是查询侧的旋钮,也是写路径内部实现细节的隐藏依赖。


九、学术谱系、工程间隙、开放问题

9.1 谱系

主题 对照
一致性/延迟权衡的统一框架 Abadi, Consistency Tradeoffs in Modern Distributed Database System Design, IEEE Computer 2012(PACELC;Milvus 官方文档直接引用)
有界陈旧的概率化模型 Bailis et al., PBS, VLDB 2012(Bounded 的旋钮与 PBS 的公式是两种解法层次)
会话一致性的经典定义 Terry et al., Session Guarantees for Weakly Consistent Replicated Data, PDIS 1994(Session 级别的命名对照,非完整实现声明)

9.2 工程间隙

9.3 开放问题

  1. 多副本下 ServiceTime 取多个副本节点的 min、max 还是法定人数——第 14 篇会展开副本拓扑,但一致性水位和副本选取的交互点官方文档未给出公式。
  2. Session 在连接池复用、跨进程客户端共享同一逻辑”会话”时的精确语义边界是什么?是否退化为只保证单个物理连接内的顺序?
  3. 能否给 Bounded 级别补一个类似 PBS 的概率化陈旧模型,让运维者用 SLO 反推该设多大的容忍窗口,而不是凭经验试参数——这是 Bailis et al. 论文留下的、尚未被 Milvus 官方文档回应的方向。
  4. per-request 可调的 ELC 权衡在多租户共享集群上,是否会出现”一个租户大量发起 Strong 请求,拖慢共享执行节点上其它租户的 Bounded 请求”的资源隔离问题?
  5. 第五节指出多 shard 查询的可观测延迟由最慢的一路决定;能否给出一个跨 shard 的水位倾斜度指标(例如各 shard Service_timestamp 相对 Proxy 时钟的差值分布),让运维者提前发现”个别 shard 持续落后”,而不是等到 Strong 查询整体变慢才排查——这类指标官方文档未公开,本篇不代入未证实的监控方案。

十、小结

三句话小结:

  1. 一致性级别本质是把 PACELC 定理里”延迟换一致性”的权衡从系统级固定架构,下放成了 Milvus 里 per-request 可调的 GuaranteeTs 参数;默认 Bounded。
  2. Strong 的代价是可观测的等待——执行侧要等 Service_timestamp 追上 Guarantee_timestamp,这段等待发生在归并树的某一层,不是 Knowhere 变慢。
  3. “能搜到”(一致性水位满足)≠“索引已最优”(Data Node 建索 + handoff 完成);两者独立,混在一起排障容易找错方向。

下一篇顺着”能搜到什么”往下走一步:Delete · Upsert · TTL,说明数据从”逻辑可见”到”物理清理”之间还有一段时间差,其中 upsert 的 merge 模式还会内部反过来依赖本篇的 Strong 一致性查询。


参考资料

核心论文

  1. Abadi, D. J., Consistency Tradeoffs in Modern Distributed Database System Design: CAP is Only Part of the Story, IEEE Computer 45(2), 2012(PACELC 定理;Milvus 官方 Consistency Level 文档直接引用)。
  2. Bailis, P., Venkataraman, S., Franklin, M. J., Hellerstein, J. M., Stoica, I., Probabilistically Bounded Staleness for Practical Partial Quorums, VLDB 2012。
  3. Terry, D. B., Demers, A. J., Petersen, K., Spreitzer, M., Theimer, M., Welch, B. B., Session Guarantees for Weakly Consistent Replicated Data, PDIS 1994。

文档

  1. Milvus Documentation v2.6.x, Consistency Level(四级定义、PACELC 引用)。
  2. Milvus Documentation v2.6.x, Timestamp(Guarantee_timestamp / Service_timestamp / Graceful_time 完整比较逻辑)。
  3. Milvus Documentation v2.6.x, Data ProcessingStreaming Service
  4. 第 4、5、9 篇系列 index

返回 系列目录 | 上一篇:混合过滤 | 下一篇:Delete · Upsert · TTL

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .