ARTICLE DETAIL

资讯详情

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

Neon Safekeeper 动态成员变更(Dynamic Membership Change)RFC 深度解读:共识级迁移协议与 storage_controller 实现

Neon Safekeeper 动态成员变更(Dynamic Membership Change)RFC 深度解读:共识级迁移协议与 storage_controller 实现 Neon Safekeeper 动态成员变更Dynamic Membership ChangeRFC 深度解读共识级迁移协议与 storage_controller 实现【免费下载链接】neonNeon: Serverless Postgres. We separated storage and compute to offer autoscaling, code-like database branching, and scale to zero.项目地址: https://gitcode.com/GitHub_Trending/ne/neon导读本文深度解读 Neon 仓库中的 RFC 035Safekeeper 动态成员变更。该 RFC 解决的核心问题是在 Serverless Postgres 架构中如何安全地在运行期改变某条 timeline 所驻留的 safekeeper 节点集合用于故障恢复与节点再平衡同时保证已提交 WAL 绝不丢失。阅读本文后你将掌握两阶段共识成员变更joint configuration的完整算法、基于 CAS 外部配置存储的线性化机制、Computewalproposer与 Safekeeper 之间的 v3 协议改动、storage_controller 中的数据库 schema 与迁移 API以及从模拟测试到生产灰度的一整套落地路径。背景为什么需要 Safekeeper 成员动态变更在 Neon 的分离式架构中safekeeper 承担 WAL 的持久化与共识quorum 确认职责。当 safekeeper 节点发生故障或集群需要重新平衡负载时必须有能力改变某条 timeline 驻留的 safekeeper 集合——即进行成员变更membership change。RFC 提出的目标如下安全性无论 safekeeper 与 compute 处于何种状态已提交日志都不能丢失活跃性只要旧集合的任意多数majority、新集合的任意多数以及 compute 在线且互联过程就能推进。RFC 明确指出这就是经典的consensus membership change问题它总是包含两个阶段RFC 原文先切换旧多数到旧 新联合配置joint configuration从而阻止未获得新集合确认的提交再引导bootstrap新集合确保新集合的多数已拥有第一阶段完成之前所有可能被提交的数据之后才安全地完成切换。为什么必须两阶段因为新集合的 quorum 可能与旧集合的 quorum不相交。RFC 给出的典型例子是ABC - ABD其中 quorumAC与BD没有交集若直接切换可能同时出现两个各自拥有提交权限的 quorum破坏线性一致性。在标准 Raft 等共识算法中成员变更通常由共识 leader 驱动配置的先后次序通过共识日志来确立。但在 Neon 中共识 leader 是 compute即 walproposer见 pgxn/neon/walproposer.cRFC 明确不希望为了迁移而唤醒所有 compute也不希望在 compute 之外重新实现一遍 leader 逻辑。因此RFC 提出配置的发布依托一个外部容错、分布式强一致、且 API 极简单 key 的 compare-and-swap的存储——配置得当的 PostgreSQL 就满足这个条件。另外注意Neon 的共识粒度是timeline 级别因此下述算法适用于单条 timeline。核心概念Configuration 与 generationRFC 用如下结构定义一条 timeline 的成员配置RFC 原文struct Configuration { generation: SafekeeperGeneration, // 唯一标识该配置的编号 sk_set: VecNodeId, // 当前 safekeeper 集合 new_sk_set: OptionalVecNodeId, // 迁移目标集合None 表示非联合配置 }new_sk_set存在Some的配置称为joint configuration联合配置用于迁移的中间步骤generation是单调递增的编号配置之间通过它排序c1.generation c2.generation即称c1高于c2。在仓库实现中这一结构体被完整落地于 libs/safekeeper_api/src/membership.rspub struct Configuration { /// 唯一 id pub generation: SafekeeperGeneration, /// 当前配置成员 pub members: MemberSet, /// Some 表示这是一个联合配置 pub new_members: OptionMemberSet, }从源码可以看到几个关键约定对应 RFC 语义SafekeeperGeneration(u32)包装一个u320 是占位值INVALID_GENERATION1 才是第一个有效代INITIAL_GENERATION并提供了next()与previous()方法MemberSet::new会拒绝重复的 safekeeper idmembership.rsConfiguration::contains(sk_id)判断某节点是否为当前或联合配置的成员members.contains(sk_id) || new_members.contains(sk_id)membership.rs文件头注释明确指出这些类型同时是safekeeper control file 的一部分和HTTP API 的一部分即下文要讲的持久化与协议改动的直接载体。持久化数据的变化RFC 规定两类持久化变更RFC 原文safekeeper 开始在自己的 control file 中持久化当前配置更新是原子的因此内存值始终与持久化值一致。这一点在源码中可以看到TimelineState通过start_change()/finish_change()的原子变更机制承载mconf字段safekeeper/src/state.rscontrol file 的校验逻辑也会检查mconf.generation INVALID_GENERATION的情况safekeeper/src/control_file.rs。外部 CAS 存储配置存储按 timeline 保存配置timeline 创建时初始化为 generation 1 与初始 safekeeper 集合在其上执行的 CAS绝不能丢失。Compute ↔ Safekeeper 协议改动RFC 对协议消息的改动RFC 原文ProposerGreeting携带 walproposer 已确立的配置未确立则为 nullAcceptorGreeting携带 safekeeper 当前的Configuration其余消息VoteRequest、VoteResponse、ProposerElected、AppendRequest、AppendResponse全部携带 generation 编号——walproposer 发往 safekeeper 的消息带 walproposer 的 generation反向消息带 safekeeper 的 generation。在 pgxn/neon/walproposer.c 中可以看到该协议的实现痕迹SendProposerGreeting向 safekeeper 发送携带mconf的问候第 730 行附近RecvAcceptorGreeting解析sk-greetResponse.mconf并比较 generation第 943 行附近vote/append 请求中设置generation wp-mconf.generation第 1002、1510、1592 行附近。算法详解两阶段成员迁移Safekeeper 侧的规则RFC 为 safekeeper 定义了三条基本规则RFC 原文一旦观察到比自己更高的配置立即切换拒绝所有 generation 低于自己的消息如果自己不是当前配置sk_set或new_sk_set的成员同样拒绝消息虽然处理它们未必不安全walproposer 反正会忽略结果。如果ProposerGreeting中携带非 null 配置且高于当前配置safekeeper 切换到它safekeeper 通过首个消息AcceptorGreeting上报自身配置并拒绝 generation 小于自己当前配置的一切 walproposer 消息拒投票、拒在handle_elected中截断 WAL、拒收 WAL并在响应中带回自己的 generation 让 walproposer 知晓。新增 HTTP 端点PUT /v1/tenants/{tenant_id}/timelines/{timeline_id}/membership接受Configuration仅当目标配置高于当前配置时才切换否则忽略无论何种情况都返回如下响应结构struct TimelineMembershipSwitchResponse { conf: Configuration, term: Term, last_log_term: Term, flush_lsn: Lsn, }该端点已在 safekeeper/src/http/routes.rs 注册为timeline_membership_handler其底层实现membership_switch的逻辑与 RFC 完全一致——只有当to.generation self.mconf.generation时才执行原子切换safekeeper/src/state.rs。同文件还注册了PUT .../term_bump端点第 805-806 行对应算法步骤中的 term 提升。Computewalproposer侧的规则RFC 为 walproposer 定义的规则RFC 原文联合配置下选举与提交 WAL 都需要来自sk_set和new_sk_set两个多数的投票/刷新确认compute 仍从 control plane 获取要连接的 safekeeper 列表但该列表不定义共识成员启动时 walproposer 跟踪从各AcceptorGreeting收到的最高配置一旦集齐sk_set多数与new_sk_set若存在多数的问候就将该配置确立为自己的配置并进入投票确立配置后应停止与不在该配置中的 safekeeper 通信继续通信也不算不安全如果 walproposer 听到比自身更高的配置例如 safekeeper 因配置变更而拒收则直接重启。迁移主算法8 个步骤RFC 给出可在任何能访问配置存储与 safekeeper 的地方执行的算法RFC 原文。它接受desired_set: VecNodeId作为输入可以安全中断/重启也允许多个实例并发执行虽然并发时通常只有一个能推进遇到上次中断的变更尝试时拒绝发起新变更但会尝试完成上次的变更只要旧多数、新多数与配置存储可达最终会收敛。从配置存储获取当前 timeline 配置。若当前已是联合配置且new_set与desired_set不同拒绝变更但把该联合配置赋给内存变量joint_conf转入步骤 4 去完成进行中的变更。否则创建联合配置joint_conf当前配置编号n加一把desired_set填入new_sk_set通过 CAS基于当前 generation持久化到配置存储——只有当当前编号仍是n时变更才生效。CAS 一方面保证配置唯一性另一方面对配置进行线性化保证新配置只在前一个配置之后被创建且我们知道这次转变是安全的。CAS 失败即中止。对当前集合的 safekeepers 调用PUT membership下发joint_conf收集多数响应才能继续。若某响应返回的 generation 高于joint_conf.generation中止说明有其他切换抢先发生。否则在各响应中取最大的last_log_term, flush_lsn作为内存变量sync_position取最大term作为sync_term。必须等新集合多数追赶到sync_position才能完成切换——因为sync_position之前的数据可能在未获新集合确认的情况下已被提交同理把新多数的 term 提升到sync_term可以保证同一个 term 永远不会选举出两个 compute。对new_sk_set中尚不存在该 timeline 的 safekeeper 执行pull_timeline从当前集合的多数拉取。虽然只需新集合多数完成即可继续但 RFC 建议确保所有新成员都完成初始化——如果某些节点宕机为何还要迁移过去对新集合的 safekeepers 调用POST bump_term(sync_term)多数成功即可。反复对新集合 safekeepers 调用PUT membership下发joint_conf并收集位置。这一步通常没必要pull_timeline已带上 joint confcompute 也会广播但只有当新集合多数的last_log_term, flush_lsn达到sync_position时才能进入下一步——这层双保险是必要的例如 timeline 可能因尝试迁移→中止→再尝试迁移的序列而早已存在。创建new_confjoint_conf的 generation 加一sk_set置为新集合new_sk_set为 None再次通过 CAS 写入配置存储。对新集合 safekeepers 调用PUT membership下发new_conf只需多数收到即可其余可由 compute 更新。RFC 作者坦言自然语言描述难免有歧义因此主张将算法写成 TLA 规范来做模型检验见下文测试一节。让算法实用且鲜活的额外考量RFC 在安全性描述之后补充了几条工程考量RFC 原文在步骤 3 之前先 ping 新集合确保迁移目标是活节点如果指定的新集合有误只要步骤 6 的 CAS 尚未完成可以再用一次 CAS回滚到旧配置步骤 4 中新集合成员上可能已存在 timeline最典型的是过程重启删除后重做pull_timeline在没有 generation 参与时一般不安全所以把已存在视为成功。缺点是一个极低概率的调度下步骤 5 的条件可能永远不满足直到 compute 被重新唤醒去同步新成员——作者认为实践中观察不到必要时可主动唤醒 compute迁移结束后应在旧集合中不属于新集合的 safekeeper 上本地删除该 timeline除非不可达。为安全起见删除也必须带 generation仅当 safekeeper 的当前配置 generation 不高于请求中的值、且它已不是该配置成员时才执行删除如果步骤 1 取到的配置已非联合且成员恰好等于desired_set直接跳到步骤 7把它当作new_conf。实现归属为什么是 storage_controllerRFC 讨论了流程的驱动者control plane 与 storage_controller 都是候选且二者都已拥有数据库不需要再引入新存储。最终提议由 storage_controller 管理 safekeepersRFC 原文理由有二storage_controller 是 Rust 实现便于编写模拟测试simulation testing它已经承担 pageserver 管理职责。这同时意味着只有把所有 tenant/timeline 都迁移到 storage_controller 之后迁移能力才完全可用。由此引出了 storcon ↔ control plane 接口的定义与改造。storage_controller ↔ control plane 接口control plane 应从按 tenant 存 safekeepers改为按 timeline 存 safekeepers因为 tenant 无法原子迁移配置的推送沿用 pageserver/notify-attach的push 模式storage_controller 负责重试通知 control plane 直到成功这样能让 storage_controller 远离 compute 启动的关键路径并保持统一性control plane 无需完整理解Configuration只需知道最新配置中的 safekeeper 列表 关联的 generation用于抵御过期更新请求也传给 compute。为此新增端点/notify-safekeepers接受如下 JSONRFC 原文{ tenant_id: String, timeline_id: String, generation: 42, safekeepers: [{node_id: 1, host: safekeeper-0.example:6401}] }其中SafekeeperId为{ node_id: u64, host: String }host原理上冗余但对可观测性有用。请求在提供的 generation 更高时更新数据库中的 safekeeper 列表cplane 数据库也要存 generation与/notify-attach类似先更新数据库更新成功即调用成功再尽量调度apply_config失败也无妨。storage_controller 应对该端点做限流但作者认为通常不需要——迁移吞吐本身受pull_timeline限制。cplane 的 timeline分支创建应像现在为分片 tenant 所做的那样调用 storage_controller 的POST tenant/:tenant_id/timeline响应新增safekeepers_generation与safekeepers字段这些字段初始可能缺失此时 cplane 按现状自行选择 safekeepers。调用需重试直到成功timeline 删除与 tenant 删除同样应走 storage_controller 对应端点并重试到成功compute 从 control plane 收到 safekeeper 列表时需要 generation 来判断是否更新compute 可能从 cplane 或 safekeepers 两处获得列表。当前neon.safekeepersGUC 只是逗号分隔的host:port列表RFC 建议加上g#generation:前缀形如RFC 原文g#42:safekeeper-0.eu-central-1.aws.neon.tech:6401,safekeeper-2.eu-central-1.aws.neon.tech:6401,safekeeper-1.eu-central-1.aws.neon.tech:6401该 GUC 前缀解析在 walproposer.c 中实现解析g#后的 generation 数字safekeepers_generation INVALID_GENERATION即 0时退化为旧行为。当 generation 有效但proto_version 3时直接 FATAL第 185-186 行与 RFC 的启用 generation 需要 v3 协议要求一致。cplane 改动汇总safekeepers 管理从 per-tenant 改为 per-timeline新增整型safekeeper_generation字段新增/notify-safekeepers端点分支创建调用可能返回 safekeeper 列表存在时应采纳它而不是像现在这样自行选择neon.safekeepersGUC 加g#generation:前缀。storage_controller 内部实现RFC 建议继续沿用启动时全量加载进内存的方式单条 timeline 数据不超过约 100 字节16 字节 tenant_id 16 字节 timeline_id int generation 约 3 个 safekeeper id 的 vec 若干标志位100 万条 timeline 也不超过 100MBRFC 原文。与 pageserver 附着的 Intent 类似storage_controller 为每条 timeline 维护一个**内存中的MigrationRequest或不存在**以及一个试图让这些请求成真的任务池——这保证单个 storage_controller 实例不会并发地对同一条 timeline 执行多个迁移。首个版本采用更手动、无重试的方式迁移失败即移除请求后续再构建重试与自动调度enum MigrationRequest { To(VecNodeId), FinishPending, }FinishPending请求用于确保状态干净当前配置非联合、多数 safekeepers 已感知该配置但不尝试迁移到任何地方若步骤 1 取到的配置已非联合则直接跳到步骤 7。它应在启动时为所有 timeline 运行首版允许手动触发。Schema 设计timelines表RFC 原文table! { // timeline_id 是主键 timelines (tenant_id, timeline_id) { timeline_id - Varchar, tenant_id - Varchar, start_lsn - pg_lsn, generation - Int4, sk_set - ArrayInt8, // safekeeper id 列表 new_sk_set - NullableArrayInt8, // safekeeper id 列表非联合配置时为 null cplane_notified_generation - Int4, sk_set_notified_generation - Int4, // sk_set 的 quorum 已感知的 generation deleted_at - NullableTimestamptz, } }start_lsn用于在 safekeeper 上正确创建 timeline也可考虑加ancestor_timeline_id保留层级但本 RFC 暂不需要cplane_notified_generation与sk_set_notified_generation用于追踪算法最后阶段——最终配置提交到 DB 之后仍需通知 safekeeper 集合与 cplane 的阶段若new_sk_set为 null 且两个*_notified_generation与generation一致则 timeline 处于最新状态无迁移进行中两个 notified 字段理论上可用一个布尔migration_completed替代但分开更利于可观测性。另有safekeeper_timeline_pending_ops表RFC 原文table! { // timeline_id, sk_id 是主键 safekeeper_timeline_pending_ops (sk_id, tenant_id, timeline_id) { sk_id - int8, tenant_id - Varchar, timeline_id - Varchar, generation - Int4, op_type - Varchar, } }启动时把全部 pending ops 加载进内存该表仅用于跨重启保留状态op_type可为include从对端种子化并确保 generation 最新、exclude本地移除、delete全局删除。严格说该字段可由当前配置推导但显式存储可观测性更好generation必不可少操作完成后 reconciler 必须按它移除本行且不能误删可能出现的更高 generation 的行插入新行时应覆盖移除同一 sk timeline 且 generation 更低的所有行新操作使旧操作过时插入delete操作会覆盖全部行关于exclude不必新增 HTTP 端点直接复用 membership 切换端点即可——如果 safekeeper 不是配置成员在切换时就会本地删除 timeline此时调用方应把404 视为 OK。per-safekeeper reconciler 主循环读取safekeeper_timeline_pending_ops与 timeline 配置联查获取该 safekeeper 的当前配置generationn然后无限重试若节点是成员include检查 timeline 是否存在不存在则从其他成员pull_timeline调用切换到当前配置若节点不是成员exclude调用切换到当前配置404 视为 OK若 timeline 已删除delete调用删除。情况 1、2 完成后在op_type非delete时移除该 sk timeline 且 generation ≤ n 的 pending 行情况 3 额外移除timelines条目当该 timeline 不再有任何 pending 行时。API 一览节点管理 API 与 pageserver 类似RFC 原文POST /control/v1/safekeeper注册 safekeeperGET /control/v1/safekeeper列出 safekeepersGET /control/v1/safekeeper/:node_id获取单个 safekeeperPUT /control/v1/safekeper/:node_id/scheduling_policy将状态改为offline或decommissioned等首版暂不在此调度迁移。safekeeper 部署脚本应像现在注册到 cplane 一样以相同 id 注册到 storage_controller。timeline 创建/删除复用已有的POST/DELETE tenant/:tenant_id/timelinecplane 需重试到成功。需要指出的是不想让单台 safekeeper 宕机阻塞 timeline 创建/删除——目前这是靠 compute 隐式在它所连接的任意 safekeeper 上创建 timeline 来兜底的这会留下timeline 已创建但 start LSN 未定义的不佳状态RFC 在下一节给出处理方案。tenant 删除即对所有 timeline 重复删除流程。迁移 API首版为最简、最命令式PUT /control/v1/safekeepers/migrate把某个 safekeeper 上的所有 timeline 迁走JSON 为{ src_sk: 1, dst_sk: 2, limit: 100 }返回已调度的请求列表PUT /control/v1/tenant/:tenant_id/timeline/:timeline_id/safekeeper_migrate把单条 timeline 迁移到给定集合struct TimelineSafekeeperMigrateRequest { new_sk_set: VecNodeId, }首版同步执行迁移调用方需重试到成功未来可能改为异步 API 并返回已调度请求还应提供订阅结果的方式除了看日志/指标并增加 tenant 级对应调用GET /control/v1/tenant/:tenant_id/timeline/:timeline_id/返回 timeline 当前内存状态与挂起的MigrationRequest若有PUT /control/v1/tenant/:tenant_id/timeline/:timeline_id/safekeeper_migrate_abort在 CAS 下把配置从联合切回之前的sk_setgeneration 照常递增尝试中止迁移。这些 API 已在 storage_controller 中有对应实现handle_tenant_timeline_safekeeper_migrate/safekeeper_migrate_abort路由注册于 storage_controller/src/http.rs服务端逻辑位于 storage_controller/src/service/safekeeper_service.rs 与 (第 1628 行)其中pull_timeline对多数新集合成员的并发请求可见于同文件第 987-1040 行附近注释也明确写着通过从当前集合多数做 pull_timeline第 1313 行。API 实现与对账reconciliation细节RFC 强调保留一个基本假设不可达的少数派3 中 1不阻塞 timeline 创建/删除完成但最终要在错过操作的节点上补齐除非该节点已被移除迁移同理被排除成员错过排除操作也不能阻塞且ABC - ABD迁移后在节点 C 上挂起的排除操作不能阻碍下一次ABD - ABE迁移某个节点错过 timeline 创建也不能阻塞后续从它发起的迁移。因此自然需要per-safekeeper 后台 reconciler无限重试这些操作。操作类型由 timeline 状态成员配置与是否删除与 safekeeper id 共同决定共三种在 sk 上创建 timeline节点加入、本地删除节点被排除类似 detach、全局删除timeline 被删除。storage_controller 重启时原则上可以通过对比 safekeepers 状态与 storcon 状态推导出这些挂起操作但 RFC 倾向于在数据库中物化它们开销不大避免启动扫描扫描本身可能失败且可以直接在事实源头看到未完成工作。Timeline 创建的实现cplane 会重试到成功因此所有动作必须幂等。难点是 start LSN初始tenant 创建调用时 cplane 不知道它但在 safekeepers 上设置start_lsn是有益的——它保证 walproposer 总能找到自己与 safekeeper 的 WAL 历史的公共点缺失它就是明确的损坏信号。步骤在 pageserver 上创建 timeline或观察到已存在从响应中得出last_record_lsn选择 safekeepers用ON CONFLICT DO NOTHING插入 timeline 行。注意last_record_lsn是移动的ingestion 开始后即变化插入绝不能覆盖它也不能覆盖成员配置等字段下一步使用的start_lsn必须取库中的值cplane_notified_generation可在插入时置 1初始 generation避免向 cplane 通知初始配置cplane 反正会在创建请求中收到它向至少多数 safekeepers 发起 timeline 创建调用。多数即可而非全部是因为这保证任何活跃多数都至少有一个已创建 timeline 的 sk从而让对账任务复用与迁移共享的pull_timeline而不是专门处理创建timeline 已存在则忽略调用为错过创建的少数派 safekeepers 插入safekeeper_timeline_pending_ops条目。这一步不会丢对 cplane 的响应只在插入完成后才发出且 cplane 会重试直到 200。实现上请求处理器先在 DB 持久化请求再向每个 safekeeper reconciler 发送内存请求处理它。至于 pg version / wal segment size可以持久化到timelines表但并非必需——步骤 3 的初始创建可从 pageserver 或 cplane 创建调用取得之后的pull_timeline会携带它们。Timeline 迁移的实现对 DB 做 CAS 创建联合配置。从此刻起迁移视为进行中——所有进行中的迁移都可以通过查库发现执行算法的步骤 4-6包括向new_sk_set做pull_timeline、更新所有 safekeepers 的成员配置、通知 cplane 等。所有操作幂等此阶段无需在 DB 持久化任何东西出错可安全重试或中止当算法允许时用另一次 CAS 退出联合配置并在同一 DB 事务内插入exclude条目到safekeeper_timeline_pending_ops。原子地加入exclude条目是必要的CAS 之后timelines表里不再有被排除的 safekeepers 列表但若 CAS 后立即中断仍需在某处持久化它们完成迁移最终成员配置此刻已提交到 DB迁移不可再中止但第 3 阶段之后失败仍可重试。需要把新成员配置发给新集合的 quorum、用新 safekeeper 列表通知 cplane、并向内存队列调度exclude请求。若算法被重试可能出现已向 DB 提交exclude但未发到内存队列的情况此时必须从safekeeper_timeline_pending_ops读取它是唯一持久化位置。sk_set_notified_generation与cplane_notified_generation在每一步之后更新两者与generation相等时迁移才算完全完成。实践上可在第 3 阶段后即上报成功把完成步骤交给 per-timeline reconciler但明智的做法是尽量同步完成让 timeline 迁移上报成功后就处于良好状态、无需旧 quorum 提交 WAL。Timeline 删除在 timeline 行上设置deleted_at并在同一事务插入safekeeper_timeline_pending_ops条目其余交给 per-sk reconciler。节点移除设为 decommissioned时须在同一事务中清空它的safekeeper_timeline_pending_ops。多 storage_controller 实例并存上述操作并发执行可能产生一些错误但不会阻止进展RFC 原文。正常不希望运行多个实例但临时并存如重新部署期间是允许的。为防某个实例创建了safekeeper_timeline_pending_ops工作后消失而无人接手per-sk reconciler 除显式唤醒外还应周期性扫描工作若所有 DB 更新都由 leadership token/term 保护则可移除该扫描只在获得 leadership 后需要扫描。任何 DB 交互都会更新内存中的控制器状态——例如迁移请求因别的迁移进行中而失败时控制器会记住并尝试完成它。测试策略从 TLA 到 e2eRFC 规划了四层测试RFC 原文TLA 模型检验用 TLA 规范描述算法并验证基本安全性。仓库中 safekeeper 目录下确实保留了一套 TLA 规范ProposerAcceptorStatic.tla、ProposerAcceptorReconfig.tla、MCProposerAcceptorStatic.tla、MCProposerAcceptorReconfig.tla配合 modelcheck.sh 使用——其中Reconfig版本正是针对成员变更场景的模型。模拟测试simulation tests把配置存储、storage_controller ↔ safekeeper 通信和pull_timeline都 mock 掉把主切换过程包装成模拟测试中的节点线程像注入 safekeeper/walproposer 重启那样注入迁移。核心断言与现有测试一致已提交的 WAL 不得丢失。全系统基本测试由于模拟测试在较高层次注入非系统调用层面会漏掉部分代码尤其是pull_timeline所以需要覆盖全系统的基础测试。扩展版test_restarts_under_load即可在后台负载下做迁移然后重启 endpoint检查没有已提交事务丢失。RFC 还建议增加经典的网络分区场景迁移ABC - ABD时一个 compute 与 AC 通信、另一个与 BD 通信。简单 e2e 测试确保包含 cplane 通知的完整流程正常工作。neon_local本地环境编排见 control_plane/src/local_env.rs应切换到使用 storage_controller扮演 control plane 角色。实施顺序与灰度发布RFC 强调如下依赖关系RFC 原文control plane 部分及其集成完全独立于其他部分测试用模拟与 neon_localcompute ↔ safekeeper 协议变更可以独立于启用 generation让 storage_controller 感知 timelines 与 safekeepers 有大量基础设施工作其实现与上线应独立于迁移本身初期 walproposer 可以在观察到联合配置时直接停止工作该窗口通常极短先在 staging 全面测试再逐步在生产启用。渐进式 rollout 构件compute 增加neon.safekeepers_proto_version标志初期 compute 与 safekeepers 都能讲两种协议版本以便推迟强制重启并方便回滚storcon 增加默认关闭的-set-safekeepers配置项只有开启时timeline 创建请求才选择 safekeepers 并在响应中返回control plane 给neon.safekeepersGUC 加 generation 前缀。generation 为 0或无前缀时 walproposer 保持现有行为——直接在给定 safekeeper 列表上提交generation 禁用非 0 时遵循本 RFC 规则提供手动迁移脚本从 control plane 数据库选取 timeline指定的或全部调用 storage_controller 的专用 import 端点——该端点与 timeline 创建非常相似插入数据库、在 safekeepers 上设置初始配置、调用 cplane 的notify-safekeepers。一个 region 的 rollout 流程现状safekeepers 由 control plane 选择手动迁移部分 timeline测试迁移移动它们启用--set-safekeepers让所有新 timeline 归 storage_controller用脚本迁移所有存量 timeline此时不应再有 compute 讲旧协议版本。在所有 timeline 归 storcon 管理之前仍需用临时脚本按需迁移为保持状态干净在此之前必须迁完所有 storcon 管理的 timeline否则需手工清空控制器数据库与 safekeepers 的配置状态。粗略的实现顺序给 safekeepers 引入配置概念含 control file实现 v3 协议实现 walproposer 变更含协议实现 storcon 部分并在 neon_local与 pytest中使用cplane 改为按 timeline 存储 safekeepers替代按 tenant实现 cplane/storcon 集成把分支创建/删除路由到 storcon随后即可测试新分支的迁移最后导入存量分支删除 cplane 的 safekeeper 选择代码在所有 compute 与 safekeepers 上逐步启用配置——在此之前所有 compute 必须只讲 v3 协议版本。边界问题与优化空间与已驱逐evictedtimeline 的集成当前pull_timeline对已驱逐 timeline 工作不正确因为拷贝会指向原始的部分文件。RFC 提出的修复是直接对文件做 S3 copy——略显笨通常是不必要的工作但在实现更智能的 timeline 归档之前这是先做出正确迁移的合理取舍对应 github issue #8542。避免 walproposer 重启上述步骤暗示 walproposer 会重启重新选举、重连 safekeepers。由于在新多数上 bump term 保证了跨代切换的 leader term 唯一理论上可以保留连接。但重连很快且避免 compute 重启远比毫秒级写停顿重要因此不做这个优化。多重联合共识算法拒绝在另一变更进行中时发起变更。虽然可以叠加多个 joint consensus作者提到 Aurora 这么做了但 RFC 认为无此必要。附录随协议一并进行的历史遗留修改RFC 在 Misc 一节顺带提出应借 compute ↔ safekeeper 协议变更之机纳入一些盼望已久的修改RFC 原文以网络字节序发送数据不直接序列化整个结构体保证跨架构arch-independent兼容从AppendRequest中移除term_start_lsn给TermHistory增加 horizon在ProposerGreeting中加入该 walproposer 到该 safekeeper 的连接序号。这些修改与成员变更共享协议升级窗口可一并实施以减少协议版本迭代次数。【免费下载链接】neonNeon: Serverless Postgres. We separated storage and compute to offer autoscaling, code-like database branching, and scale to zero.项目地址: https://gitcode.com/GitHub_Trending/ne/neon创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表