功能开关 Feature Flag 实践:让上线和发布解耦
wxk1991 Lv5

功能开关 Feature Flag 实践:让上线和发布解耦

很多团队把“代码上线”和“功能发布”当成一件事。

这会带来一个问题:

1
只要代码上线,功能就立刻暴露给所有用户。

如果功能有 bug,只能回滚整次发布。

如果只想给部分用户试用,也很难控制。

功能开关的价值,就是把这两件事拆开:

1
2
代码可以先上线
功能可以晚点打开

这对真实项目很有用。


一、什么是功能开关

功能开关就是一个判断:

1
这个功能现在要不要启用?

代码里可能长这样:

1
2
3
4
5
if (featureFlags.newCheckout) {
return renderNewCheckout();
}

return renderOldCheckout();

或者后端:

1
2
3
4
5
if flags.Enabled(ctx, "new-pricing") {
return newPricing(user)
}

return oldPricing(user)

这看起来很简单。

但如果设计得好,它能解决很多发布问题。


二、功能开关解决什么问题

常见用途:

  • 新功能灰度发布
  • 临时关闭故障功能
  • A/B 测试
  • 内部用户先体验
  • 按租户开放功能
  • 按地区开放功能
  • 大重构期间新旧逻辑并存

比如你重写了支付页。

没有功能开关时:

1
上线 = 所有人使用新版支付页

有功能开关时:

1
2
3
4
5
先上线代码
只给内部用户打开
再给 5% 用户打开
观察稳定后逐步放量
最后全量

风险会小很多。


三、最简单的开关可以从配置开始

小项目不需要一上来就接复杂平台。

可以先从配置文件或环境变量开始:

1
FEATURE_NEW_CHECKOUT=false

代码读取:

1
const newCheckoutEnabled = process.env.FEATURE_NEW_CHECKOUT === "true";

这种方式适合:

  • 本地开发开关
  • 环境级别开关
  • 不需要动态调整的功能

缺点也明显:

  • 修改需要重启或重新部署
  • 不能按用户灰度
  • 没有管理界面
  • 没有变更记录

所以它适合起步,不适合复杂灰度。


四、动态开关要有规则

稍微复杂一点的功能开关,需要支持规则。

比如:

1
2
3
4
5
6
内部用户打开
指定用户打开
指定租户打开
按百分比打开
指定地区打开
指定版本打开

可以设计一个规则结构:

1
2
3
4
5
6
7
8
9
{
"key": "new-checkout",
"enabled": true,
"rules": {
"user_ids": [1, 2, 3],
"tenant_ids": ["acme"],
"percentage": 10
}
}

判断时传入上下文:

1
2
3
4
5
{
"user_id": 1,
"tenant_id": "acme",
"region": "cn"
}

功能开关不是一个全局布尔值。

真正有价值的是:

1
根据用户、租户、环境和比例做判断。

五、百分比灰度要稳定

百分比灰度不要每次随机。

错误示例:

1
return Math.random() < 0.1;

这样用户刷新一次,可能进新版。

再刷新一次,又回旧版。

体验会很混乱。

更好的方式是用稳定 hash:

1
hash(user_id + feature_key) % 100 < percentage

这样同一个用户对同一个功能的结果稳定。

当比例从 10% 调到 20% 时,只会新增一部分用户,不会每次乱跳。

这点很重要。

灰度发布不是抽奖。


六、开关判断要放在边界处

不要让功能开关散落到业务深处。

比如:

1
2
3
controller 判断一次
service 判断一次
repository 又判断一次

这样很容易变成混乱的分支地狱。

更好的做法是在边界处决定走哪条路径:

1
2
3
4
5
if (flags.enabled("new-checkout", user)) {
return newCheckoutService.createOrder(input);
}

return oldCheckoutService.createOrder(input);

也就是说:

1
2
入口处选择新旧逻辑
内部逻辑尽量保持单一

这会让后续清理开关容易很多。


七、每个开关都要有负责人和过期时间

功能开关最大的问题是:

1
开了以后没人删。

几年后代码里全是:

1
2
3
if old_flag
if temp_flag
if new_flag_v2

没人敢动。

所以创建开关时,最好记录:

  • 开关名称
  • 负责人
  • 创建时间
  • 预期删除时间
  • 类型
  • 默认值
  • 影响范围

例如:

1
2
3
4
5
new-checkout
owner: payment-team
expires_at: 2026-07-15
type: release
default: false

功能开关不是永久配置。

它应该有生命周期。


八、区分几类开关

不要把所有开关都当成一种东西。

常见类型:

1
2
3
4
release flag:发布开关
experiment flag:实验开关
ops flag:运维开关
permission flag:权限开关

发布开关用于灰度新功能,稳定后应该删除。

实验开关用于 A/B 测试,实验结束后应该固化结果。

运维开关用于临时降级,比如关闭推荐系统。

权限开关用于不同用户或套餐能力,生命周期可能很长。

不同开关的管理方式不一样。

不要把临时发布开关写成永久权限逻辑。


九、开关也要可观察

如果一个功能通过开关灰度,你至少要知道:

  • 当前是否开启
  • 对哪些用户开启
  • 命中了多少请求
  • 新旧路径错误率差异
  • 新旧路径耗时差异
  • 谁在什么时候改了规则

否则灰度就只是心理安慰。

日志里可以加:

1
feature.new_checkout=true

或者在指标里按开关维度区分。

这样出问题时,才能快速判断是不是新功能导致的。


十、前端开关不要泄露敏感能力

前端功能开关只能控制展示。

不能控制权限。

比如按钮隐藏:

1
用户看不到删除按钮

这不等于用户不能调用删除接口。

真正的权限必须在后端验证。

前端开关适合:

  • 显示新版页面
  • 切换 UI
  • 控制实验体验

后端仍然要做:

  • 权限判断
  • 套餐判断
  • 数据范围判断

不要把前端 feature flag 当成安全边界。


十一、我的建议

功能开关可以从简单做起:

1
2
3
4
第一阶段:环境变量开关
第二阶段:配置表动态开关
第三阶段:支持用户/租户/百分比规则
第四阶段:管理后台 + 审计日志 + 指标

不要为了一个小项目一上来搭复杂平台。

但也不要把所有功能都和部署绑死。

真正有价值的功能开关,应该做到:

  • 代码上线和功能发布解耦
  • 支持小范围灰度
  • 出问题能快速关闭
  • 有负责人和过期时间
  • 能观察新旧逻辑差异
  • 稳定后及时清理

功能开关不是让代码里多几个 if

它是让发布变得更可控。