核心问题
当我们不确定时,如何让错误代价变小?
软件工程里最危险的状态不是“不知道”,而是:
不知道自己不知道。
测试能覆盖已知场景。
可观察性能帮助理解未知问题。
反馈回路能缩短从改变到证据的距离。
但真实世界仍然会带来意外:
- 用户行为和预期不同
- 数据状态比测试环境更脏
- 第三方服务不稳定
- 性能瓶颈出现在没想到的地方
- 新功能影响了旧流程
- 权限边界被真实组织结构撞穿
所以成熟工程不是假装自己能一次做对,而是:
设计系统,让错误可以小范围发生、快速发现、快速撤回。
不确定性不是失败
很多团队害怕承认不确定。
于是架构评审、产品评审、上线评审里充满确定性语言:
这个方案没问题。
这个功能用户会喜欢。
这个优化肯定会提升性能。
这次发布风险不大。
这些话也许是真的,但如果没有证据和回退路径,就只是愿望。
更成熟的说法是:
我们认为这个方案能提升支付完成率,但不确定幅度。
先对 10% 流量开放,观察支付完成率、错误率和客服投诉。
如果错误率超过阈值,立即关闭 feature flag。
这不是缺乏信心,而是工程成熟。
Feature Flag:把部署和发布分开
Feature flag 的核心价值是:
代码可以先部署,功能可以稍后打开。
没有 feature flag 时,部署等于发布:
merge -> deploy -> 所有用户立刻使用新逻辑
有 feature flag 时:
merge -> deploy -> 只对内部用户打开 -> 小流量打开 -> 全量打开
这让风险变小。
例如新的课程访问逻辑:
if (featureFlags.newCourseAccessPolicy.enabledFor(accountId)) {
return newCanAccessCourse(accountId, courseId)
}
return legacyCanAccessCourse(accountId, courseId)
这允许你在生产环境里验证新逻辑,同时保留快速回退路径。
灰度发布:让风险逐步暴露
灰度发布的思想是:
不要让所有用户同时承担未知风险。
常见灰度方式:
- 内部用户
- 单个组织
- 1% 流量
- 10% 流量
- 指定地区
- 指定课程类型
- 新用户优先
灰度不是形式,必须配监控。
每一阶段都要看:
- 错误率
- 延迟
- 核心业务指标
- 用户投诉
- 日志里的拒绝原因
- 回滚是否可用
如果没有观察指标,灰度只是慢一点的全量发布。
回滚能力是设计能力
上线前要问:
如果这个改动错了,我们怎么撤?
不同改动的回滚难度不同。
纯代码改动
通常容易回滚。
deploy previous version
配置改动
如果配置有版本和审计,也比较容易回滚。
数据迁移
最危险。
例如:
把 user.role 迁移成 organization_membership
删除旧字段
如果迁移不可逆,回滚会非常痛苦。
更稳的做法是 expand-contract:
1. 新增 organization_membership 表
2. 双写旧字段和新表
3. 后台回填历史数据
4. 读路径切到新表
5. 验证一致性
6. 停止写旧字段
7. 最后删除旧字段
每一步都应该可验证、可暂停。
Shadow Mode:先观察,不影响用户
有些高风险逻辑可以先 shadow run。
例如新的访问权判断:
const legacyDecision = legacyCanAccessCourse(accountId, courseId)
const newDecision = newCanAccessCourse(accountId, courseId)
if (legacyDecision.allowed !== newDecision.allowed) {
logger.warn("course_access_policy_mismatch", {
accountId,
courseId,
legacyReason: legacyDecision.reason,
newReason: newDecision.reason,
})
}
return legacyDecision
这里新逻辑只运行、记录差异,但不影响真实用户。
等 mismatch 降到可接受范围,再切流量。
Shadow mode 特别适合:
- 权限系统重构
- 推荐算法
- 风控规则
- 价格计算
- 搜索排序
- 缓存策略
Kill Switch:紧急停止开关
有些功能必须有 kill switch。
例如:
- 新支付渠道
- 新权限策略
- 新推荐系统
- 新消息推送
- 大规模后台任务
Kill switch 的目标是:
不需要重新部署,就能立刻停止危险行为。
例如:
if (featureFlags.disablePurchaseEmails.enabled()) {
return
}
这比事故中临时改代码、重新发布安全得多。
数据修复路径
有些错误即使回滚代码,也已经留下坏数据。
例如:
- 多发了课程访问权
- 错误撤销了访问权
- 重复扣费
- 发错通知
- 错误分配企业课程
所以高风险上线前要问:
如果产生坏数据,我们怎么发现、怎么修、怎么向用户解释?
这需要:
- 审计日志
- 幂等操作
- 可重放事实
- 修复脚本
- 客服查询入口
- 用户通知策略
回滚代码不等于修复系统。
不确定性管理清单
上线前问:
- 这个改动最可能错在哪里?
- 错了会影响哪些用户和数据?
- 能不能先对内部用户打开?
- 能不能小流量灰度?
- 有哪些指标证明它正常?
- 有哪些指标说明要停止?
- 能不能用 feature flag 关闭?
- 数据是否可回滚或可修复?
- 是否需要 shadow mode?
- 谁负责观察上线后的信号?
这些问题的目标不是阻止上线,而是让上线更可控。
小结
- 不确定性不是失败,假装确定才危险。
- 成熟工程会让错误小范围发生、快速发现、快速撤回。
- Feature flag 把部署和发布分开。
- 灰度发布必须配监控,否则只是慢一点的全量发布。
- 回滚能力是设计能力,尤其是数据迁移。
- Shadow mode 可以先观察新逻辑,不影响用户。
- Kill switch 让危险功能无需重新部署即可停止。
- 高风险功能必须考虑数据修复路径。