技术 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只是一份文件,而非决策工具。建立以下规范:
- 提供上下文进行宣布:不要只在Slack中丢一个链接。应写:"RFC-042提议通过将会话迁移到PostgreSQL来消除我们对Redis的依赖。我需要在3月29日前获得反馈,特别是关于性能权衡(第3节)和加密问题(开放问题#2)。"
- 明确指定审查者:"后端团队,请审查迁移计划。安全团队,请评估加密问题。"没有明确指向的请求得不到回应。
- 召开决策会议:异步审查后,安排30分钟的会议。议程为:解决开放问题,处理未解决的评论,并做出通过/不通过的决定。这次会议不应重新讨论RFC内容——这正是异步审查的目的。
- 记录决策:将RFC状态更新为"已接受"或"已拒绝",并附上一段理由说明。未来的工程师会感谢你。
RFC文化中的常见错误
- 所有事情都要求 RFC。 并非每个变更都需要 RFC。对于那些难以逆转的决策才使用它们:架构变更、新依赖、数据模型变更、API 合同等。
- RFC 作为许可单。 如果工程师将 RFC 视为官僚审批流程,他们会编写最低限度的 RFC 来达到通过标准。RFC 应该是思考工具,而不是管控机制。
- 无限循环的评审。 设定截止日期。如果评审者在截止日期前没有发表评论,RFC 将继续推进。沉默即同意。
- 不归档决策。 RFC 在决策之后同样有价值,而不仅仅是在决策之前。搜索"我们为什么选择 PostgreSQL 作为会话存储?",就能找到包含完整上下文的 RFC-042。
提案获得批准的工程师并非拥有最佳想法的人,而是那些能够最清晰地表达这些想法、预见正确的反对意见,并让评审者容易说"是"的人。写作是一项技术技能。值得投资。
