Feature Flag是一种开发技术,通过运行时条件开关来启用或禁用应用程序功能,无需部署新代码。与传统方法“commit — deploy”不同,功能标志允许将部署时刻与功能启用时刻分离。根据LaunchDarkly(2024)的数据,使用功能标志的团队将新功能上线时间缩短了40%。功能标志已成为现代移动和Web应用程序CI/CD的必备元素。
要点
Feature Flag(功能切换开关)是一种无需更改代码即可改变应用程序行为的机制。其最简单的形式是在执行新功能之前检查标志值的条件结构。标志可以存储在配置文件、数据库或外部服务中,并实时更改。这种方法使团队能够将未完成的代码提交到主分支,而无需担心在开发完成之前被用户看到。
功能标志的主要目的是分离部署与发布。部署是将代码部署到服务器或应用商店的过程。发布是功能对用户可用的时刻。没有功能标志,这些事件同时发生:代码进入生产环境 — 用户就能看到。使用功能标志,代码可以比发布提前数周部署到生产环境,用于内部测试或逐步向用户推出。这对于基于主干开发和持续交付至关重要。
让我们来看看Kotlin移动应用中功能标志的基本实现。标志存储在Firebase Remote Config中,在应用启动时加载。根据标志的值,显示旧版或新版个人资料页面。这种实现允许发布新版本的个人资料而无需在App Store中发布更新 — 只需在Firebase控制台中更改值即可。
class ProfileFeature {
private val flags = FeatureFlagProvider()
private val profileFlag = FlagKey("new_profile_enabled")
fun getProfileScreen(): Screen {
return if (flags.isEnabled(profileFlag)) {
NewProfileScreen()
} else {
LegacyProfileScreen()
}
}
}
class FeatureFlagProvider {
fun isEnabled(key: FlagKey): Boolean {
val raw = Firebase.remoteConfig.getString(key.name)
return raw.toBoolean()
}
}
并非所有功能标志都相同。Martin Fowler在他的分类中区分了四种类型的标志,它们在使用目的、生命周期和管理要求上各不相同。正确的标志分类有助于选择合适的基础设施并避免典型问题。
发布切换开关是最常见的标志类型。它们用于在生产环境中隐藏未完成的功能。开发人员将代码包裹在标志中提交到主分支,并逐渐完成功能。完成后经过测试,标志对所有用户启用。此类标志的生命周期从几天到两周不等。完全推出后,标志从代码中删除。发布切换开关是基于主干开发的基础。
实验切换开关与A/B测试配合使用。它们不仅仅是启用/禁用功能,而是将用户分配到某个实验组。这些标志通常支持复杂的定位规则(按地区、操作系统版本、订阅)并与分析系统集成。运维切换开关用于操作控制 — 例如,在高负载时禁用重量级功能,或临时关闭有问题的模块而无需立即部署。运维切换开关必须尽可能快速可靠,因为服务的稳定性依赖于它们。
| 类型 | 持续时间 | 动态性 | 目的 |
|---|---|---|---|
| Release | 天-周 | 静态 | 隐藏未完成代码 |
| Experiment | 天-月 | 动态 | A/B测试和推出 |
| Ops | 小时-天 | 动态 | 操作控制 |
| Permission | 月以上 | 静态 | 访问权限区分 |
功能标志管理是一门独立的学科,包括存储、配置、监控和审计标志。没有管理系统,标志会变成不可控的技术债务,拖慢开发速度。让我们以生产系统为例,探讨管理的关键方面。
每个功能标志经历四个阶段:创建、使用、稳定和删除。在创建阶段,定义标志键、类型和默认值。在使用过程中,团队监控谁启用了标志、针对什么受众以及出于什么目的。在稳定之后(功能完全准备就绪并经过测试),标志必须从代码中删除。删除过程通过代码审查自动化:CI检查所有对100%用户启用的标志是否有删除任务。
功能标志应集中存储,而不是分散在每个服务的配置文件中。理想情况下 — 一个带有UI的专用服务(LaunchDarkly、Unleash)。最低可接受的方案是存储库中的JSON配置,对更改进行代码审查。用于存储标志的数据库不太可取,因为它需要单独的界面进行管理。每个标志必须有所有者(团队或特定开发人员)、描述和生命周期(TTL)。定期审计陈旧标志是必须的实践,通过CI任务自动化,检查超过N天没有更改的标志。
功能标志管理工具市场包括商业平台(具有完整管理周期)和开源解决方案(用于自行部署)。工具的选择取决于团队规模、延迟和合规要求。
LaunchDarkly是市场领导者,为所有流行语言和平台(iOS、Android、Web、后端)提供SDK。支持多环境、基于规则的定位、A/B实验和自动标志删除。Split是一个替代方案,专注于企业功能:基于角色的访问、审计日志和合规(SOC2、HIPAA)。ConfigCat是一个更轻量级且易于访问的解决方案,适合小型团队。所有平台都提供带有值缓存和最小化对应用程序延迟影响的SDK。
Unleash是最流行的开源解决方案,具有UI、API和所有主要平台的SDK。支持激活策略、自定义上下文以及与Prometheus集成进行监控。Flagsmith是一个替代方案,内置A/B测试和环境管理。开源解决方案需要部署和维护基础设施,但提供对数据的完全控制且没有许可限制。对于移动应用程序,两种解决方案都提供带有离线标志值缓存的原生SDK。
功能标志是一个强大的工具,但如果没有纪律,它们会创造技术债务并使代码复杂化。Martin Fowler和LaunchDarkly的工程师们制定了一套实践,帮助从功能标志中获得最大收益而不会产生负面后果。让我们来看看生产系统的关键建议。
每个在推出完成后未被删除的功能标志都会成为技术债务。LaunchDarkly(2024)的研究表明,平均30–40%的标志在不再需要后仍留在代码中。解决方案:实施“一个标志 — 一个任务”规则。在创建标志时,在任务跟踪器中创建一个带有截止日期的删除任务。CI检查是否有100%启用超过30天的标志。代码审查不仅要检查标志的添加,还要检查标志的删除。
功能标志为测试创造了组合复杂性:每个标志使应用程序可能的状态数量加倍。为了管理这种复杂性,使用矩阵测试(检查所有标志组合)和功能标志切换集成测试。在CI流水线中添加一个步骤,用不同的标志值组合运行测试。对于关键标志(运维切换开关),必须进行负载测试,检查标志切换是否会导致延迟峰值或错误。
class FeatureFlagService:
def __init__(self, storage):
self.storage = storage
def is_enabled(self, flag_key, user_context):
flag = self.storage.get(flag_key)
if not flag:
return False
for rule in flag["rules"]:
if self._match_rule(rule, user_context):
return rule["value"]
return flag["default"]
def _match_rule(self, rule, context):
return (
rule["percentage"] > self._hash(context.user_id)
)
常见问题
这两个术语常被用作同义词,但有一个细微差别:feature flag通常指更成熟的系统,具有集中管理、UI和SDK,而feature toggle是代码中的简单二进制开关。Martin Fowler将feature toggle作为通用术语使用,但在行业中,feature flag通常与商业平台(LaunchDarkly、Split)相关联。
如果实现得当,对性能的影响微乎其微。最佳实践:将标志值缓存在内存中,TTL为30–60秒;避免在检查标志时进行同步HTTP调用;使用具有本地缓存和后台同步的SDK。根据LaunchDarkly的数据,其SDK的p99延迟小于5毫秒,对大多数应用程序来说可以忽略不计。
不建议在关键金融操作中使用功能标志来更改业务逻辑,因为在这些操作中准确了解正在执行的代码至关重要。同样,应避免对安全功能(授权、加密)使用标志 — 关闭此类标志会创建漏洞。对于基础设施更改(更换数据库、迁移到新架构),功能标志很有用,但需要特别彻底的测试。
主要方法是矩阵测试:使用所有标志组合运行测试。对于CI/CD来说,这可能过于昂贵(2^n种组合),因此实践中所有标志分别在两种状态(开/关)下进行测试,而对于组合,只测试关键组合。单元测试应模拟标志的值。集成测试使用已知的标志值测试特定场景。端到端测试覆盖最可能的组合。
删除过程:1)确认标志已对所有用户100%启用且未在实验模式下使用;2)从代码中删除所有标志的条件检查,只保留“新”分支;3)从管理系统中删除标志定义;4)更新测试,删除已删除标志的模拟。建议通过CI自动化此过程:超过N天没有更改的标志被标记为陈旧,需要确认删除。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。