灰度切流:按用户 / 按租户 / 按百分比

一句话速记

灰度发布(Canary Release)= 把新版本先暴露给少量用户,观察效果,逐步扩大,有问题立即回滚,确保大规模故障不上线。核心设计要素:路由规则(谁能看到)+ 流量比例控制(多少人能看到)+ 监控告警(怎么知道出问题了)+ 快速回滚(出问题后怎么恢复)。

关键细节

1)灰度策略类型

按用户灰度

适用:ToC 产品(用户量大,需要平滑验证)

方法 1:Hash 分桶
  user_id % 100 = bucket_id
  bucket_id < 5 → 新版本(5% 流量)
  bucket_id >= 5 → 旧版本
  
  优点:确定性(同一用户每次进同一桶),无需存储
  缺点:bucket 分配不完全均匀(如果 user_id 不均匀)

方法 2:白名单(内测用户)
  List<Long> betaUsers = [1001, 1002, 1003, ...];
  if (betaUsers.contains(userId)) → 新版本
  
  适用:先给内部员工/KOL/种子用户测试

方法 3:用户属性(精准圈选)
  城市 = '北京' AND 注册天数 > 30 → 新版本
  适用:特定用户群体(如新功能对某城市先开放)

按租户灰度(ToB 产品)

多租户 SaaS 产品(如企业微信功能灰度)

tenant_id 维度:
  租户 A(小公司,愿意体验新功能)→ 新版本
  租户 B(大企业,保守)→ 旧版本
  
配置中心管理:
  grayscale_tenants: ["tenant-001", "tenant-002"]  # YAML 配置
  
  启用逻辑:
  if tenant_id in grayscale_tenants → 新功能
  
优点:ToB 场景下影响面可控(一个租户出问题,不影响其他租户)

按百分比灰度(流量分割)

不区分用户,随机按比例分流

方法:
  随机数分流:random() < 0.1 → 新版本(10% 随机流量)
  
  问题:同一用户可能一次新版、一次旧版(体验不一致)
  解决:结合 user_id Hash(保证用户粘性)

流量比例调整计划:
  Day 1:1%(内部验证)
  Day 2:5%(小规模用户验证)
  Week 1:10%
  Week 2:30%
  Week 3:100%(全量放开)
  
  遇到问题 → 立即回退到上一步百分比(快速回滚)

2)工程实现

网关层路由(推荐,统一控制)

// Spring Cloud Gateway 自定义过滤器
@Component
public class GrayRouteFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private GrayRouteConfig grayConfig;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String userId = request.getHeaders().getFirst("X-User-Id");
        
        // 灰度判断
        if (isGrayUser(userId)) {
            // 路由到灰度服务实例
            URI newUri = URI.create("http://service-v2:" + request.getURI().getPath());
            ServerHttpRequest newRequest = request.mutate().uri(newUri).build();
            return chain.filter(exchange.mutate().request(newRequest).build());
        }
        
        return chain.filter(exchange);  // 路由到正式服务
    }
    
    private boolean isGrayUser(String userId) {
        if (userId == null) return false;
        // 白名单优先
        if (grayConfig.getWhitelist().contains(userId)) return true;
        // Hash 分桶
        int bucket = Math.abs(userId.hashCode()) % 100;
        return bucket < grayConfig.getPercent();  // 如:10 = 10%
    }
}

配置中心动态调整(无需发布)

# Nacos / Apollo 配置(可动态推送)
grayscale:
  enabled: true
  percent: 10          # 流量百分比(随时调整)
  whitelist:           # 白名单(优先级高于 percent)
    - "user-1001"
    - "user-2002"
  tenant_list:         # 租户白名单
    - "tenant-abc"
@RefreshScope  // Apollo/Nacos 支持动态刷新
@ConfigurationProperties(prefix = "grayscale")
public class GrayConfig {
    private boolean enabled;
    private int percent;
    private List<String> whitelist;
    private List<String> tenantList;
}

服务端灰度(功能级开关)

// Feature Flag 模式(比网关路由粒度更细)
public class FeatureFlags {
    
    @Autowired
    private FlagConfig flagConfig;
    
    public boolean isNewAlgorithmEnabled(Long userId) {
        // 先查用户白名单
        if (flagConfig.getWhitelistUsers().contains(userId)) return true;
        
        // 按 userId Hash 分桶
        int bucket = (int)(userId % 100);
        return bucket < flagConfig.getNewAlgorithmPercent();
    }
}
 
// 使用:
@Service
public class RecommendService {
    public List<Item> recommend(Long userId) {
        if (featureFlags.isNewAlgorithmEnabled(userId)) {
            return newAlgorithm.recommend(userId);    // 新算法
        }
        return oldAlgorithm.recommend(userId);       // 旧算法
    }
}

3)监控与自动回滚

灰度监控指标

对比新旧版本:
  错误率(5xx):灰度 vs 非灰度
  响应时间 p99:灰度 vs 非灧度
  核心业务指标:下单转化率、支付成功率(不能因灰度下降)
  
告警阈值(自动触发):
  灰度错误率 > 旧版本 2 倍 → 立即回滚(自动)
  灰度 p99 延迟 > 旧版本 1.5 倍 → 告警(人工决策)

快速回滚

网关层回滚(秒级):
  修改配置:grayscale.percent = 0(立即停止灰度)
  配置中心动态推送(无需发布代码)
  
K8s 层回滚:
  kubectl rollout undo deployment/service-v2
  或:把 canary 部署的实例数调为 0
  
功能开关回滚:
  featureFlag.setEnabled(false)(关闭功能,旧版代码自动生效)

4)蓝绿发布 vs 灰度发布

蓝绿发布(Blue-Green):
  同时运行两个完整环境(蓝=旧,绿=新)
  切换:DNS/负载均衡一次性把流量切到绿
  优点:切换和回滚都是秒级(DNS 切换)
  缺点:需要双倍资源
  适用:数据库无 schema 变更,或 schema 向前兼容的场景

灰度发布(Canary):
  新旧版本同时运行,逐步增加新版本流量
  优点:风险可控,逐步验证
  缺点:两个版本同时维护的复杂性(DB 要兼容两个版本的操作)
  适用:大多数互联网发布场景

金丝雀部署(Canary Deploy):
  K8s 中:主 Deployment + Canary Deployment
  通过 Service 的权重(Nginx Ingress / Istio)控制流量比

延伸追问

  • Q:灰度期间,新旧版本共用同一个数据库,如何避免数据兼容性问题? → 数据库变更必须向前兼容(Expand-Contract 模式):先 ADD COLUMN(不 NOT NULL)→ 新代码写新列、旧代码不感知旧列 → 验证完成后 DROP 旧列。避免 RENAME COLUMN、改数据类型等破坏性变更在灰度期间上线。
  • Q:灰度比例 10% 时,出了问题,10% 用户的数据怎么处理? → 这是灰度最大的风险:数据类问题(写入了脏数据)很难回滚。因此:1) 灰度前做好数据兼容性设计;2) 优先选”读”功能灰度(不写数据);3) 写数据灰度时,保证旧代码能兼容新格式数据(向后兼容);4) 出问题后需要手动数据修复(DBA 支持)。
  • Q:如何实现”同一用户每次都路由到同一版本”(会话黏性)? → 用 userId Hash 分桶(确定性),相同 userId 每次哈希值相同,桶号不变,版本不变。避免用 sessionId(会话到期后会变)。也可以在第一次访问时,在 Cookie 中写入灰度标记(gray=1),后续根据 Cookie 路由。

我的记法

  • 灰度三要素:路由规则(谁)+ 比例控制(多少)+ 监控回滚(兜底)
  • 按用户:userId Hash % 100 < percent → 新版本(确定性+无状态)
  • 按租户:tenantId 白名单(ToB 常用)
  • 回滚:配置中心动态推送,percent=0,秒级生效
  • 蓝绿 = 双环境一次切换;灰度 = 单环境逐步放量
  • 一句话:「灰度 = Hash 分桶路由 + 动态配置控比例 + 监控自动回滚」

状态

  • 已背速记
  • 能写 userId Hash 分桶代码
  • 能解释灰度期间数据库兼容性处理