← 返回资讯
陈默
AI 行业分析师
已审核

Feature Flags 实战:渐进式发布与 A/B 测试

title: "Feature Flags 实战:渐进式发布与 A/B 测试"

Feature Flags 实战:渐进式发布与 A/B 测试

title: "Feature Flags 实战:渐进式发布与 A/B 测试"

date: "2026-07-10"

tags: ["Feature Flags", "发布策略", "A/B测试", "DevOps"]


Feature Flags 实战:渐进式发布与 A/B 测试

Feature Flags(功能开关)是现代软件交付的核心实践。它让你能在不部署新代码的情况下控制功能的启用和禁用。

基础实现

简单开关

PYTHON
# feature_flags.py
from dataclasses import dataclass
from typing import Optional

@dataclass
class FeatureFlag:
    name: str
    enabled: bool
    rollout_percentage: float = 100.0
    target_users: list[str] = None

class FeatureFlagManager:
    def __init__(self):
        self.flags: dict[str, FeatureFlag] = {}
    
    def register(self, flag: FeatureFlag):
        self.flags[flag.name] = flag
    
    def is_enabled(self, flag_name: str, user_id: Optional[str] = None) -> bool:
        flag = self.flags.get(flag_name)
        if not flag or not flag.enabled:
            return False
        
        # 目标用户列表
        if flag.target_users and user_id:
            return user_id in flag.target_users
        
        # 百分比灰度
        if flag.rollout_percentage < 100 and user_id:
            hash_value = hash(f"{flag_name}:{user_id}") % 100
            return hash_value < flag.rollout_percentage
        
        return True

# 使用
manager = FeatureFlagManager()
manager.register(FeatureFlag(
    name="new_checkout_flow",
    enabled=True,
    rollout_percentage=10  # 10% 用户
))

@app.get("/checkout")
async def checkout(user: User = Depends(get_current_user)):
    if manager.is_enabled("new_checkout_flow", user.id):
        return new_checkout_page()
    else:
        return old_checkout_page()

数据库存储

PYTHON
import json
from datetime import datetime

class DatabaseFeatureFlags:
    def __init__(self, db):
        self.db = db
        self.cache = {}
        self.cache_ttl = 60  # 秒
    
    async def get_flag(self, name: str) -> Optional[dict]:
        # 检查缓存
        if name in self.cache:
            cached = self.cache[name]
            if datetime.utcnow().timestamp() - cached["time"] < self.cache_ttl:
                return cached["data"]
        
        # 从数据库获取
        result = await self.db.fetch_one(
            "SELECT * FROM feature_flags WHERE name = $1 AND active = true",
            name
        )
        
        if result:
            data = {
                "enabled": result["enabled"],
                "rollout_percentage": result["rollout_percentage"],
                "conditions": json.loads(result["conditions"] or "{}")
            }
            self.cache[name] = {"data": data, "time": datetime.utcnow().timestamp()}
            return data
        
        return None
    
    async def is_enabled(self, name: str, context: dict = None) -> bool:
        flag = await self.get_flag(name)
        if not flag or not flag["enabled"]:
            return False
        
        context = context or {}
        conditions = flag["conditions"]
        
        # 用户白名单
        if "whitelist" in conditions:
            if context.get("user_id") in conditions["whitelist"]:
                return True
        
        # 用户属性条件
        if "user_segment" in conditions:
            segment = conditions["user_segment"]
            if context.get("segment") != segment:
                return False
        
        # 百分比灰度
        percentage = flag["rollout_percentage"]
        if percentage < 100:
            user_id = context.get("user_id", "")
            hash_val = hash(f"{name}:{user_id}") % 100
            return hash_val < percentage
        
        return True

渐进式发布

PYTHON
class ProgressiveRollout:
    def __init__(self, flag_manager):
        self.flag_manager = flag_manager
    
    async def start_rollout(self, flag_name: str, stages: list[dict]):
        """
        stages = [
            {"percentage": 1, "duration_hours": 24},
            {"percentage": 5, "duration_hours": 24},
            {"percentage": 25, "duration_hours": 48},
            {"percentage": 50, "duration_hours": 48},
            {"percentage": 100, "duration_hours": 0}
        ]
        """
        for stage in stages:
            await self.flag_manager.update_rollout(
                flag_name,
                stage["percentage"]
            )
            
            # 监控指标
            metrics = await self.monitor_metrics(flag_name)
            
            if metrics["error_rate"] > 0.01:  # 错误率超过 1%
                await self.rollback(flag_name)
                raise Exception(f"Rollback triggered: error rate {metrics['error_rate']}")
            
            if stage["duration_hours"] > 0:
                await asyncio.sleep(stage["duration_hours"] * 3600)
    
    async def rollback(self, flag_name: str):
        await self.flag_manager.update_rollout(flag_name, 0)

A/B 测试

PYTHON
import hashlib

class ABTest:
    def __init__(self, test_name: str, variants: list[str], weights: list[float]):
        self.test_name = test_name
        self.variants = variants
        self.weights = weights  # 权重之和应为 100
    
    def get_variant(self, user_id: str) -> str:
        """根据用户 ID 确定性地分配变体"""
        hash_input = f"{self.test_name}:{user_id}"
        hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16) % 100
        
        cumulative = 0
        for variant, weight in zip(self.variants, self.weights):
            cumulative += weight
            if hash_value < cumulative:
                return variant
        
        return self.variants[-1]

# 使用
checkout_test = ABTest(
    test_name="checkout_redesign",
    variants=["control", "variant_a", "variant_b"],
    weights=[50, 25, 25]  # 50% 对照,25% A,25% B
)

@app.get("/checkout")
async def checkout(user: User = Depends(get_current_user)):
    variant = checkout_test.get_variant(user.id)
    
    # 记录曝光
    await log_exposure(user.id, "checkout_redesign", variant)
    
    if variant == "control":
        return render_checkout_v1()
    elif variant == "variant_a":
        return render_checkout_v2a()
    else:
        return render_checkout_v2b()

分析结果

PYTHON
async def analyze_ab_test(test_name: str) -> dict:
    """分析 A/B 测试结果"""
    results = await db.fetch_all("""
        SELECT
            variant,
            COUNT(*) as exposures,
            SUM(CASE WHEN converted THEN 1 ELSE 0 END) as conversions
        FROM ab_test_events
        WHERE test_name = $1
        GROUP BY variant
    """, test_name)
    
    analysis = {}
    for row in results:
        conversion_rate = row["conversions"] / row["exposures"] if row["exposures"] > 0 else 0
        analysis[row["variant"]] = {
            "exposures": row["exposures"],
            "conversions": row["conversions"],
            "conversion_rate": conversion_rate
        }
    
    # 计算统计显著性(简化版)
    control = analysis.get("control", {})
    for variant_name, data in analysis.items():
        if variant_name == "control":
            continue
        
        lift = (data["conversion_rate"] - control["conversion_rate"]) / control["conversion_rate"]
        data["lift"] = lift
    
    return analysis

最佳实践

| 实践 | 说明 |

|------|------|

| 命名规范 | feature:描述,如 checkout:new_flow |

| 定期清理 | 已全量的功能应移除 Flag |

| 监控指标 | 每个 Flag 关联关键指标 |

| 文档化 | 记录每个 Flag 的目的和负责人 |

| 测试覆盖 | 测试 Flag 开启和关闭两种状态 |

Feature Flags 让发布变得可控、可逆。它是持续交付和渐进式发布的基础设施。

49
2480 阅读
2 评论
分享
链接已复制
编辑说明

本文由 MakeSense 编辑团队撰写并审核。文中引用的数据和观点均经过交叉验证,如有疏漏欢迎在评论区指正。最后更新:2026年07月11日 09:06

陈默

AI 行业分析师

前某大厂 AI 实验室研究员,关注大模型技术演进和商业化落地。写过 200+ 篇行业分析,擅长从产品视角拆解技术趋势。

读者评论 2

张工 1周前
写得很实在,特别是实测对比那部分,跟我自己的使用感受一致。
回复 点赞 (12)
前端工程师 2天前
代码示例很清晰,直接用到项目里了。
回复 点赞 (6)