每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领域的领跑者。点击订阅,与未来同行! 订阅:https://rengongzhineng.io/

复刻一套类似 Amazon RDS for PostgreSQL 的托管数据库服务,并不是简单地对着实例跑一遍 pg_dump 就能结束的事情。托管环境为了稳定性和安全性强行加上了一套独特的约束,这些约束需要付出相当“硬核”的工程投入才能绕过去。

自 2025 年 9 月 SerenDB 成立以来的三个月里,团队每天工作 12 小时,目标是为 SerenAI 的 agent 后端服务打造一款高性能、开源的数据库复制工具。本文将这段经历提炼成 5 条面向工程师的技术要点,专门聚焦在 AWS RDS 托管 Postgres 上。

Lesson 1:State Dump 需要“兼容层”
标准、可靠的工具 pg_dumpall 在自建集群上的确可以生成完美快照,但这个快照对 Amazon RDS 来说是不兼容的。直接原样恢复会失败,因为 AWS RDS 禁止执行一些只有 superuser 才能运行的命令(如 ALTER ROLE ... SUPERUSER)、高权限操作(如 GRANT pg_checkpoint),以及对某些 GUC 的修改(如 ALTER ROLE ... SET log_statement)。

工程上的难题在于:如何把这份“不兼容的状态快照”转换成一个可移植的格式。为此,SerenDB 构建了一条多轮清洗(multi-pass)流水线,对 SQL dump 进行解析,并将不具可移植性的命令“注释掉”。这不是简单的文本过滤,而是一个有状态的解析器,需要理解上下文。


// From: src/migration/dump.rs // 这是众多清洗步骤中的一个专门处理 GRANT 语句的 pass。 // 它会识别并注释掉对 RDS 受限默认角色的授权, // 以及以内部 RDS 管理角色为 grantor 的授权语句。 pub fn remove_restricted_role_grants(path: &str) -> Result<()> { // 一份手工维护的角色名单,这些角色在 RDS 上禁止被 GRANT。 const RESTRICTED_ROLES: &[&str] = &[ "pg_checkpoint", "pg_read_all_data", "pg_write_all_data", // ... 以及另外 11 个受限角色 ]; // 在其他系统上无法作为 grantor 的内部角色。 const RESTRICTED_GRANTORS: &[&str] = &["rdsadmin", "rds_superuser"]; // 实现逻辑:逐行遍历 dump 文件,检查是否命中上述黑名单, // 命中则整行注释,同时保留有效语句。 // ... }

从本质上看,这一套流水线就是数据库状态的“兼容层”,让 AWS RDS 在上层逻辑里可以被当作“普通 PostgreSQL 实例”,尽管底层有各种限制。

Lesson 2:静态链接 TLS 是部署层面的“超能力”
AWS RDS 强制要求使用 SSL/TLS,这让 TLS 库的选型变得尤为关键。起初项目使用的是 native-tls,它在运行时会动态链接宿主系统里的 OpenSSL 库。结果是 CI/CD 和部署立刻变得一团糟:

  • 没有正确版本 OpenSSL 的构建机会编译失败;

  • 想打一个真正可移植、静态链接的二进制变得非常困难。

后来项目迁移到了 rustls,主要有两个原因:

  1. 构建可移植性rustls 是纯 Rust 实现,团队得以编译出单个无系统依赖的静态二进制,在不同 Linux 发行版及容器环境(如 Alpine、Debian)上行为一致。

  2. 安全性与可控性rustls 提供内存安全保证,并在证书处理上暴露出更清晰、可验证的 API,这在涉及客户数据时格外重要。


// From: src/postgres/connection.rs // `rustls` 的配置相比 native-tls 更啰嗦,但胜在明确可控、且可移植。 let mut root_store = RootCertStore::empty(); root_store.add_parsable_certificates(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| { // 显式地从 webpki-roots 构建根证书存储。 rustls::pki_types::TrustAnchor::from_subject_spki_name_constraints( ta.subject, ta.subject_public_key_info, ta.name_constraints, ) })); let mut client_config = ClientConfig::builder() .with_root_certificates(root_store) .with_no_client_auth(); // 注意这里显式使用 `dangerous()` API 在测试环境中允许自签名证书。 // 这样一来,安全上的取舍在代码层面就非常清晰。 if allow_self_signed { client_config .dangerous() .set_certificate_verifier(Arc::new(DangerAcceptInvalidCerts)); } let tls = MakeRustlsConnect::new(client_config); let (client, connection) = tokio_postgres::connect(&connection_string, tls).await;

通过这一切换,Rust 静态链接 TLS 的优势在部署环节充分体现出来:更少的外部依赖,更一致的运行行为。

Lesson 3:网络本质上是不可靠的(尤其是在 AWS 云里)
长时间运行的数据库复制任务,往往会在“客户端视角”下经历一段段闲置期。AWS 的网络基础设施——例如 Elastic Load Balancer 甚至部分 NAT 网关——都带有空闲连接超时(比如 NLB 为 350 秒)。这些组件会静默关闭空闲的 TCP 连接,导致复制进程在运行数小时后突然炸掉,报一个模糊的 “connection reset by peer” 错误。

解决方案是:从一开始就假定网络不可靠,并主动维持连接存活。SerenDB 直接在连接逻辑里实现了 TCP keepalive。这是一个“网络层”的修复,却解决了一个“数据库层”暴露出来的问题。

Keep-alives,真的是“救命”的。


// From: src/postgres/connection.rs /// 自动为 PostgreSQL 连接串添加 keepalive 参数, /// 用于避免 AWS 网络基础设施导致的空闲连接超时。 pub fn add_keepalive_params(connection_string: &str) -> String { // ... 函数会检查连接串中是否已经存在 keepalive 参数 ... let mut params = Vec::new(); // 在 TCP 层启用 keepalive。 if needs_keepalives { params.push("keepalives=1"); } // 在空闲 60 秒后发送第一枚探测包。 if needs_idle { params.push("keepalives_idle=60"); } // 其后每隔 10 秒发送一次探测。 if needs_interval { params.push("keepalives_interval=10"); } // ... 函数最后将这些参数附加到连接串上 ... }

在 AWS 上构建长连接服务时,将网络视作“会随时断”的系统,往往能避免很多看似“偶发”的生产事故。

Lesson 4:屏蔽云环境里“多余的错误噪音”
错误处理的核心在于:让上层看到的是有用的抽象

原始的 tokio-postgres 错误可能只会告诉使用者:Connection refused,但不会告诉“为什么”。在 AWS RDS 的语境中,“为什么”往往是云环境特有的:

  • Security Group 配置错误;

  • AWS IAM 策略不匹配;

  • 连错了 endpoint。

为此,SerenDB 在底层数据库驱动的错误之上,又构建了一层诊断层(diagnostic layer)。这个诊断层会解析错误信息,并给出可操作的、针对 RDS 的建议。这样一来,一个泛泛的网络错误就能变成一条“可以立刻行动”的提示。


// From: src/postgres/connection.rs // 下面是错误映射逻辑片段。 .map_err(|e| { let error_msg = e.to_string(); if error_msg.contains("no pg_hba.conf entry") { anyhow::anyhow!( "Access denied: No pg_hba.conf entry for host.\n\n On AWS RDS, this often means your security group is blocking the connection, \ or you are not connecting to the private IP from within the VPC." ) } else if error_msg.contains("Connection refused") { anyhow::anyhow!( "Connection refused: Unable to reach database server.\n\n Please check:\n\n - The RDS instance endpoint and port are correct.\n - The instance's Security Group allows inbound traffic from your IP.\n - The instance is in a public subnet or you are connecting from within the VPC." ) } else { // 对其他错误的兜底处理。 anyhow::anyhow!("Failed to connect to database: {}", error_msg) } })?;

对用户来说,这样的错误信息不再是“谜语”,而是精准指出 AWS 环境下最常见的配置坑

Lesson 5:反向理解 AWS RDS 的“托管内部细节”
随着工程推进,团队发现:单纯维护一个“黑名单命令列表”远远不够。每修掉一类错误,就会冒出另一类例外。

原因在于:RDS 有自己的一套内部元数据与对象,这些东西并不属于标准 PostgreSQL。它们必须被识别出来,并以“手术刀”般的精细度处理。

这几乎是一种持续的“逆向工程”过程。比如:

  • 团队发现 RDS 使用了内部的 tablespace,如 rds_temp_tablespace,而这些在其他 PostgreSQL 实例上是不存在的;

  • pg_dumpall 会尝试把内部的 rdsadmin 数据库也一起 dump 出来。

于是,清洗逻辑不得不继续扩展,引入各种模式匹配规则,将这些 RDS 特有的结构排除。


// From: src/migration/dump.rs /// 注释掉与 tablespace 相关的语句,包括 RDS 特有的那些。 pub fn remove_tablespace_statements(path: &str) -> Result<()> { // ... for line in content.lines() { let lower_trimmed = line.trim().to_ascii_lowercase(); // 模式匹配:识别 RDS 对内部 tablespace 的各种引用形式。 let references_rds_tablespace = lower_trimmed.contains("'rds_") || lower_trimmed.contains("\"rds_") || lower_trimmed.contains("tablespace rds_"); if is_create_tablespace || references_rds_tablespace { // 若命中,整行注释掉。 updated.push_str("-- "); updated.push_str(line); updated.push('\n'); modified = true; } else { updated.push_str(line); updated.push('\n'); } } // ... }

这是一项持续性的工作。每当 RDS 发布新版本,就有可能出现新的内部对象或者命令类型,迫使这一“兼容层”继续迭代。SerenDB 团队必须时刻做好更新的准备。

艰难得来的经验总结
与 RDS 这种托管服务打交道,本质上是在各类抽象层之间“绕迷宫”。最重要的经验是:不能把它当作一个完全黑盒的 PostgreSQL 实例。工具必须从一开始就对托管环境的特定约束与行为有所感知。

对于 SerenDB 来说,这意味着:

  • 打造一个可移植、无外部依赖的静态链接二进制,配备稳健的网络层;

  • 构建一套专门面向托管环境的数据库状态兼容层

  • 在此基础上,加上一个能提供具备上下文感知能力的诊断抽象层,帮助用户对症下药。

这就是在用 Rust 复刻 Amazon RDS Postgres 的过程中,被一次次“痛出来”的 5 条工程教训。

更多推荐