使用 Feature Flag 的常见错误,SRE 总要懂的一些最佳实践

Ian Vanagas 2025-05-16 15:21:55

在软件工程中,Feature Flag(特性开关 / 功能标志) 是一种通过配置动态控制功能发布的技术手段,无需修改代码即可实现功能的开启、关闭或分阶段发布。通常 Feature Flag 有如下好处:

  • 降低发布风险,实现渐进式发布,同时可以快速回滚(因为回滚动作可能仅仅只是关闭某个开关)
  • 加速开发与部署流程,因为有开关控制,开发团队可在主分支中并行开发多个未完成功能,降低分支管理复杂度
  • 提升测试与调试效率,比如可以很方便做 A/B 测试,精准控制测试范围
  • 增强系统可观测性与运维能力,比如打开某个开关,就可以让某个行为输出更详尽的日志,便于排查问题

但是,Feature Flag 也并非完美无瑕,使用不当可能会导致系统复杂度增加、代码可读性下降、性能问题等。本文将介绍一些常见的 Feature Flag 使用错误,小心踩坑哦。

混合业务逻辑和标志逻辑

正确地结构化与标志相关的代码至关重要。这种情况最大的错误之一就是将标志逻辑与业务逻辑混合在一起。例如,如果你想通过功能标志推出新的定价算法,错误的结构可能看起来像这样:

// Bad mixed logic ❌

function calculatePrice(product) {
  let price = product.basePrice;
  
  if (posthog.isFeatureEnabled('new_pricing_algorithm')) {
    price = price * 1.1;
  }
  
  if (posthog.isFeatureEnabled('holiday_discount')) {
    price = price * 0.8;
  }
  
  if (posthog.isFeatureEnabled('member_pricing')) {
    if (user.isMember) {
      price = price * 0.9;
    }
  }

  return price;
}

这是因为:

  • 三个布尔标志会产生八个不同的状态,需要进行测试。
  • 结合定价规则会使每个规则单独测试变得困难。
  • 添加或更改规则可能会产生意想不到的后果。

如果我们添加一个绝对折扣,金额为 5 美元呢?如果我们期望价格是某个特定的数字,或者总是大于零,这可能会破坏我们的测试。每个测试都需要处理新增的绝对折扣。

相反,最好像这样将定价逻辑与功能标志逻辑分离:

// Good separated logic ✅

function calculatePrice(product, pricingStrategy) {
  return pricingStrategy.calculatePrice(product);
}

// Flag logic separated into configuration/initialization
const pricingStrategy = posthog.isFeatureEnabled('new_pricing_algorithm') 
  ? new NewPricingStrategy()
  : new StandardPricingStrategy();

通过这样做,每种定价策略都可以有自己的测试集,对于 calculatePrice 函数来说,需要担心的状态更少了。当我们想要更改或添加一种定价策略时,不需要担心会影响到其他策略。理想情况下,尽量在尽可能少的地方使用标志,并尽可能将内容抽象到一个标志后面。这会创建标志相关代码的清晰分离和隔离,从而限制复杂性并提高可维护性。避免这个错误的好目标是仅在一处使用功能标志,并使其易于移除。同时实现这两个目标可以让你的代码结构良好。

在标志被关闭时感到惊讶

整个功能标志的目的是在需要时可以关闭它,因此它们在任何时间点都需要为此做好准备。

作为一个例子,有人在 Hacker News 上评论说,由于他们的功能标志服务未能返回标志状态,且无意中将旧的标志设置为 false,他们在两年内发生了两次重大事件。这触及了我们切身的问题。去年,我们遇到了一个特征标志服务的事故,导致特征标志回退为空响应。更糟糕的是,有些请求的超时时间异常长。这种组合导致了一些客户的应用程序崩溃。当然,在修复这个问题之后,我们学到了一些经验并进行了相应的更改,例如启用自定义超时、将默认超时从 10 秒降低到 3 秒,以及将标志请求移出我们的 Django 单体架构。

无论如何,这都是一个很好的例子,说明标志可能会有意外的状态并导致问题。你可以做两件简单的事情来避免这个错误:

  • 测试标志的每种可能状态。这包括 Feature Flag 服务本身挂了、超时、返回了 null、返回了错误类型的数据等等
  • 恢复到能正常工作的代码。默认情况下,你的应用应该能够正常工作。即 Feature Flag 服务不可用时,可以使用某个让程序正常工作的默认值。

被僵尸标志淹没

留下过时的标志可能会造成灾难性的后果,曾经在纽约证券交易所(NYSE)最大的美国股票交易商 Knight Capital Group (KCG) 的前员工可以作证。2012 年,KCG 的团队被指派构建并部署一个新的私人市场交易执行系统。该系统每秒需要处理数千笔订单。他们只有一个月的时间。

在急于按时交付的情况下,他们发布了一个更新,替换了一部分自 2003 年以来未被使用的代码。这段旧代码本不打算在生产环境中使用,但在代码库中仍然被一个特性开关保留着。

作为发布的一部分,他们重用了控制旧代码的这个旧功能标志。当他们部署更新并启用该标志时,有 7/8 台服务器按预期工作,但第 8 台服务器触发了旧代码来处理订单。影响如下:

服务器在 45 分钟内执行了超过 400 万笔交易,交易总额为 76.5 亿美元。这些交易使 KCG 损失了 4.4 亿美元,并使其股票价值蒸发了 70%。最终,KCG 得到了外部投资者的救援并被收购。

正如这个故事所示,旧且未使用的标志可能会触发意外的副作用,并被用作永久配置。简单来说,它们是技术债务,可能会破坏你的应用,在某些情况下还会造成无法言说的损害。那么,解决方案是什么呢?

说“just remove the flag”很容易,但这过于简化了问题。即使是“要有移除标志的流程”这样的建议也太过泛泛。相反,你需要一个简单的问责流程,例如:

  • 一致的过时标志标准。例如,在 PostHog 中,当一个标志被滚动推出 100%,并且在过去 30 天内未被评估(使用)时,该标志被认为是过时的。
  • Flag 必须有负责人。这会让某人负责维护并最终移除该 Flag。我们将在代码中存储 Flag 的所有者信息。
  • 自动检测和警报。当标志满足条件时,通知某人(比如所有者)可能到了移除它的時候。不断提醒他们直到他们采取行动。优步开发了一个名为 Piranha 的工具来实现这一功能。

标志名称中的危险信号

Flag 名称必须有意义。差的名称:

  • 缺乏清晰性和目的性。模糊的名称可能会使标志更容易被重复使用或误解。
  • 搜索能力差。通用或不一致的名称需要更多的开销来弄清楚,并且在代码库中更难找到。
  • 功能不明确。仅仅看一个标志的名字就应该能让你知道它是否用于实验、配置或发布。名字还应该提示它返回的是字符串、数组还是布尔值。

好的名字:

  • 描述性较强。例如, is-v2-billing-dashboard-enabledis-dashboard-enabled 更清晰。它包含有用的产品和版本上下文。
  • 使用名称“类型”。这有助于组织它们并使其目的更加明确。类型可能包括 experimentrelease 等。例如,不要用 new-billing ,而应该用 new-billing-experimentnew-billing-release
  • 名称要能反应 Flag 返回的数据类型。布尔值使用 is-premium-user ,数组使用 enabled-integrations ,单个字符串使用 selected-theme
  • 使用正向语言来描述布尔标志。例如,使用 is-premium-user 而不是 is-not-premium-user 。这有助于避免在检查标志值时出现令人困惑的双重否定(例如 if !is_not_premium_user )。

不监控它们

这里是一个典型的例子,说明当你不这样做时会发生什么:

2020 年 5 月 6 日,Facebook 的 iOS SDK 仓库收到了大量报告,称其导致应用程序崩溃。评论显示,在问题被报告之前,崩溃已经持续了数小时,但 Facebook 的监控漏掉了这个问题,修复被推迟,用户体验显著下降。

社区最终发现原因是 Facebook 将一个配置标志属性从字典类型改为布尔类型。当 SDK 获取这个值时,会导致错误并使最终用户的应用崩溃。你不能杀死一个未知有故障的功能。这就像没有烟雾探测器的喷水系统,这就是为什么特性标志+监控=成功

不幸让标志成为了瓶颈

作为标志与关键代码路径相关,它们甚至会导致轻微的性能下降,从而对最终用户造成重大问题并累积起来。

使用标志进行 A/B 测试时的一个常见问题是,页面会在加载过程中闪烁并显示“未样式化的内容”。在客户端唯一真正解决这个问题的方法是减慢页面加载速度,直到标志被评估,但这对很多人来说不是一个选项。

如果实现得当,这类瓶颈应该是永远不会发生的。防止这种情况发生是通过两种技术来实现的:

  • 缓存标志。这意味着评估标志并将它们存储在内存中以供使用。例如,我们建议在服务器端请求用户的标志,并将其初始化到客户端。
  • 本地评估。你需要做的另一部分是本地评估这些标志。这意味着使用缓存的标志值而不是进行更多的网络请求。默认情况下,我们的 JavaScript Web SDK 会为你做到这一点,我们的服务器端 SDK 也可以设置为这样做。

没有使用 Feature Flag

看了以上问题,你不会吓得直接不敢用 Feature Flag 了吧?其实 Feature Flag 是一个非常有用的工具,直接不用,就是因噎废食了。在 PostHog(和其他许多公司中),Feature Flag 提供了两种主要好处:

  • 安全。标志使渐进式发布和回滚切换开关成为可能,从而使您能够安全地部署和发布新代码。
  • 速度。通过将部署与发布分离,它们使我们能够进行主分支开发,这意味着我们可以更快地发布更多更改。

此外,它们还让我们可以运行 A/B 测试、beta 计划,并内部测试我们的更改。作为远程和异步团队,这对我们非常重要。没有它们,我们开发和发布软件的策略会受到很大限制。

结语

希望本文能够帮助你更好地理解 Feature Flag 的使用,避免常见的错误和陷阱。Feature Flag 是一个强大的工具,但需要谨慎使用。通过遵循最佳实践,你可以充分利用它们的优势,同时避免潜在的问题。原文地址如下:

https://newsletter.posthog.com/p/dont-make-these-classic-feature-flag

标签: FeatureFlag
快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云
OpenSource
开源版
Flashcat
Flashcat