Language:Chinese VersionEnglish Version

技术 RFC(请求评论)是软件工程中最被低估的工具。一份写得好的 RFC 能够通过及早暴露分歧、为未来的工程师记录决策以及在编写任何代码之前建立共识,从而避免数月的工作浪费。一份写得差的 RFC 则会在评审会议上浪费每个人的时间,最终被遗忘在共享驱动器中。

我在四家公司中撰写了超过 40 份 RFC,审阅了数百份,并观察到了哪些 RFC 成功、哪些陷入停滞的模式。差异很少在于提案的技术价值,而在于写作的清晰度、论证的结构以及作者是否预见到了正确的反对意见。

为什么大多数 RFC 失败

最常见的失败模式:

  • 先有解决方案的思维: RFC 详细描述了一个解决方案,但几乎没有解释问题所在。评审者无法在不理解为何需要解决方案的情况下对其进行评估。
  • 缺少替代方案: 提出一种方法而不解释为何拒绝其他方法,会让评审者产生怀疑。他们会想你是否考虑了其他可能性。
  • 范围蔓延: RFC 试图一次性解决三个问题。评审者会感到不知所措,从而停止参与。
  • 没有成功标准: “提高性能”是不可衡量的。”将 p99 延迟从 800ms 降低到 200ms”才是可衡量的。
  • 为作者而非读者写作: 密集的技术细节没有为不熟悉系统的读者提供上下文。

有效的 RFC 模板

经过多年的迭代,这是我使用的模板。每个部分都有其存在的意义。

标题

# RFC-042: 将会话存储从 Redis 迁移到 PostgreSQL

**作者:** Jane Smith
**状态:** 审阅中
**创建日期:** 2026-03-15
**评审者:** @backend-team, @security-team
**决策截止日期:** 2026-03-29

决策截止日期至关重要。没有它,RFC 将永远停留在”审阅中”状态。两周通常合适——足够进行彻底评审,又能创造紧迫感。

1. 摘要(3-4句话)

最后写这部分。它应该是一段独立的文字,让副总裁阅读后能够理解提案的要点:

## 摘要

本 RFC 提议将会话存储从独立的 Redis 实例迁移到我们已用于应用数据的 PostgreSQL。
这消除了一个独立的基础设施依赖,每月可节省约 120 美元,并简化了我们的备份和恢复流程。
迁移可以通过双写策略在两周内完成,且实现零停机。

2. 问题陈述

描述问题不要暗示解决方案。这部分决定了审阅者是否认为该RFC值得一读。

## 问题陈述

我们的会话管理目前依赖于在 AWS ElastiCache 中运行的专用 Redis 7.2 实例 (r6g.large)。这带来了三个运营问题:

1. **基础设施复杂性:** Redis 是我们堆栈中唯一作为托管服务运行在主 PostgreSQL 数据库之外的组件。它有独立的监控、独立的备份流程和独立的访问控制。我们的 6 人工程师团队需要维护 2 个数据库系统的基础设施,而本可以只维护 1 个。

2. **成本效率低下:** ElastiCache 实例每月成本为 $156。我们当前的会话量(12,000 个活跃会话,平均大小 340 字节)将消耗约 4MB 的 PostgreSQL 存储空间——远低于我们现有的 RDS 容量。

3. **灾难恢复缺口:** 我们的 PostgreSQL 备份每 6 小时运行一次,具有时间点恢复功能。Redis 快照每天运行一次。两次快照之间的故障会丢失最多 24 小时的会话数据,迫使用户重新认证。

注意具体数字:12,000 个会话,每个 340 字节,每月 $156,24 小时恢复缺口。具体数据让问题变得真实。”Redis 增加了复杂性”是观点。”我们 6 人团队维护 2 个数据库系统”是事实。

3. 提出的解决方案

现在描述你想要构建的内容。要足够具体,以便工程师能够实现它,但不要编写代码。专注于设计决策及其理由。

## 提议的解决方案

### 数据库架构

在 PostgreSQL 中创建一个 `sessions` 表:

```sql
CREATE TABLE sessions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id),
  data JSONB NOT NULL DEFAULT '{}',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  expires_at TIMESTAMPTZ NOT NULL,
  last_accessed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
```

**设计决策:**
- JSONB 用于会话数据:允许灵活的会话属性,无需为每个新字段进行架构迁移
- 单独的 expires_at 列:能够高效清理查询,无需解析 JSONB
- UUID 主键:与我们现有的 ID 策略保持一致

### 会话清理

计划任务每 15 分钟运行一次:

```sql
DELETE FROM sessions WHERE expires_at < now();
```

在我们当前的容量(12K 会话)下,此查询在 5 毫秒内完成。在 10 倍容量下,expires_at 上的索引可保持查询时间在 50 毫秒以内。

### 性能考虑

当前 Redis 会话查找:~1ms
预期 PostgreSQL 会话查找:~3-5ms(索引 UUID 查找)

这 2-4ms 的增加是可以接受的,因为:
- 会话查找每个请求发生一次(在中间件中)
- 我们的 p50 响应时间是 45ms;增加 4ms 小于 10%
- 连接池(PgBouncer)消除了连接开销

4. 考虑的替代方案

这是大多数 RFC 跳过但大多数审查人员最关心的部分。表明你评估了其他选项并解释了为什么拒绝它们:

5. 迁移计划

审查者想知道的是风险,而不仅仅是目的地。分阶段的迁移计划表明你已经考虑了故障模式:

## 迁移计划

### 第1阶段:双写(第1周)
- 部署会话表和新会话管理器
- 同时写入 Redis 和 PostgreSQL
- 从 Redis 读取(数据源)
- 监控 PostgreSQL 写入延迟和错误率

### 第2阶段:双读验证(第1-2周)
- 从两个存储读取数据并比较结果
- 记录差异而不影响用户
- 修复发现的任何边缘情况

### 第3阶段:切换读取源(第2周)
- 从 PostgreSQL 读取(新数据源)
- 继续写入 Redis 作为备用
- 监控48小时

### 第4阶段:停用 Redis(第3周)
- 移除 Redis 写入
- 归档 Redis 数据
- 删除 ElastiCache 实例
- 更新监控仪表板

### 回滚计划
在任何阶段,都可以通过将读取源切换回 Redis 来回滚。在第1-3阶段,两个存储包含相同的数据。回滚时间 < 5分钟(功能标志切换)。

6. 成功标准

## 成功标准

当满足以下条件时,此 RFC 即为成功:
- [ ] 所有会话仅存储在 PostgreSQL 中
- [ ] 会话查询 p99 延迟低于 10ms
- [ ] ElastiCache 实例已停用
- [ ] 月度基础设施成本减少 >= $100
- [ ] 迁移期间无面向用户的会话中断

7. 开放性问题

明确列出你不知道的内容。这在智力上是诚实的,并将审查者的注意力集中在真正的不确定性上:

## 开放性问题

1. 我们是否应该按月份对会话表进行分区,以便在
   大规模情况下更容易清理?当前数据量不需要这样做,
   但现在添加分区比以后添加更容易。

2. 我们是否需要加密 PostgreSQL 中静态存储的会话数据?
   Redis 数据在静态状态下未加密。如果我们添加加密,
   应该使用 PostgreSQL 的 pgcrypto 还是应用级
   加密?

3. 清理作业应该是 cron 任务还是 PostgreSQL
   定时函数(pg_cron)?

重要的写作技巧

先说明原因

每个设计决策都应包含其基本原理。不是"我们将使用 JSONB",而是"我们将使用 JSONB,因为会话数据因功能而异,我们希望避免为每个新会话属性进行模式迁移。"

为浏览者而写

大多数审查者先浏览,如果感兴趣再深入阅读。为这种行为设计结构:

  • 摘要可在30秒内读完
  • 每个部分突出关键点
  • 使用表格进行比较(而非描述性文字)
  • 使用代码块展示技术细节(而非内联描述)

量化一切

用测量数据取代模糊的陈述:

模糊 具体
"提高性能" "将p99从800ms降低到200ms"
"降低成本" "每月节省$120(每年$1,440)"
"简化基础设施" "消除2个数据库系统中的1个"
"最小化风险" "通过功能标志回滚耗时< 5分钟"

应对质疑者

提交前,以不同意你的RFC的人的角度阅读它。他们会质疑什么?预先写下回答。最有效的方法是"钢铁人"——陈述最强烈的反对意见并直接回应:

### 预期反对意见:"PostgreSQL比Redis慢"

确实。Redis提供亚毫秒级的查找。PostgreSQL会话查找需要3-5ms。然而,在当前上下文中,这种差异无关紧要:我们的平均响应时间是45ms,且会话查找每个请求只发生一次。3ms的增加在我们整体延迟预算中只是噪音。

如果会话查找延迟在高负载下成为问题,我们可以添加连接池或应用级缓存,而无需重新引入Redis作为基础设施。

审查流程

没有明确定义审查流程的RFC只是一份文件,而非决策工具。建立以下规范:

  1. 提供上下文进行宣布:不要只在Slack中丢一个链接。应写:"RFC-042提议通过将会话迁移到PostgreSQL来消除我们对Redis的依赖。我需要在3月29日前获得反馈,特别是关于性能权衡(第3节)和加密问题(开放问题#2)。"
  2. 明确指定审查者:"后端团队,请审查迁移计划。安全团队,请评估加密问题。"没有明确指向的请求得不到回应。
  3. 召开决策会议:异步审查后,安排30分钟的会议。议程为:解决开放问题,处理未解决的评论,并做出通过/不通过的决定。这次会议不应重新讨论RFC内容——这正是异步审查的目的。
  4. 记录决策:将RFC状态更新为"已接受"或"已拒绝",并附上一段理由说明。未来的工程师会感谢你。

RFC文化中的常见错误

  • 所有事情都要求 RFC。 并非每个变更都需要 RFC。对于那些难以逆转的决策才使用它们:架构变更、新依赖、数据模型变更、API 合同等。
  • RFC 作为许可单。 如果工程师将 RFC 视为官僚审批流程,他们会编写最低限度的 RFC 来达到通过标准。RFC 应该是思考工具,而不是管控机制。
  • 无限循环的评审。 设定截止日期。如果评审者在截止日期前没有发表评论,RFC 将继续推进。沉默即同意。
  • 不归档决策。 RFC 在决策之后同样有价值,而不仅仅是在决策之前。搜索"我们为什么选择 PostgreSQL 作为会话存储?",就能找到包含完整上下文的 RFC-042。

提案获得批准的工程师并非拥有最佳想法的人,而是那些能够最清晰地表达这些想法、预见正确的反对意见,并让评审者容易说"是"的人。写作是一项技术技能。值得投资。

By

Leave a Reply

Your email address will not be published. Required fields are marked *

You missed