Feature Flag 灰度发布系统
自建 Feature Flag 方案 - 原理、架构与使用指南
1. 什么是 Feature Flag
Feature Flag(功能开关)是一种在代码中通过条件判断控制功能是否可用的技术。核心思想:新旧代码同时部署,但通过外部开关控制谁走哪条路径。
// 示例:后端路由
if (await isFeatureOn(req, 'new-payment-flow')) {
return processPaymentV2(req, res); // 新逻辑
}
return processPaymentV1(req, res); // 旧逻辑(已稳定)
开关状态存储在数据库中,可通过管理界面实时修改,无需重新部署。
2. 相关概念对比
| 概念 |
目标 |
粒度 |
在 Celoria 中的实现 |
| Feature Flag |
控制功能开关,随时开/关 |
代码路径级 |
自建 feature_flags 表 + 中间件 |
| 灰度发布 (Canary) |
新版本先给一小部分流量 |
部署/基础设施级 |
Feature Flag + 租户维度控制 |
| A/B Testing |
对比两个方案的业务指标 |
用户体验级 |
Feature Flag + 数据分析(未来扩展) |
| Alpha/Beta |
内部 → 受邀 → 全量 |
用户群体级 |
Feature Flag + target_tenants 字段 |
核心认知:Feature Flag 是基础设施,灰度发布、A/B Testing、Alpha/Beta 都建立在它之上。
3. 系统架构
graph TB
subgraph "Platform Admin 管理界面"
UI[Feature Flag 管理页面]
end
subgraph "后端 API"
API["/api/platform/feature-flags"]
MW["中间件: isFeatureOn(req, key)"]
CACHE["内存缓存 (30s TTL)"]
end
subgraph "数据库 (public schema)"
DB["feature_flags 表"]
end
subgraph "业务代码"
BE["后端路由 if/else"]
FE["前端组件 if/else"]
end
UI -->|"CRUD 操作"| API
API -->|"读写"| DB
MW -->|"查询(带缓存)"| DB
MW -->|"缓存结果"| CACHE
BE -->|"调用"| MW
FE -->|"通过 API 获取 flag 状态"| API
4. 数据库设计
feature_flags 表 (public schema)
| 字段 | 类型 | 说明 |
| id | SERIAL PK | 主键 |
| flag_key | VARCHAR(100) UNIQUE | 唯一标识,代码中引用,如 'new-payment-flow' |
| name | VARCHAR(200) | 显示名称,如 '新支付流程' |
| description | TEXT | 描述此 flag 的目的和影响范围 |
| enabled | BOOLEAN | 全局总开关。false = 对所有人关闭 |
| target_tenants | TEXT[] | 指定租户列表,如 {'spa001', 'salon002'} |
| rollout_percent | INTEGER (0-100) | 灰度百分比,0=关闭,100=全量 |
| created_by | VARCHAR(100) | 创建者 |
| created_at | TIMESTAMPTZ | 创建时间 |
| updated_at | TIMESTAMPTZ | 更新时间 |
5. 判断逻辑:三层过滤
flowchart TD
START["isFeatureOn(req, 'flag-key')"] --> CHECK_ENABLED{"enabled = true?"}
CHECK_ENABLED -->|No| RETURN_FALSE_1["return false - 总开关关闭"]
CHECK_ENABLED -->|Yes| CHECK_TARGETS{"target_tenants 有值?"}
CHECK_TARGETS -->|Yes| IN_LIST{"当前租户在列表中?"}
IN_LIST -->|Yes| RETURN_TRUE_1["return true"]
IN_LIST -->|No| RETURN_FALSE_2["return false"]
CHECK_TARGETS -->|"No (空数组)"| CHECK_PERCENT{"rollout_percent?"}
CHECK_PERCENT -->|"100"| RETURN_TRUE_2["return true - 全量"]
CHECK_PERCENT -->|"0"| RETURN_FALSE_3["return false"]
CHECK_PERCENT -->|"1-99"| HASH["hash(tenant_id) % 100 < percent?"]
HASH -->|Yes| RETURN_TRUE_3["return true"]
HASH -->|No| RETURN_FALSE_4["return false"]
style RETURN_TRUE_1 fill:#2ecc71,color:#000
style RETURN_TRUE_2 fill:#2ecc71,color:#000
style RETURN_TRUE_3 fill:#2ecc71,color:#000
style RETURN_FALSE_1 fill:#e74c3c,color:#fff
style RETURN_FALSE_2 fill:#e74c3c,color:#fff
style RETURN_FALSE_3 fill:#e74c3c,color:#fff
style RETURN_FALSE_4 fill:#e74c3c,color:#fff
百分比灰度的原理
使用租户 ID 的哈希值来决定是否命中灰度:
function isInRollout(tenantId, percent) {
// 对 tenant_id 做哈希,得到 0-99 的数字
const hash = simpleHash(tenantId) % 100;
return hash < percent;
}
为什么用哈希而不是随机数?因为同一个租户每次请求必须得到相同的结果。如果用随机数,同一个租户的不同请求可能一会走新逻辑一会走旧逻辑,这会导致数据不一致。
6. 缓存机制
每个 API 请求都查数据库太浪费。中间件会把 flag 数据缓存在内存中:
| 机制 | 说明 |
| 缓存位置 | Node.js 进程内存 (Map) |
| 刷新间隔 | 30 秒 |
| 生效延迟 | 修改 flag 后最多 30 秒全部节点生效 |
| 强制刷新 | 管理 API 修改 flag 后主动清除缓存 |
7. 灰度发布工作流
gantt
title 功能灰度发布时间线
dateFormat YYYY-MM-DD
section 开发
编写代码(flag 后面) :done, dev, 2026-01-01, 3d
section 部署
部署到生产(flag=off) :done, deploy, after dev, 1d
section Alpha
内部租户测试 :active, alpha, after deploy, 4d
section Beta
友好租户测试 :beta, after alpha, 7d
section Canary
30% 租户灰度 :canary, after beta, 7d
section GA
全量发布 100% :ga, after canary, 3d
section 清理
删除旧代码和 flag :cleanup, after ga, 2d
每个阶段的操作
| 阶段 | flag 配置 | 影响范围 |
| 部署 |
enabled: false |
无人受影响,新代码静默上线 |
| Alpha |
enabled: true, target_tenants: ['spa001'] |
仅内部门店 |
| Beta |
target_tenants: ['spa001', 'salon002', 'nail003'] |
3 个友好租户 |
| Canary |
target_tenants: [], rollout_percent: 30 |
30% 的租户 |
| GA |
rollout_percent: 100 |
全部租户 |
| 清理 |
删除 flag,删除旧代码路径 |
代码精简 |
重要:清理旧 flag!全量发布稳定后必须清理旧代码路径和 flag 记录。否则代码会变成意大利面条,到处都是 if/else 分支。建议每个 flag 设定一个预期清理日期。
8. 系统文件清单
| 文件 | 职责 |
backend/database/migrations/xxx_create-feature-flags.js |
数据库迁移:建表 |
backend/middleware/feature-flags.js |
核心中间件:导出 isFeatureOn(req, flagKey) 函数,含缓存逻辑 |
backend/api/platform/feature-flags.js |
管理 API:CRUD 操作,权限 platform:manage_tenants |
前端 platform 菜单下 |
管理页面:创建、编辑、开关、删除 flag |
管理 API 端点
| 方法 | 端点 | 说明 |
| GET | /api/platform/feature-flags | 列出所有 flag |
| POST | /api/platform/feature-flags | 创建新 flag |
| PATCH | /api/platform/feature-flags/:id | 修改 flag(开关、灰度配置等) |
| DELETE | /api/platform/feature-flags/:id | 删除 flag |
9. 使用示例
后端使用
const { isFeatureOn } = require('../middleware/feature-flags');
// API 路由中
router.post('/api/appointments', async (req, res) => {
if (await isFeatureOn(req, 'new-booking-flow')) {
return createAppointmentV2(req, res);
}
return createAppointmentV1(req, res);
});
// Service 层中
async function calculatePrice(req, items) {
if (await isFeatureOn(req, 'dynamic-pricing')) {
return dynamicPriceCalculation(items);
}
return staticPriceCalculation(items);
}
前端使用
// 通过 API 获取当前租户的 flag 状态
const { data: flags } = useQuery(['feature-flags'], fetchFlags);
// 组件中判断
function BookingPage() {
if (flags?.['new-booking-ui']) {
return <NewBookingUI />;
}
return <LegacyBookingUI />;
}
10. 业界对比:为什么自建
| 自建方案 | Unleash (开源) | LaunchDarkly (商业) |
| 费用 | 零 | 零(自托管) | $10/月/席位起 |
| 额外服务 | 无 | 需要 Docker 容器 | SaaS 云服务 |
| 功能 | Flag + 租户灰度 | 完整 Flag 管理 | 企业级全套 |
| 复杂度 | 极低(200行代码) | 中等 | 低(但有依赖) |
| 适合阶段 | 早期/中小规模 | 中等规模 | 大规模团队 |
Celoria 选择自建的原因:
- 多租户架构天然适合按租户灰度(不需要复杂的用户分群)
- 租户数量有限,不需要复杂的百分比灰度引擎
- 已有 PostgreSQL + Express + Next.js 全套基础设施
- 不想维护额外的 Docker 服务
- 未来如需升级,可迁移到 Unleash/GrowthBook