Flagsmith:远程配置、自托管与身份特征
线上刚出现错误率抬头时,最危险的动作往往不是关闭新功能,而是为了关闭功能再发一个版本。构建、审批、调度和实例滚动会把几秒钟的止损变成几十分钟,而且旧实例与新实例可能同时存在。Flagsmith 把“代码是否已经部署”和“某个环境、某类身份此刻得到什么值”拆开:应用提前部署两个行为分支,控制台发布 flag 状态,SDK 在运行时取值。这样做并不会自动带来安全性;真正决定系统是否可靠的,是评估发生在哪里、凭据能看见什么、断网时返回什么,以及 flag 最终有没有被删除。
先建立一张不会混淆的运行图
Flagsmith 的持久对象从组织、项目、环境一直落到 feature。一个 feature 在不同 environment 中可以有不同的 enabled 状态和 value;identity 用稳定 identifier 表示一个评估主体,trait 是参与 segment 条件的属性。segment override、identity override 和环境默认值共同决定结果。这里的 value 是远程配置值,不只限于布尔开关,因此调用方必须同时处理“是否启用”和“值能否解析”。
SaaS 模式下,Dashboard、Core API、数据库和托管 Edge API 由供应方运行。自托管模式下,Dashboard 是管理入口,Core API 同时服务管理操作和 SDK 数据读取,PostgreSQL 保存组织、项目、环境、feature、identity、审计等持久数据;团队还可以在应用附近部署 Edge Proxy。官方的平台架构说明把 Dashboard、Core API、Edge API、SDK、集成和数据库分成了不同职责,选型时应沿着这些边界算可用性,而不是只问“是不是开源”。
Flagsmith 主平台大部分代码按 BSD-3-Clause 开源,少量配套仓库使用 MIT;flags、segments、identities、remote config、multivariate 和本地评估属于开源核心。审计日志、细粒度角色权限、SAML/SSO、change request 等企业治理能力需要有效的 Enterprise 许可证,Cloud 也受托管套餐和合同约束。自托管 Community 能运行评估链,不等于自动拥有企业审批、身份治理和商业支持;选型表必须把“可运行核心功能”和“满足组织控制要求”分开核价。
一条典型链路是:管理员在 Dashboard 修改生产环境规则,Dashboard 调 Core API 写入 Postgres;远程评估 SDK 随业务请求向 API 提交 environment key、identifier 和 traits,由服务端返回结果;本地评估 SDK 则周期性拉取 environment document,在进程内用本次调用携带的 traits 计算。Edge Proxy 介于两者之间:它用 server-side key 从上游拉 environment document,在本地运行评估引擎,而下游应用仍按远程评估协议调用它。于是“本地”有两个视角:对 Edge Proxy 而言是本地计算,对业务应用而言仍有一次网络调用。
不要把 flag 当权限系统。浏览器可以改 JavaScript、伪造客户端返回值;服务端即使得到 checkout_v2=false,也仍要做登录、租户、资源归属和授权判断。flag 决定功能路径,授权决定某人是否有权执行动作,两条链必须独立成立。
SaaS 先跑通最短闭环
在 Dashboard 创建项目后保留 Development、Staging、Production 等独立环境,不要用一个环境加 stage trait 模拟多环境。创建布尔 feature checkout_v2,默认关闭;再为测试身份增加 segment 条件,例如 plan = pro。环境隔离保证测试配置不会意外发布到生产,trait 条件则只描述生产环境中的目标群体。
本例会提交 identity traits,因此必须使用 server-side environment key,并把它视为秘密。只读环境 flags 的远程评估可以使用公开的 client-side environment key;一旦需要创建 identity、写 trait、拉完整 environment document 或启用本地评估,就必须改用 server-side key。客户端 key 可以出现在浏览器或移动端包中,但不能据此写身份数据;管理 API token 又是另一类高权限凭据,不能当 SDK key。示例使用占位符,真实值只从密钥管理系统注入:
export FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY='<server-side-environment-key>'
npm install @flagsmith/nodejsimport { Flagsmith } from '@flagsmith/nodejs';
const flagsmith = new Flagsmith({
environmentKey: process.env.FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY,
requestTimeoutSeconds: 2,
defaultFlagHandler: (featureName) => ({
featureName,
enabled: false,
value: null,
isDefault: true,
}),
});
export async function resolveCheckout(user) {
const flags = await flagsmith.getIdentityFlags(user.stableId, {
plan: user.plan,
region: user.region,
});
return {
enabled: flags.isFeatureEnabled('checkout_v2'),
config: flags.getFeatureValue('checkout_v2'),
};
}先用固定身份 demo-user-001、trait plan=pro 请求两次,再换成 plan=free。预期证据不是“接口返回 200”,而是同一 identity 与相同 traits 连续得到相同 enabled/value,命中 segment 的 pro 与未命中的 free 得到不同结果,应用日志只记录 flag 名、结果、是否 default 和匿名化 subject,不记录 key 或完整 traits。
远程评估有一个容易忽略的状态行为:SDK 请求 identity flags 时,传入 traits 会持久化到 Flagsmith API,并与该 identity 先前保存的 traits 合并,然后使用完整集合评估。一次请求没有传 region,不代表旧 region 消失。若业务语义要求属性删除,要显式管理 trait 生命周期;否则旧值会像幽灵条件一样继续命中规则。与之相反,本地评估不会在每次 identity 评估时访问 API,也不会从 environment document 得到 trait 数据,只使用本次调用传入的 traits。因此切换模式前必须证明每次请求都能构造完整评估上下文。
不希望把 identifier 或 traits 持久化到 Flagsmith 时,应使用官方 transient identity/trait 能力,而不是假设省略字段就会自动删除旧数据。transient 只改变数据是否持久化,不会把客户端输入变可信;涉及折扣、额度和权限的条件仍需由服务端从权威数据源构造。生产验收要用相同 identifier 分别执行持久与 transient 请求,再从身份查询和审计侧证明数据边界符合预期。
用 Docker 与 Compose 建立自托管练习场
官方Docker 指南提供仓库内的 Compose 文件,适合开发和评估:
curl -o docker-compose.yml \
https://raw.githubusercontent.com/Flagsmith/flagsmith/main/docker-compose.yml
docker compose -f docker-compose.yml up -d
docker compose -f docker-compose.yml ps当前官方 Compose 首次启动会引导创建 admin@example.com 超级用户,并把一次性密码设置链接写入 Compose 日志;不要依赖旧的公开 /signup 注册流程。先执行 docker compose logs --tail=200 api(服务名以下载到的 Compose 文件为准),只在本机终端打开该链接并立即设置独立密码,随后关闭公开注册入口。Compose 拓扑把 Dashboard 与 REST API 暴露在 8000 端口,API 使用 PostgreSQL;SDK 接入自托管实例时必须把 API URL 改为 http://localhost:8000/api/v1/。用下面的请求先证明入口存在,再进入控制台创建环境和 key:
curl --fail --show-error http://localhost:8000/api/v1/flags/ \
-H 'X-Environment-Key: <client-side-environment-key>'预期响应是 flag 列表;401 首先检查 key 类型、复制时空格和环境归属,连接拒绝则先看 docker compose ps 与 docker compose logs --tail=200。如果 Dashboard 能开但 SDK 调错地址,常见原因是仍指向 SaaS 默认 Edge API。远程访问时还需配置反向代理、TLS 与 Dashboard 的 API_URL,否则浏览器页面会向内部或错误域名发请求。
这套 Compose 不是生产承诺。生产至少要把镜像 tag 固定到经过验证的版本,把 Postgres 放到有备份、恢复演练和连接容量治理的位置,把 API/Dashboard 放在 TLS 入口后,限制注册入口,配置管理员身份与审计,并在升级前读迁移说明。Django migration 会随升级改变数据库结构,数据库备份必须与应用镜像、环境变量、加密材料一起形成可恢复点;只备份 Postgres 但丢失密钥和外部分析配置,仍不能完整恢复。
清理练习环境前先导出需要保留的配置证据,再执行:
docker compose -f docker-compose.yml down
# 确认练习数据无需保留后才删除命名卷
docker compose -f docker-compose.yml down --volumes第二条会删除本地 Postgres 数据,不能在共享或生产主机上照抄。SaaS 中的测试 identity、trait、segment 和 key 也应删除或轮换;删除容器并不会清理云端对象。
API、Dashboard、Postgres 与 Edge Proxy 怎么选
Dashboard 适合人工变更和审阅;管理 REST API 适合自动化创建、审批后发布和审计集成。管理 API 不能进入每次业务请求:它的延迟、限流和故障会直接放大成业务故障,而且管理凭据权限远高于 SDK key。业务请求只走 SDK 评估入口,配置变更走独立控制链。
直接远程评估最容易理解,也能使用服务端保存的 identity traits,但每次取 flags 都存在网络延迟和上游可用性依赖。SDK 本地评估周期性拉 environment document,业务请求不再逐次访问 Flagsmith,适合高吞吐后端;代价是文档体积、规则传播延迟、轮询流量和完整 traits 构造责任。前端通常使用客户端 SDK,不应下载包含秘密规则或 server-side key 的本地评估文档。
本地评估默认约每 60 秒异步刷新一次 environment document,适合受控的长生命周期进程;Node.js、Java、PHP 等实现还要在退出时关闭轮询线程。它不会读取 Flagsmith 中已持久化的 traits,analytics-based integrations 也不会运行;若 SDK 显式启用 flag analytics,评估计数仍可发送。使用托管 Edge API 拉取 identity overrides 时,应启用环境中的本地评估 override 选项,并把 override 数量控制在约 500 以内;环境文档超过 1 MB 后,普通 SDK 拉取路径不再是可靠基线,需要减少 override,或直接调用 environment-document 端点并按 Link 响应头处理分页。把 Lambda 等短生命周期运行时直接套成本地评估,常会得到重复冷启动、陈旧文档和无法及时关闭线程,应优先远程评估或 Edge Proxy。
Edge Proxy 适合把评估靠近应用、集中隐藏 server-side key,又不想在每个业务进程内维护本地评估。官方Edge Proxy 指南说明它无外部依赖、拉取 environment document,并向客户端提供远程评估端点。最小 Compose 如下:
services:
flagsmith-edge:
image: flagsmith/edge-proxy:<validated-version>
ports:
- "8001:8000"
volumes:
- type: bind
source: ./config.json
target: /app/config.json
read_only: true{
"environment_key_pairs": [
{
"server_side_key": "ser.<server-side-key>",
"client_side_key": "<client-side-key>"
}
],
"api_url": "https://flagsmith.example.invalid/api/v1",
"api_poll_frequency_seconds": 10,
"api_poll_timeout_seconds": 2,
"allow_origins": "https://app.example.invalid",
"logging": {"log_level": "INFO", "log_format": "json"}
}config.json 含 server-side key,不应提交仓库;部署时从 Secret 渲染。下游 SDK 使用对应 client-side key,把 API URL 指向 Edge Proxy。/proxy/health/liveness 只证明进程活着,/proxy/health/readiness 还会判断环境文档是否在允许陈旧窗口内。绝不能把 readiness 当 liveness:上游短暂失败时重启代理会丢掉可服务的缓存,形成自我放大的重启循环。
Edge Proxy 是无状态进程,但每个副本都独立轮询每个环境;副本数翻倍,上游轮询量也翻倍。它可以缓存 flags 和 identities 端点响应,环境文档更新时清缓存。身份评估错误时先确认每次请求都发送完整 traits,因为代理不持久化调用之间的 trait。401 unknown key 检查下游是否误用 server-side key;代理日志中的上游 403 检查 ser. key、轮换状态和 api_url 是否指向正确实例。
Identity override 的删除至少要求 Edge Proxy v2.21.1;更早版本可能继续从缓存文档返回已删除 override。生产镜像必须固定到已验证的 v2.21.1 或更高版本,升级前用一组 identifier/traits 同时回放直连 API 与代理结果。Edge Proxy 自身没有持久数据库:进程运行中上游中断时可继续服务最后文档,全部副本同时冷启动且无法拉取文档时则不会凭空恢复旧状态。
离线文件与运行时缓存是两套机制
本地评估进程在成功拉取后可以继续使用内存中的 environment document,但这不等于重启后自动拥有 bootstrap。需要无网络冷启动时,应先用 Flagsmith CLI 生成受控环境文件,再让支持该能力的 SDK 进入 offlineMode 并配置 LocalFileHandler。Node.js 的最小形态如下:
import { Flagsmith, LocalFileHandler } from '@flagsmith/nodejs';
const offlineFlags = new Flagsmith({
offlineMode: true,
offlineHandler: new LocalFileHandler('./flagsmith.json'),
defaultFlagHandler: () => ({ enabled: false, value: null, isDefault: true }),
});离线模式会阻止 SDK 调用 Flagsmith API,文件不会自动刷新。它必须像配置制品一样带环境标识、来源 revision、校验和、生成时间和最大允许年龄,并通过只读挂载交付;不能把 server-side key、真实 identity 或 traits 一起打包。上线前先验证文件解析和代表性评估,再阻断网络重启进程;恢复在线模式时用新配置替换整个客户端,而不是让 offline 与 online 实例同时决定同一请求。
正向实验:trait 覆盖、稳定结果与可解释证据
建立 checkout_v2,环境默认关闭;segment paid-users 条件为 plan = pro,override 为开启且 value 为 {"theme":"compact"}。选固定 identity subject-1001:
远程评估传 plan=free,记录 enabled=false。同一 identity 传 plan=pro,记录 enabled=true 与 compact 配置。再次请求但不传 plan。远程模式可能继续使用已持久化的 pro trait;本地模式只使用本次 traits,预期回到环境默认。
在 Dashboard 增加 identity override 为关闭。等待规则传播后,无论 segment 是否命中,预期 identity override 获胜。删除 override,再观察直接 API、本地 SDK 或 Edge Proxy 是否在各自刷新窗口后恢复 segment 结果。
证据应包含匿名 subject、evaluation mode、flag、enabled、value schema version、default 标记和配置更新时间,不包含完整环境文档。若第三步在两种模式中完全相同,不要急着判定正确;先确认远程 identity 是否真的保存过 trait,以及本地调用是否意外复用了应用自己的用户属性缓存。
项目接入时把评估收口到一个领域适配器,而不是让几十个控制器直接调用 SDK:
export class CheckoutFeatures {
constructor(client, logger) {
this.client = client;
this.logger = logger;
}
async resolve(subject) {
try {
const flags = await this.client.getIdentityFlags(subject.id, {
plan: subject.plan,
region: subject.region,
});
const enabled = flags.isFeatureEnabled('checkout_v2');
const raw = flags.getFeatureValue('checkout_v2');
const config = raw ? JSON.parse(raw) : { theme: 'classic' };
this.logger.info({ flag: 'checkout_v2', enabled, source: 'flagsmith' });
return { enabled, config };
} catch (error) {
this.logger.warn({ flag: 'checkout_v2', source: 'fallback', reason: error.name });
return { enabled: false, config: { theme: 'classic' } };
}
}
}适配器负责稳定 identifier、最小 traits、超时、默认值、JSON schema 校验和低基数日志;业务代码只消费领域决策。单元测试用假 client 分别返回开启、关闭、非法 JSON 和异常,避免测试依赖真实控制台。
反向实验:让错误稳定暴露
第一组实验把 API URL 改到 http://127.0.0.1:9/api/v1/ 并用全新进程冷启动。远程评估必须在短超时后走 default handler,预期 enabled=false、source=fallback,进程不崩溃。若请求挂到网关总超时,说明 SDK 超时与重试预算超过了业务预算;若默认开启高风险写路径,说明 fallback 方向设计反了。
第二组实验先成功初始化本地评估,再阻断到上游 API 的网络。预期已加载的规则继续给出 last-known-good,日志或指标出现刷新失败与配置 age 增长;新规则不会生效。随后重启应用且不配置 offline handler;SDK 没有可用 environment document 时应明确返回默认值,不能把“运行中断网可用”误写成“冷启动断网可用”。最后使用经过校验的离线文件启动独立客户端,证明冷启动可按固定快照评估且没有任何 API 外联。恢复在线客户端后,证据是配置 age 归零、新规则在轮询窗口后出现,而不只是网络探针成功。
第三组实验故意把 plan 从字符串 pro 改成布尔值或完全省略。预期 segment 不命中并走环境默认。这个实验能抓出上下文 schema 漂移:身份存在不等于条件可计算,类型不匹配也不应被静默当成命中。
第四组实验给 value 写入非法 JSON 或缺少必填字段。SDK 取值成功不代表业务配置有效;适配器应拒绝非法值并回到安全配置,同时发出 flag_value_invalid_total{flag="checkout_v2"}。不要把原始 value 放进日志,它可能包含内部 URL 或商业参数。
分析存储、遥测与数据边界
Flag analytics 用于统计某个 flag 被评估或访问的情况,不等于业务转化分析。服务端 SDK 默认不启用 analytics,启用后会缓冲并批量发送数据;本地评估仍可发送 flag analytics,但依赖服务端保存 identity 的 analytics-based integrations 不会按远程模式工作。进程快速退出、网络阻断或队列溢出都可能让计数不完整。将 analytics 视为近似使用信号,不要用它结算账单或证明授权。进程退出时按所用 SDK 的能力 flush/close,并给退出阶段设置上限,避免为了发送分析而阻塞服务终止。
自托管时要单独决定分析数据放在哪里、异步处理器是否启用、保留多久,以及高基数会带来多少写入和存储成本。Postgres 是核心配置与身份数据的持久层,并不意味着所有分析能力都天然落在同一库、无需扩容。升级前应核对目标发行版的分析存储组件、队列和迁移要求;禁用分析时也要确认 SDK 没有通过第三方 integration 外发事件。
平台遥测与 flag analytics 是两类问题:前者可能描述自托管实例、版本或使用情况,后者描述应用评估。对隔离网络部署,应审计容器出站 DNS/HTTP、镜像更新检查、错误上报、邮件、OAuth 与第三方集成,显式关闭不允许的外联,并用防火墙流量证明,而不是由“self-hosted”三个字推断零外发。
identity identifier 和 traits 可能是个人信息。优先传不可逆的内部稳定 ID,不传邮箱、姓名、完整 IP;plan、region、tenant_tier 等字段也只保留评估所需粒度。建立 trait 字典,记录类型、来源、保留期、是否允许客户端提供。客户端可伪造的 trait 不能决定折扣、额度或权限。
故障排查从现象反推状态位置
“控制台已改,应用没变”按顺序检查:是否改了正确 project/environment;是否完成发布而非只保存草稿;SDK key 是否属于该环境;自托管 api_url 是否仍指 SaaS;本地评估或 Edge Proxy 的最后刷新时间、HTTP 状态与配置 age;identity override 是否盖过 segment;traits 类型和值是否完整;应用是否又缓存了最终决策。不要第一步就重启所有实例,这会毁掉 last-known-good 证据。
“同一用户反复换组”通常不是 Flagsmith 随机,而是 identifier 不稳定:匿名阶段用 session ID,登录后改 user ID;多个服务分别使用邮箱、数字 ID;或者每次生成 UUID。先在隐私安全的前提下记录 identifier 的哈希和规则版本,验证同一业务主体是否真的使用相同 key。匿名到登录需要明确合并策略,不能假设平台自动理解两者是同一人。
“本地与远程结果不同”重点检查 trait 持久化差异、完整上下文、规则文档版本、identity override 设置与引擎版本。将同一组 identifier/traits 固化成评估契约测试,在升级 SDK、API 或 Edge Proxy 前批量回放;只比较 enabled 不够,还要比较 value 与命中原因。
“延迟突然升高”先区分业务线程在做远程请求,还是本地评估规则复杂度上升。远程模式看上游延迟、连接池、超时与重试;本地模式看 environment document 大小、segment 数、trait 条件数和刷新锁竞争;Edge Proxy 看 CPU、请求缓存命中率、轮询失败与副本造成的上游压力。
从创建到删除的团队治理
每个 flag 创建时就填写 owner、类型、风险方向、默认值、预计固定时间和删除工单。release flag 用于短期发布,ops flag 用于运行控制,experiment flag 需要事件链,permission 不能由 flag 代替。命名应表达领域行为,如 checkout_v2,不要写 temp_new_flag。
生产变更采用四眼审批:操作者提交目标环境、规则差异、影响比例、观测指标和回滚值;审批者验证稳定 identifier、trait 来源、默认方向和密钥边界。紧急 kill switch 可以缩短审批,但事后仍要补审计。Dashboard 人工变更与 API 自动化必须进入同一审计轨迹。
放量从内部身份、低风险 segment 到小比例,再到全量。每一步先定义停止条件:错误率、延迟、业务拒绝率或人工投诉越线时,把 flag 恢复到旧值。回滚动作只改变配置,不能替代数据库和消息兼容;若新旧代码写入不同 schema,关闭 flag 也未必能撤销已写数据。
全量稳定后不要永久保留 if/else。先固定最终值,确认所有活跃版本都兼容;再删除旧分支和 SDK 引用;部署并观察;最后从控制台归档或删除 flag、segment 和专用 trait。删除顺序反过来会让旧实例命中“flag 不存在”默认值。季度盘点应从代码引用、分析访问、owner 活跃度和过期时间四条证据找僵尸 flag。
选 SaaS 还是自托管,最终要算责任:SaaS 把升级、数据库和全球边缘交给供应方,团队仍负责 SDK 默认值、隐私和治理;自托管获得数据驻留与网络控制,也接手 Postgres、迁移、容量、备份、TLS、管理员安全、遥测审计和灾难恢复;Edge Proxy 降低评估延迟与上游耦合,却增加代理发布、密钥、轮询和缓存陈旧度。最成熟的方案不是组件最多,而是每个失败点都有可预测返回值、可观测证据和演练过的恢复动作。
