LaunchDarkly:从特性开关接入到渐进交付治理
一次普通发布把“代码是否部署”和“功能是否开放”绑在了一起:新结算链路已经进入所有实例,只要配置写错就会同时影响全部用户;回滚镜像要十几分钟,而产品只想先让内部员工和 1% 的租户试用。LaunchDarkly 把这两个动作拆开。代码可以先部署,运行时再依据上下文、规则和分桶决定返回哪个变体;发生异常时,团队改变开关配置即可停止暴露,而不必等待重新构建和部署。
这个能力不是远程读取一个布尔值那么简单。真正的运行链包含控制台中的规则配置、Flag Delivery Network、进程或设备里的 SDK、最后已知状态、调用点 fallback、分析事件以及权限审计。只理解控制台上的开关按钮,会在冷启动、断网、隐私、实验归因和凭证泄露时得到完全不同于预期的结果。
先建立正确的对象关系
LaunchDarkly 的托管入口是 Web 控制台。组织承载成员、团队、角色、账单与组织级安全设置;project 通常对应一个产品或一组共享同一套 flag 定义的服务;environment 位于 project 内,常见为 Test、Staging、Production。一个 flag key 在同一 project 的多个 environment 中保持同一个逻辑身份,但每个 environment 拥有独立的启停状态、目标、规则和放量配置。
因此,checkout-v2 在 Test 环境对所有人开启,并不意味着 Production 也开启。环境也不是部署集群的自动发现结果,它是团队建立的治理边界。若把欧洲、美国、预发和生产全部塞进一个 environment,再试图靠 context 属性区分,凭证、权限、审计和误操作爆炸半径都会混在一起。反过来,给每个临时 Pod 建 environment,又会制造大量复制、清理和成本负担。通常让“规则需要独立变更、凭证需要独立轮换、权限需要独立控制”的生命周期形成 environment,让短命实例共享该环境。
每个 environment 都有自己的 SDK 凭证,凭证把应用精确连接到某个 project/environment:
| 凭证 | 典型使用者 | 是否必须保密 | 关键约束 |
|---|---|---|---|
| SDK key | server-side SDK、AI SDK、Relay Proxy | 是 | 可读取该环境的完整规则集,必须放入服务端 Secret,泄露后轮换 |
| mobile key | Android、iOS、Flutter 等移动 SDK | 否 | 只能用于移动客户端,不等于服务端 SDK key,可创建和轮换 |
| client-side ID | 浏览器 JavaScript、React/Vue 与部分 edge SDK | 否 | 会出现在公开客户端中,不提供服务端完整规则读取能力,不能轮换 |
| API access token | 自动化管理 project、environment、flag 等资源 | 是 | 属于管理 API 凭证,不能拿来初始化评估 SDK |
不要把“无需保密”误解为“可以跨环境乱用”。公开凭证仍决定客户端连接哪个环境;把生产 client-side ID 写进开发构建,会让开发页面读取生产可见 flag 并发送生产事件。服务端 SDK key 的风险更高,它不应进入浏览器 bundle、移动安装包、日志、镜像层或普通配置仓库。轮换时先创建新 key,双 key 并行迁移所有实例,确认连接与评估健康后再让旧 key 过期;先删旧 key 会使尚未更新的实例认证失败并返回代码中的 fallback。
LaunchDarkly 控制面和 Flag Delivery Network 是商业托管产品,能力随订阅变化;当前计划为 Developer、Foundation、Enterprise 与 Guardian。基础评估、Experimentation key 数量、服务连接、MAU、Data Export、审批、release management、guarded rollout 和 Relay Proxy Enterprise 不能仅凭控制台截图推断,采购与容量评审应按当前组织 entitlement 验证。SDK 与开源 Relay Proxy 的代码采用 Apache-2.0,但这不等于获得托管平台、Enterprise Relay 或全部产品能力。架构图中必须把“开源客户端组件”和“商业控制面服务”分开记账。
SDK 类型决定评估发生在哪里
所有 SDK 都能评估 flag,但数据暴露、连接模型和生命周期不同。
Server-side SDK 在可信服务进程中运行。它使用 SDK key 下载环境中的完整 flag 与 segment 规则,在本地内存中对每次请求求值。正常路径不是每次业务请求访问 LaunchDarkly,所以规则评估延迟通常只是本地计算;SDK 通过流式连接接收更新,也可在特定网络条件下使用轮询。服务端进程应复用一个长生命周期客户端,绝不能按 HTTP 请求创建一个客户端,否则会制造连接风暴、重复缓存和事件线程。
Client-side Web SDK 在浏览器中运行,使用 client-side ID。只有显式允许客户端可用的 flag 才能进入这条边界,用户可以观察网络和运行时值,所以它适合控制界面呈现,不适合隐藏安全决策、授权结果、价格底线或秘密算法。浏览器通常只持有当前 context 可见的变体值,而不是服务端完整规则集;context 改变时通过 identify 获取新状态。
Mobile SDK 使用 mobile key,要面对应用后台、飞行模式、进程被杀和长版本尾巴。它通常使用平台存储保存最近值,并在网络恢复后更新。移动端发版后多年仍可能存在旧 SDK,因此新增 flag 必须给旧版本准备兼容默认行为,删除 flag 前也要确认活跃版本不再评估它。自动采集应用版本、系统版本、设备型号等环境属性应按隐私策略显式决定是否开启。
Edge SDK 在 Cloudflare、Fastly、Vercel、Akamai 等边缘运行时求值,适合在靠近请求入口的位置决定路由、页面变体或缓存键。边缘平台的存储、执行时间、事件发送能力并不一致;部分 edge SDK 能发送事件,Akamai SDK 不能直接发送。若先在 edge 得到 flag,再 bootstrap 给浏览器 SDK,必须保证两端使用同一 context key,否则首屏与 hydration 后可能跳变,实验暴露也可能重复或丢失。
一个实用的判断是:安全或业务不变量必须在服务端评估;纯展示可以在客户端评估;需要在 CDN 入口改变路由或缓存时才把评估前移到 edge。不要为“少一次后端调用”把权限判断放入浏览器,也不要为了统一而让浏览器每次渲染都调用自建后端取单个 flag。
Context 是定向模型,不是用户数据库
SDK 求值时接收 context。最简单的 context 有 kind: "user" 和稳定 key;多上下文可以同时描述 user、organization、device 等实体:
const context = {
kind: 'multi',
user: {
key: 'user-1842',
name: '示例用户',
plan: 'pro',
country: 'CN'
},
organization: {
key: 'org-acme',
tier: 'enterprise',
region: 'ap-east'
},
device: {
key: 'device-7f31',
appVersion: '6.4.0'
}
};key 是稳定分桶和实体关联的核心。把随机请求 ID 当 user key,会让同一个人每次请求进入不同分桶;把邮箱当 key,会扩大个人信息面并在邮箱变化时重分桶;所有匿名访客共用 anonymous,又会让整个群体得到同一变体。匿名设备应生成并持久化随机 key,登录后是否继续沿用设备 key或切换用户 key,要作为产品实验语义明确决定。
LaunchDarkly 控制台里的 Contexts 列表不是 SDK 自动补全的主数据源。每次求值只使用当前调用传入的属性,不会从控制台把上次见过的 plan、region 自动合并回来,不同 SDK 实例之间也不会同步 context 属性。规则依赖 organization.tier 时,每个相关评估路径都必须提供它;属性缺失不是“沿用旧值”,而是条件无法按预期匹配。
属性类型也属于契约。字符串 "true" 与布尔值 true 不相等,数字 3 与字符串 "3" 不应混用,语义版本比较需要规范版本字符串。把 context 构造集中到一个模块,在入口处完成类型校验和缺失策略,比让每个调用点临时拼对象更可靠。
一次评估如何得到最终变体
开启状态下,flag 的求值不是“最后一条规则覆盖前面规则”,而是按优先级命中即停止。可以把顺序理解为:先检查 flag 是否关闭;再检查 prerequisite flag;再看 individual targets;然后按界面从上到下检查 targeting rules;都不命中时走默认规则。规则内部的 percentage rollout 使用稳定哈希把 context 分到桶中,并非每次随机抽签。
这带来四个常见误判。第一,排在上面的宽规则会遮住下面的精确规则。第二,individual target 的优先级高于普通规则,某个测试账号可能永远看不到后续放量。第三,prerequisite 不是简单复用另一个 flag 的布尔值;前置 flag 必须对当前 context 返回指定变体,否则主 flag 直接走其 prerequisite 失败路径。第四,关闭 flag 返回的是配置里的 off variation,而 SDK 因无法评估返回的是调用点提供的 fallback,两者不是同一层。
需要解释结果时,使用 SDK 的 variation detail API,而不是只记录布尔值。detail 通常能给出 variation index 和 reason,例如 RULE_MATCH、TARGET_MATCH、FALLTHROUGH、OFF 或错误原因。日志应记录 flag key、调用方、reason、是否使用 fallback 和匿名化后的 context key 哈希,不要完整打印 context 与凭证。
对放量而言,稳定性来自“相同 flag、相同 salt、相同 bucket 属性得到相同分桶”。把 rollout 从 5% 提到 10%,原 5% 通常应保持;但改变 context kind、bucketBy 属性、key 或重建语义不同的 flag 都可能重新分桶。跨服务要得到一致结果,就要统一 flag key、环境、context kind、key 归一化和所需属性,而不是只保证百分比数字相同。
初始化、stream、cache 与三层降级
服务端 SDK 初始化后取得环境规则集,评估读取本地 feature store。经典数据源通常以 stream 接收当前状态和变化;短暂断流不会让每次调用立刻 fallback,因为内存中仍有最后已知状态。Node.js 服务端 SDK 9.10+ 的 dataSystem 属于 Data Saving Mode 的 EAP 能力:standard 数据源可用 polling 初始化、stream 增量同步并在两者间自动切换。不能把这套 EAP 行为写成所有账号、语言和旧 SDK 的默认保证,必须按账号资格与锁定版本验证。真正需要区分的是三种启动状态:
已初始化并持有当前规则:正常本地评估。尚未连上服务,但持久化 store 有旧规则:可用 last-known state 评估,同时状态是陈旧的。既无当前规则也无 last-known state:返回 variation(flagKey, context, fallback) 传入的 fallback。
应用启动不能无限等待,也不能完全忽略初始化结果。常见做法是给初始化一个明确预算,例如 3 至 10 秒:在预算内成功则进入正常状态;超时后,如果关键 flag 的 fallback 已经设计为安全路径,可以带告警启动;如果 flag 决定数据库写格式、支付路由或不可逆迁移,则应把该依赖设为启动阻断条件,或干脆不要用运行时 flag 承担这种不变量。
每个调用点都必须提供类型正确、业务安全的 fallback:
const enabled = await client.variation('checkout-v2', context, false);
const timeoutMs = await client.variation('payment-timeout-ms', context, 1500);
const algorithm = await client.variation('ranking-algorithm', context, 'stable-v1');布尔 flag 的 fallback 不应写成字符串,JSON flag 的 fallback 要满足消费者 schema。安全也不是永远等于 false:关闭新认证链可能退回已废弃且不安全的旧认证,关闭双写可能破坏正在进行的数据迁移。fallback 要从业务后果推导,并通过故障实验验证。
客户端和移动端还有 bootstrap 与本地缓存。服务端渲染页面可把同一 context 的初始 flag 值注入浏览器,避免首屏先按默认值渲染、连接后再跳变;移动 SDK 可在离线时使用平台持久化的最近值。bootstrap 是启动快照,不是永久真相,连接建立后仍要接受更新。若服务端和浏览器 context 不同,bootstrap 只会把不一致提前隐藏到后续更新时。
轮询不是默认的“更稳”方案。流连接断开后 SDK 会重连,已有缓存仍可评估;高频轮询则增加网络和控制面负载,并延长更新传播窗口。只有网络设备不支持长连接、平台生命周期明确要求或经过测量的特殊场景,才切换 polling,并把轮询间隔、出口流量与最大陈旧时间一起评估。
用本地测试数据源验证命中与未命中
下面的 Node.js 实验不需要 LaunchDarkly 账号,不会访问 SaaS。代码按当前 Node.js server SDK 9.12.1 API 编写,实际项目仍应通过 lockfile 和制品清单固定经过验证的精确版本。它使用 SDK 自带的 TestData,先构造“仅 organization key 为 org-canary 时开启”的 flag,再动态改为所有人开启。把文件放到一个空目录即可执行:
npm init -y
npm install --save-exact @launchdarkly/node-server-sdk// test-data.mjs
import * as ld from '@launchdarkly/node-server-sdk';
import { TestData } from '@launchdarkly/node-server-sdk/integrations';
const td = new TestData();
await td.update(
td.flag('checkout-v2')
.booleanFlag()
.variationForContext('organization', 'org-canary', true)
.fallthroughVariation(false)
);
const client = ld.init('local-test-key', {
updateProcessor: td.getFactory(),
sendEvents: false
});
await client.waitForInitialization({ timeout: 5 });
const canary = {
kind: 'multi',
user: { key: 'user-1' },
organization: { key: 'org-canary' }
};
const ordinary = {
kind: 'multi',
user: { key: 'user-2' },
organization: { key: 'org-normal' }
};
console.log('canary =', await client.variation('checkout-v2', canary, false));
console.log('ordinary =', await client.variation('checkout-v2', ordinary, false));
await td.update(td.flag('checkout-v2').booleanFlag().variationForAll(true));
console.log('ordinary after update =', await client.variation('checkout-v2', ordinary, false));
console.log('missing flag =', await client.variation('not-exists', ordinary, false));
await client.close();node test-data.mjs预期结果是:canary = true,ordinary = false,动态更新后 ordinary after update = true,不存在的 flag 返回调用点 fallback,因此 missing flag = false。这组正反实验同时证明:规则依赖的是 organization context,而不是 user;更新处理器可以改变进程内规则;缺失 flag 不会自动取控制台某个默认值。
如果第一行也是 false,先检查 context 是否真的是 kind: 'multi',organization key 是否精确匹配;如果初始化超时,检查当前 SDK 版本的 waitForInitialization 签名和 TestData 工厂是否按该版本 API 配置;如果进程退出前仍挂起,确认执行了 client.close()。
用本地文件证明 fallback 的边界
测试数据源适合单元与组件测试;本地文件数据源适合开发机、离线演示或把一份已审查规则快照作为启动输入。它不等于生产控制台,也不会模拟 SaaS 的权限、审计和事件接收。当前 Node 服务端 SDK 已从 integrations 子路径导出文件数据源工厂,不需要安装额外数据源包:
npm install --save-exact @launchdarkly/node-server-sdk创建 flags.json:
{
"flags": {
"checkout-v2": {
"key": "checkout-v2",
"on": true,
"variations": [false, true],
"fallthrough": { "variation": 1 },
"offVariation": 0,
"version": 1
}
},
"segments": {}
}创建 file-source.mjs:
import * as ld from '@launchdarkly/node-server-sdk';
import { FileDataSourceFactory } from '@launchdarkly/node-server-sdk/integrations';
const source = new FileDataSourceFactory({ paths: ['./flags.json'] });
const client = ld.init('unused-local-key', {
updateProcessor: source.getFactory(),
sendEvents: false
});
await client.waitForInitialization({ timeout: 5 });
const context = { kind: 'user', key: 'local-user' };
console.log('known =', await client.variation('checkout-v2', context, false));
console.log('missing =', await client.variation('missing-flag', context, 'LOCAL_FALLBACK'));
await client.close();执行 node file-source.mjs,预期 known = true,missing = LOCAL_FALLBACK。然后把 flags.json 改成非法 JSON 再执行,这是反向实验:初始化应失败或超时,不能把语法错误当成“关闭 flag”。恢复合法文件后再运行,已知 flag 应回到 true。
文件模式还有一个容易忽略的风险:规则文件是评估数据,不是只有最终布尔值的配置字典。手写复杂 targeting JSON 很容易偏离 SDK 数据模型。团队若需要高保真快照,应由受控导出流程生成、校验并签名,仓库中只保存无敏感属性的测试样本;日常业务测试优先使用 TestData 的构造 API。
接入真实 Node 服务:把 flag 留在业务边界
安装服务端 SDK 后,从 Secret 注入环境对应的 SDK key:
npm install --save-exact @launchdarkly/node-server-sdk// feature-flags.mjs
import * as ld from '@launchdarkly/node-server-sdk';
let client;
let ready = false;
export async function startFeatureFlags() {
if (client) return;
const sdkKey = process.env.LAUNCHDARKLY_SDK_KEY;
if (!sdkKey) throw new Error('LAUNCHDARKLY_SDK_KEY is required');
client = ld.init(sdkKey, {
application: { id: 'checkout-api', version: process.env.APP_VERSION ?? 'dev' },
privateAttributes: ['/email', '/billingId']
});
try {
await client.waitForInitialization({ timeout: 5 });
ready = true;
} catch (error) {
ready = false;
console.error('feature flag initialization failed', error);
}
}
export async function checkoutV2(context) {
if (!client) return false;
return client.variation('checkout-v2', context, false);
}
export function featureFlagHealth() {
return { configured: Boolean(client), initialized: ready };
}
export async function stopFeatureFlags() {
if (!client) return;
await Promise.race([
client.flush(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('event flush timeout')), 2000)
)
]).catch((error) => console.error('feature flag event flush failed', error));
await client.close();
client = undefined;
ready = false;
}业务层不要到处散落字符串和 fallback。用一个面向业务的适配层固定 flag key、返回类型、默认行为和 context schema;路由处理器只问“是否走 checkout V2”,不会知道供应商客户端细节。进程收到 SIGTERM 后先停止接收新请求,再在有限超时内 flush 事件并 close;不要因为事件端点异常无限阻塞优雅退出。
健康检查也要拆开:进程存活不应依赖 LaunchDarkly;就绪状态是否因 SDK 未初始化而失败,取决于 flag 的业务关键性;监控则单独暴露 initialized、最近成功更新、stream 状态、fallback 计数和事件丢弃。把所有问题都映射成 /health = 500,可能在控制面抖动时触发实例集体重启,反而清空可用的内存 last-known state。
Relay Proxy 与 persistent store 解决的不是同一个问题
SDK 直连是最简单且通常优先的形态。Relay Proxy 是部署在自己基础设施中的 Go 服务,它与 LaunchDarkly 建立上游连接,再让多个 SDK 连接本地端点。它适合出口受限、需要汇聚大量服务端连接、跨网络区统一出口或配合持久化 store 的场景。它不是完整自托管 LaunchDarkly 控制面,也不是不维护就自动提高可用性的缓存盒子;SDK 一旦指向 Relay,Relay 不可达时不会自动退回 LaunchDarkly 公网端点。
引入 Relay Proxy 后,应用到 LaunchDarkly 的故障域变成应用、负载均衡、Relay 集群、上游网络和 SaaS。生产部署要跨可用区、多实例、监控连接数与带宽,并避免把高波动的浏览器/移动流量和服务端流量混在同一组 Relay。客户端海量 streaming 连接会成为容量主因;多数公开客户端更适合直连、bootstrap 或平台本地缓存。多地域系统通常在每个地域就近部署,不能让所有地域绕到单个中心 Relay。
服务端默认 feature store 在内存中,进程重启会丢失 last-known state。Redis、DynamoDB、Consul 等 persistent store 能让新进程在尚未取得当前规则时读取上次已知规则。它改善的是冷启动韧性,不保证规则新鲜,也不能替代上游更新来源。SDK 读取持久化旧值时可能仍未被判定为“initialized”,日志也会提示正在使用 last-known values。
Relay Proxy 加 persistent store 时,还要决定 TTL。若 Relay 内存缓存到期而持久化 store 又不可用,规则可能变成 FLAG_NOT_FOUND 并走 fallback;为了在 store 故障时继续使用已加载规则,可按官方建议评估无限缓存 TTL。但无限 TTL 会延长陈旧状态,因此必须同时监控“最后收到上游更新的时间”和“store 可用性”。store 断开期间收到更新、之后恢复连接时,还要验证数据是否完整;怀疑缺失或损坏时,重启 Relay 触发完整环境状态重新下载与写入,而不是假定恢复连接等于数据已修复。
Relay 的 proxy mode 与 daemon/LDD mode 不是同一个拓扑。proxy mode 最常见,SDK 连负载均衡后的 Relay,客户端和服务端 SDK都可使用;生产阵列需支持 SSE、跨可用区并具备不低于上游目标的可用性。daemon mode 只适用于 server-side SDK,SDK 不连接 Relay,而是与 Relay 共用 persistent store,官方主要建议 PHP 或 serverless 等正常长连接不合适的环境使用;增加多个 daemon Relay 并不会随 SDK 数量线性扩展读取能力。客户端和移动 SDK不能使用 daemon mode。
daemon mode 减少重复上游连接,却把 store 变成评估关键依赖。SDK 与 Relay 必须使用同一 store 配置,Redis、DynamoDB 或 Consul 的可用范围还受 big segment 类型约束;要对它做容量、隔离、备份、权限、延迟和故障演练,不应把业务 Redis 的无前缀 key 空间直接拿来混用。事件路径必须单独选择:SDK 可以直发 LaunchDarkly,也可以在 Relay 开启 forwarding 后发给 Relay,测试环境才适合禁用。只改 stream/base URI 而漏掉 events URI,会出现“评估正常、实验和 Contexts 全空”的半失效状态。
事件、flush 与实验暴露边界
flag 评估与分析事件走两条逻辑路径。variation 从本地状态返回结果,同时 SDK 将 summary、feature 等事件放入缓冲区;track 记录业务自定义指标事件。缓冲和批量发送降低请求开销,因此“业务拿到变体”不等于“事件已经送达”。无服务器短生命周期、命令行任务和进程退出前尤其要 flush,并给 flush 设置上限时间。
关闭 sendEvents 不影响本地求值,却会失去 Contexts 可见性、使用分析、详细评估和实验所需数据。网络白名单也不能只放行 stream 端点而漏掉 events 端点。事件量受评估次数、详细跟踪、allFlags 调用和自定义事件影响;在模板渲染循环里重复 variation,既浪费 CPU,也可能增加聚合或详细事件负担。一个请求内对相同 flag/context 可在业务层复用结果,但不要跨用户缓存最终变体。
实验最危险的误解是“调用 track 就算进入实验”。真正的曝光来自实验 flag 对 context 的评估事件,指标事件随后用同一 context 关联转化。若服务端用 organization key 评估,浏览器却用随机 user key 发送点击,两者无法可靠关联;若代码在用户尚未看到功能前预取并评估实验 flag,就会提前记录曝光,稀释效果;若同一页面在 edge、server 和 browser 各评估一次,又可能形成重复或语义不一致的曝光。
因此实验 flag 应尽量在“用户真正获得该体验”的最窄边界评估,评估 context kind、key 与 metric context 保持一致。后台任务、健康检查、爬虫和预渲染不得无意评估实验 flag。track 的事件 key、数值单位与去重语义要版本化,支付金额用分还是元、一次订单重试算一次还是多次,都必须在指标契约中固定。
LaunchDarkly-hosted metric 会在同一 context 的 evaluation 与 conversion 之间做归因,当前窗口最长为 90 天;超过窗口的转化不进入该实验。若同一 context 在窗口内先后收到多个 variation,当前归因会落到最早收到的 variation,这意味着身份复用、跨设备 key 合并和中途重分桶都可能污染结果。上线前应先做 A/A 与事件对账,确认 exposure、conversion、context kind、随机化单位和时间戳一致,再解释 uplift。
LaunchDarkly 的 manual、percentage、progressive、guarded release 与 Experimentation 是不同能力。percentage 只给固定比例;progressive 按时间推进阶段;guarded release 结合指标检测回归并可按配置自动回滚;Experimentation 比较 variation 对指标的因果影响。它们的计划 entitlement、可用次数、指标和权限要求不同,同一条 rule 也不能同时运行互斥的 progressive、guarded 与 experiment 状态。运维 kill switch 不应顺手加入产品实验;否则应急关闭会污染实验分配与指标解释。
隐私属性与数据最小化
context 属性既参与本地 targeting,也可能通过事件发送到 LaunchDarkly。private attributes 允许 SDK 在本地用属性求值,但从事件数据中移除属性值。服务端 SDK不会把私有值发回;客户端 SDK为完成求值可能在传输链路中使用该属性,但不会把它作为事件数据存储或显示。context 的 key 与 kind 不能设为私有,所以 key 本身就不应直接使用邮箱、手机号、证件号或内部敏感编号。
属性可按 JSON Pointer 配置,例如 /email,多 context 的嵌套路径要用 SDK 支持的准确语法。生产前抓一份脱敏后的事件样本,验证私有路径是否命中;只在代码里写了 privateAttributes,却把真实字段放在另一个嵌套位置,是常见泄露原因。也可以配置所有属性私有,但这样会降低控制台 context 搜索、自动补全和诊断能力。
匿名 context 主要控制是否进入 Contexts 列表,并不天然等于完整隐私方案。移动端自动环境属性会带来应用、操作系统和设备信息,应根据数据分类开启。隐私评审至少要回答:哪些属性进入 SDK、哪些只用于本地求值、哪些进入事件、保存多久、谁能查看、删除请求怎样传递,以及 Data Export 下游是否复制了同样的数据。
Data Saving Mode 与“少收集个人数据”不是一回事。它优化的是 flag 配置传输:初始通过轮询取得所需数据,随后流式接收差量,流不可用时可回退轮询,并可配置备用数据源。该能力目前属于 Early Access,只有列出的 SDK 与 Relay 版本支持。采用前要验证账号资格、SDK 版本、回退行为和移动长尾;不要因为名称里有 data saving,就把它当作隐私开关或稳定版默认配置。
OpenFeature Provider:隔离 API,不假装消灭差异
OpenFeature 提供供应商中立的评估 API,Provider 负责把调用转给 LaunchDarkly。对希望减少业务代码绑定的团队,适配层可以使用 OpenFeature:
import { OpenFeature } from '@openfeature/server-sdk';
import { LaunchDarklyProvider } from '@launchdarkly/openfeature-node-server';
await OpenFeature.setProviderAndWait(
new LaunchDarklyProvider(process.env.LAUNCHDARKLY_SDK_KEY)
);
const client = OpenFeature.getClient('checkout-service');
const enabled = await client.getBooleanValue(
'checkout-v2',
false,
{
targetingKey: 'user-1842',
kind: 'user',
plan: 'pro'
}
);包名和构造参数要以所选语言 Provider 的发布说明为准,并锁定经过验证的版本。官方 Provider 当前集中在 .NET、Java、Node.js 与 PHP 等实现;某些语言是社区 Provider,支持责任不同。
OpenFeature 能统一 getBooleanValue、evaluation context、hooks 和错误模型,却不会自动统一控制台规则语义、事件模型、实验曝光、persistent store、Relay、凭证和迁移数据。Provider 切换前要建立契约测试:相同 context 对布尔、字符串、数字、对象 flag 的结果一致;缺失 flag 的 fallback 一致;错误 reason 可观测;hook 不重复发事件;百分比分桶在迁移期间是否允许变化。只替换 import 而不验证这些差异,得到的是编译期可移植,运行期仍被绑定。
故障排查从“为什么用了 fallback”开始
所有用户都得到 fallback。 先看初始化状态和 detail reason。若是认证失败,检查 SDK 类型与凭证类型是否匹配、key 是否过期、Secret 是否注入到当前进程;若是 FLAG_NOT_FOUND,确认 flag key、project/environment 和客户端可用设置;若尚未初始化,检查 stream/poll/events 域名的 DNS、TLS、代理与防火墙。不要先在控制台反复开关,因为请求可能根本没有进入正确环境。
只有部分用户不命中。 打印经过脱敏的 context kind、key 哈希、属性名称与类型,再看 variation detail。高频原因是属性缺失、字符串/布尔类型错误、organization 和 user 放反、individual target 遮挡规则、上方宽规则先命中,或 rollout 使用了不同 bucket 属性。控制台 Contexts 页面显示过某属性,不能证明本次求值传入了它。
控制台改了但服务仍旧。 检查 SDK 是否复用单例、stream 是否连接、Relay 上游是否健康、环境凭证是否正确、最近更新时间是否前进。若进程刚重启并使用 persistent store,日志可能明确表示在用 last-known values。不要把“能返回值”当作“已收到最新值”。
浏览器首屏闪烁。 比较服务端渲染、bootstrap 和浏览器 identify 使用的 context;确认初始化完成前是否按 fallback 渲染;检查 flag 是否允许客户端使用。解决方向通常是同 context bootstrap、延迟渲染相关区域,或把首屏关键决策固定在服务端,而不是增加随机延时。
有评估却没有实验数据。 检查该 flag 是否真的关联正在运行的实验,评估是否命中实验的 targeting rule,SDK 是否发送事件,events 域名是否可达,进程是否在退出前 flush,评估 context 与 metric context 是否相同。track 成功不代表已记录曝光。
Relay 引入后更不稳定。 检查客户端与服务端流量是否隔离、负载均衡是否支持长连接、实例连接数/带宽/内存、上游 stream 和 events 转发,以及 SDK 是否被频繁重建。若直连原本健康而 Relay 没有明确网络或连接收益,移除这一层通常比继续加实例更合理。
权限、审计与成本要一起设计
成员是能登录控制台的人,context 是被评估的业务实体,两者不能混淆。组织至少区分只读、非生产变更、生产变更、审批与组织管理。生产 environment 标记为 critical 后,可要求确认和变更评论;更成熟的团队把维护者、审批者和紧急操作责任写入 flag 元数据,并定期检查离职成员、长期未登录账号、外包账号、团队继承角色和 API token。
审计日志要能回答谁在何时改变了哪个环境中的哪条规则、前后值是什么、关联哪个发布或事故。仅靠 Slack 通知不是审计,因为消息可能丢失且无法表达完整 patch。高风险 flag 的变更要关联工单、发布单或 incident,自动化 token 采用最小权限并区分机器人身份。紧急 kill switch 可以缩短审批,但必须留下事后复盘和恢复条件。
成本不只是订阅席位。架构侧要盘点活跃 context、服务连接、事件量、Data Export、实验、Relay 基础设施、persistent store、网络出口和维护人力。一个页面循环评估几十次、每个函数实例创建客户端、每个临时分支建立 environment,都会把成本推高。配置 application metadata 后,可按应用观察服务连接使用;事件和连接出现“Unknown”时,先补身份标记再谈优化。
flag 债务也有持续成本。每个 flag 应有类型、owner、创建原因、清理期限、fallback 语义、告警和清理工单。release flag 完成全量并稳定一个观察窗口后,先把代码固定到最终分支,再停止评估,最后归档或删除控制台 flag;不要先删 flag,否则旧实例会突然走 fallback。永久 entitlement 或运维开关可以长期存在,但仍要定期验证 owner、权限和恢复演练。
清理、回滚与退出迁移
一次安全的功能回滚先改变暴露,再决定代码回滚。发现指标异常时,把 rollout 降回上一个已验证比例或切到安全变体,观察错误率、延迟和业务指标;若新旧路径共享了不可逆数据写入,必须执行专门的数据兼容方案,不能假定关 flag 会撤销已写数据。确认稳定后再回滚制品或修复前进。
本地实验的清理很直接:执行 client.close(),删除临时目录和其中的 node_modules、package-lock.json、flags.json。真实项目卸载前则先用代码搜索和评估数据列出所有 flag key、SDK 初始化、事件 track、Relay 端点、Secret 与 API token。对每个 flag 固定最终行为并发布,等待旧版本与移动长尾退出,再删除远端配置和凭证。
退出 LaunchDarkly 或迁移到另一 Provider 时,按以下顺序降低双写和双评估风险:
建立供应商无关的业务适配层,并为现有结果录制契约样本。导出 flag 定义、各环境规则、segments、owner、审计与实验配置;敏感 context 和事件数据按合规要求处理。在目标系统重建规则,明确不兼容的操作符、prerequisite、分桶算法、multi-context 和 off variation。
影子评估但只让旧系统控制业务,比较结果、reason、延迟和缺失率;影子路径默认不发送实验曝光,避免双计。按非关键服务、内部用户、低比例生产流量逐步切换,保留快速切回旧 Provider 的开关,但避免形成无限递归的“用 flag 切 flag”。停止旧系统事件,再停止评估连接,轮换并删除 SDK key、API token、Relay 配置和 Data Export;最终取消席位与合同。
迁移最难的是稳定分桶。不同平台通常不会对同一 key 得到同一桶;直接切换会让用户换组,污染实验并造成体验跳变。正在运行的实验应在原平台结束,或把既有分配导出为显式 cohort,在新平台按 cohort 定向。无法保留分配时,要把切换视为一次重新随机化,并让产品和数据团队接受指标断点。
架构选型:保持最短的可靠路径
单体或少量服务、网络可直达时,选择服务端 SDK 单例直连、内存 store、明确 fallback 与事件发送。这是组件最少、故障域最清楚的基线。
浏览器首屏受 flag 控制时,在服务端用同一 context 求值并 bootstrap,浏览器 SDK负责后续更新和交互事件。授权、价格和数据访问仍留在服务端复核,前端 flag 只控制体验。
大量服务端实例或严格出口网络中,再评估 Relay Proxy。只有冷启动时必须在上游不可达情况下保留旧规则,才增加 persistent store;增加后同时承担 store 可用性、陈旧监控与数据修复责任。
边缘路由确实需要低延迟决策时选 edge SDK,并把 context、缓存键和事件路径作为一个整体设计。若只是页面按钮显隐,客户端 SDK通常更简单。移动端优先接受其离线和生命周期模型,不把 Web 初始化假设照搬过去。
希望降低代码绑定时引入 OpenFeature Provider,但保留 LaunchDarkly 专属能力的隔离模块和契约测试。希望减少配置传输时再评估 Data Saving Mode,并把 EAP 状态纳入升级风险。架构成熟度不体现在组件数量,而体现在每一层都有明确问题、可观测证据和可撤销路径。
上线前最后走一遍真实故障:正确 context 命中、错误 context 不命中;flag 关闭返回 off variation、flag 缺失返回调用点 fallback;初始化超时能按策略启动或阻断;断 stream 后继续使用 last-known state;清空内存并阻断上游后验证冷启动;events 不可达时业务评估不被拖死;退出前 flush 有超时;SDK key 轮换期间新旧实例都能工作;私有属性不出现在事件;实验曝光与指标使用同一 context。能把这些结果解释清楚,特性开关才从“远程 if”变成可治理的渐进交付基础设施。
