Neon 控制面拆分(console split)RFC 解读:从闭源 cloud 仓库到开源存储控制服务的架构演进
2026/9/13 19:45:34 网站建设 项目流程

Neon 控制面拆分(console split)RFC 解读:从闭源 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 明确表示不打算改动neonpostgres仓库,而是只在cloud仓库内做拆分,并把控制面源码迁移进neon仓库。这也是本 RFC 的核心结论之一:

拆分之后,所有存储服务(compute、safekeeper、pageserver、proxy)都将拥有开源源码与 Docker 镜像;proxy 负责监听外部连接并按需创建 compute,控制面服务则通过 HTTP API 完成租户(tenant)的创建与管理。

闭源cloud仓库里到底装了什么

RFC 将cloud仓库中的内容分成两类。第一类是不属于 console 应用的杂项

  • 命令行工具(cloudbench、neonadmin)
  • Markdown 文档
  • 云运维脚本(helm、terraform、ansible)
  • 各类配置
  • e2e Python 测试
  • 事故处理 playbook
  • UI 前端
  • Make 构建脚本与代码生成脚本
  • 数据库迁移
  • swagger 定义

第二类是console 应用本身(编译为./console二进制的那部分 Go 代码),包括:

  • API Server:Public API v2、Management API v2、Public API v1、Admin API v1(与 Public API v1 同端口)、Management API v1;
  • Workers:Monitor Compute Activity、Watch Failed Operations、Availability Checker、Business Metrics Collector;
  • 内部服务:Auth Middleware/UserIsAdmin/Cookies、Cable Websocket Server、Admin Services(Global 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 管理、Operations(k8s/docker/common 逻辑)、Platforms/Regions、Project State、Projects Roles/SCRAM、Global Settings;
  • 其他:segment analytics 集成、sentry 集成、通用工具包。

正是这份清单决定了拆分的颗粒度——哪些组件属于“用户”,哪些属于“存储”。

动机:为什么必须拆分

RFC 提出两个最重要的目标:

  1. 发布全部云/存储特性的开源实现。拆分前,cloud仓库里运行 Neon compute 于 k8s 的实现是闭源的,没有它就无法自动伸缩 PostgreSQL compute,因此当时不存在真正开源的 serverless PostgreSQL。
  2. 打造一套统一控制面,同时用于云端(serverless)与本地(测试)环境,缩小 cloud 与 local 两种部署形态的差异。

此外拆分还带来研发体验收益:storage 团队可以不必关心用户管理、计费、分析等“业务”功能;console 当前强依赖 GitHub OAuth 等认证提供方和本地 nodejs 环境才能构建,拆出控制面后,控制面可以无需这些依赖即可本地构建运行。

拆分线:什么是“用户相关”,什么是“存储相关”

RFC 将拆分线的判定(Drawing the splitting line)称为“最具挑战也最重要”的部分,并提出四条原则:

  • 一切用户相关的留在 console;
  • 一切存储相关的进入 control-plane;
  • 两者之间的灰色地带,大概率留在 console;
  • 一些相似部分(admin/management/db_migrations)可以两边都有。

用户相关的定义是:能够关联到某个用户的请求。而控制面的设计准则是:整个服务不出现任何user_id,只操作tenant_id+timeline_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 API;
  • Workers:Monitor Compute Activity、Watch Failed Operations、Availability Checker;
  • 内部服务:Admin Services(Global Settings、Operations、Pageservers、Platforms、Tenants、Safekeepers)、Authenticate Proxy、Branches、Psql Connect、Cloud Metrics、Pageserver/Safekeeper 管理、Operations(k8s/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 v1);
  • Workers 仅保留 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 定义。

拆分完成后,存储的唯一入口是控制面 API:console 侧只需要做三件事——客户端鉴权、把user_id + project_id映射成tenant_id、然后调用控制面 API。这样 console 中原来的存储实现代码被 API 调用填平了“空洞”。

控制面 API 设计:从 project 语义到 tenant 语义

拆分前 console 已有一套 projects API,且已有客户端依赖,RFC 主张暂时不动它。但它几乎全部与存储相关,正好可以作为控制面 API 的蓝本——只需把project_id替换为tenant_id

GET /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 而非 gRPC:RFC 承认 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/tenantDELETE /v1/tenant/:tenant_idGET /v1/tenant/:tenant_id(见 http.rs);
  • 租户配置与位置:PATCH /v1/tenant/configPUT /v1/tenant/configGET /v1/tenant/:tenant_id/configPUT /v1/tenant/:tenant_shard_id/location_config(见 http.rs);
  • 时间线管理:POST /v1/tenant/:tenant_id/timelineDELETE /v1/tenant/:tenant_id/timeline/:timeline_id(见 http.rs);
  • 节点管理:POST /control/v1/nodeDELETE /control/v1/node/:node_idGET /control/v1/nodePUT /control/v1/node/:node_id/config(见 http.rs);
  • pageserver 的上行回调(upcall):POST /upcall/v1/re-attachPOST /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_tokenreqwest::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 API:

GET /events/<cursor> { "events": [...], "next_cursor": 123 }

由于事件是不可变的,可以从任意时间点**重放(replay)**事件日志,重建存储服务的几乎任何状态。这意味着:如果控制面数据库维护了某份状态,而 console 数据库因业务需要也要有同样的状态,console 只需轮询控制面 API 的事件流并按事件更新自身状态即可——两个服务之间不再需要直接共享数据库。

分步实施路线与后续收益

四步拆分路线

RFC 将复杂拆分拆成四个可独立交付的步骤:

  1. 重构 console 代码,使 console 与 control-plane 代码分目录存放、互不依赖;
  2. 重构 console 数据库中的表:删除同时跨 console 与 control-plane 取数的查询,把控制面表迁移到独立数据库;
  3. 在独立 TCP 端口上实现控制面 HTTP API,console→control-plane 的所有调用都改走该 HTTP API;
  4. 把控制面源码迁入 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.md):

cargo 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 Goals:RFC 不覆盖实际云部署脚本与 schema(terraform、ansible、k8s yaml 等)。
  • 影响组件:主要是 console,但可能波及部分存储服务。
  • 可扩展性:控制面必须支持多实例同时运行;同时也要支持单实例运行以方便本地测试——这一要求与上文“控制面本地进程版”设想直接对应。

安全考量:内部服务 vs 全量租户权限

控制面是内部服务,外部请求无法直接触达,但它持有对任意租户做任何操作的能力,因此内部恶意行为者理论上可以读写所有租户。RFC 给出两种缓解方案:

  • 简单方案:用单一私钥保护所有请求,没有该密钥就无法发起任何请求;
  • 更安全方案:为每个租户发放独立 token,并存放在另一个安全位置,因 token 各不相同而难以一次性访问全部租户。

在仓库实现中,鉴权采用 JWT(SwappableJwtAuth+auth_middleware,见 http.rs),客户端侧则通过jwt_token附加Bearer头(见 control_api.rs),可以理解为“单一密钥方案”在生产形态下的落地。

备选方案与命名讨论

RFC 曾考虑过用 k8s operator 来管理存储服务与 compute,但作者自认对其不熟悉,未深入展开。控制面服务的候选命名包括storage-ctlcloudcloud-ctl,最终仓库选择了storage_controller

优缺点小结

RFC 给出的 Pros:

  • 所有存储特性完全开源;
  • 测试覆盖更好,cloud 与 local 差异更小;
  • 无需搭建 console 即可开发存储与云特性;
  • 更易于把仅存储的服务部署到任意云。

Cons:

  • 分布式服务意味着连接不同服务的代码更多、潜在网络问题更多;
  • console 需要依赖存储 API,分支上开发新特性时可能出现联动复杂度;
  • 从不同服务(console 与 control-plane)JOIN 数据的代码变多。

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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询