ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Neon 用户与数据库管理设计解析:DDL 转发、Console 同步与故障自愈

Neon 用户与数据库管理设计解析:DDL 转发、Console 同步与故障自愈 Neon 用户与数据库管理设计解析DDL 转发、Console 同步与故障自愈【免费下载链接】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 仓库中的设计文档 docs/rfcs/024-user-mgmt.md 展开深度解析 Serverless Postgres计算与存储分离场景下角色Role与数据库Database双端一致的核心难题如何让用户在 Postgres 里执行CREATE ROLE/CREATE DATABASE的同时让控制面Console同步掌握全部用户与库信息。读者读完本文将理解 Neon 如何通过 Postgres 扩展钩子 控制面管理 API 的提交时原子转发机制实现这一目标掌握其消息格式、GUC 配置、compute_ctl 的 spec 对账流程以及失败自愈策略并能在当前仓库源码与测试中逐一验证这些设计。背景与动机为什么要让 Postgres 与 Console 双向同步Neon 将计算compute与存储分离控制面 Console 承担着元数据中心的职责。RFC 明确指出当前把用户roles和数据库databases同时存放在 Console 与 Postgres 两处主要有两个原因代理认证不依赖 Postgresproxy 需要能直接对着 Console 认证用户。如果认证必须唤醒 Postgres恶意的暴力破解尝试会把昂贵的 compute 唤醒甚至耗尽 Postgres 连接数形成拒绝服务攻击UI 渲染不唤醒 computeConsole 界面需要展示数据库列表时不应为一条展示请求付出冷启动代价。然而既有方案积累了两大问题不允许在 Postgres 里直接创建角色和数据库用户对此持续抱怨细粒度的角色管理在 Postgres 侧与 Console 侧都无法实现。此外RFC 特别声明本文档不讨论给数据库 root 权限的问题那被安全的运行时环境secure runtime setup所阻止。总体设计两个核心部件RFC 给出了两个核心改动方向构成了整套机制的骨架新增一个 Postgres 扩展每当一个修改用户/数据库的事务即将提交时向控制面发送一次 HTTP 请求在 Console 内部 API代码中称为 mgmt API中新增用户管理接口同时Console 需要把 JWT token 注入 compute使 compute 里的扩展能够访问该管理 API。其核心理念是Postgres 是权威数据源source of truth用户在 SQL 层做的任何变更都要在提交前同步给 Console确保事务提交成功 ⇒ Console 一定也有这份数据。Postgres 侧行为默认角色权限与密码强度默认用户角色的权限设计RFC 提出默认用户角色形如username应当具备CREATE ROLE、CREATE DB和BYPASSRLS权限。仓库中的迁移脚本印证了这一设计compute_tools/src/migrations/0001-add_bypass_rls_to_privileged_role.sql 执行ALTER ROLE {privileged_role_name} BYPASSRLS;compute_tools/src/migrations/0002-alter_roles.sql 随后修正了一个问题——之前BYPASSRLS曾被应用到所有角色上该迁移对普通角色执行ALTER ROLE ... NOBYPASSRLS收回权限仅保留给特权角色。之所以重视BYPASSRLS的精确控制是因为 Neon 的 Postgres 端口暴露在公网上权限面必须收敛。密码强度与明文密码的取舍因为 Postgres 端口直接暴露在公网必须校验密码强度。RFC 指出当前 Console 生成的都是强密码因此没有弱密码风险一旦允许用户自定义密码弱密码风险就真实存在。另一个关键矛盾是明文密码的存储Console 里存了密码那么角色创建/变更时也应把未加密密码同步过去因此 compute 与 Console 之间的通信必须加密。但 Postgres 本身也支持用哈希创建角色这种情况下扩展拿不到原始密码。RFC 给出了两个选项通过 SQL 创建的角色在 Console没有明文密码通过 SQL 创建的角色在 Console有明文密码但用哈希创建的角色除外。作者倾向第二个选项理由是更一致——如果明文密码存储被启用那么在所有能存明文密码的情况下都存。neon 扩展在提交前拦截 DDL 并转发源码级实现RFC 提出的扩展方案在仓库中有完整落地pgxn/neon/neon_ddl_handler.c。文件头注释将其职责概括为用ProcessUtility_hook捕获对 roles/databases 的修改并在事务提交时通过 HTTP 发送到 GUCneon.console_url指定的 URL可通过neon.forward_ddl临时关闭转发。ProcessUtility_hook拦截七类 DDL扩展在InitDDLHandler()中保存旧的ProcessUtility_hook并把自己的NeonProcessUtility挂上去见 neon_ddl_handler.c。NeonProcessUtility针对七类语句做专门处理CREATE DATABASET_CreatedbStmt→HandleCreateDbALTER ... OWNER TOT_AlterOwnerStmt→HandleAlterOwner仅处理OBJECT_DATABASERENAMET_RenameStmt→HandleRename分别派发到HandleDbRename/HandleRoleRenameDROP DATABASET_DropdbStmt→HandleDropDbCREATE ROLET_CreateRoleStmt→HandleCreateRoleALTER ROLET_AlterRoleStmt→HandleAlterRole只关心密码变更无password选项直接跳过DROP ROLET_DropRoleStmt→HandleDropRole关键设计点正如 RFC 所述hook 处理器本身不立即拨号 Console而是把变更暂存stash到哈希表里留待提交时统一处理。这避免了在语句执行中途发起网络请求、破坏事务语义的问题。对于HandleCreateRole扩展会从语句选项中取出password用MemoryContextStrdup拷贝到当前事务上下文数据库创建时则解析OWNER选项缺省时用GetUserId()作为 owner即需要一些编码来拿到当前用户正是 RFC 中提到的难点。HandleCreateDb/HandleAlterOwner还特别禁止把 owner 设为特权角色{privileged_role_name}。事务回调与子事务栈保证原子性扩展通过RegisterXactCallback和RegisterSubXactCallback注册事务与子事务回调在XACT_EVENT_PRE_COMMIT或XACT_EVENT_PARALLEL_PRE_COMMIT时调用SendDeltasToControlPlane()把暂存的变更一次性发送出去发送失败会elog(ERROR)从而回滚整个事务——这就是事务提交 ⇒ Console 一定收到了的保证子事务通过一叠DdlHashTable处理SUBXACT_EVENT_START_SUB时压栈PushTable提交子事务时合并到父表MergeTable同名条目以后写入为准子事务回滚时丢弃PopTable借助内存上下文自动释放。MergeTable中处理了重命名链与先删后建等复杂情况例如在一个事务里DROP ROLE bork又ALTER ROLE stork RENAME TO bork最终控制面收到的是一致的净效果。这与 RFC 中扩展需要注意同一事务内创建又删除角色的要求一一对应。HTTP 转发细节方法、鉴权与重试SendDeltasToControlPlane()使用 libcurl构造的请求特征如下HTTP 方法PATCH请求头Content-Type: application/json以及从环境变量NEON_CONTROL_PLANE_TOKEN读取的Authorization: Bearer jwt这正是 RFC 中Console 把 JWT 放入 compute的落地方式InitDDLHandler()末尾通过getenv读取缺失时仅记录日志超时3 秒失败后最多重试 5 次每次间隔 1 秒响应码非 200 时elog(ERROR)携带控制面返回的错误文本。相关 GUC 全部在InitDDLHandler()中注册GUC上下文默认值作用neon.console_urlPGC_POSTMASTER需 postmaster 启动时设置空Console 管理 API 地址未设置时跳过转发并记录日志neon.forward_ddlPGC_SUSETtrue是否向前端转发 DDL 变更false时静默跳过neon.regress_test_modePGC_SUSETfalse回归测试模式放开部分限制如允许CREATE TABLESPACEneon.event_triggersPGC_USERSETtrue控制事件触发器是否触发与 DDL 转发同源实现实际消息格式与 RFC 提案的演进差异RFC 提案中的示例消息是扁平数组 op/type字段的形态curl -X PATCH /api/v1/roles_and_databases -d [ {op:create, type:role, name: kurt, password:lYgT3BlbkFJ2vBZrqv}, {op:drop, type:role, name: trout}, {op:alter, type:role, name: kilgore, password:3BlbkFJ2vB}, {op:create, type:database, name: db2, owner: eliot}, ] 而落地实现ConstructDeltaMessage()见 neon_ddl_handler.c演化为按类型分组、以set/del表达操作语义的 JSONB 结构{ dbs: [ {op: set, name: db2, owner: eliot}, {op: set, name: nu_pogodi, owner: volk, old_name: bork}, {op: del, name: old_db} ], roles: [ {op: set, name: kurt, password: lYgT3BlbkFJ2vBZrqv, encrypted_password: SCRAM-SHA-256$4096:...}, {op: del, name: trout}, {op: set, name: kilgore, password: 3BlbkFJ2vB, old_name: prev_name} ] }值得注意的增强点encrypted_password字段发送明文password的同时扩展调用get_role_password()从pg_authid中取回 Postgres 实际存储的加密哈希一并上报供控制面在不存明文密码模式下使用old_name字段用于表达重命名语义控制面据此迁移 owner 关联而不是简单的新增删除同一 JSON 内所有变更要么全部接受、要么全部拒绝这正是 RFC 强调API 不应是 REST-like、应原子处理同一事务内多个角色/数据库变更的原因。端到端测试 test_runner/regress/test_ddl_forwarding.py 中的handle_role/handle_db完整验证了这套消息语义包括重命名时同步迁移数据库 owner 的逻辑。Console 用户管理 APImgmt APIRFC 要求管理 API 与公开 REST API 不同强调原子性当用户在同一个事务里创建了多个角色/数据库时管理 API 必须一次性接受全部或拒绝全部。同时建议不要对重复的 create/delete 操作报错见下方失败模式以保证幂等重放。测试环境中的模拟实现可以视为该 API 行为的最小参考test_runner/regress/test_ddl_forwarding.py 中ddl_forward_handler接收PATCH请求先按roles数组顺序处理、再按dbs数组顺序处理返回 200 表示接受返回 500 则模拟控制面拒绝用于验证 Postgres 侧的回滚行为。从 Console 管理用户spec 文件与 compute_ctl 的对账机制RFC 描述了既有机制Console 把包含数据库/角色列表及增量操作delta的spec 文件下发到所有 compute podcompute_ctl拾取该文件并固执地执行 delta、校验 spec 与 Postgres 实际状态一致。用户在前台 UI 创建角色时Console 生成新 spec 并重启 compute启动过程中完成建库建号。这一机制在仓库中的数据结构体现为 libs/compute_api/src/spec.rsCluster { roles: VecRole, databases: VecDatabase, postgresql_conf, settings }Role { name, encrypted_password, options }——注意 spec 中存放的是encrypted_password加密哈希而不是明文Database { name, owner, options, restrict_conn, invalid }——其中restrict_conn、invalid是从 Postgres 派生的标志位控制面从不写入DeltaOp { action, name, new_name }——用于表达无法用静态Cluster结构表示的变更如DROP DATABASE、DROP ROLE、ALTER ROLE ... RENAME TO ...。compute 侧拉取配置的入口在 compute_tools/src/spec.rsget_config_from_control_plane()以Authorization: Bearer NEON_CONTROL_PLANE_TOKEN请求{base_uri}/compute/api/v2/computes/{compute_id}/spec最多重试 3 次503/502 视为可重试500/404 视为不可恢复。切断递归SET neon.forward_ddl false这里存在明显的递归风险既然 Postgres 每建一个角色就会向 Console 发 HTTP而 Console 又通过 spec 驱动 compute 建角色那么 Console 发起的变更会被扩展再次上报回 Console形成死循环。RFC 提出基于application_name、某个 GUC 或本地用户来切断递归local no HTTP hook。仓库的落地方式在 compute_tools/src/compute.rsget_maintenance_client()建立维护连接后第一件事就是执行SET neon.forward_ddl false注释明确写道Disable DDL forwarding because control plane already knows about the roles/databases were about to modify。apply_config()随后的 spec 应用apply_spec_sql即在此连接上进行。四种管理路径的取舍RFC 系统性地列出了从 Console 建号的四种选项并给出了明确结论重启 compute 新 spec 文件本地执行 SQL扩展中切断递归推送 spec 到运行中的 compute本地执行 SQL扩展中切断递归推送 spec 到运行中的 compute本地执行 SQL让扩展把这些角色同步到 Console绕开 spec 文件直接向 compute 发 SQL让扩展把角色同步到 Console。选项 4 最直接但存在硬伤在明文密码存储被关闭的情况下Console 没有密码无法建立 SQL 连接此外 spec 对于预配置provisioning和潜在的去同步desync修复仍是必需的。因此 RFC 最终推荐保持现有角色管理方式不变在compute_ctl执行 SQL 时切断扩展递归后续为compute_ctl增加push端点避免apply_config期间重启 compute——这一步被明确列为 follow-up以控制改动范围。spec 应用的分阶段执行在 compute_tools/src/spec_apply.rs 中有完整清单从CreatePrivilegedRole、DropInvalidDatabases、RenameRoles、CreateAndAlterRoles、RenameAndDeleteDatabases、CreateAndAlterDatabases一直到DropRoles、FinalizeDropLogicalSubscriptions每一阶段都按get_operations()生成的 SQL 顺序执行管道的执行细节见apply_operations()spec_apply.rs。失败模式与自愈RFC 重点分析了如下故障场景通过 SQL 创建角色时Console 已收到创建请求但连接在 Postgres 得到确认前断开或确认后发生错误磁盘满、死锁等。结果就是Console 里有这个角色Postgres 里却没有。自愈路径有两条compute 重启自愈由于 spec 文件的存在compute 重启后会按 spec 重新对账把缺失的角色/库补上幂等重放Console 允许重复的创建/删除操作用户可以重放事务。测试代码对控制面故障导致的事务回滚与自愈给出了完整验证在 test_ddl_forwarding.py 中模拟控制面返回 500 后CREATE DATABASE failure会抛出psycopg2.InternalError事务被扩展回滚且该数据库被标记为invalidpg_database.datconnlimit -2-1 表示无限制、-2 表示无效恢复控制面后用户重复执行DROP DATABASE failure即可成功清理另一条路径由compute_ctl在全量配置阶段自动完成DropInvalidDatabases阶段会清扫所有 invalid 数据库见test_ddl_forwarding_invalid_dbtest_ddl_forwarding.py测试特意把neon.console_url指向不存在的地址来制造转发失败。此外为了让 Console UI 不唤醒 compute 也能渲染数据库列表compute 侧还暴露了只读的/dbs_and_rolesHTTP 端点路由定义见 compute_tools/src/http/server.rs测试封装见 test_runner/fixtures/endpoint/http.py与 RFC 中渲染 UI 不应唤醒 compute的诉求呼应。可扩展性与角色数量上限RFC 在文末给出了一个重要的容量评估作者在本机实测每秒可创建约 4200 个角色对应每天约 3.63 亿个角色。由于每个角色创建最终都要落到 Console 数据库因此建议为角色数量增加上限——不需要太小取一个合理偏大、不会频繁触达的值即可例如1k 或 10k。这个数字同时解释了整套设计的另一个动机正因为角色创建吞吐可以极高才必须在 Postgres 侧先聚合stash再批量原子提交而不是每个语句单独同步也正因为要落在 Console 库中才需要限额来保护控制面数据库。小结从 RFC 到落地的设计闭环回顾 docs/rfcs/024-user-mgmt.md 的原始设计在 pgxn/neon/neon_ddl_handler.c、compute_tools/src/compute.rs 与 test_runner/regress/test_ddl_forwarding.py 中可以看到几乎一一对应的落地ProcessUtility_hook拦截 事务提交回调转发XACT_EVENT_PRE_COMMIT→ 实现Postgres 提交即 Console 同步子事务栈式哈希表 → 解决 RFC 提到的同一事务内创建又删除的语义合并neon.forward_ddlGUC compute_ctl 维护连接前置SET→ 切断 Console→compute 与 compute→Console 的递归encrypted_password双字段上报 → 为明文密码可选存储两种模式都保留了路径失败回滚 invalid 标记 spec 对账 幂等重放 → 形成完整的故障自愈闭环/dbs_and_roles只读端点 → 满足 UI 免唤醒渲染。这套Postgres 为权威、提交时原子转发、spec 对账兜底的模式既是 Neon 用户与数据库管理的内核也是理解 Serverless 数据库控制面与计算节点如何保持元数据一致的一份高质量参考实现。【免费下载链接】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),仅供参考
返回列表