RFC 解读:从闭源 cloud 仓库到开源存储控制服务的架构演进)
Neon 控制面拆分console splitRFC 解读从闭源 cloud 仓库到开源存储控制服务的架构演进【免费下载链接】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 017-console-split.md 展开系统解读 Neon 如何将承载用户业务的 console控制台服务与存储相关的控制面control-plane服务进行拆分使全部存储特性得以开源并最终沉淀为当前仓库中独立运行的storage_controller与本地开发控制面control_plane。读完本文你将理解这条拆分线的判定标准、控制面 HTTP API 的设计思路、事件日志event log机制以及拆分在本地测试、UI 测试与云端一致化上带来的收益并能在 storage_controller 与 control_plane 源码中找到这些设计的落地证据。背景拆分之前的仓库格局RFC 撰写时的代码格局是三个仓库并存开源的postgres——Neon 维护的 PostgreSQL fork开源的neon——存储源码主仓库即本文所在仓库闭源的cloud——包含 console 后端、UI 前端以及大量存储相关的内部管理代码。RFC 明确表示不打算改动neon与postgres仓库而是只在cloud仓库内做拆分并把控制面源码迁移进neon仓库。这也是本 RFC 的核心结论之一拆分之后所有存储服务compute、safekeeper、pageserver、proxy都将拥有开源源码与 Docker 镜像proxy 负责监听外部连接并按需创建 compute控制面服务则通过 HTTP API 完成租户tenant的创建与管理。闭源cloud仓库里到底装了什么RFC 将cloud仓库中的内容分成两类。第一类是不属于 console 应用的杂项命令行工具cloudbench、neonadminMarkdown 文档云运维脚本helm、terraform、ansible各类配置e2e Python 测试事故处理 playbookUI 前端Make 构建脚本与代码生成脚本数据库迁移swagger 定义第二类是console 应用本身编译为./console二进制的那部分 Go 代码包括API ServerPublic API v2、Management API v2、Public API v1、Admin API v1与 Public API v1 同端口、Management API v1WorkersMonitor Compute Activity、Watch Failed Operations、Availability Checker、Business Metrics Collector内部服务Auth Middleware/UserIsAdmin/Cookies、Cable Websocket Server、Admin ServicesGlobal Settings、Operations、Pageservers、Platforms、Projects、Safekeepers、Users、Authenticate Proxy、API Keys、App Controller提供 UI HTML、Auth Controller、Branches、Projects、Psql Connect Passwordless login、Users、Cloud Metrics、User Metrics、Invites、Pageserver/Safekeeper 管理、Operationsk8s/docker/common 逻辑、Platforms/Regions、Project State、Projects Roles/SCRAM、Global Settings其他segment analytics 集成、sentry 集成、通用工具包。正是这份清单决定了拆分的颗粒度——哪些组件属于“用户”哪些属于“存储”。动机为什么必须拆分RFC 提出两个最重要的目标发布全部云/存储特性的开源实现。拆分前cloud仓库里运行 Neon compute 于 k8s 的实现是闭源的没有它就无法自动伸缩 PostgreSQL compute因此当时不存在真正开源的 serverless PostgreSQL。打造一套统一控制面同时用于云端serverless与本地测试环境缩小 cloud 与 local 两种部署形态的差异。此外拆分还带来研发体验收益storage 团队可以不必关心用户管理、计费、分析等“业务”功能console 当前强依赖 GitHub OAuth 等认证提供方和本地 nodejs 环境才能构建拆出控制面后控制面可以无需这些依赖即可本地构建运行。拆分线什么是“用户相关”什么是“存储相关”RFC 将拆分线的判定Drawing the splitting line称为“最具挑战也最重要”的部分并提出四条原则一切用户相关的留在 console一切存储相关的进入 control-plane两者之间的灰色地带大概率留在 console一些相似部分admin/management/db_migrations可以两边都有。用户相关的定义是能够关联到某个用户的请求。而控制面的设计准则是整个服务不出现任何user_id只操作tenant_idtimeline_id与既有存储服务compute、safekeeper、pageserver的工作方式保持一致。存储相关的判定标准是满足以下任一条件使用 k8s API向任一存储服务proxy、compute、safekeeper、pageserver 等发起请求跟踪 tenant/timeline 的当前状态、管理 compute 的生命周期。组件归属控制面拿什么、console 留什么按上述原则control-plane 服务应当拥有单一 HTTP API创建与管理 tenant 和 timeline管理全局设置与存储配置regions、platforms、safekeepers、pageservers提供用于存储健康检查与调试的 Admin APIWorkersMonitor Compute Activity、Watch Failed Operations、Availability Checker内部服务Admin ServicesGlobal Settings、Operations、Pageservers、Platforms、Tenants、Safekeepers、Authenticate Proxy、Branches、Psql Connect、Cloud Metrics、Pageserver/Safekeeper 管理、Operationsk8s/docker/common 逻辑、Platforms/Regions、Tenant State、Compute Roles/SCRAM、Global Settings。留在 console的组件包括API Server 五件套保持不变Public API v2、Management API v2、Public API v1、Admin API v1、Management API v1Workers 仅保留 Business Metrics Collector内部服务保留 Auth Middleware/UserIsAdmin/Cookies、Cable Websocket Server、Users admin、API Keys、App Controller、Auth Controller、Projects、User Metrics、Invites、Users、Passwordless login其他segment analytics、sentry、通用工具包。两边都可以有的杂项Markdown 文档、e2e Python 测试、Make 构建脚本与代码生成脚本、数据库迁移、swagger 定义。拆分完成后存储的唯一入口是控制面 APIconsole 侧只需要做三件事——客户端鉴权、把user_id project_id映射成tenant_id、然后调用控制面 API。这样 console 中原来的存储实现代码被 API 调用填平了“空洞”。控制面 API 设计从 project 语义到 tenant 语义拆分前 console 已有一套 projects API且已有客户端依赖RFC 主张暂时不动它。但它几乎全部与存储相关正好可以作为控制面 API 的蓝本——只需把project_id替换为tenant_idGET /tenants/{tenant_id} PATCH /tenants/{tenant_id} POST /tenants/{tenant_id}/branches GET /tenants/{tenant_id}/databases POST /tenants/{tenant_id}/databases GET /tenants/{tenant_id}/databases/{database_id} PUT /tenants/{tenant_id}/databases/{database_id} DELETE /tenants/{tenant_id}/databases/{database_id} POST /tenants/{tenant_id}/delete GET /tenants/{tenant_id}/issue_token GET /tenants/{tenant_id}/operations GET /tenants/{tenant_id}/operations/{operation_id} POST /tenants/{tenant_id}/query GET /tenants/{tenant_id}/roles POST /tenants/{tenant_id}/roles GET /tenants/{tenant_id}/roles/{role_name} DELETE /tenants/{tenant_id}/roles/{role_name} POST /tenants/{tenant_id}/roles/{role_name}/reset_password POST /tenants/{tenant_id}/start POST /tenants/{tenant_id}/stop POST /psql_session/{psql_session_id}注意这里/psql_session/{psql_session_id}不做语义替换因为它本身是会话级资源而非租户级资源。为何选 HTTP 而非 gRPCRFC 承认 gRPC 有一些有用特性但给出了三个选择 HTTP 的理由HTTP API 对客户端更易用pageserver/safekeeper/console 已经有 HTTP API技术栈一致希望控制面 API 与 cloud 中的 console API 形态相似。落地印证storage_controller 的真实路由这一设想在今天的仓库中已成为现实。storage_controller/src/http.rs 中的make_router使用routerify构建路由并挂载了领导权检查、指标采集与 JWT 鉴权中间件未启用鉴权时/status、/live、/ready、/metrics、/profile/cpu、/profile/heap等健康与调试路由保持白名单开放。其路由表几乎完全复刻了 RFC 提出的 tenant 语义租户生命周期POST /v1/tenant、DELETE /v1/tenant/:tenant_id、GET /v1/tenant/:tenant_id见 http.rs租户配置与位置PATCH /v1/tenant/config、PUT /v1/tenant/config、GET /v1/tenant/:tenant_id/config、PUT /v1/tenant/:tenant_shard_id/location_config见 http.rs时间线管理POST /v1/tenant/:tenant_id/timeline、DELETE /v1/tenant/:tenant_id/timeline/:timeline_id见 http.rs节点管理POST /control/v1/node、DELETE /control/v1/node/:node_id、GET /control/v1/node、PUT /control/v1/node/:node_id/config见 http.rspageserver 的上行回调upcallPOST /upcall/v1/re-attach、POST /upcall/v1/validate见 http.rs对应 http.rs 中“pageserver 启动时向控制面询问应挂载哪些租户”“删除前向控制面确认仍持有最新 generation”的语义——这正是 RFC 所说“跟踪 tenant/timeline 当前状态”的直接体现。服务端还实现了基于governor的按租户限流maybe_rate_limit见 http.rs说明控制面不只是“会转发请求”还承担了资源保护职责。客户端视角代码生成的理想被轻量封装取代RFC 设想为 API 生成 client 与 server 代码。实际仓库中storage_controller/client/src/control_api.rs 提供了一个轻量 HTTP 客户端封装持有base_url、可选jwt_token与reqwest::Client通过dispatch(method, path, body)泛型方法发起请求并在存在 token 时自动附加Authorization: Bearer ...头。整体保持“单一入口 JSON 交互”的简单风格与 RFC 追求“HTTP API 易用”的目标一致。从存储侧获取变更不可变事件日志RFC 进一步提出console以及任何外部系统可能需要感知存储侧的变更典型场景包括用户查询/启动过 compute之后 compute 缩容到零——用于计费达到磁盘空间上限分析类需求如“一个月内有多少用户至少有一个活跃项目”。这些场景在用户不经 console、直接通过 proxy 访问 compute 时也会发生因此需要独立于 console 的观测通道。方案是引入存储事件日志event log——它与现有 operations 表相似但事件不可变落库后不可修改。候选事件类型处理完某个 HTTP API 查询如重置密码状态发生变更如启动或停止 compute操作被创建操作首次开始操作首次失败操作完成。事件日志配套一个可订阅的 HTTP APIGET /events/cursor { events: [...], next_cursor: 123 }由于事件是不可变的可以从任意时间点**重放replay**事件日志重建存储服务的几乎任何状态。这意味着如果控制面数据库维护了某份状态而 console 数据库因业务需要也要有同样的状态console 只需轮询控制面 API 的事件流并按事件更新自身状态即可——两个服务之间不再需要直接共享数据库。分步实施路线与后续收益四步拆分路线RFC 将复杂拆分拆成四个可独立交付的步骤重构 console 代码使 console 与 control-plane 代码分目录存放、互不依赖重构 console 数据库中的表删除同时跨 console 与 control-plane 取数的查询把控制面表迁移到独立数据库在独立 TCP 端口上实现控制面 HTTP APIconsole→control-plane 的所有调用都改走该 HTTP API把控制面源码迁入 neon 仓库控制面作为独立服务启动。拆分后的本地测试收益达成第 4 步后本地测试架构可以变成控制面以本地进程运行使用本地控制面数据库compute、pageserver、safekeeper、proxy 均以本地进程方式启动而非 k8s 部署本地控制面与 k8s 部署版共享同一套 API 与几乎相同的实现因此同一批 e2e 测试可同时跑在 cloud 与 local 两种环境上。RFC 设想这可以完全取代当时用于测试的./neon_local二进制。对于 Python 的 test_runner只需把./neon_localCLI 命令替换为对控制面的 API 调用即可。虽然本地进程方式无法暴露 k8s 查询类 bug但可以很容易地在本地如 k3s拉起 k8s 跑同样的测试。console/UI 测试也可以因为控制面 API 边界清晰而直接 mock 掉存储侧验证 UI 交互后发出的请求是否正确、API 报错时是否渲染了正确信息。仓库现状印证control_plane crate 正是“本地控制面”RFC 中“本地进程版控制面”的设想落地为 control_plane crate。其 README.md 明确说明这是本地开发控制面neon_local通过cargo neon命令使用并注明它仅适用于测试本地代码改动的最小控制面不适用于生产系统——这与 RFC 中“控制面本地服务与 k8s 部署版有相同 API 和几乎相同实现”的设计目标一致同时诚实地区分了本地与生产形态。本地启动控制面时使用的 Postgres 版本STORAGE_CONTROLLER_POSTGRES_VERSION与数据库名storage_controller可在 control_plane/src/storage_controller.rs 中查到。本地环境典型用法摘自 control_plane/README.mdcargo neon init cargo neon start cargo neon tenant create --set-default --pg-version 16 cargo neon endpoint create main --pg-version 16 cargo neon endpoint start main如需模拟云端角色的测试账号cargo neon endpoint create main --pg-version 16 --update-catalog true cargo neon endpoint start main --create-test-user true控制面的状态存储与数据模型RFC 要求“控制面表迁入独立数据库”。在今天的实现中storage_controller 使用 diesel 迁移管理自己的 Postgres 数据库核心表可以直接看到这条设计原则tenant_shards表up.sql主键为(tenant_id, shard_number, shard_count)记录分片数、分片条带大小、generation、placement policy 等——没有任何user_id字段完全符合 RFC“控制面只操作 tenant_id timeline_id”的约束nodes表up.sql记录 pageserver/safekeeper 节点的调度策略、HTTP 与 Postgres 监听地址/端口对应 RFC 中“管理 regions、platforms、safekeepers、pageservers”的全局设置职责。数据库层面还特别强调 generation 的单调递增以保障数据安全见 persistence.rs 的注释reconciler 与 pageserver 重挂载都会批量递增 shard 的 generation。非目标、影响面与可扩展性Non GoalsRFC 不覆盖实际云部署脚本与 schematerraform、ansible、k8s yaml 等。影响组件主要是 console但可能波及部分存储服务。可扩展性控制面必须支持多实例同时运行同时也要支持单实例运行以方便本地测试——这一要求与上文“控制面本地进程版”设想直接对应。安全考量内部服务 vs 全量租户权限控制面是内部服务外部请求无法直接触达但它持有对任意租户做任何操作的能力因此内部恶意行为者理论上可以读写所有租户。RFC 给出两种缓解方案简单方案用单一私钥保护所有请求没有该密钥就无法发起任何请求更安全方案为每个租户发放独立 token并存放在另一个安全位置因 token 各不相同而难以一次性访问全部租户。在仓库实现中鉴权采用 JWTSwappableJwtAuthauth_middleware见 http.rs客户端侧则通过jwt_token附加Bearer头见 control_api.rs可以理解为“单一密钥方案”在生产形态下的落地。备选方案与命名讨论RFC 曾考虑过用 k8s operator 来管理存储服务与 compute但作者自认对其不熟悉未深入展开。控制面服务的候选命名包括storage-ctl、cloud、cloud-ctl最终仓库选择了storage_controller。优缺点小结RFC 给出的 Pros所有存储特性完全开源测试覆盖更好cloud 与 local 差异更小无需搭建 console 即可开发存储与云特性更易于把仅存储的服务部署到任意云。Cons分布式服务意味着连接不同服务的代码更多、潜在网络问题更多console 需要依赖存储 API分支上开发新特性时可能出现联动复杂度从不同服务console 与 control-planeJOIN 数据的代码变多。Definition of Done 与后续演进RFC 的 DoD 是k8s 中运行着一个新的控制面服务其源码位于开源的 neon 仓库。达成 DoD 后即可继续推进本地测试、UI 测试 mock 化等后续优化。从当前仓库看DoD 不仅达成还出现了超出 RFC 预期的演进control-plane 演化为负责调度与调谐的storage_controller含分片、generation 管理、节点生命周期管理本地开发则保留control_planecrate 提供neon_local体验——两者共同构成今天“云端控制面 本地控制面”的双轨结构。结语017-console-split这份 RFC 的核心价值在于它给出了一条清晰、可执行的“用户/存储”拆分方法论用user_id是否出现来划分职责边界用tenant_id timeline_id作为控制面的唯一数据语言用不可变事件日志解耦两个服务的数据一致性用“同一套 API、两种运行形态”同时满足云端与本地测试。对想理解 Neon 控制面架构的读者建议结合 RFC 原文、storage_controller 路由实现 与 control_plane 本地工具 对照阅读可以完整看到一份设计文档从纸面到源码的演变过程。【免费下载链接】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),仅供参考