使用 Feature Flag 的常见错误,SRE 总要懂的一些最佳实践
在软件工程中,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-enabled
比is-dashboard-enabled
更清晰。它包含有用的产品和版本上下文。 - 使用名称“类型”。这有助于组织它们并使其目的更加明确。类型可能包括
experiment
、release
等。例如,不要用new-billing
,而应该用new-billing-experiment
或new-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