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)

字段类型说明
idSERIAL PK主键
flag_keyVARCHAR(100) UNIQUE唯一标识,代码中引用,如 'new-payment-flow'
nameVARCHAR(200)显示名称,如 '新支付流程'
descriptionTEXT描述此 flag 的目的和影响范围
enabledBOOLEAN全局总开关。false = 对所有人关闭
target_tenantsTEXT[]指定租户列表,如 {'spa001', 'salon002'}
rollout_percentINTEGER (0-100)灰度百分比,0=关闭,100=全量
created_byVARCHAR(100)创建者
created_atTIMESTAMPTZ创建时间
updated_atTIMESTAMPTZ更新时间

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 选择自建的原因: