灰度策略设计:五种主流切流方式
灰度策略怎么选?说白了,就是回答一个问题:你凭什么规则决定哪些用户走新版本,哪些走老版本?
我这些年经手的项目,灰度策略五花八门,但核心套路就五种。今天我把它们掰开揉碎讲清楚。每种策略我都会给出适用场景、代码骨架,以及我踩过的坑。
核心原则:灰度策略没有银弹。你选的策略,必须跟你的业务场景、基础设施、团队能力匹配。别为了炫技选复杂的,也别因为偷懒选错的。
2.1 基于用户ID的灰度
这是最常用的策略。把用户ID哈希后取模,根据余数决定走哪个版本。简单、可控、可追踪。
实现思路:
- 取用户ID(或手机号、邮箱)的哈希值
- 对100取模,得到0-99的区间
- 配置灰度比例,比如20%,则0-19走新版本,20-99走老版本
// 基于用户ID的灰度判断
public class UserIdGrayStrategy implements GrayStrategy {
private static final int TOTAL_BUCKETS = 100;
@Override
public boolean isInGray(String userId, int grayPercent) {
if (userId == null || userId.isEmpty()) {
return false; // 默认走老版本
}
int bucket = Math.abs(userId.hashCode()) % TOTAL_BUCKETS;
return bucket < grayPercent;
}
}
我的经验:哈希函数一定要选均匀分布的。我见过有人直接用userId.length()取模,结果短ID全进了灰度组,长ID全没进,灰度比例完全失真。用MD5或CityHash这类成熟算法,别自己造轮子。
优点:同一个用户始终走同一版本,体验一致。方便问题追踪——出问题了直接查用户ID就知道他在哪个版本。
缺点:用户ID必须稳定。如果用户未登录,或者ID会变(比如匿名用户),这招就不好使了。
2.2 基于IP/地域的灰度
按IP段或地理位置切流。适合做区域试点、合规测试、网络兼容性验证。
常见做法:
- IP白名单:公司内网IP、测试团队IP直接走新版本
- IP段匹配:比如10.0.0.0/8、172.16.0.0/12这些内网段
- 地域映射:通过IP库查城市/省份,只对特定地区开放
// 基于IP的灰度判断
public class IpGrayStrategy implements GrayStrategy {
private final Set<String> whiteListIps;
private final List<Subnet> graySubnets;
@Override
public boolean isInGray(String clientIp, String region) {
// 1. 先检查IP白名单
if (whiteListIps.contains(clientIp)) {
return true;
}
// 2. 再检查IP段
for (Subnet subnet : graySubnets) {
if (subnet.contains(clientIp)) {
return true;
}
}
// 3. 最后检查地域
return "beijing".equals(region) || "shanghai".equals(region);
}
}
我曾经踩过的坑:有一次做地域灰度,把"北京"写成了"beijing",结果北京用户全没进灰度组。排查了半天才发现是大小写问题。建议统一用小写,或者用城市编码(如110000)代替中文名。
注意:IP库不是100%准确的。移动端用户切WiFi或4G,IP会变。CDN回源IP也可能跟客户端真实IP不一样。记得取X-Forwarded-For头里的真实IP。
2.3 基于请求头的灰度
通过HTTP Header传递灰度标识。适合内部测试、A/B实验、金丝雀发布。
常用Header:
X-Gray-Version:指定目标版本号X-Gray-Tag:灰度标签,如canary、beta、stableX-User-Role:用户角色,如admin、vip、normalCookie:通过cookie中的灰度标记
// 基于请求头的灰度判断
public class HeaderGrayStrategy implements GrayStrategy {
private static final String GRAY_HEADER = "X-Gray-Tag";
@Override
public boolean isInGray(HttpRequest request, String expectedTag) {
String grayTag = request.getHeader(GRAY_HEADER);
if (grayTag == null) {
return false;
}
// 支持多个灰度标签,逗号分隔
return Arrays.asList(grayTag.split(","))
.contains(expectedTag);
}
}
我建议:请求头灰度一定要跟网关配合。在API Gateway或Ingress层统一注入灰度Header,业务代码只读Header做判断。这样灰度逻辑跟业务逻辑解耦,改灰度策略不用改代码。
场景举例:
- 测试人员用浏览器插件注入
X-Gray-Version: v2,直接验证新功能 - 线上1%的流量通过网关随机打上
X-Gray-Tag: canary,走金丝雀发布 - VIP用户请求头里带
X-User-Role: vip,优先体验新版本
2.4 基于权重的灰度
按比例随机分配流量。不依赖用户ID、IP等标识,纯粹的概率游戏。
实现方式:
- 随机数法:生成0-100的随机数,小于灰度比例则走新版本
- 加权轮询:给每个版本分配权重,按权重比例调度
- 一致性哈希:把请求哈希后映射到环上,按权重分配区间
// 基于权重的灰度判断(随机数法)
public class WeightGrayStrategy implements GrayStrategy {
private final Random random = new Random();
@Override
public boolean isInGray(int grayWeight, int totalWeight) {
// grayWeight: 新版本的权重
// totalWeight: 所有版本权重之和
int rand = random.nextInt(totalWeight);
return rand < grayWeight;
}
}
注意:随机数法有个问题——同一个用户的多次请求可能被分到不同版本。如果新版本有状态变更(比如改了数据库字段),用户会看到数据不一致。我建议:权重灰度只适合无状态场景,或者配合Session粘性使用。
权重灰度 vs 用户ID灰度:
| 对比维度 | 权重灰度 | 用户ID灰度 |
|---|---|---|
| 用户一致性 | 不保证 | 保证 |
| 实现复杂度 | 低 | 中 |
| 适用场景 | 无状态服务、静态资源 | 有状态服务、用户侧功能 |
| 问题排查 | 难(无法复现) | 易(按用户追踪) |
| 灰度比例调整 | 实时生效 | 实时生效 |
2.5 策略组合:实战中的选择
实际项目中,很少只用一种策略。我通常的做法是:分层组合。
举个例子:
- 第一层:基于请求头——内部测试人员直接走新版本
- 第二层:基于用户ID——5%的线上用户进入灰度组
- 第三层:基于地域——灰度组里只开放北京和上海
- 第四层:基于权重——灰度组内的流量,80%走新版本,20%走老版本(兜底)
这样组合的好处是:
- 内部测试不受灰度比例限制
- 线上用户按ID稳定分流
- 地域限制降低影响面
- 权重兜底防止新版本全量崩溃
// 组合策略示例
public class CompositeGrayStrategy {
private final List<GrayStrategy> strategies;
public boolean isInGray(GrayContext context) {
for (GrayStrategy strategy : strategies) {
if (!strategy.evaluate(context)) {
return false; // 任一策略不通过,则不走灰度
}
}
return true;
}
}
核心思路:灰度策略是层层过滤的漏斗。每一层只做一件事,组合起来形成完整的灰度决策链。这样每个策略都可以独立测试、独立调整,互不干扰。
2.6 灰度策略决策流程
下面这张图,是我做灰度系统时画的决策流程。你照着这个结构设计,基本不会出大问题。
你看这个流程:请求先过请求头,再过用户ID。任何一层没命中,直接走老版本。只有所有条件都满足,才进灰度组。这就是我前面说的漏斗模型。
我的习惯:灰度策略的配置一定要做成动态的,不要写死在代码里。用配置中心(Apollo、Nacos、Consul)管理灰度比例、白名单、地域列表。改配置不用重启服务,灰度调整就是改个数字的事。
好了,五种灰度策略讲完了。你可能会问:到底选哪种?我的建议是:从最简单的开始。先做基于用户ID的灰度,够用了就别加别的。等业务复杂了,再一层层往上叠。灰度策略不是越多越好,是越合适越好。