核心问题

当我们不确定时,如何让错误代价变小?

软件工程里最危险的状态不是“不知道”,而是:

不知道自己不知道。

测试能覆盖已知场景。

可观察性能帮助理解未知问题。

反馈回路能缩短从改变到证据的距离。

但真实世界仍然会带来意外:

  • 用户行为和预期不同
  • 数据状态比测试环境更脏
  • 第三方服务不稳定
  • 性能瓶颈出现在没想到的地方
  • 新功能影响了旧流程
  • 权限边界被真实组织结构撞穿

所以成熟工程不是假装自己能一次做对,而是:

设计系统,让错误可以小范围发生、快速发现、快速撤回。

不确定性不是失败

很多团队害怕承认不确定。

于是架构评审、产品评审、上线评审里充满确定性语言:

这个方案没问题。
这个功能用户会喜欢。
这个优化肯定会提升性能。
这次发布风险不大。

这些话也许是真的,但如果没有证据和回退路径,就只是愿望。

更成熟的说法是:

我们认为这个方案能提升支付完成率,但不确定幅度。
先对 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
}

这比事故中临时改代码、重新发布安全得多。

数据修复路径

有些错误即使回滚代码,也已经留下坏数据。

例如:

  • 多发了课程访问权
  • 错误撤销了访问权
  • 重复扣费
  • 发错通知
  • 错误分配企业课程

所以高风险上线前要问:

如果产生坏数据,我们怎么发现、怎么修、怎么向用户解释?

这需要:

  • 审计日志
  • 幂等操作
  • 可重放事实
  • 修复脚本
  • 客服查询入口
  • 用户通知策略

回滚代码不等于修复系统。

不确定性管理清单

上线前问:

  1. 这个改动最可能错在哪里?
  2. 错了会影响哪些用户和数据?
  3. 能不能先对内部用户打开?
  4. 能不能小流量灰度?
  5. 有哪些指标证明它正常?
  6. 有哪些指标说明要停止?
  7. 能不能用 feature flag 关闭?
  8. 数据是否可回滚或可修复?
  9. 是否需要 shadow mode?
  10. 谁负责观察上线后的信号?

这些问题的目标不是阻止上线,而是让上线更可控。

小结

  1. 不确定性不是失败,假装确定才危险。
  2. 成熟工程会让错误小范围发生、快速发现、快速撤回。
  3. Feature flag 把部署和发布分开。
  4. 灰度发布必须配监控,否则只是慢一点的全量发布。
  5. 回滚能力是设计能力,尤其是数据迁移。
  6. Shadow mode 可以先观察新逻辑,不影响用户。
  7. Kill switch 让危险功能无需重新部署即可停止。
  8. 高风险功能必须考虑数据修复路径。