每天一道面试题之架构篇|动态功能开关(Feature Flag)系统架构设计

面试官直接问道:如果要设计一个支持百万级用户AB测试的动态功能开关系统,你会怎么设计?如何保证实时性和一致性?

一、开篇:为什么需要Feature Flag?

想象一下:周五晚上准备上线新功能,突然发现重大bug,是紧急回滚还是让部分用户体验降级?

传统发布的痛点

  • 发布时间固定:只能在低峰期发布
  • 回滚成本高:发现问题需要全量回滚
  • 测试覆盖有限:无法在生产环境真实测试
  • 用户体验一致:无法针对不同用户差异化发布

Feature Flag就像电灯开关,可以随时控制功能的开启和关闭,实现灰度发布、AB测试、紧急降级

二、核心架构设计

2.1 系统架构概览

四层架构设计

复制代码
[客户端SDK] -> [功能开关服务] -> [规则引擎] -> [数据存储]
    |               |               |               |
    v               v               v               v
[本地缓存]      [决策引擎]      [AB测试引擎]    [配置中心]
[降级策略]      [流量调度]      [数据分析]        [版本管理]

2.2 数据模型设计

核心数据模型

复制代码
@Data
public class FeatureFlag {
    private String key;                 // 功能唯一标识
    private String name;                // 功能名称
    private String description;         // 功能描述
    private boolean enabled;            // 是否全局启用
    private String rolloutStrategy;     // 发布策略
    private Map<String, Object> rules; // 规则配置
    private Date createdAt;            // 创建时间
    private Date updatedAt;            // 更新时间
}

@Data
public class TargetingRule {
    private String ruleId;             // 规则ID
    private String ruleType;           // 规则类型:用户ID、设备、地域等
    private List<String> included;     // 包含列表
    private List<String> excluded;     // 排除列表
    private int percentage;            // 流量百分比
    private Map<String, Object> attributes; // 自定义属性
}

@Data
public class EvaluationContext {
    private String userId;             // 用户ID
    private String deviceId;          // 设备ID
    private String version;           // 应用版本
    private String platform;          // 平台:iOS/Android/Web
    private String country;           // 国家地区
    private Map<String, Object> customAttributes; // 自定义属性
}

三、关键技术实现

3.1 核心服务实现

功能开关决策服务

复制代码
@Service
@Slf4j
public class FeatureFlagService {
    
    @Autowired
    private FeatureFlagRepository flagRepository;
    
    @Autowired
    private RuleEngine ruleEngine;
    
    private final Cache<String, FeatureFlag> localCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .build();
    
    // 判断功能是否对用户开启
    public boolean isEnabled(String featureKey, EvaluationContext context) {
        FeatureFlag featureFlag = getFeatureFlag(featureKey);
        
        if (featureFlag == null) {
            return false; // 功能不存在,默认关闭
        }
        
        if (!featureFlag.isEnabled()) {
            return false; // 功能全局关闭
        }
        
        // 应用规则引擎判断
        return ruleEngine.evaluate(featureFlag, context);
    }
    
    // 获取功能配置(带缓存)
    private FeatureFlag getFeatureFlag(String featureKey) {
        return localCache.get(featureKey, key -> 
            flagRepository.findByKey(featureKey).orElse(null));
    }
    
    // 批量判断多个功能
    public Map<String, Boolean> batchEvaluate(
        List<String> featureKeys, EvaluationContext context) {
        
        return featureKeys.stream()
            .collect(Collectors.toMap(
                key -> key,
                key -> isEnabled(key, context)
            ));
    }
    
    // 刷新本地缓存
    @Scheduled(fixedRate = 30000)
    public void refreshCache() {
        List<FeatureFlag> flags = flagRepository.findAllActive();
        flags.forEach(flag -> localCache.put(flag.getKey(), flag));
    }
}

3.2 规则引擎实现

多维度规则引擎

复制代码
@Component
@Slf4j
public class RuleEngine {
    
    private final Map<String, RuleEvaluator> evaluators = new ConcurrentHashMap<>();
    
    @PostConstruct
    public void init() {
        // 注册内置规则评估器
        registerEvaluator("user_id", new UserIdRuleEvaluator());
        registerEvaluator("percentage", new PercentageRuleEvaluator());
        registerEvaluator("device", new DeviceRuleEvaluator());
        registerEvaluator("version", new VersionRuleEvaluator());
        registerEvaluator("country", new CountryRuleEvaluator());
    }
    
    public void registerEvaluator(String ruleType, RuleEvaluator evaluator) {
        evaluators.put(ruleType, evaluator);
    }
    
    public boolean evaluate(FeatureFlag featureFlag, EvaluationContext context) {
        Map<String, Object> rules = featureFlag.getRules();
        
        // 如果没有规则,使用全局开关状态
        if (rules == null || rules.isEmpty()) {
            return featureFlag.isEnabled();
        }
        
        // 按优先级顺序评估规则
        for (Map.Entry<String, Object> entry : rules.entrySet()) {
            RuleEvaluator evaluator = evaluators.get(entry.getKey());
            if (evaluator != null) {
                Boolean result = evaluator.evaluate(entry.getValue(), context);
                if (result != null) {
                    return result; // 返回第一个匹配的结果
                }
            }
        }
        
        return false; // 默认不开启
    }
    
    // 用户ID规则评估器
    @Component
    public class UserIdRuleEvaluator implements RuleEvaluator {
        @Override
        public Boolean evaluate(Object ruleConfig, EvaluationContext context) {
            TargetingRule rule = parseTargetingRule(ruleConfig);
            
            if (rule.getIncluded().contains(context.getUserId())) {
                return true;
            }
            
            if (rule.getExcluded().contains(context.getUserId())) {
                return false;
            }
            
            return null; // 不匹配,继续下一个规则
        }
    }
    
    // 百分比规则评估器
    @Component
    public class PercentageRuleEvaluator implements RuleEvaluator {
        @Override
        public Boolean evaluate(Object ruleConfig, EvaluationContext context) {
            TargetingRule rule = parseTargetingRule(ruleConfig);
            
            // 基于用户ID的稳定哈希分桶
            int bucket = getConsistentBucket(context.getUserId(), 100);
            return bucket < rule.getPercentage();
        }
        
        private int getConsistentBucket(String userId, int totalBuckets) {
            // 使用一致性哈希算法确保用户始终在同一个分桶
            return Math.abs(userId.hashCode()) % totalBuckets;
        }
    }
}

3.3 AB测试集成

AB测试统计分析

复制代码
@Service
@Slf4j
public class ABTestService {
    
    @Autowired
    private MetricCollector metricCollector;
    
    @Autowired
    private ExperimentRepository experimentRepository;
    
    // 分配实验分组
    public String assignVariant(String experimentId, EvaluationContext context) {
        Experiment experiment = experimentRepository.findById(experimentId);
        
        if (experiment == null || !experiment.isRunning()) {
            return "control"; // 默认对照组
        }
        
        // 基于用户ID的稳定分组
        int hash = Math.abs(context.getUserId().hashCode());
        int bucket = hash % 100;
        
        for (Variant variant : experiment.getVariants()) {
            if (bucket < variant.getTrafficPercentage()) {
                return variant.getId();
            }
            bucket -= variant.getTrafficPercentage();
        }
        
        return "control";
    }
    
    // 记录实验指标
    public void trackConversion(String experimentId, String variantId, 
                               String eventName, double value) {
        MetricEvent event = new MetricEvent();
        event.setExperimentId(experimentId);
        event.setVariantId(variantId);
        event.setEventName(eventName);
        event.setValue(value);
        event.setTimestamp(new Date());
        
        // 异步上报指标
        metricCollector.collectAsync(event);
    }
    
    // 获取实验报告
    public ExperimentReport getReport(String experimentId) {
        List<MetricEvent> events = metricCollector.getEvents(experimentId);
        
        ExperimentReport report = new ExperimentReport();
        report.setExperimentId(experimentId);
        report.setStartDate(experimentRepository.findById(experimentId).getStartDate());
        report.setEndDate(new Date());
        
        // 计算各变体的统计指标
        Map<String, VariantStats> stats = calculateVariantStats(events);
        report.setVariantStats(stats);
        
        // 计算统计显著性
        calculateSignificance(report);
        
        return report;
    }
}

四、高级特性实现

4.1 实时配置更新

基于WebSocket的实时推送

复制代码
@Configuration
@EnableWebSocket
@Slf4j
public class RealTimeConfigUpdate implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new FeatureFlagWebSocketHandler(), "/ws/flags")
                .setAllowedOrigins("*")
                .withSockJS();
    }
    
    @Component
    public class FeatureFlagWebSocketHandler extends TextWebSocketHandler {
        
        private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
        
        @Override
        public void afterConnectionEstablished(WebSocketSession session) {
            String clientId = session.getHandshakeHeaders().getFirst("Client-Id");
            sessions.put(clientId, session);
            log.info("客户端连接建立: {}", clientId);
        }
        
        @Override
        protected void handleTextMessage(WebSocketSession session, TextMessage message) {
            // 处理客户端消息
            handleClientMessage(session, message.getPayload());
        }
        
        // 通知配置变更
        public void notifyConfigChange(String featureKey, FeatureFlag newConfig) {
            String message = createUpdateMessage(featureKey, newConfig);
            
            sessions.forEach((clientId, session) -> {
                try {
                    if (session.isOpen()) {
                        session.sendMessage(new TextMessage(message));
                    }
                } catch (IOException e) {
                    log.warn("发送配置更新失败: {}", clientId, e);
                    sessions.remove(clientId);
                }
            });
        }
        
        private String createUpdateMessage(String featureKey, FeatureFlag config) {
            return String.format("""
                {
                    "type": "config_update",
                    "featureKey": "%s",
                    "enabled": %b,
                    "timestamp": %d
                }
                """, featureKey, config.isEnabled(), System.currentTimeMillis());
        }
    }
}

4.2 权限管理与审计

基于RBAC的权限控制

复制代码
@Service
@Slf4j
public class PermissionService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private AuditService auditService;
    
    // 检查功能开关操作权限
    public boolean checkPermission(String userId, String action, String resource) {
        User user = userRepository.findById(userId);
        
        if (user == null) {
            return false;
        }
        
        // 获取用户角色和权限
        Set<String> userPermissions = getUserPermissions(user);
        
        // 检查权限
        boolean hasPermission = userPermissions.contains(
            String.format("%s:%s", action, resource));
        
        // 记录审计日志
        auditService.logAccess(userId, action, resource, hasPermission);
        
        return hasPermission;
    }
    
    // 功能开关操作拦截器
    @Aspect
    @Component
    public class PermissionAspect {
        
        @Around("@annotation(RequiresPermission)")
        public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            RequiresPermission annotation = signature.getMethod()
                .getAnnotation(RequiresPermission.class);
            
            String action = annotation.action();
            String resource = annotation.resource();
            
            // 从安全上下文中获取当前用户
            String currentUserId = SecurityContext.getCurrentUserId();
            
            if (!permissionService.checkPermission(currentUserId, action, resource)) {
                throw new AccessDeniedException("权限不足");
            }
            
            return joinPoint.proceed();
        }
    }
    
    // 审计日志服务
    @Service
    @Slf4j
    public class AuditService {
        
        @Autowired
        private AuditLogRepository auditLogRepository;
        
        public void logAccess(String userId, String action, String resource, 
                            boolean success, String details) {
            AuditLog log = new AuditLog();
            log.setUserId(userId);
            log.setAction(action);
            log.setResource(resource);
            log.setSuccess(success);
            log.setDetails(details);
            log.setTimestamp(new Date());
            log.setIpAddress(RequestContext.getClientIp());
            
            auditLogRepository.save(log);
        }
    }
}

五、完整架构示例

5.1 系统架构图

复制代码
[客户端SDK] -> [API网关] -> [功能开关服务] -> [规则引擎] -> [配置存储]
    |              |             |              |             |
    v              v             v              v             v
[本地缓存]    [身份认证]    [决策服务]      [AB测试引擎]   [版本管理]
[降级策略]    [限流防护]    [实时推送]      [数据分析]     [审计日志]

5.2 配置示例

复制代码
# application-feature.yml
feature:
  flag:
    cache:
      enabled: true
      type: caffeine
      maximum-size: 1000
      expire-after-write: 60s
    
    rule:
      evaluators:
        - type: user_id
          priority: 1
        - type: percentage  
          priority: 2
        - type: device
          priority: 3
    
    ab-test:
      enabled: true
      assignment-method: consistent_hash
      min-sample-size: 1000
      significance-level: 0.05
    
    security:
      rbac:
        enabled: true
        admin-roles: [admin, super_admin]
        editor-roles: [editor, product_manager]
        viewer-roles: [viewer, developer]
    
    audit:
      enabled: true
      retention-days: 90
      sensitive-actions: [create, delete, update_permission]

六、面试陷阱与加分项

6.1 常见陷阱问题

问题1:"客户端和服务端规则判断不一致怎么办?"

参考答案

  • 使用相同的一致性哈希算法
  • 定期同步规则版本和配置
  • 客户端降级策略和服务端兜底
  • 实时监控和告警机制

问题2:"如何保证高并发下的性能?"

参考答案

  • 多级缓存:本地缓存+分布式缓存
  • 异步决策和批量处理
  • 连接池和资源优化
  • 水平扩展和负载均衡

问题3:"功能开关泄露导致安全问题怎么办?"

参考答案

  • 严格的权限控制和审计
  • 敏感功能二次确认
  • 操作日志和变更追踪
  • 定期安全审计和漏洞扫描

6.2 面试加分项

  1. 业界最佳实践
    • LaunchDarkly:专业的Feature Flag服务
    • 阿里云功能开关:集成AB测试和监控
    • 谷歌优化工具:强大的实验分析平台
  2. 高级特性
    • 渐进式发布:逐步放大流量比例
    • 定向发布:按用户属性精准投放
    • 实验分析:自动计算统计显著性
  3. 云原生支持
    • Kubernetes Operator自动部署
    • Service Mesh集成
    • 多集群多地域支持

七、总结与互动

Feature Flag设计哲学配置要实时,决策要精准,发布要可控,数据要驱动------四大原则构建智能功能开关系统

记住这个架构公式:实时推送 + 多级缓存 + 规则引擎 + 数据分析 = 完美功能开关平台


思考题:在你的业务系统中,最复杂的发布场景是什么?欢迎在评论区分享实战经验!

关注我,每天搞懂一道面试题,助你轻松拿下Offer!

相关推荐
renke33644 小时前
Flutter 2025 模块化与微前端工程体系:从单体到可插拔架构,实现高效协作、独立交付与动态加载的下一代应用结构
前端·flutter·架构
小韩博4 小时前
小迪第42课:PHP应用&MYSQL架构&SQL注入&跨库查询&文件读写&权限操作
sql·mysql·网络安全·架构·php
小股虫5 小时前
从Tair虚拟桶到数据库分库分表:解耦逻辑与物理的架构艺术
数据库·架构·解耦
是一个Bug5 小时前
云原生架构
云原生·架构
lusasky5 小时前
基于 LangChain 的海量 API 动态检索与调用架构
网络·架构·langchain
dylanstudy6 小时前
【Droidrun自然语言控制Android和iOS设备】
ai·智能手机·系统架构
Gavin在路上6 小时前
企业架构之深度解析企业架构与流程架构的共生关系(3)
架构
GIOTTO情6 小时前
Infoseek 危机公关系统技术实现深度解析:AI 驱动的全链路舆情处置架构与工程实践
人工智能·架构
Joy T7 小时前
【快速入门】提示工程(PE,Prompt Engineering):大模型时代的自然语言编程范式
架构·llm·prompt·人机交互