ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Rivet Epoxy 深入解析:基于单decree Fast Paxos 的地理分布强一致 KV 存储

Rivet Epoxy 深入解析:基于单decree Fast Paxos 的地理分布强一致 KV 存储 Rivet Epoxy 深入解析基于单decree Fast Paxos 的地理分布强一致 KV 存储【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actorsEpoxy 是 Rivet Actors 项目中服务于有状态工作负载的地理分布、强一致 KV 存储Actor 密钥Key预留、幂等写、乐观缓存读取等能力全部建立在它之上。本文将带你从设计动机、数据布局、Paxos 提案全流程、仲裁计算、读取路径到集群重配置与遗留迁移逐层拆解 Epoxy 的实现原理并结合engine/packages/epoxy的源码与测试给出可验证的工程细节。读完你将对如何在多数据中心之间以 1 RTT 完成一次不可变键提交、又如何在不引入全局日志与领导选举的前提下保证强一致有完整的实战级认识。Epoxy 在 Rivet 中的角色在 Rivet 的 Actor 体系中Epoxy 承担两个核心工作流见 epoxy/README.mdpegboard::workflows::actor::actor_keys::Propose用一次单键 set-if-absent 写预留 Actor 密钥pegboard::ops::get_reservation_for_key用一次乐观读解析该预留结果。这两个负载都不需要跨键排序或共享的全局提案元数据因此 Epoxy 把共识状态保持在每个键独立的粒度上。这也决定了它选择单 decree Paxos 而非共享日志型共识协议的根本原因。核心设计一为什么键默认不可变Epoxy 的键默认不可变immutable只有显式选择可变更mutable的工作负载才获得覆盖写语义。文档给出的理由非常直接Immutability is not a limitation. It is the design choice that makes everything else cheap——不可变不是限制而是让其他一切变便宜的设计选择无读仲裁No read quorums已提交值永不改变任何持有它的副本都能本地服务读请求稳态下读完全不触碰网络激进缓存Aggressive caching副本可以无限期缓存来自其他数据中心的已提交值因为没有东西需要失效幂等复制Idempotent replicationCommit 消息与 changelog 追赶可以安全地多次投递同一键第二次写入永远是 no-op无冲突解决No conflict resolution没有 merge 逻辑、没有 last-writer-wins要么你的值胜出要么别人先到无序 changelogUnordered changelogs每个副本的 changelog 条目顺序可以不同因为最终状态是同一组已提交值与应用顺序无关。可变更键则为覆盖写能力付出代价乐观缓存必须在提交时失效、changelog 追赶变成版本感知的、重复提交的值只有版本匹配时才幂等。对应源码中CommittedValue { value, version, mutable }的结构见 keys.rs正是这套语义的载体——mutablefalse时版本恒为 1 且永不变化mutabletrue时每次覆盖写递增version。核心设计二为什么单 decree per key每个键只需要对一个值达成一致因此每个键拥有一个独立的单 decree Paxos 实例由此获得无共享日志No shared log键之间相互独立无需维护跨无关键的全局顺序无领导选举No leader election任何副本可以在任何时刻为任何键发起提案没有需要选举或故障切换的 distinguished leader惰性崩溃恢复Lazy crash recovery卡住的提案只影响一个键没有故障检测器、没有后台扫描恢复只在另一个提议者试图写同一键时发生。作为对照文档指出对于确实需要跨键排序的高延迟地理分布负载EPaxosMoraru et al., 2013是更有用的参考——但 Rivet 的 Actor 密钥预留不需要这种能力。术语对照Epoxy 与标准 Fast Paxos早期代码从 EPaxos 血统继承了PreAccept这个名字但那个阶段始终是标准 Paxos 的 Phase 2a。如今 Epoxy 采用标准 Fast Paxos 术语EpoxyFast Paxos用途PreparePhase 1aLeader 请求副本承诺一个 ballotPrepare OkPhase 1b副本承诺并报告已接受的值AcceptPhase 2aLeader 请求副本在某个 ballot 上接受值Accept OkPhase 2b副本确认已接受CommitLearn/DecideLeader 告知副本值是最终值源码中消息类型与此完全对应PrepareRequest、AcceptRequest、CommitRequest见 propose.rs 中send_prepare_request、send_accept_request、broadcast_commits响应类型包括PrepareResponseOk / PrepareResponseHigherBallot / PrepareResponseAlreadyCommitted与AcceptResponseOk / AcceptResponseHigherBallot / AcceptResponseAlreadyCommitted等。模型与数据布局v2 UDB 子空间每个键由单 decree Paxos 独立处理。所有新的共识状态存放在每个副本的v2 UDB 子空间下没有共享全局日志、没有共享 ballot 计数器。/rivet/epoxy_v2/下的键布局如下/rivet/epoxy_v2/ replica/{replica_id}/ config ClusterConfig kv/{key}/value CommittedValue { value, version, mutable } kv/{key}/ballot Ballot kv/{key}/accepted KvAcceptedValue { value, ballot, version, mutable } kv/{key}/cache CommittedValue { value, version, mutable } changelog/{versionstamp} ChangelogEntry { key, value, version, mutable }源码中每个 FormalKey 都对应一个叶子常量VALUE、BALLOT、ACCEPTED、ACCEPTED2、CACHEchangelog 使用 FDB versionstamp 作为键以避免追加写相互竞争见 keys.rs。config当前副本的集群配置协调者coordinator副本 ID、配置 epoch、每个副本的状态与 peer URL。kv/{key}/value键的已提交值。不可变键写入CommittedValue { value, version 1, mutable false }此后永不改变可变更键每次成功提交以更高version覆盖该记录遗留原始值仍可读被视为version 0, mutable false。kv/{key}/ballot该副本见过的该键最高 ballot。副本不会接受更低 ballot 的提案。Ballot 是(counter, replica_id)元组按字典序比较先比 counter再以 replica_id 决胜。每个 ballot 只作用于一个逻辑用户键跨无关键没有共享 ballot。ballot 的两个分量各司其职counter 提供活性liveness任何副本都可以通过递增 counter 来取代另一个副本卡住的提案这是崩溃恢复的前提。没有它一个崩溃的高 ID 副本会让某个键永久阻塞——因为任何低 ID 副本都无法生成更高 ballotreplica_id 提供确定性决胜deterministic tiebreak当多个提案者使用相同 counter多个副本同时写一个新键时高 ID 者在同时看到两个请求的副本上胜出。源码中Ballot { counter, replica_id }的Ord实现精确体现了counter 优先、replica_id 决胜的比较规则见 ballot.rs。Ballot 选择在一个可串行化事务里原子地读取kv/{key}/value与kv/{key}/ballot来决定走快速路径还是 Prepare。kv/{key}/accepted该键最新已接受但尚未提交的提案{ value: Vecu8, ballot: Ballot, version: u64, mutable: bool, }Accept将本记录与 ballot 一起写入Prepare返回它使恢复 leader 能够重新提案最高已接受值Commit与 changelog 追赶会在值提交后清除它。源码中该记录定义为KvAcceptedValue见 keys.rsv2 路径使用带内嵌版本号的AcceptedValueKvAccepted2Key并保留旧的KvAcceptedKey作为只读遗留读取。kv/{key}/cache从其他副本观察到的已提交值的乐观缓存仅由kv_get_optimistic使用。不可变键可无限期缓存可变更键在缓存中保存 version提交时使旧缓存条目失效SkipCache读完全绕过此键且不会填充它。changelog/{versionstamp}每条副本本地、追加型的 changelog 条目{ key: Vecu8, value: Vecu8, version: u64, mutable: bool, }条目以 FDB versionstamp 作为键写入追加操作互不争用每个副本维护自己的 changelog因为 versionstamp 是本地排序令牌跨副本不可全局比较Learner 在追赶时翻页读取该 changelog协调者使用 learner 游标垃圾回收旧条目。ChangelogRead返回的游标只对产生它的副本有意义因此一次追赶会话必须始终停留在一个源副本上。提案全流程Proposal Flow编译后的提案路径为兼容性保留旧的Proposal包装器但支持的操作被刻意收窄为恰好一条命令、恰好一个键、一个具体值。不可变写使用 set-if-absent 语义可变更写通过操作输入显式选择覆盖语义。源码SetProposal::from_proposal直接校验commands.len() ! 1即报错并对CheckAndSetCommand要求expect_one_of恰好一个且必须为None见 propose.rs。1. Ballot SelectionLeader 在一个可串行化 UDB 事务中读取目标键的本地状态见 ballot.rs读kv/{key}/value读kv/{key}/ballot四种结果对应源码BallotSelection枚举值已存在且键不可变停止并返回AlreadyCommitted携带已提交值调用方稍后与请求值比较值已存在、键可变更且调用方请求可变更写以version committed.version 1继续。若上一次可变更提交已清除 ballot提案者可再次走快速路径ballot 缺失或为零生成新 ballot(1, replica_id)直接进入 Acceptballot 已存在先运行 Prepare。这既保住了从未触碰的键的 1 RTT 快速路径又能从先前的在途提案中安全恢复。源码中reserve_next_ballot在同一个事务里写入递增后的 ballot见 ballot.rs。2. PreparePaxos Phase 1只要 ballot selection 发现先前的 ballot 状态Leader 就进入 Prepare。请求PrepareRequest { key, ballot, mutable, version, }Leader 选择ballot (max_counter 1, replica_id)其中max_counter是它见过的该键最高 counter。副本行为见 prepare.rs每个副本在一个事务中读value、ballot、accepted。value已存在回复AlreadyCommitted若请求针对更高可变更版本则继续正常处理request.ballot current_ballot回复HigherBallot相等则视为幂等允许已本地预留 ballot 的提案者把自己纳入 Prepare 仲裁否则写入kv/{key}/ballot request.ballot回复Ok并携带highest_ballot、accepted_value含 version 与 mutable 标志、accepted_ballot。Leader 行为见 propose.rs向慢仲裁发送 Prepare通过标准消息路径包含自己。任一副本返回AlreadyCommitted停止并返回已提交值因更高 ballot 被拒过多导致无法达到仲裁抬高 ballot 并以指数退避 抖动重试 Prepare初始 10ms、2x 倍增、1s 上限、最多 10 次重试仲裁承诺成功决定 Phase 2 的值若某副本报告了已接受值重新提案最高已接受 ballot所对应的值否则保留客户端请求的值。重新提案最高已接受值正是防止先前多数接受的值丢失的安全规则。源码常量与实现精确对应PREPARE_RETRY_INITIAL_DELAY_MS 10、PREPARE_RETRY_MAX_DELAY_MS 1_000、PREPARE_RETRY_MAX_ATTEMPTS 10退避区间为[base/2, base*1.5]以在竞争提案者之间去相关见 [propose.rs](https://link.gitcode.com/i/7597d762819ce90a5968cdd86b57277a#L226-L228, L1022-L1044)并有单元测试prepare_retry_base_delay_doubles_and_caps、prepare_retry_delay_applies_bounded_jitter验证。3. AcceptPaxos Phase 2Accept 是 Epoxy 的 Phase 2 消息。请求AcceptRequest { key, value, ballot, mutable, version, }副本行为见 accept.rs每个副本在一个事务中读value与ballot。值已存在且请求不可变或请求的可变更版本过期回复AlreadyCommittedcurrent_ballot request.ballot回复HigherBallot否则写kv/{key}/ballot request.ballot、写kv/{key}/accepted { value, ballot, version, mutable }回复Ok。若同一 ballot 上已存在完全相同的已接受值值、版本、可变性都一致则幂等返回Ok。Leader 行为向当前路径的目标仲裁发送 Accept。任一副本返回AlreadyCommitted停止并返回该值部分副本返回HigherBallot继续等待其他响应只要剩余副本数仍足以达到目标目标仲裁回复Ok进入提交目标仲裁不可达返回ConsensusFailed。新 Actor 键的常见路径是 ballot selection 一轮 Accept即1 RTT 写路径。源码用AcceptRoundState跟踪target / ok_responses / remaining并在apply_accept_observation中实现容忍单个 HigherBallot但一旦ok_responses remaining target立即判定ConsensusFailed的收敛逻辑见 [propose.rs](https://link.gitcode.com/i/7597d762819ce90a5968cdd86b57277a#L193-L224对应单元测试accept_round_tolerates_single_higher_ballot_if_quorum_still_reachable与accept_round_fails_once_quorum_becomes_unreachable。4. CommitLearn/DecideLeader先在本地提交再广播复制提交。本地提交事务见 commit_kv.rs一个可串行化 UDB 事务读kv/{key}/ballot若出现更高 ballot 且本副本没有与该值匹配的 accepted 记录则拒绝读kv/{key}/value若键已提交返回AlreadyCommitted写kv/{key}/value { value, version, mutable }删除kv/{key}/accepted可变更提交时删除kv/{key}/ballot与kv/{key}/cache追加{ key, value, version, mutable }到changelog/{versionstamp}若事务因ballot 在本副本接受该值之前已被抢占而失败提案返回ConsensusFailed。提交广播CommitRequest { key, value, ballot, mutable, version, }副本行为kv/{key}/value已存在返回AlreadyCommitted可变更提交仅在request.version高于已提交版本时覆盖commit.ballot current_ballot返回StaleCommit否则写kv/{key}/value、清除accepted、可变更键清除ballot与cache、追加本地 changelog、返回Ok。Commit 刻意保持幂等——这是重试处理与 learner 追赶的硬性要求。源码中run_accept_path在本地提交成功后以tokio::spawn异步广播 Commitfire-and-forget本地已成功则广播失败不使提案失败可变更提交后还会广播缓存清除见 propose.rs。调用方结果映射不可变写保留旧的 set-if-absent 结果映射见SetProposal::result_for_committed_value与ProposalResult/ConsensusFailedReason[propose.rs](https://link.gitcode.com/i/7597d762819ce90a5968cdd86b57277a#L48-L81, L137-L153)不可变已提交值与请求值一致返回Committed不可变已提交值与请求值不一致返回ExpectedValueDoesNotMatch携带当前值可变更写的覆盖提交成功返回Committed无法在无更高 ballot 重试的情况下达到仲裁返回ConsensusFailed由调用方重试。序列图四种关键场景快速路径新键1 RTTActor 密钥预留的常见情形键无任何先验状态。Replica A 是提议 leader三个副本都参与仲裁leader 通过同一 handler 发送给自己。Replica A (leader) Replica B Replica C | | | | [read local: no value, no ballot] | | [generate ballot (1, A)] | | | | | |--- Accept(val, 1,A) -------| | |--- Accept(val, 1,A) --------------------------------| | | | |------------- Ok --------------| | |------------- Ok ----------------------------------------| | | | | [fast quorum reached (3/3)] | | | [commit local tx] | | | | | |--- Commit(key, val) -----------| | |--- Commit(key, val) ----------------------------------| | | | v done: Committed v v已提交键0 RTT 不可变写本地读读或重复的不可变写命中已提交的键。Ballot selection 短路零网络流量。可变更键不在此停下——它们用已提交值的 version 开启下一轮覆盖写。Replica A (leader) | | [read local: value exists] | [return AlreadyCommitted(v)] | v done慢路径先前的在途状态2 RTTReplica C 发起提案但在提交前崩溃。稍后 Replica A 为无关预留尝试写同一键其 ballot selection 发现 C 的搁浅 ballot 并触发恢复。尽管 A 的副本 ID 低于 C它仍通过把 counter 递增到(2, A)压过 C 的停滞提案——因为 counter 先比较(2, A) (1, C)。Replica A Replica B Replica C | | | | | [generate ballot (1, C)] | | | |--- Accept(val, 1,C) --------|-- Accept(val, 1,C) -- | | | |-------------- Ok --------------|------------- Ok -----| | | | | | [fast quorum (3/3)] | | [crash before Commit] | | x | | x | --- later, A tries to write this key --- x | | x | [read local: no value, ballot (1, C)] x | [prior state found, must Prepare] x | [generate ballot (2, A)] | x | [(2, A) (1, C) because 2 1] | x | | x |--- Prepare(key, 2,A) ---------| x | | x |--- Ok(acceptedval1,C) ------| x | | x | [slow quorum reached (2/3)] | x | [re-propose Cs accepted value] | x | | x |--- Accept(val, 2,A) -------| x | | x |------------- Ok --------------| x | | x | [fast quorum reached (2/2)] | x | [commit local tx] | x | | x |--- Commit(key, val) -----------| x | | x v done: Committed v x这里体现了文档强调的惰性崩溃恢复没有故障检测器、超时或后台扫描来发现搁浅状态恢复只发生在另一个提案者写同一键时。之所以成立是因为 Epoxy 只服务 Actor 密钥预留——如果某个键重要调用方最终会尝试预留它从而顺带触发恢复。标准 Paxos 安全性要求重新提案最高已接受值恢复 leader 正是这么做的确保先前多数接受的值不丢失。并发提案者写同一个新键竞争Replica A 与 Replica C 同时写同一个新键两者都看到无先验状态并跳过 Prepare。n3时快速仲裁为 3全部副本最多只有一个提案者能达到输家重试时回退到慢路径。Replica A (leader) Replica B Replica C (leader) | | | | [no value, no ballot] | [no value, no ballot] | [generate ballot (1, A)] | [generate ballot (1, C)] | | | |--- Accept(val_a, 1,A) -----| | | |-- Accept(val_c, 1,C) -- | | | | [B accepts (1,A)] | | [then sees (1,C) (1,A)] | | | | |------- HigherBallot ----------| | | |----------- Ok -------| | | | | [only 1/3, no fast quorum] | [3/3, fast quorum reached] | [ConsensusFailed] | | | | [commit local tx] | |--- Commit(val_c) ----| |---------- Commit(val_c) ------| | | | | | [retry: read local] | | | [sees committed val_c] | | | [AlreadyCommitted or | | | ExpectedValueDoesNotMatch] | | | | | v v vballot 的 replica_id 分量是决胜因素当两个 leader 使用相同 counter 时在同时看到两个请求的副本上高 ID 者赢得 Accept。由于n3时快速仲裁需要全部副本低 ID leader 无法达到仲裁。Learner 追赶重配置新数据中心加入集群。协调者运行在 Replica A 上以Joining状态加入新副本广播更新后的配置使实时提交能到达它然后开始追赶。加入中的副本从一个活动源翻页 changelog同时应用并发到达的实时提交。Replica A (coordinator) Replica B (source) Replica C (joining) | | | | [detect new DC (C)] | | | [health check OK] | | | [add C as Joining to config] | | | | | |--- UpdateConfig(C joining) --| | |--- UpdateConfig(C joining) --------------------------------| | | | | [config broadcast done] | | | [live commits now reach C] | | | | | |--- BeginLearning ------------------------------------------| | | | | | [choose B as source] | |-- ChangelogRead(cursor0, N) | |--- [{k1,v1},{k2,v2}] ---| | | [apply k1, k2] | | | | | |--- Commit(k3,v3) ---| | | [apply k3] | | | | | |-- ChangelogRead(cursorpage1.last) | |--- [{k3,v3}] --------| | | [k3 exists, no-op] | | | | |-- ChangelogRead(cursorpage2.last) | |--- empty page --------| | | [catch-up complete] | | | |----------- UpdateReplicaStatus(Active) ----------------| | | | | [mark C Active, bump epoch] | | |--- UpdateConfig(C active) ----| | |--- UpdateConfig(C active) ---------------------------------| | [trigger changelog GC] | | | | | v v v关键安全属性是幂等性同一个已提交值无论经由实时Commit还是 changelog 页面到达都只会被应用一次因为kv/{key}/value已存在时第二次写入是 no-op。仲裁计算QuorumsEpoxy 使用 Fast Paxos 仲裁尺寸实现见 utils.rs慢仲裁floor(n / 2) 1快仲裁n - floor((slow_q - 1) / 2)安全不变量2 * fast_q slow_q 2 * n该不变量保证慢仲裁是严格多数且任意两个快仲裁与一个慢仲裁之间必然相交。小规模集群的取值nslow_qfast_q323433534746只有Active副本参与提案仲裁。发送者排除的 fanout 仲裁助手会对Fast、Slow、All减一源码calculate_fanout_quorum的saturating_sub(1)而Any仍只目标单响应。Commit fanout 仍发送给加入中的 learner使其在引导期间保持追赶。上述计算与不变量由单元测试quorum_sizes_match_expected_values_for_small_clusters、quorum_invariants_hold_for_small_clusters逐 n 验证见 utils.rs。此外提案支持target_replicas作用域scoped proposalresolve_active_quorum_members校验作用域非空、包含本地副本、只含活动副本否则报错见 utils.rs。调用方需自行保证同一键在时间上保持稳定作用域或把变更作为更高层的显式重配置处理。读取路径Reads读取支持两种缓存行为Optimisticmiss 时检查本地乐观缓存并将找到的第一个远端已提交值写回缓存SkipCache完全绕过本地乐观缓存且不填充但仍先检查本地已提交值。本地读取顺序为v2kv/{key}/value遗留kv/{key}/committed_value遗留kv/{key}/value使用Optimistic时v2kv/{key}/cache使用Optimistic时向其他数据中心 fanout缓存找到的第一个已提交值这让稳态读路径保持本地化0 RTT。源码epoxy_kv_get_optimistic注释还给出了三条重要使用警告值写入后不得变化、频繁未命中会造成大量 fanout 开销、以及所有提交节点离线时可能错误返回None见 get_optimistic.rs。单副本作用域quorum_members.len() 1直接短路返回None以避免无谓 fanout。为什么读使用独立缓存键乐观缓存 miss 时fanout 结果写入kv/{key}/cache而非kv/{key}/value。这使读与 Paxos 共识路径完全隔离——直接写kv/{key}/value将需要完整 Commit 事务检查 ballot、清除kv/{key}/accepted、追加 changelog跳过任何一步都会使副本处于不一致状态。独立缓存键的代价是每个远端读值多一个键但避免了把读变成写事务。TODO消除缓存键文档记录的演进方向在 fanout 命中时运行正规 Commit 事务来移除缓存键。值已在其他副本提交且不可变因此这是安全的收益包括每个远端读值少一个键、后续读直接命中kv/{key}/value、changelog 完整从而使从此副本追赶的 learner 不丢条目。代价是缓存 miss 读从单缓存键写入变为一次可串行化 UDB 写事务。目前独立缓存键是更简单的方案因为读完全处于明确定义的 Paxos 流程之外。重配置Reconfiguration与 Learner 追赶每个数据中心运行一个 replica workflowleader 数据中心还运行 coordinator workflow注册见 lib.rs。协调者流程协调者检测到拓扑中的新数据中心后读取当前拓扑并与存储的集群配置比较对每个新副本做健康检查直到可达或拓扑再次变化以Joining状态把新副本加入集群配置在协调者 learner 状态中注册每个加入副本用于 changelog GC 跟踪在追赶开始前把 joining 配置广播给每个副本向每个加入副本发送BeginLearning。第 5 步至关重要Commit fanout 使用当前配置的完整副本列表因此先广播 joining 配置才能让实时提交在 learner 下载历史期间到达它。Learner 流程收到BeginLearning后learner本地存储提供的集群配置从该配置中选择一个活动源副本若尚无活动源副本跳过追赶并报告Active否则翻页源副本的 changelog 直到末尾。Changelog 追赶循环每页执行向源副本发送ChangelogRead(afterVersionstamp, count)对每个返回的{ key, value, version, mutable }条目本地应用键缺失时写kv/{key}/value不可变重复值仅在完全匹配时保留可变更值仅当传入版本更高时覆盖清除kv/{key}/accepted可变更条目清除kv/{key}/ballot与kv/{key}/cache追加条目到 learner 自己的 changelog将afterVersionstamp推进到页面的lastVersionstamp重复直到源返回空页。该路径幂等因此追赶期间到达的实时Commit是安全的同一已提交值到达两次时第二次应用是 no-op。提升为 Active追赶完成后learner 发送CoordinatorUpdateReplicaStatus(statusActive)。协调者随后在集群配置中将副本标记为Active将其移出 learner GC 跟踪递增配置 epoch向每个副本广播更新后的配置触发 changelog GC。配置 epoch 为集群成员变化定版不属于 per-key Paxos ballot。Changelog GC协调者按源副本跟踪 learner并记住仍在使用的最旧追赶游标learner 正从某源副本读取时最旧游标成为该源的 GC 水位线无 learner 读取时GC 可截断到该副本当前最新可见的 changelog 条目learner 尚未建立游标时GC 不得截断该源副本的 changelog。每个副本的 changelog 独立 GC因为 versionstamp 游标是副本本地的。传输层Transport副本间流量使用固定 v2 HTTP 端点上的原始 BARE 载荷路由挂载见 http_routes.rsPOST /v{version}/epoxy/message副本 RPC如Prepare、Accept、Commit、UpdateConfig与状态更新POST /v{version}/epoxy/changelog-readlearner 追赶期间的翻页 changelog 读GET /epoxy/protocol-version刻意不带版本号peer 探测彼此支持的协议版本。路由层仍校验请求种类使 changelog 读不会静默回退到通用消息端点messagehandler 显式拒绝ChangelogReadRequestchangelog_readhandler 只接受 changelog 读请求见 http_routes.rs。handle_request还会校验request.to_replica_id current_replica_id防止消息被错误副本消费。遗留回退Legacy Fallback遗留键布局把已提交值放在/rivet/epoxy/replica/{replica_id}下当前布局使用/rivet/epoxy_v2/replica/{replica_id}。没有后台迁移——所有新写入都进 v2旧已提交值通过本地双读回退发现。双读回退读按序检查两种布局v2kv/{key}/value、遗留kv/{key}/committed_value、遗留kv/{key}/value、然后 v2kv/{key}/cache。查找由 get_local.rs 处理而 ballot.rs 使用相同的遗留子空间检查使已提交的遗留键在任何新 ballot 写入之前就短路。未保留的遗留状态只有已提交值从遗留子空间读取。遗留的 ballots、accepted 值、changelogs 与 config 在切换后一律忽略。若旧副本有未提交的在途 accepted 值该提案即丢失——后续提案者要么在遗留子空间找到已提交值要么从头重试该键。工程实现与测试导航若想深入源码推荐以下入口均为仓库内相对路径提案全流程ops/propose.rs——ballot selection 分派、fast/slow path、Prepare 退避重试、Commit 广播与缓存清除含 8 个单元测试Ballot 与 selectionreplica/ballot.rs——Ballot 序关系与四种BallotSelection结果副本消息处理replica/messages/——prepare.rs/accept.rs/commit.rs的副本侧行为本地提交事务replica/commit_kv.rs——Commit 的 8 步事务与幂等约束仲裁计算utils.rs——仲裁尺寸公式、fanout 减一、scoped 副本校验含不变量测试乐观读ops/kv/get_optimistic.rs——本地读 → cache → fanout → 写回缓存的完整路径键布局keys/keys.rs——value / ballot / accepted / accepted2 / cache / changelog各 FormalKey 与序列化细节传输路由http_routes.rs——两个 BARE 端点与协议版本探测集成测试tests/——proposal.rs提案、proposal_concurrent_mutable.rs并发可变更写、consensus_regressions.rs共识回归、reconfigure.rs重配置、kv.rs/kv_get_optimistic.rs读写路径、migration.rsv1→v2 迁移、backfill.rs/backfill_snapshot.rs回填。总结Epoxy 用三个朴素但环环相扣的设计选择解决了地理分布 强一致的经典难题不可变键让缓存与读路径零成本单 decree per key消除了共享日志与领导选举两大运维负担惰性崩溃恢复把故障处理从监控变成写同一键时的顺带副作用。而 1 RTT 快速路径、2 RTT 慢路径的二分法则让最常见的 Actor 密钥预留工作在理想情况下只花费一次跨数据中心往返。配合 UDB 可串行化事务的原子状态读写与幂等提交Epoxy 在保证 Fast Paxos 安全性的同时把工程复杂度控制在了可读、可测、可迁移的范围之内。【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表