正在加载,请稍候…

功能开关:规模化实施与管理

学习如何实施和管理功能开关以实现安全部署,涵盖百分比发布、用户定向、A/B测试及标志清理策略。

Feature Flags: Implementation and Management at Scale

功能开关:规模化实施与管理

功能开关(Feature Toggles)支持主干开发和控制发布。

简单的内存实现

interface FlagConfig {
  enabled: boolean;
  percentage?: number;      // 启用用户的百分比
  allowedUsers?: string[];  // 特定用户ID
  allowedGroups?: string[]; // 用户组
}

class FeatureFlagService {
  private flags: Map<string, FlagConfig>;

  constructor(config: Record<string, FlagConfig>) {
    this.flags = new Map(Object.entries(config));
  }

  isEnabled(flagName: string, context?: { userId?: string; groups?: string[] }): boolean {
    const flag = this.flags.get(flagName);
    if (!flag || !flag.enabled) return false;

    // 用户特定覆盖
    if (context?.userId && flag.allowedUsers?.includes(context.userId)) return true;

    // 基于组
    if (context?.groups && flag.allowedGroups?.some(g => context.groups?.includes(g))) return true;

    // 百分比发布(基于哈希以保证一致性)
    if (flag.percentage !== undefined && context?.userId) {
      const hash = this.hashUserId(context.userId);
      return (hash % 100) < flag.percentage;
    }

    return flag.enabled && !flag.percentage;
  }

  private hashUserId(userId: string): number {
    let hash = 0;
    for (const char of userId) {
      hash = ((hash << 5) - hash) + char.charCodeAt(0);
      hash |= 0;
    }
    return Math.abs(hash);
  }
}

Feature Flags: Implementation and Management at Scale illustration

LaunchDarkly 集成

import * as LaunchDarkly from '@launchdarkly/node-server-sdk';

const client = LaunchDarkly.init(process.env.LD_SDK_KEY!);

async function isFeatureEnabled(
  flagKey: string,
  user: { key: string; email?: string; groups?: string[] }
): Promise<boolean> {
  await client.waitForInitialization();

  return client.variation(flagKey, {
    key: user.key,
    email: user.email,
    custom: { groups: user.groups },
  }, false);
}

// 在 Express 中间件中使用
app.use(async (req, res, next) => {
  const userId = req.user?.id ?? 'anonymous';
  req.features = {
    newCheckout: await isFeatureEnabled('new-checkout-flow', { key: userId }),
    mlRecommendations: await isFeatureEnabled('ml-recommendations', { key: userId }),
  };
  next();
});

Feature Flags: Implementation and Management at Scale illustration

A/B 测试

class ABTestingService {
  async getVariant(
    testName: string,
    userId: string
  ): Promise<'control' | 'treatment'> {
    const hash = hashUserId(userId + testName);
    const variant = (hash % 2) === 0 ? 'control' : 'treatment';

    // 记录分配用于分析
    await analytics.track('ab_test_assignment', {
      userId,
      testName,
      variant,
      timestamp: new Date(),
    });

    return variant;
  }
}

// 在路由处理器中
const variant = await abTest.getVariant('checkout-button-color', userId);
const buttonColor = variant === 'treatment' ? '#00a651' : '#0066cc';

Feature Flags: Implementation and Management at Scale illustration

标志清理

// 技术债务:应移除的标志
// 添加带有过期日期的 TODO
const NEW_CHECKOUT = 'new-checkout-flow'; // TODO: 在 2025-03-01 前移除

// 自动检测过时标志
// 在 CI 中,检查超过 30 天的标志
#!/bin/bash
# 在代码中查找旧的功能标志
grep -r "featureFlags.isEnabled\|isFeatureEnabled" src/ |   grep -v "node_modules" |   awk -F"'" '{print $2}' |   sort -u

基于环境的标志

// 简单的环境变量标志(适用于基础设施/配置标志)
const FLAGS = {
  USE_NEW_DB: process.env.USE_NEW_DB === 'true',
  ENABLE_TRACING: process.env.ENABLE_TRACING === 'true',
};

// 但对于面向用户的功能,更推荐使用适当的标志服务

功能开关需要纪律:创建时就要计划好何时移除它们。