灰度/金丝雀发布怎么做?

1. 概述

1.1 灰度/金丝雀发布的重要性

灰度发布(Gray Release)金丝雀发布(Canary Release)是微服务架构中重要的发布策略,用于降低发布风险,提高系统稳定性。

发布策略的意义

  • 降低发布风险:逐步发布,及时发现和解决问题
  • 提高系统稳定性:避免全量发布导致的系统故障
  • 快速回滚:发现问题后快速回滚,减少影响范围
  • 用户体验:保证大部分用户不受影响

1.2 发布策略分类

常见发布策略

  1. 全量发布(Rolling Release):一次性发布所有实例
  2. 蓝绿部署(Blue-Green Deployment):同时运行两个版本,切换流量
  3. 灰度发布(Gray Release):按比例逐步发布新版本
  4. 金丝雀发布(Canary Release):先发布少量实例,验证后逐步扩大

1.3 本文内容结构

本文将从以下几个方面全面解析灰度/金丝雀发布:

  1. 灰度发布原理:什么是灰度发布、为什么需要灰度发布
  2. 金丝雀发布原理:什么是金丝雀发布、与灰度发布的区别
  3. 实现方式:基于网关、基于服务注册中心、基于负载均衡等
  4. 流量分配策略:按比例、按用户、按地域等分配策略
  5. 版本管理:版本标识、版本路由、版本回滚
  6. 监控告警:发布监控、指标收集、告警机制
  7. 实战案例:实际项目中的灰度/金丝雀发布实现

2. 灰度发布原理

2.1 什么是灰度发布

2.1.1 定义

灰度发布(Gray Release):将新版本逐步发布给部分用户,验证无问题后逐步扩大范围,最终全量发布。

特点

  • 逐步发布,降低风险
  • 可以按比例控制流量
  • 支持快速回滚
  • 适合大规模系统

2.1.2 发布流程

灰度发布流程

  1. 准备阶段:部署新版本到部分实例
  2. 灰度阶段:将部分流量切换到新版本
  3. 验证阶段:监控新版本运行情况
  4. 扩大阶段:逐步增加流量比例
  5. 全量阶段:所有流量切换到新版本
  6. 回滚阶段:发现问题后快速回滚

2.2 为什么需要灰度发布

2.2.1 降低发布风险

全量发布的风险

  • 新版本有Bug,影响所有用户
  • 性能问题导致系统崩溃
  • 数据兼容性问题

灰度发布的优势

  • 只影响部分用户
  • 可以快速发现问题
  • 可以快速回滚

2.2.2 提高系统稳定性

灰度发布的优势

  • 逐步验证新版本
  • 监控新版本指标
  • 及时发现问题并处理

3. 金丝雀发布原理

3.1 什么是金丝雀发布

3.1.1 定义

金丝雀发布(Canary Release):先发布新版本到少量实例(金丝雀),验证无问题后逐步扩大范围。

名称来源:煤矿工人用金丝雀检测有毒气体,如果金丝雀死亡,说明有危险。

特点

  • 先发布少量实例
  • 验证无问题后扩大
  • 风险最小
  • 适合关键系统

3.1.2 与灰度发布的区别

区别

  • 灰度发布:按比例逐步发布(如10% → 50% → 100%)
  • 金丝雀发布:先发布少量实例,验证后全量发布

适用场景

  • 灰度发布:适合大规模系统,需要逐步验证
  • 金丝雀发布:适合关键系统,需要最小风险

3.2 金丝雀发布流程

金丝雀发布流程

  1. 准备阶段:部署新版本到1-2个实例
  2. 金丝雀阶段:将少量流量切换到新版本
  3. 验证阶段:监控新版本运行情况(错误率、响应时间等)
  4. 扩大阶段:验证通过后,逐步增加实例和流量
  5. 全量阶段:所有实例和流量切换到新版本
  6. 回滚阶段:发现问题后快速回滚

4. 实现方式

4.1 基于网关的灰度发布

4.1.1 实现原理

基于网关的灰度发布:在网关层根据规则将流量路由到不同版本的服务。

优势

  • 集中控制,易于管理
  • 支持多种路由规则
  • 不影响服务本身

4.1.2 Spring Cloud Gateway实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@Configuration
public class GrayReleaseConfig {

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("order-service", r -> r
.path("/api/order/**")
.filters(f -> f
.filter(new GrayReleaseGatewayFilter())
)
.uri("lb://order-service")
)
.build();
}
}

@Component
public class GrayReleaseGatewayFilter implements GatewayFilter {

@Autowired
private GrayReleaseService grayReleaseService;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();

// 1. 获取用户信息
String userId = getUserId(request);
String version = getVersion(request);

// 2. 判断是否走灰度
String targetVersion = grayReleaseService.getTargetVersion("order-service", userId, version);

// 3. 设置版本Header
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-Version", targetVersion)
.build();

return chain.filter(exchange.mutate().request(modifiedRequest).build());
}

private String getUserId(ServerHttpRequest request) {
// 从Header或Cookie获取用户ID
return request.getHeaders().getFirst("X-User-Id");
}

private String getVersion(ServerHttpRequest request) {
// 从Header获取版本号
return request.getHeaders().getFirst("X-Version");
}
}

@Service
public class GrayReleaseService {

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 获取目标版本
*/
public String getTargetVersion(String serviceName, String userId, String requestVersion) {
// 1. 检查是否有指定版本
if (requestVersion != null) {
return requestVersion;
}

// 2. 检查用户是否在白名单
if (isInWhitelist(serviceName, userId)) {
return "v2"; // 新版本
}

// 3. 按比例分配流量
double grayRatio = getGrayRatio(serviceName);
if (shouldRouteToGray(userId, grayRatio)) {
return "v2"; // 新版本
}

return "v1"; // 旧版本
}

/**
* 检查用户是否在白名单
*/
private boolean isInWhitelist(String serviceName, String userId) {
String key = "gray:whitelist:" + serviceName;
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, userId));
}

/**
* 获取灰度比例
*/
private double getGrayRatio(String serviceName) {
String key = "gray:ratio:" + serviceName;
String ratioStr = redisTemplate.opsForValue().get(key);
return ratioStr == null ? 0.0 : Double.parseDouble(ratioStr);
}

/**
* 判断是否路由到灰度版本
*/
private boolean shouldRouteToGray(String userId, double ratio) {
if (ratio <= 0) {
return false;
}
if (ratio >= 1) {
return true;
}
// 根据用户ID的hash值判断
int hash = userId.hashCode();
return (hash % 100) < (ratio * 100);
}
}

4.1.3 Nginx实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# nginx.conf
upstream order-service-v1 {
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}

upstream order-service-v2 {
server 192.168.1.20:8080;
server 192.168.1.21:8080;
}

server {
listen 80;
server_name api.example.com;

location /api/order/ {
# 灰度发布:根据Cookie或Header路由
set $version "v1";

# 检查Cookie中的版本
if ($cookie_version = "v2") {
set $version "v2";
}

# 检查Header中的版本
if ($http_x_version = "v2") {
set $version "v2";
}

# 检查用户ID是否在白名单(通过Lua脚本)
access_by_lua_block {
local user_id = ngx.var.cookie_user_id or ngx.var.http_x_user_id
if user_id then
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
local is_whitelist = red:sismember("gray:whitelist:order-service", user_id)
if is_whitelist == 1 then
ngx.var.version = "v2"
end
red:close()
end
}

# 按比例分配流量
set_by_lua_block $gray_ratio {
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
local ratio = red:get("gray:ratio:order-service") or "0"
red:close()
return ratio
}

# 根据用户ID的hash值判断
set_by_lua_block $user_hash {
local user_id = ngx.var.cookie_user_id or ngx.var.http_x_user_id or "0"
return tostring(math.abs(tonumber(string.sub(tostring(user_id), -2)) % 100))
}

if ($user_hash < $gray_ratio) {
set $version "v2";
}

# 路由到对应版本
if ($version = "v2") {
proxy_pass http://order-service-v2;
}
if ($version = "v1") {
proxy_pass http://order-service-v1;
}
}
}

4.2 基于服务注册中心的灰度发布

4.2.1 实现原理

基于服务注册中心的灰度发布:在服务注册时标记版本,消费方根据版本选择服务。

优势

  • 服务级别控制
  • 支持多版本共存
  • 不影响网关

4.2.2 Nacos实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@Service
public class OrderService {

@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;

/**
* 注册服务时设置版本
*/
@PostConstruct
public void registerWithVersion() {
// 设置服务版本元数据
Map<String, String> metadata = new HashMap<>();
metadata.put("version", "v2");
metadata.put("gray", "true");

nacosDiscoveryProperties.setMetadata(metadata);
}
}

@Configuration
public class GrayReleaseRibbonConfig {

@Bean
public IRule grayReleaseRule() {
return new GrayReleaseRule();
}
}

public class GrayReleaseRule extends AbstractLoadBalancerRule {

@Autowired
private GrayReleaseService grayReleaseService;

@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
List<Server> servers = lb.getReachableServers();

if (servers.isEmpty()) {
return null;
}

// 获取当前请求的版本
String targetVersion = getTargetVersion();

// 过滤出目标版本的服务
List<Server> targetServers = servers.stream()
.filter(server -> {
String version = getServerVersion(server);
return targetVersion.equals(version);
})
.collect(Collectors.toList());

if (targetServers.isEmpty()) {
// 如果没有目标版本的服务,使用默认版本
return servers.get(0);
}

// 使用轮询算法选择服务
return targetServers.get(new Random().nextInt(targetServers.size()));
}

private String getTargetVersion() {
// 从ThreadLocal或RequestContext获取版本
RequestContext context = RequestContext.getCurrentContext();
String version = (String) context.get("X-Version");
return version != null ? version : "v1";
}

private String getServerVersion(Server server) {
// 从服务元数据获取版本
if (server instanceof NacosServer) {
NacosServer nacosServer = (NacosServer) server;
return nacosServer.getMetadata().getOrDefault("version", "v1");
}
return "v1";
}

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// 初始化配置
}
}

4.3 基于负载均衡的灰度发布

4.3.1 实现原理

基于负载均衡的灰度发布:在负载均衡层根据规则将流量分配到不同版本的服务。

优势

  • 灵活的路由规则
  • 支持多种负载均衡算法
  • 可以动态调整

4.3.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Service
public class GrayReleaseLoadBalancer {

@Autowired
private GrayReleaseService grayReleaseService;

/**
* 选择服务实例
*/
public ServiceInstance chooseService(List<ServiceInstance> instances, String serviceName) {
// 1. 获取目标版本
String targetVersion = grayReleaseService.getTargetVersion(serviceName, getUserId(), getRequestVersion());

// 2. 过滤出目标版本的服务
List<ServiceInstance> targetInstances = instances.stream()
.filter(instance -> {
String version = instance.getMetadata().getOrDefault("version", "v1");
return targetVersion.equals(version);
})
.collect(Collectors.toList());

if (targetInstances.isEmpty()) {
// 如果没有目标版本的服务,使用默认版本
targetInstances = instances.stream()
.filter(instance -> {
String version = instance.getMetadata().getOrDefault("version", "v1");
return "v1".equals(version);
})
.collect(Collectors.toList());
}

// 3. 使用负载均衡算法选择服务
return loadBalance(targetInstances);
}

private ServiceInstance loadBalance(List<ServiceInstance> instances) {
// 使用轮询算法
int index = new Random().nextInt(instances.size());
return instances.get(index);
}

private String getUserId() {
// 从RequestContext获取用户ID
RequestContext context = RequestContext.getCurrentContext();
return (String) context.get("X-User-Id");
}

private String getRequestVersion() {
// 从RequestContext获取版本
RequestContext context = RequestContext.getCurrentContext();
return (String) context.get("X-Version");
}
}

5. 流量分配策略

5.1 按比例分配

5.1.1 实现原理

按比例分配:根据配置的比例将流量分配到新版本。

特点

  • 简单易用
  • 可以动态调整比例
  • 适合大规模系统

5.1.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Service
public class RatioBasedGrayRelease {

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 按比例分配流量
*/
public boolean shouldRouteToGray(String serviceName, String userId) {
// 1. 获取灰度比例
double ratio = getGrayRatio(serviceName);

if (ratio <= 0) {
return false;
}
if (ratio >= 1) {
return true;
}

// 2. 根据用户ID的hash值判断
int hash = Math.abs(userId.hashCode());
int bucket = hash % 100;

return bucket < (ratio * 100);
}

/**
* 动态调整灰度比例
*/
public void setGrayRatio(String serviceName, double ratio) {
String key = "gray:ratio:" + serviceName;
redisTemplate.opsForValue().set(key, String.valueOf(ratio));
}

private double getGrayRatio(String serviceName) {
String key = "gray:ratio:" + serviceName;
String ratioStr = redisTemplate.opsForValue().get(key);
return ratioStr == null ? 0.0 : Double.parseDouble(ratioStr);
}
}

5.2 按用户分配

5.2.1 实现原理

按用户分配:根据用户ID、用户标签等将特定用户路由到新版本。

特点

  • 精确控制
  • 适合内测、VIP用户
  • 可以快速验证

5.2.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Service
public class UserBasedGrayRelease {

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 按用户分配流量
*/
public boolean shouldRouteToGray(String serviceName, String userId) {
// 1. 检查用户是否在白名单
if (isInWhitelist(serviceName, userId)) {
return true;
}

// 2. 检查用户是否在黑名单
if (isInBlacklist(serviceName, userId)) {
return false;
}

// 3. 检查用户标签
if (hasGrayTag(serviceName, userId)) {
return true;
}

return false;
}

/**
* 添加用户到白名单
*/
public void addToWhitelist(String serviceName, String userId) {
String key = "gray:whitelist:" + serviceName;
redisTemplate.opsForSet().add(key, userId);
}

/**
* 从白名单移除用户
*/
public void removeFromWhitelist(String serviceName, String userId) {
String key = "gray:whitelist:" + serviceName;
redisTemplate.opsForSet().remove(key, userId);
}

/**
* 检查用户是否在白名单
*/
private boolean isInWhitelist(String serviceName, String userId) {
String key = "gray:whitelist:" + serviceName;
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, userId));
}

/**
* 检查用户是否在黑名单
*/
private boolean isInBlacklist(String serviceName, String userId) {
String key = "gray:blacklist:" + serviceName;
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, userId));
}

/**
* 检查用户是否有灰度标签
*/
private boolean hasGrayTag(String serviceName, String userId) {
String key = "user:tags:" + userId;
Set<String> tags = redisTemplate.opsForSet().members(key);
if (tags == null) {
return false;
}
return tags.contains("gray:" + serviceName);
}
}

5.3 按地域分配

5.3.1 实现原理

按地域分配:根据用户地域将特定地区的用户路由到新版本。

特点

  • 适合国际化系统
  • 可以按地区逐步发布
  • 降低全局风险

5.3.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Service
public class RegionBasedGrayRelease {

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 按地域分配流量
*/
public boolean shouldRouteToGray(String serviceName, String region) {
// 1. 获取灰度地区列表
Set<String> grayRegions = getGrayRegions(serviceName);

// 2. 检查当前地区是否在灰度列表中
return grayRegions.contains(region);
}

/**
* 添加地区到灰度列表
*/
public void addGrayRegion(String serviceName, String region) {
String key = "gray:regions:" + serviceName;
redisTemplate.opsForSet().add(key, region);
}

/**
* 从灰度列表移除地区
*/
public void removeGrayRegion(String serviceName, String region) {
String key = "gray:regions:" + serviceName;
redisTemplate.opsForSet().remove(key, region);
}

/**
* 获取灰度地区列表
*/
private Set<String> getGrayRegions(String serviceName) {
String key = "gray:regions:" + serviceName;
Set<String> regions = redisTemplate.opsForSet().members(key);
return regions != null ? regions : Collections.emptySet();
}
}

6. 版本管理

6.1 版本标识

6.1.1 版本号规则

版本号规则

  • 语义化版本:主版本号.次版本号.修订号(如:1.2.3)
  • 时间版本:基于时间戳(如:20240428.001)
  • Git版本:基于Git Commit Hash(如:abc1234)

6.1.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
public class VersionService {

/**
* 获取当前版本
*/
public String getCurrentVersion() {
// 从配置文件或环境变量获取
return System.getProperty("app.version", "1.0.0");
}

/**
* 比较版本号
*/
public int compareVersion(String version1, String version2) {
String[] v1Parts = version1.split("\\.");
String[] v2Parts = version2.split("\\.");

int maxLength = Math.max(v1Parts.length, v2Parts.length);

for (int i = 0; i < maxLength; i++) {
int v1Part = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0;
int v2Part = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0;

if (v1Part < v2Part) {
return -1;
} else if (v1Part > v2Part) {
return 1;
}
}

return 0;
}
}

6.2 版本路由

6.2.1 路由规则

路由规则

  • 精确匹配:版本号完全匹配
  • 前缀匹配:版本号前缀匹配
  • 范围匹配:版本号范围匹配

6.2.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Service
public class VersionRouter {

@Autowired
private VersionService versionService;

/**
* 路由到目标版本
*/
public String route(String serviceName, String requestVersion) {
// 1. 检查是否有指定版本
if (requestVersion != null) {
return requestVersion;
}

// 2. 获取默认版本
String defaultVersion = getDefaultVersion(serviceName);

// 3. 根据路由规则选择版本
String targetVersion = applyRoutingRules(serviceName, defaultVersion);

return targetVersion;
}

/**
* 应用路由规则
*/
private String applyRoutingRules(String serviceName, String defaultVersion) {
// 1. 检查灰度配置
if (isGrayEnabled(serviceName)) {
return getGrayVersion(serviceName);
}

// 2. 检查版本兼容性
if (isVersionCompatible(serviceName, defaultVersion)) {
return defaultVersion;
}

// 3. 返回默认版本
return defaultVersion;
}

private String getDefaultVersion(String serviceName) {
// 从配置获取默认版本
return "v1";
}

private boolean isGrayEnabled(String serviceName) {
// 检查是否启用灰度
return true;
}

private String getGrayVersion(String serviceName) {
// 获取灰度版本
return "v2";
}

private boolean isVersionCompatible(String serviceName, String version) {
// 检查版本兼容性
return true;
}
}

6.3 版本回滚

6.3.1 回滚策略

回滚策略

  • 快速回滚:立即切换回旧版本
  • 逐步回滚:逐步减少新版本流量
  • 数据回滚:回滚数据变更

6.3.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Service
public class VersionRollbackService {

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 快速回滚
*/
public void quickRollback(String serviceName) {
// 1. 将灰度比例设置为0
setGrayRatio(serviceName, 0.0);

// 2. 清空白名单
clearWhitelist(serviceName);

// 3. 发送回滚通知
sendRollbackNotification(serviceName);

log.info("Quick rollback completed: service={}", serviceName);
}

/**
* 逐步回滚
*/
public void gradualRollback(String serviceName) {
// 1. 获取当前灰度比例
double currentRatio = getGrayRatio(serviceName);

// 2. 逐步减少比例:50% → 25% → 10% → 0%
double[] steps = {0.25, 0.10, 0.05, 0.0};

for (double step : steps) {
setGrayRatio(serviceName, step);
log.info("Gradual rollback: service={}, ratio={}", serviceName, step);

// 等待一段时间,观察效果
try {
Thread.sleep(60000); // 等待1分钟
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}

private void setGrayRatio(String serviceName, double ratio) {
String key = "gray:ratio:" + serviceName;
redisTemplate.opsForValue().set(key, String.valueOf(ratio));
}

private double getGrayRatio(String serviceName) {
String key = "gray:ratio:" + serviceName;
String ratioStr = redisTemplate.opsForValue().get(key);
return ratioStr == null ? 0.0 : Double.parseDouble(ratioStr);
}

private void clearWhitelist(String serviceName) {
String key = "gray:whitelist:" + serviceName;
redisTemplate.delete(key);
}

private void sendRollbackNotification(String serviceName) {
// 发送告警通知
alertService.sendAlert("版本回滚", serviceName);
}
}

7. 监控告警

7.1 发布监控

7.1.1 监控指标

监控指标

  • 错误率:新版本的错误率
  • 响应时间:新版本的响应时间
  • QPS:新版本的QPS
  • 资源使用:CPU、内存使用率

7.1.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@Component
public class GrayReleaseMonitor {

@Autowired
private MeterRegistry meterRegistry;

/**
* 记录请求指标
*/
public void recordRequest(String serviceName, String version, boolean success, long duration) {
// 记录请求数
meterRegistry.counter("gray.request",
"service", serviceName,
"version", version,
"status", success ? "success" : "error")
.increment();

// 记录响应时间
meterRegistry.timer("gray.response_time",
"service", serviceName,
"version", version)
.record(duration, TimeUnit.MILLISECONDS);
}

/**
* 检查是否需要回滚
*/
public boolean shouldRollback(String serviceName, String version) {
// 1. 检查错误率
double errorRate = getErrorRate(serviceName, version);
if (errorRate > 0.05) { // 错误率超过5%
return true;
}

// 2. 检查响应时间
double avgResponseTime = getAvgResponseTime(serviceName, version);
double baselineResponseTime = getBaselineResponseTime(serviceName);
if (avgResponseTime > baselineResponseTime * 2) { // 响应时间超过基线2倍
return true;
}

return false;
}

private double getErrorRate(String serviceName, String version) {
// 从监控系统获取错误率
Counter errorCounter = meterRegistry.counter("gray.request",
"service", serviceName,
"version", version,
"status", "error");
Counter totalCounter = meterRegistry.counter("gray.request",
"service", serviceName,
"version", version);

if (totalCounter.count() == 0) {
return 0.0;
}

return errorCounter.count() / totalCounter.count();
}

private double getAvgResponseTime(String serviceName, String version) {
// 从监控系统获取平均响应时间
Timer timer = meterRegistry.timer("gray.response_time",
"service", serviceName,
"version", version);
return timer.mean(TimeUnit.MILLISECONDS);
}

private double getBaselineResponseTime(String serviceName) {
// 获取基线响应时间(旧版本的平均响应时间)
Timer timer = meterRegistry.timer("gray.response_time",
"service", serviceName,
"version", "v1");
return timer.mean(TimeUnit.MILLISECONDS);
}
}

7.2 告警机制

7.2.1 告警规则

告警规则

  • 错误率告警:错误率超过阈值
  • 响应时间告警:响应时间超过阈值
  • 资源告警:CPU、内存使用率超过阈值

7.2.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Component
public class GrayReleaseAlert {

@Autowired
private AlertService alertService;

@Autowired
private GrayReleaseMonitor monitor;

/**
* 检查并发送告警
*/
@Scheduled(fixedDelay = 60000) // 每分钟检查一次
public void checkAndAlert() {
List<String> grayServices = getGrayServices();

for (String serviceName : grayServices) {
String version = getGrayVersion(serviceName);

// 1. 检查错误率
double errorRate = monitor.getErrorRate(serviceName, version);
if (errorRate > 0.05) {
alertService.sendAlert("灰度发布错误率过高",
String.format("服务:%s,版本:%s,错误率:%.2f%%", serviceName, version, errorRate * 100));
}

// 2. 检查响应时间
double avgResponseTime = monitor.getAvgResponseTime(serviceName, version);
double baselineResponseTime = monitor.getBaselineResponseTime(serviceName);
if (avgResponseTime > baselineResponseTime * 2) {
alertService.sendAlert("灰度发布响应时间过长",
String.format("服务:%s,版本:%s,响应时间:%.2fms,基线:%.2fms",
serviceName, version, avgResponseTime, baselineResponseTime));
}

// 3. 检查是否需要回滚
if (monitor.shouldRollback(serviceName, version)) {
alertService.sendAlert("灰度发布需要回滚",
String.format("服务:%s,版本:%s,建议立即回滚", serviceName, version));
}
}
}

private List<String> getGrayServices() {
// 获取所有灰度服务列表
return Arrays.asList("order-service", "payment-service");
}

private String getGrayVersion(String serviceName) {
// 获取灰度版本
return "v2";
}
}

8. 实战案例

8.1 案例:订单服务灰度发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@RestController
@RequestMapping("/api/order")
public class OrderController {

@Autowired
private OrderService orderService;

@Autowired
private GrayReleaseService grayReleaseService;

/**
* 创建订单(灰度发布)
*/
@PostMapping("/create")
public Result<Order> createOrder(@RequestBody OrderRequest request, HttpServletRequest httpRequest) {
// 1. 获取用户ID
String userId = getUserId(httpRequest);

// 2. 判断是否走灰度
boolean isGray = grayReleaseService.shouldRouteToGray("order-service", userId);

if (isGray) {
// 3. 使用新版本服务
return Result.success(orderService.createOrderV2(request));
} else {
// 4. 使用旧版本服务
return Result.success(orderService.createOrderV1(request));
}
}

private String getUserId(HttpServletRequest request) {
// 从Header或Cookie获取用户ID
String userId = request.getHeader("X-User-Id");
if (userId == null) {
userId = getCookieValue(request, "user_id");
}
return userId != null ? userId : "anonymous";
}

private String getCookieValue(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
}

9. 总结

9.1 核心要点

  1. 灰度发布:按比例逐步发布新版本,降低发布风险
  2. 金丝雀发布:先发布少量实例,验证后逐步扩大
  3. 实现方式:基于网关、基于服务注册中心、基于负载均衡
  4. 流量分配:按比例、按用户、按地域等分配策略
  5. 版本管理:版本标识、版本路由、版本回滚
  6. 监控告警:发布监控、指标收集、告警机制

9.2 关键理解

  1. 灰度发布:适合大规模系统,需要逐步验证
  2. 金丝雀发布:适合关键系统,需要最小风险
  3. 流量分配:根据业务特点选择合适的分配策略
  4. 监控告警:实时监控,及时发现问题并回滚

9.3 最佳实践

  1. 逐步发布:从10% → 50% → 100%逐步扩大
  2. 监控指标:监控错误率、响应时间、资源使用等
  3. 快速回滚:发现问题后立即回滚
  4. 白名单机制:内测用户使用白名单
  5. 版本兼容:保证新版本向后兼容

相关文章