Unleash:把开关控制面与请求内本地评估拆开
功能已经随制品部署,却不能立刻对所有用户开放;新结算链路需要先给内部员工,再给稳定的 5% 用户,发现错误时还要在几秒内关闭。把这个需求写成数据库配置或 if (tenantId == ...) 很容易,但规则一多,发布人就无法回答三个问题:谁会命中、控制面失联时返回什么、这段分支何时删除。
Unleash 把问题拆成两条链。控制面保存项目、环境、flag、strategy、segment、variant 和变更事件;后端 SDK 定期从 Client API 拉取规则,在应用进程内结合 Unleash Context 做评估并缓存规则。正常业务请求不需要同步调用管理 API。前端 SDK 的模型不同:它把上下文发给 Frontend API 或 Edge,由服务端评估后只返回该上下文可见的结果。这个差异决定了延迟、隐私与故障半径。
先在托管与自托管之间分清责任
Unleash Cloud 适合不希望维护数据库、升级和控制面可用性的团队。研发仍要负责 SDK 初始化、fallback、上下文字段、token 轮换、事件成本和 flag 清理;托管不会替应用决定断网时应走新链路还是旧链路。采购时应按环境、项目、成员、SSO、审计、Edge、数据驻留和支持能力核对当前套餐,不把某个价格或席位数写进长期架构假设。
自托管 Open Source 或 Enterprise 适合必须把控制面、上下文处理和审计数据留在指定网络,或者已有成熟 PostgreSQL 与平台运维能力的团队。代价是 PostgreSQL 备份恢复、镜像供应链、数据库迁移、证书、入口高可用、监控告警和升级窗口都归自己。自托管也不等于没有外联:镜像拉取、身份提供方、邮件、集成、遥测和版本检查要逐项盘点;不希望实例执行版本检查时可设置 CHECK_VERSION=false。
许可边界必须跟部署制品一起审查。Unleash 从 v8 开始把主仓库源码和 unleash-server npm 包改为 AGPLv3;官方 Open Source Docker 镜像仍按 Apache-2.0 分发,直接运行未修改的官方镜像不因此变成 AGPL 部署。修改源码后通过网络向用户提供服务、基于 npm 包制作自定义发行版,和直接运行官方镜像是三种不同情形,前两种应由法务按 AGPL 与商业许可评估。SDK 继续使用各自的宽松许可证;Cloud 与 Enterprise 则受商业合同约束。不能用“Open Source”一个标签替代组件级许可证清单。
选择信号可以直接落到责任表:
| 决策面 | Cloud 更合适 | 自托管更合适 |
|---|---|---|
| 控制面可用性 | 团队希望购买托管 SLO | 已有跨可用区应用与 PostgreSQL 运维能力 |
| 数据驻留 | 供应商区域与合同满足要求 | 规则、事件或前端上下文必须留在内网 |
| 交付速度 | 先建立治理和 SDK 规范 | 能承担部署、升级、备份与值班 |
| 定制与网络 | 标准公网或专线接入 | 私有 CA、隔离网、内部身份与代理要求强 |
| 总成本 | 席位和用量成本低于自建人力 | 长期规模足以摊薄平台人力与基础设施 |
用 Compose 建立可丢弃的实验控制面
开发机需要 Docker Engine、Compose V2、可用的 4242 端口,以及拉取 PostgreSQL 和 Unleash 镜像的网络。企业代理环境先为 Docker daemon 配置代理和内部 CA;只在终端设置 HTTPS_PROXY 往往不能解决 daemon 拉镜像失败。下面的密码仅属于本地实验,保存为未提交的 .env,不要复制到共享环境。
POSTGRES_PASSWORD=replace-with-local-only-password
UNLEASH_IMAGE=unleashorg/unleash-server:8.0.3
POSTGRES_IMAGE=postgres:17-alpine# compose.yaml
services:
db:
image: ${POSTGRES_IMAGE}
environment:
POSTGRES_DB: unleash
POSTGRES_USER: unleash
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- unleash-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U unleash -d unleash"]
interval: 5s
timeout: 3s
retries: 20
unleash:
image: ${UNLEASH_IMAGE}
ports:
- "127.0.0.1:4242:4242"
environment:
DATABASE_HOST: db
DATABASE_NAME: unleash
DATABASE_USERNAME: unleash
DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
DATABASE_SSL: "false"
CHECK_VERSION: "false"
LOG_LEVEL: info
depends_on:
db:
condition: service_healthy
volumes:
unleash-db:启动并观察迁移完成,而不是只看容器处于 running:
docker compose up -d
docker compose ps
docker compose logs --tail=100 unleash
curl --fail http://127.0.0.1:4242/health预期证据是数据库健康、Unleash 日志没有持续迁移错误、健康端点返回成功,并且浏览器能打开 http://127.0.0.1:4242。如果容器循环重启,先比较 DATABASE_* 与 PostgreSQL 初始化账号;已经用错误密码初始化过 volume 时,改 .env 不会重置数据库密码。实验数据允许丢弃时才执行 docker compose down --volumes,共享环境绝不能把删除 volume 当作修复手段。
固定镜像版本能让实验可复现。准备升级时,再把目标镜像写入单独分支并阅读对应主版本迁移说明;latest 会把数据库迁移和应用变更隐藏在一次普通重启中。
Admin API 与 Client API 使用不同身份
管理动作与运行时取配置必须分权。当前 Unleash 用 Backend token 供后端 SDK 或 Edge 读取 Client API、注册实例和发送指标;Frontend token 供浏览器或移动端访问 Frontend API;个人访问 token 适合人工调试,也是在 Open Source 中调用 Admin API 的替代方案;服务账号 token 适合 Enterprise 的 CI、同步器和其他非人自动化。旧式全权 Admin token 已被弃用,不应继续作为团队默认方案。
在管理界面为实验分别创建:
Open Source 实验创建一个有到期时间、权限受限的专用用户个人访问 token;Enterprise 则创建只允许管理测试项目与测试环境的服务账号 token。二者任选其一作为 UNLEASH_ADMIN_TOKEN,不要拿日常管理员身份给流水线长期使用;一个只读 default 项目、development 环境的 Backend token,作为 UNLEASH_CLIENT_TOKEN。
token 只在创建时进入密码库或 CI secret,不写入 Compose、Git、日志和截图。服务端 token 泄漏后先撤销并签发替代 token,再滚动更新 SDK;不要先删除旧 token 导致所有实例同时失去刷新能力。Frontend token 可以暴露给终端用户,但它权限有限并不意味着上下文可以随意携带邮箱、IP 或商业敏感字段。
用 Admin API 创建开关,再显式开启环境。API 路径中的项目、环境和 flag 名称都是稳定标识,不要使用中文展示名代替机器 key:
export UNLEASH_URL=http://127.0.0.1:4242
export UNLEASH_ADMIN_TOKEN='<service-account-token>'
curl --fail-with-body -X POST \
"$UNLEASH_URL/api/admin/projects/default/features" \
-H "Authorization: $UNLEASH_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "checkout.v2",
"description": "切换结算编排实现",
"type": "release"
}'
curl --fail-with-body -X POST \
"$UNLEASH_URL/api/admin/projects/default/features/checkout.v2/environments/development/on" \
-H "Authorization: $UNLEASH_ADMIN_TOKEN"然后用 Backend token 读取 SDK 实际看见的规则:
export UNLEASH_CLIENT_TOKEN='<backend-token>'
curl --fail-with-body \
"$UNLEASH_URL/api/client/features" \
-H "Authorization: $UNLEASH_CLIENT_TOKEN" \
-H 'UNLEASH-APPNAME: checkout-api' \
-H 'UNLEASH-INSTANCEID: local-lab'响应中应出现 checkout.v2。若 Admin 调用成功而 Client API 看不到它,依次检查 token 的环境和项目 scope、flag 是否在该环境启用、请求 URL 是否多写或漏写 /api。401 说明凭据无效,403 说明身份存在但权限不足;不要通过换成全局 token 掩盖 scope 配置错误。
Strategy、segment、variant 与 stickiness 如何共同决定结果
一个 flag 在一个环境内可以拥有多个 activation strategy。每个 strategy 独立求值,任意一个为真即可启用,整体是 OR;同一个 strategy 内的 constraints 必须全部满足,整体是 AND。Segment 是可复用的一组 constraints,适合表达“内部员工”“已签署试用协议的租户”等稳定群体。更新 segment 会影响所有引用它的 strategy,因此修改前必须查看引用关系并做影响评估。
渐进放量不是每次请求抽一次随机数。Unleash 对 stickiness 字段和 groupId 做确定性哈希,把实体映射到稳定桶。默认优先 userId,缺失时使用 sessionId;两者都缺失时可能退化为随机,用户会在新旧体验之间跳动。多服务必须传同一规范化 key,例如不可变的内部 subject ID,不能一处传邮箱、一处传订单号。
Variant 在启用后继续做稳定分配,每个 variant 有名称、权重和可选 payload。payload 适合小型展示参数,不适合下发秘密或大段业务配置。多个 strategy 带各自 variants 时,strategy 顺序会影响最终 variant;修改 groupId 会重新分桶,等价于重新分配实验人群,必须作为实验破坏性变更审查。
推荐把放量阶段写成可审计状态机:
0%:代码已部署,旧链路仍是安全默认;内部 segment 单独通过另一条 strategy 放行。5%:以稳定 userId 放量,观察新旧链路业务指标、错误率和 SDK 刷新状态。50%:同一批实体持续命中,不通过修改 stickiness 字段“修正”样本。
100%:保持一段业务观察窗口,确认回滚仍有效。固化并删除:代码只保留胜出路径,移除 SDK 判断,归档 flag。
Playground 可以用给定 context 解释规则命中,但生产证据还应包含应用日志中的 flag key、结果、variant 和不含敏感值的 subject 哈希。不要记录完整 token 或全部 context。
后端 SDK 在进程内评估并保存 last-known state
Node.js 服务使用 unleash-client。SDK URL 指向实例或 Edge 的 /api/,不是 Admin API 端点:
npm install unleash-clientimport { initialize } from 'unleash-client';
const unleash = initialize({
url: process.env.UNLEASH_API_URL,
appName: 'checkout-api',
instanceId: process.env.HOSTNAME ?? 'local',
customHeaders: {
Authorization: process.env.UNLEASH_CLIENT_TOKEN,
},
refreshInterval: 15_000,
metricsInterval: 60_000,
});
unleash.on('ready', () => console.info('unleash ready'));
unleash.on('error', (error) => console.error('unleash error', error));
export function selectCheckout(user) {
const context = {
userId: String(user.subjectId),
properties: {
tenant: String(user.tenantId),
plan: user.plan,
},
};
const enabled = unleash.isEnabled('checkout.v2', context, false);
const variant = unleash.getVariant('checkout.v2', context);
return { enabled, variant: variant.name };
}
export async function shutdownFlags() {
await unleash.destroy();
}第三个参数 false 是 flag 不存在或没有可用规则时的业务 fallback。支付、权限提升、不可逆写入通常选择旧且已验证的路径;紧急 kill switch 可能反过来要求失联时关闭高风险能力。fallback 必须由业务风险决定并进入测试,不能全仓统一写 false 后就认为安全。
SDK 通常异步初始化。强依赖新规则的 worker 可以在消费消息前等待 ready 并设置有界启动超时;面向请求的服务更常用 bootstrap 或本地缓存立即提供 last-known state,同时后台刷新。无限等待控制面会把一个配置系统故障升级成整个应用无法启动。
后端 SDK 将评估计数聚合后周期性发送给 Unleash。指标能推动生命周期判断,也会产生网络和高基数成本。监控至少包含首次成功同步时间、最近成功同步时间、规则版本或 ETag、刷新错误数、缓存年龄、评估总量、fallback 次数、指标发送失败和队列积压。进程优雅退出时调用 SDK 的停止或销毁方法,为最后一次 metrics flush 留出有界时间,但不能让 flush 阻塞业务关停。
Bootstrap 解决冷启动,不负责自动保持新鲜
运行中断网与冷启动断网是两类故障。SDK 已成功同步后,内存或持久缓存可以继续评估旧规则;新实例第一次启动时没有任何规则,只能返回 disabled 或调用方 fallback。多数后端 SDK 支持从文件、环境或自定义 provider bootstrap,但具体格式与覆盖缓存的优先级要按语言 SDK 固定版本验证。
Java SDK 可以读取与 /api/client/features 相同结构的 bootstrap 文件;Go SDK 可用 BootstrapStorage,并会在 bootstrap 解析失败时尝试默认磁盘缓存。通用发布链可以在受控步骤导出某环境的 Client API 响应,经过 schema 校验和敏感信息检查后随配置制品发布:
curl --fail-with-body \
"$UNLEASH_URL/api/client/features" \
-H "Authorization: $UNLEASH_CLIENT_TOKEN" \
-H 'UNLEASH-APPNAME: bootstrap-exporter' \
> bootstrap.json.tmp
node -e "const f=require('./bootstrap.json.tmp'); if(!Array.isArray(f.features)) process.exit(1)"
mv bootstrap.json.tmp bootstrap.jsonbootstrap 不应包含 Admin token,也不应由生产应用临时使用高权管理凭据生成。文件要有来源版本、校验和、环境标签和过期阈值;把生产 bootstrap 误装到测试环境会造成比断网更隐蔽的错误。成功连回控制面后,SDK 应以新规则替换 bootstrap,而不是永久固定在发布时快照。
Edge 缩小中心控制面的读压力与地域距离
当数千 SDK 都直接轮询中心实例,连接数、出口流量和故障放大会成为瓶颈。Unleash Edge 作为轻量读副本位于 SDK 与中心 Unleash 之间,缓存规则并承接连接。后端 SDK 仍在本地评估;前端 SDK 的上下文由 Edge 评估,因此自托管 Edge 能避免终端上下文继续上送中心控制面。
Edge 不是数据库备份,也不是第二个管理控制面。它需要 Backend token 从上游获得指定项目与环境规则,SDK 则用各自 Backend 或 Frontend token 访问 Edge。部署多个 Edge 实例时要监控上游连接、同步落后、各实例规则版本、启动 bootstrap、token scope 和负载均衡健康;只增加副本而没有可用的初始状态,区域整体冷启动时仍会失败。
Enterprise Edge 的生产韧性还依赖显式持久层。运行中的实例可以从内存继续服务旧快照,但实例重启且上游不可达时,没有持久快照就无法就绪。多副本生产部署应使用共享 Redis,或按约束评估 S3;本地文件只适合开发和单实例验证。验收必须分别覆盖“运行中断开上游”和“清空进程后冷启动”两种故障,不能用前者证明后者。离线模式则从本地 feature 文件启动并使用启动时配置的 client/frontend token,只适合开发、测试或严格隔离场景;它不会自动接收控制面的新规则。
是否引入 Edge 由连接规模、地域延迟、前端隐私和中心限流决定。小团队几十个后端实例直接连接中心通常更简单;多地域、大量前端会话或隔离网络则更容易从 Edge 获益。Open Source Edge 已进入长期维护,并已有明确的生命周期终止窗口;其开源 crate 使用 MIT,仓库中的 Enterprise crate 使用商业许可。Enterprise Edge 可托管或自托管,自托管要求 Unleash Enterprise v7.3+ 与包含 Edge 的许可证。新生产架构不能把即将 EOL 的 Open Source Edge 当成无期限基线,也不能把 Enterprise 镜像视作 Open Source 镜像的无许可替代品。
两组实验验证成功路径与故障语义
稳定分桶的正向实验
在 development 环境为 checkout.v2 配置 gradual rollout,stickiness 选 userId,依次设置 0%、50%、100%。准备固定的 100 个 subject ID,每个 ID 连续评估 20 次,并让两个应用实例各评估一遍。
预期证据:
0% 时内部 segment 之外全部为 false;100% 时全部为 true;50% 时启用数量接近一半,但验收重点是同一 ID 在重复请求、重启和不同实例间结果不变;Client API 返回的环境与项目符合 token scope;
SDK 最近同步时间更新,Unleash 收到按 flag 聚合的 yes/no metrics;variant 实验中同一 ID 保持同一 variant,除非权重、strategy 或 groupId 被修改。
反向对照把 userId 与 sessionId 都删掉,重复评估。结果可能漂移,证明“比例正确”不能替代“实体稳定”。修复不是缓存第一次随机结果,而是定义跨服务一致、不可变且允许用于评估的 subject key。
断网与冷启动的反向实验
先让 SDK 成功同步并记录 checkout.v2 的结果,然后阻断应用到 Unleash 或 Edge 的网络,保持业务流量继续评估。可以在隔离测试网络暂停控制面容器;不要在共享主机随意修改全局防火墙。
预期证据是业务请求仍返回同步前的稳定结果,刷新错误和缓存年龄增长,控制面中的新变更暂时不可见。恢复网络后,最近同步时间推进,新规则在刷新间隔后生效,错误计数停止增长。
接着删除测试实例的 SDK 持久缓存、移走 bootstrap,并在控制面不可达时启动一个全新进程。预期它返回调用方 fallback 或 disabled,ready 不应被伪造为成功。再装回经过校验的 bootstrap 重启,预期进程可以在无网络时按快照评估,同时暴露“bootstrap/陈旧”状态。这个实验能区分 last-known-good 与“系统天然离线可用”。
实验清理顺序是:恢复网络、确认 SDK 再次同步、删除测试 strategy 与 segment、关闭并归档测试 flag、撤销临时 token、停止测试进程,最后在确认 volume 只含实验数据后执行 docker compose down --volumes。保留日志中的相对时序、规则版本、结果计数和错误类型,删除 token、真实用户属性与内部地址。
生产拓扑要让故障停在控制面
典型自托管拓扑由无状态 Unleash API 多副本、托管或高可用 PostgreSQL、TLS 入口、身份提供方和可选 Edge 组成。管理流量与 SDK 数据流量可以使用独立入口、限流和网络策略;Admin UI/API 只允许办公网、VPN 或自动化网络访问,Client/Frontend API 面向应用网络。数据库只接受 Unleash 服务账号连接,不暴露给 SDK。
容量估算不能只看每秒业务请求。后端本地评估几乎不增加控制面 QPS,控制面负载来自 SDK 实例数除以刷新间隔、Frontend API 请求、metrics 上报、管理变更和 Edge 上游连接。把 refresh interval 从 15 秒改为 1 秒,1000 个实例会从约 67 次拉取每秒膨胀到约 1000 次,还会放大滚动发布的同步峰值。先通过 Edge、抖动和合理刷新周期降载,再扩中心实例。
高可用验收至少包含:杀掉一个 API 副本时 UI 与 SDK 同步继续;数据库主节点切换时 API 有界恢复;所有中心实例不可用时已初始化 SDK 继续评估;区域 Edge 与上游断开时继续服务缓存并明确陈旧;全新 SDK 在无 bootstrap 时按预定 fallback 启动。业务 SLO 与“规则传播延迟 SLO”分开统计,避免控制面陈旧被请求成功率掩盖。
升级、备份与恢复必须带着数据库迁移一起演练
Unleash 的权威状态在 PostgreSQL,包括 flag 配置、项目、环境、用户、token 元数据、事件和迁移状态。备份要使用 PostgreSQL 一致性备份,并同时保存部署清单、镜像 digest、环境变量键名、TLS 与身份配置、外部 secret 引用和恢复说明。明文 token 不应散落在备份脚本;真正的 secret 由密码库独立备份和恢复。
升级流程应当是:阅读跨主版本迁移说明与许可变化;在恢复出的数据库副本上运行目标镜像;验证迁移、登录、Admin API、Client API、SDK 同步和回滚策略;生产变更前创建可恢复备份并记录校验结果;先升级非生产,再滚动生产。数据库 schema 已向前迁移后,简单把镜像降级可能无法运行,因此回滚点通常是旧镜像加升级前数据库恢复,而不是只改 image tag。
Compose 实验可以这样生成逻辑备份并验证文件非空:
docker compose exec -T db \
pg_dump --format=custom --no-owner -U unleash unleash \
> unleash.dump
test -s unleash.dump恢复演练必须在新的空数据库中执行,并验证对象数量、代表性 flag、token 重签、Client API 与 SDK 行为。只成功执行 pg_restore 不算完成。恢复环境不能连接生产 Edge、webhook 或邮件,避免旧事件和旧 token 产生副作用。
把 flag 当成有 owner 和退出日期的临时代码
团队模板至少记录 flag key、类型、业务说明、owner、创建工单、目标环境、fallback、stickiness 字段、指标、预期生命周期、100% 后的删除工单和紧急联系人。命名使用领域前缀和稳定机器 key,例如 checkout.v2;不要把工号、日期或环境名塞进 key,环境由 Unleash 环境对象表达。
Unleash 的生命周期从 Define、Develop、Production、Cleanup 到 Archived,usage metrics 会帮助识别停滞阶段。类型的 expected lifetime 到期后 flag 可进入 potentially stale;标记 stale 是清理信号,不会自动删除应用中的分支。治理机器人可以监听 stale 事件、搜索代码引用、创建删除 PR,但最终仍要由 owner 证明旧路径、测试、监控和配置都已移除。
每次评审检查以下证据:
| 对象 | 责任与审计证据 |
|---|---|
| 项目/环境 | 平台 owner;成员、角色、环境变更审批记录 |
| Admin 自动化 | 交付平台 owner;服务账号、最小项目权限、调用审计 |
| Backend token | 应用 owner;单环境 scope、密码库位置、轮换演练 |
| Context 字段 | 数据与安全 owner;字段目录、用途、保留与脱敏决定 |
| Strategy/segment | 业务 owner;命中样本、影响面、审批和回滚阈值 |
| Flag 生命周期 | 研发 owner;TTL、stale 告警、代码删除与归档证据 |
| 控制面与 Edge | 平台 owner;SLO、备份恢复、升级、容量与成本报表 |
最后还要守住一条边界:feature flag 不是授权系统。客户端可观察或篡改 flag 结果,后端也可能因 fallback 打开路径;真正的 entitlement 和权限校验必须在可信服务端独立执行。Unleash 决定“是否尝试新实现”,权限系统决定“调用者是否被允许”。两者混在一起,最方便的开关会变成最脆弱的安全边界。
现场排障从状态来源而不是 UI 开始
| 现象 | 先取证 | 常见原因 | 修复与再验证 |
|---|---|---|---|
| UI 已开启,应用仍为 false | Client API payload、token scope、SDK 最近同步时间 | 环境或项目不匹配、刷新失败、flag key 拼错 | 修正 scope/URL,等待刷新并比较规则版本 |
| 同一用户结果跳变 | 实际 context、stickiness、groupId | userId 缺失或各服务 key 不一致 | 统一 subject key,重跑跨实例稳定性实验 |
| 新实例启动卡死 | 初始化等待栈、ready/error 事件 | 把首次联网设为无限启动屏障 | 设置有界超时并引入缓存或 bootstrap |
| 断网后一直用旧规则 | 最近成功同步、缓存年龄、网络错误 | last-known state 正常但没有 stale 告警 | 恢复链路,设置陈旧阈值与告警,不清空缓存 |
| 指标突然归零 | SDK metrics 队列、发送错误、应用实例注册 | token 无发送权限、进程未 flush、代理阻断 | 修复权限/代理,验证下一周期聚合数据 |
| 数据库升级后旧镜像失败 | migration 日志与 schema 版本 | 数据库迁移不可逆或旧版不识别 | 按演练恢复升级前数据库和旧镜像 |
排障时不要为了“立即刷新”让业务请求同步调用 Admin API,也不要删除缓存来证明网络已修复。先标出当前结果来自 bootstrap、持久缓存、内存快照还是新网络响应,再决定是否等待、回滚规则、恢复连接或重启实例。能说清状态来源,才算真正掌握了渐进交付的故障边界。
