Neon WAL Service 架构详解:safekeeper、WAL proposer 与 Paxos 共识机制

Neon WAL Service 架构详解:safekeeper、WAL proposer 与 Paxos 共识机制 Neon WAL Service 架构详解safekeeper、WAL proposer 与 Paxos 共识机制【免费下载链接】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/neonNeon 将存储与计算分离其中 WAL serviceWAL 服务是计算节点 → 页服务器之间承上启下的关键缓冲层它既充当主 PostgreSQL 节点的同步复制副本又作为 WAL 的中转站与容错暂存区把新生成的 WAL 可靠地分发给页服务器。本文以 docs/walservice.md 为骨架结合 safekeeper-protocol.md、walproposer.c 等源码与 TLA 规格系统讲解 WAL service 的整体数据流、safekeeper 角色、Paxos 共识与恢复算法、核心 LSN 概念并给出源码级实现佐证与常见问题解答帮助读者完整掌握 Neon 的 WAL 复制与持久化链路。一、WAL service 在 Neon 架构中的定位Neon 采用分离计算与存储separation of compute and storage详见 separation-compute-storage.md的架构计算节点Compute node即主 PostgreSQL只做查询与写入处理数据页持久化由页服务器Pageserver负责。两者之间由 WAL service 作为蓄水池与中转中心holding area and redistribution center负责暂存、去重分发最近生成的 WAL。整体数据流向如下-------------- ------------------ | | WAL | | | Compute node | ---------- | WAL Service | | | | | -------------- ------------------ | | | WAL | | V -------------- | | | Pageservers | | | --------------关键事实来自 docs/walservice.md主节点把 WAL 流式推送给 WAL service并把 WAL service 当作一个同步副本synchronous replica来对待。主节点上会保持一个复制槽replication slot防止主节点丢弃尚未流式发送到 WAL service 的 WAL。页服务器连接到 WAL safekeeper 拉取 WAL使用的是 PostgreSQL 主备之间相同的流式复制协议作为替代也可以在测试时将页服务器直接连接到主 PostgreSQL 节点。WAL service 多个 WAL safekeeper 的集合WAL service 并非单点而是由多个 WAL safekeeper 组成每个 safekeeper 都保存一份 WAL 副本------------------------------------------- | WAL Service | | | | ------------ | | | safekeeper | | | ------------ | | | | ------------ | | | safekeeper | | | ------------ | | | | ------------ | | | safekeeper | | | ------------ | | | -------------------------------------------只有当多数派majority/quorum的 safekeeper 已经把 WAL 写入本地磁盘时这条 WAL 记录才被认为是持久化durable的。管理这个 quorum 的是一套基于Paxos的共识算法。在真实生产部署中多个 safekeeper 运行在不同的节点上只有超过半数的 safekeeper 完成落盘后WAL 才算 durablePaxos 与崩溃恢复算法同时保证任意时刻只有一个主节点能够向 safekeeper quorum 活跃地推送 WAL。在仓库中safekeeper 是一个独立的 Rust crate位于 safekeeper 目录。它的模块划分见 safekeeper/src/lib.rs直接对应本文后续将要讲到的职责receive_wal接收 WAL、send_wal向页服务器/副本发送 WAL、control_file持久化 acceptor 状态、recovery恢复、wal_backupWAL 备份到远端存储等。二、核心角色WAL proposer、WAL acceptor 与 Pager文档的 Terminology 一节给出了本领域的关键术语这里结合协议文档与源码逐一展开WAL service整个服务的总称负责保证 WAL 被持久化存储。WAL safekeeper参与 quorum 的一个节点。所有 safekeeper 合起来构成 WAL service。在 Paxos 语境下safekeeper 也被称为WAL acceptor接受者。WAL proposer提出者即 PostgreSQL 计算节点。它把 WAL 广播给 safekeepers是共识算法中的 proposer 角色。PagerNeon 中从 WAL 流恢复数据页的组件即页服务器。Replica只读的计算节点read replica同样通过 safekeeper 获取 WAL 流。与标准流式复制的差异推而非拉传统 PostgreSQL 流式复制是备库主动发起连接去拉取WAL而 Neon 反其道而行之主节点计算节点主动连接到 safekeeper 去推 WAL。实现这一推模式的核心组件叫WAL proposer——它是运行在主 PostgreSQL 进程内的一个后台进程background worker连接到 WAL safekeeper 并发送全部 WAL。文档还给出了一个类比PostgreSQL 的archive_command同样是push风格但它以 WAL 段segment为粒度工作如果 PostgreSQL 提供 push 风格的流式 APIWAL proposer 就可以直接构建在它之上。在代码层面WAL proposer 的主体实现位于 pgxn/neon/walproposer.cC 语言作为neon扩展的一部分打进 PostgreSQL其文件头注释walproposer.c明确说明这是postgres 与 WAL safekeepers 之间的全序广播协议total order broadcast protocol的 proposer/leader 部分并提供了两种启动方式作为后台进程运行伪装成一个物理 WAL senderphysical WalSender收到新 WAL 可用通知后立即广播给存活的 safekeepers——这是生产环境的主路径。作为独立工具通过postgres --sync-safekeepers运行用于确定一个安全启动 PostgreSQL 的 LSN见下文第五节。除此之外libs/walproposerRust crate提供了对 C API 的高层 Rust 封装其 walproposer.rs 定义了ApiImpltrait 与WalProposerCreate、WalProposerStart、WalProposerPoll、WalProposerBroadcast等绑定用于在 Rust 侧如测试与工具驱动同一套 walproposer 逻辑。主节点上的复制槽由于主节点把 safekeeper 当作同步副本它必须确保尚未被 safekeeper 接收的 WAL 不会被本地清理这正是**复制槽replication slot**的用途。这一点在 docs/walservice.md 中被明确为设计事实复制槽的存在避免了主节点丢弃尚未流式发送到 WAL service 的 WAL。三、Paxos 共识、多数派与安全保证生产环境中多个 safekeeper 运行在不同节点因此需要共识机制回答两个问题一段 WAL 何时算 durable—— 只有当它被超过一半的 safekeeper 落盘flush 到本地磁盘之后。谁有权写—— Paxos 与崩溃恢复算法保证同一时刻只有唯一一个主节点在向 quorum 活跃地推送 WAL。文档还解释了一个容易被忽略的设计动机QA为什么多个 PostgreSQL 节点会同时存在因为在实际运维中会存在一台正在启动、另一台正在关闭的时刻例如滚动升级、故障切换。为了避免不同节点同时写入造成脑裂就必须通过共识选出唯一的 primary。共识协议的完整描述见 safekeeper-protocol.md其核心设计要求包括存在一个**无状态 master即 walproposer**与若干 safekeepersafekeeper 数量由冗余级别决定。为尽量减少对 PostgreSQL 内核的改动主节点通过标准流式复制WAL sender产生复制流。使用同步复制保证持久性主节点只有收到 WAL receiver 的确认后才会向客户端返回 commit 响应而 WAL proposer 只有在commit 记录的 LSN 被 safekeeper quorum 确认后才发送该确认。每个 safekeeper 在任意时刻只能服务一个 proposer但可以接受新的连接。任何一个 safekeeper 都可以作为 WAL 服务器向外提供复制流因此Pager 和 Replica 都可以连接 safekeeper 拉取 WALsafekeeper 会一直流式发送 WAL直到追上min(commitLSN, flushLSN)此后暂停复制等待新数据。safekeeper 之间不直接通信一个重要的架构事实QAWAL safekeepers 之间从不直接通信它们只能通过计算节点walproposer互相传递信息。这一约束让 safekeeper 的实现更简单、故障面更小同时把全部共识逻辑收敛到 proposer 一侧。四、共识协议的关键流程握手、选举、恢复与主循环safekeeper-protocol.md 把协议分为几个阶段本文按握手机制、恢复、主循环三块展开并以文件末尾的 Python 伪代码算法作为精确参照。4.1 握手Handshake收集 quorum 并避免脑裂握手的目标是收集到 quorum以便进行恢复并避免新旧 master 同时存在造成的脑裂。步骤为walproposer 向所有 safekeeper 广播自己的服务器信息WAL 段大小、system_id等。接收各 safekeeper 的应答获得它们的状态信息。一旦收到quorum 数量的握手应答向它们提议新的NodeId(max(term)1, server.uuid)。safekeeper 收到提议的nodeId后与本地保存的nodeId比较若提议值大于或等于本地值则接受并把这一选择持久化到本地控制文件control file中。若 quorum 的 safekeeper 批准了提议的nodeId则 walproposer 认为握手成功进入恢复阶段。其中NodeId是(term, UUID)二元组term单调递增UUID是 proposer 的唯一标识。C 侧实现中walproposer 的状态机在握手阶段经历WPS_COLLECTING_TERMS等状态并通过SendProposerGreeting/RecvAcceptorGreeting、SendVoteRequest/RecvVoteResponse等函数推进见 walproposer.c 中的函数声明。在 safekeeper 一侧握手/投票/推送分别由receive_wal.rs处理的消息对应例如handle_start_wal_push见 receive_wal.rs处理START_WAL_PUSH消息VOTE类消息对应投票阶段。4.2 恢复RecoveryVCL 与 epoch 的由来恢复阶段要回答从哪个 LSN 开始继续。proposer 从 quorum 中计算max(restartLSN)和max(flushLSN)RestartLSN已知被所有safekeeper 收到的 WAL 位置cut-off horizon之前的所有 WAL 段都可以删除。FlushLSNsafekeeper 已经写入本地持久化存储的位置。若max(restartLSN) ! max(flushLSN)则必须执行恢复proposer 与最领先的 safekeeperflushLSN最大者建立复制通道下载max(restartLSN)..max(flushLSN)之间的全部 WAL 消息按 LSN 有序插入内存中的 L1 消息队列再根据各 safekeeper 的flushLSN定位它们在该列表中的位置逐一补发缺失部分。为什么必须取max(flushLSN)因为本次投票的 quorum 可能与上次提交最后一条消息时的 quorum 不同我们无法确定max(flushLSN)处的记录是否已被某个 quorum 提交为了不丢失已提交数据必须把该位置视为已提交。这个计算出的max(flushLSN)称为VCLVolume Complete LSN——即可以保证其之前所有记录都可用的最大 LSN。由于 quorum 之外可能还有离线的 safekeeper 拥有更大的flushLSN一旦它上线需要把它超出 VCL 的 WAL 覆盖掉。为此引入epoch号与 Paxos 的term类似但递增算法不同VCL 与新 epoch 由 proposer 在投票阶段下发给 safekeeper。safekeeper 投票后不会立即切换到新 epoch而是等待收到 LSN max(flushLSN, VCL)的记录后再切换——这保证先恢复完旧代的所有记录再切换到新代。proposer 计算max(flushLSN)时先比较 Epoch即实际比较的是(Epoch, FlushLSN)二元组。文档用 S1/S2/S3 三个 safekeeper 的演进例子完整演示了这一过程含离线节点、崩溃、再次投票等场景核心结论是无论选哪个 quorumVCL 都由 epoch 最大的节点决定旧代的记录总会被同 LSN 的新代记录覆盖最终所有 safekeeper 收敛到一致状态。在源码侧safekeeper 把 acceptor 状态持久化在控制文件中state.rs 定义了持久化状态结构TimelinePersistentState其中包含commit_lsn被 quorum 确认且本地可用的 WAL 位置、local_start_lsn、peer_horizon_lsnwalproposer 协议中称为truncate_lsn等字段见 state.rs。术语对照与含义如下术语全称/含义维护方CommitLSN被 quorum safekeeper 确认的 WAL 位置proposer 计算下发 safekeeperRestartLSN被所有safekeeper 确认的 WAL 位置proposer 计算下发 safekeeperFlushLSNsafekeeper 已落盘到本地的 WAL 位置各 safekeeper 上报VCL能保证其之前所有记录均可用可恢复的最大 LSN恢复阶段由 proposer 计算NodeID(term, UUID)二元组标识当前选举代协商产生4.3 主循环Main loop消息队列、确认掩码与 commitLSN恢复完成后proposer 进入正常处理循环从 PostgreSQL 接收 WAL 流把 WAL 消息追加进消息列表queue。同时向 safekeepers 推送消息每个 safekeeper 对应队列中的某个元素一旦它确认收到某条消息指针前移。每个队列元素携带一个确认掩码acknowledgment mask其位bit对应各 safekeeper当所有safekeeper 都确认收到相应位全部置位后该元素出队restartLSN前移。proposer 基于 safekeeper 的应答维护restartLSN与commitLSNrestartLSN 队首消息的 LSNcommitLSN 对 safekeeper 的flushLSN排序后取第nSafekeepers - quorum个元素即quorum 侧第几位的落盘位置。commitLSN与restartLSN会随请求一起发送给 safekeepers并存入其控制文件。注意为了避免额外的 fsync 开销控制文件不会在每次请求时都 fsync而是周期性刷盘。这意味着 safekeeper 上存储的restartLSN/commitLSN可能略微滞后但这是可接受的——最多只是导致某些 WAL 记录被冗余处理flushLSN会在节点重启后通过扫描本地 WAL 文件重新计算。4.4 容错与当前限制若 WAL proposer 与 safekeeper 的连接断开它会用同一个 nodeId尝试重连不会重新选举。PostgreSQL 重启会触发新一轮投票并切换到新 epoch。当前实现的消息队列完全驻留在主内存中、不会落盘见 safekeeper-protocol.md 的 Limitations 一节。因此当存在落后lagging的 safekeeper 时可能造成内存膨胀文档也指出若某 safekeeper 丢失了本地数据需要借助外部机制如从其他节点/远端存储恢复来重建。4.5 算法的精确描述Python 伪代码协议文档末尾给出了完整的 Python 伪代码process WalProposer(...)与process safekeeper(...)精确刻画了上述各阶段。其核心函数与本文 4.1–4.3 一一对应do_recovery(epoch, restart_lsn, VCL)定位最领先节点下载restart_lsn..VCL区间消息并按各节点flushLsn补发。send_message/do_broadcast单播/广播携带restartLsn、commitLsn的 WAL 消息。get_commit_lsn()对反馈排序后取safekeepers.size() - quorum位置的值即 quorum 确认位。response_handler更新反馈、推进确认掩码、在队首消息被全部确认后推进restart_lsn并出队。safekeeper 侧的handshake()读取 proposer 的server_info比较proposal.nodeId与本地state.nodeId接受则持久化nodeId、proposed_epoch、VCL并写控制文件。safekeeper 主循环校验req.nodeId匹配写 WAL 文件、更新restartLsn、按条件切换 epoch、更新flushLsn、写控制文件、应答并通过notify_wal_sender(Min(req.commitLsn, req.endPos))通知本地 WAL sender供页服务器/副本拉流。4.6 TLA 形式化规格共识协议的正确性不是仅靠代码评审保证的仓库在 safekeeper/spec 中提供了TLA 形式化规格用于模型检查ProposerAcceptorStatic.tla与MCProposerAcceptorStatic.tla静态成员safekeepers 集合不变场景。ProposerAcceptorReconfig.tla与MCProposerAcceptorReconfig.tla成员动态重配safekeeper 增删场景。models/下有多组配置例如MCProposerAcceptorStatic_p2_a3_t3_l2.cfg2 个 proposer、3 个 acceptor、3 个 term、2 个 LSN 深度等不同规模的模型配置。modelcheck.sh是执行 TLC 模型检查的入口脚本readme.md介绍了使用方式。这与 docs/walservice.md 中spec/ contains TLA specification of it的说明直接对应是验证 Paxos 协议性质如任意时刻只有一个主节点不丢已提交数据的关键资产。五、--sync-safekeepers鸡生蛋问题的解法WAL proposer 还有一个经常被忽视的实战用途。正如 walproposer.c 注释所解释的计算节点启动 PostgreSQL 前需要从页服务器下载数据目录basebackup而 basebackup 需要一个 LSN这个 LSN 不是任意取的它必须包含所有已提交事务必须通过共识投票产生——而投票恰恰发生在 walproposer计算节点的一部分中形成鸡生蛋循环。仅保证这样一个 LSN 还不够还必须真正提交commit它并确保至少有一个 safekeeper 知道该 LSN 已提交否则 basebackup 会因等待 WAL 而挂起。而推进commit_lsn不可能跳过共识过程随便向 safekeepers 询问一个未来 epoch 的起始 LSN 就跑 basebackup的投机方案行不通。因此postgres --sync-safekeepers作为独立工具模式运行 walproposer完成一轮共识以确定安全启动 LSN。在配置上walproposer 通过neon.safekeepersGUC 获取 safekeeper 列表列表可以g#generation:前缀开头携带 generation 号见 walproposer_pg.c 的 GUC assign hook 说明该 GUC 的变更会触发 walproposer 重启。六、常见问题深度解读QA文档的 QA 部分回答了几个最容易被问到的架构问题这里结合上下文再作展开Q为什么需要独立的 WAL service而不是让页服务器直接连主 PostgreSQLA页服务器是单个服务器可能会丢失。由于 Neon 的主要容错存储是S3事务提交不能等待页服务器否则可用性与延迟不可接受。WAL service 作为临时的容错存储在数据到达页服务器、最终到达 S3 之前先为近期数据提供冗余一旦 WAL 与页面都提交到 S3WAL 的本地存储就可以被裁剪trim。这正是WAL service 是暂存区的本质它不承担长期存储职责只负责弥合提交时刻到S3 落盘之间的窗口。Q计算节点驱逐了一个页之后又需要它但该页还没到页服务器怎么办A被驱逐页的修改必然已经被 WAL 记录这正是Write Ahead Logging名字的由来索引构建等少数例外不在此列。这些 WAL 记录最终会到达页服务器。页服务器注意到计算节点请求的是非常新的 LSN 时在从 safekeeper 收到对应 WAL 之前不会响应该请求——即等 WAL 到位再提供页面从而保证读到的页面永远不落后于已提交的数据。Q页服务器最长可能等多久A通常不会太久既然某页被驱逐它多半已经很久没被使用WAL service 有充足时间把变更推给页服务器。如果仍然担心滞后可以通过max_replication_*_lag设置来限制积压backpressure。这些参数同样作用于页服务器侧的 WAL 摄入背压例如max_replication_write_lag默认 500 MB页服务器 WAL 摄入滞后时启用、max_replication_flush_lag默认 10 GBL0 flush 滞后时启用详见 pageserver-compaction.md 与 glossary.md。Qsafekeeper 之间如何通信A它们从不直接通信只能通过计算节点互相传递消息。这简化了故障模型——safekeeper 之间不存在需要协调的网络依赖。Q只有一个计算节点时为什么还需要共识算法A因为同一时刻可能短暂存在多个 PostgreSQL 节点一台正在启动、另一台正在关闭。为杜绝不同节点同时写入必须对谁是主节点达成共识。七、总结WAL service 在 Neon 持久化链路中的位置把整个链路串起来看计算节点主 PostgreSQL通过 WAL proposer 后台进程把 WAL推给多个 WAL safekeeper。Paxos 共识决定谁是唯一活跃 proposer并保证 WAL 在超过半数safekeeper 落盘后才向客户端确认 commit。页服务器作为 Pager 连接任一 safekeeper按需拉取 WAL重建数据页并最终把数据落到 S3只读副本Replica同样从 safekeeper 拉流。数据安全落 S3 后safekeeper 上的 WAL 即可裁剪WAL service 始终只扮演临时容错暂存区的角色。这套设计让 Neon 在计算节点可以随时缩容到零、数据必须跨多个故障域持久化的前提下做到了事务提交不必等待远端页服务器而 WAL 又不至于单点失效。想进一步深挖的读者可以继续阅读 safekeeper-protocol.md 的完整协议描述、safekeeper/spec 的 TLA 规格与 walproposer.c 的 C 实现页服务器侧的摄入与背压机制则见 pageserver-compaction.md。【免费下载链接】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),仅供参考