介绍
工作中经常要涉及到功能发布,这个时候也经常是业务系统最有可能遇到问题的时候,需要要尽量减少发布引起的风险。比如在系统负载比较小的时候使用。还有蓝绿发布、灰度发布等等,今天介绍一下这几种常见的发布,并使用springcloud 实现。
1.传统发布方式
一个系统最初的时候,使用量小,用户少,系统也是单体架构,有时候连系统挂了都没有人发现,这个时候常见的上线新功能就是替换包,然后重启服务。
这种方式最简单,也最方便,适合很多小型项目,但是也有一定的缺陷,在服务发布的过程中。系统是不能对外提供服务的。最严重的问题是,如果新功能发布失败,可能会导致长时间不可用。所以为了尽量减少在发布的过程中对用户的影响。提升系统的可用性。我们需要引入更好的发布方法。
2.常见的发布方式
我们在工作中经常遇到这种问题,一个新功能在测试环境运行的很好,测试也测了好几遍都是正常的,但是在生产环境一部署就不行了,不是启动失败,就是功能逻辑不对。这个是由于环境的差异造成的。毕竟生产环境很复杂,用的人也多。为了减少这种情况对业务系统的影响,我们可以引入蓝绿发布。
蓝绿发布:
蓝绿发布的流程如上图所示,我们把生产环境的系统分为2部分,1套绿环境,1套蓝环境,绿环境是线上正在运行的v1版本,我们现在要发布新的v2版本,具体的操作如下:
- 生产的所有用户的流量都切换为绿环境,不经过蓝环境。
- 蓝环境的版本升级为v2,然后让测试测试蓝环境的功能。
- 确定蓝环境没有功能问题时,可以先将少部分生产的流量切到蓝环境。运行正常后,将所有的生产流量切换为蓝环境。
- 将绿环境的版本升级为v2。再将所有的生产的流量都切换为绿环境,至此,发布完成。
不像之前的直接发布方式,蓝绿发布可以解决大部分因为发布新功能导致的问题。测试也有条件去测试生产环境的功能了,在测试的过程中不用担心新功能对生产用户造成的影响。
优点:
- 升级过程无需停机,用户感知小
- 升级过程一半资源提供服务
- 升级/回滚速度快
缺点:
- 如果出了问题,影响面较广
蓝绿发布中,如果业务在测试生产环境没有发现问题,等到完全升级完成之后才发现问题,比如多发了优惠券,接口有bug等等,这个时候对系统的影响就很大了。为了尽量减少这种情况的影响。我们可以使用灰度发布。
灰度发布
灰度发布也叫金丝雀发布。
金丝雀发布由来:以前矿工开矿,在下矿洞前需要检查下方是否有毒气,矿工们先会放一只金丝雀进去探是否有毒气体,看金丝雀能否活下来。
灰度发布(金丝雀发布)主要是为了减少发布过程中的问题对用户的影响,主要流程如上图所示,如果要发布v2版本,先发布一部分节点,导入一部分生产环境的流量,然后再逐步增加,如果发现问题,可以及时修复,这样在发布过程中的问题,可以尽量少的影响用户体验。
优点:
- 风险控制:灰度发布可以降低新版本引入生产环境时的风险。通过逐步引入新功能,团队可以更容易地发现和解决潜在的问题,而不会对整个用户群体造成影响。
- 反馈循环:通过逐步引入新版本,团队可以及时获取用户反馈,从而更好地了解新功能的表现和用户体验。这有助于及时调整和改进新功能。
- 逐步推广:灰度发布允许团队逐步将新版本引入到整个用户群体中,从而更好地控制资源和性能的负载。这有助于避免突然的流量激增和性能问题。
缺点:
- 部署复杂性:灰度发布需要更复杂的部署和管理流程。需要确保新版本可以与旧版本和灰度版本共存,并且需要确保灰度流量的正确路由和控制。
- 增加开发时间:由于需要逐步引入新版本,灰度发布可能会增加整个发布周期的时间。这可能会对产品的快速迭代和更新速度产生影响。
- 管理成本:灰度发布需要更多的管理和监控成本,包括对不同版本的流量和性能进行监控,以及对灰度发布过程中的问题进行跟踪和解决。
3. 蓝绿灰度发布的实现
现在主流的微服务接口调用为springcloud + nacos,下面我以这个技术栈来实现一下动态的蓝绿发布和灰度发布。
主要的技术点有以下:
- 如何标记服务为蓝绿环境
服务标记可以利用nacos的元数据 metadata:
spring:
application:
name: provider1
cloud:
nacos:
discovery:
# nacos 注册地址
server-addr: localhost:8848
# nacos 元数据,标记版本和环境
metadata:
version: green-v1
config:
server-addr: localhost:8848
server:
port: 8001
- 如何获取不同环境的服务 springcloud 可以通过 继承 DelegatingServiceInstanceListSupplier 类来获取某个服务上面注册中心的所有节点,然后可以通过 nacos 的元数据进行过滤,这样就可以区分蓝绿环境的服务了。
获取注册的服务列表:
Flux<List<ServiceInstance>> listFlux = delegate.get(request);
根据消费者的环境来获取生产者的环境:
private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String version) {
for (ServiceInstance instance : instances) {
log.info("==> 服务:[{}]在注册中心的实例信息:[{}]", getServiceId(), writeValueAsString(instance));
}
// 1、获取 请求头中的 version 和 ServiceInstance 中 元数据中 version 一致的服务
List<ServiceInstance> selectServiceInstances = instances.stream()
.filter(instance -> instance.getMetadata().get(VERSION_HEADER_NAME) != null
&& Objects.equals(version, instance.getMetadata().get(VERSION_HEADER_NAME)))
.collect(Collectors.toList());
if (!selectServiceInstances.isEmpty()) {
log.info("返回请求服务:[{}]为version:[{}]的有:[{}]个", getServiceId(), version, selectServiceInstances.size());
for (ServiceInstance instance : selectServiceInstances) {
log.info("==> 服务:[{}]根据version过滤选择实例信息:[{}]", getServiceId(), writeValueAsString(instance));
}
return selectServiceInstances;
}
// 2、返回 versinotallow=default 的实例
selectServiceInstances = instances.stream()
.filter(instance -> Objects.equals(instance.getMetadata().get(VERSION_HEADER_NAME), "default"))
.collect(Collectors.toList());
log.info("返回请求服务:[{}]为version:[{}]的有:[{}]个", getServiceId(), "default", selectServiceInstances.size());
for (ServiceInstance instance : selectServiceInstances) {
log.info("==> 服务:[{}]根据version过滤选择实例信息:[{}]", getServiceId(), writeValueAsString(instance));
}
if (!selectServiceInstances.isEmpty()) {
log.info("返回请求服务:[{}]为version:[{}]的有:[{}]个", getServiceId(), version, selectServiceInstances.size());
for (ServiceInstance instance : selectServiceInstances) {
log.info("==> 服务:[{}]根据version过滤选择实例信息:[{}]", getServiceId(), writeValueAsString(instance));
}
return selectServiceInstances;
}
return instances;
}
从请求头中获取version 字段来确定版本,实际生产中可以根据自己的需求来确定规则,比如根据用户账号进行hash。
public void apply(RequestTemplate requestTemplate) {
String version = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
.getHeader("version");
log.info("feign 中传递的 version 请求头的值为:[{}]", version);
requestTemplate
.header("version", version);
}
请求蓝环境v2接口进行测试:
wget --no-check-certificate --quiet \
--method GET \
--timeout=0 \
--header 'version: blue-v2' \
'http://localhost:7001/consumer/consumer1/test1'
调用结果:
请求绿环境v1接口进行测试:
wget --no-check-certificate --quiet \
--method GET \
--timeout=0 \
--header 'version: green-v1' \
'http://localhost:7001/consumer/consumer1/test1'
调用结果:
灰度发布的原理类似,不同的地方是灰度发布需要逐步切换生产流量到新版本,可以通过nacos 修改服务的权重来实现。
4. 代码地址
springcloud nacos 实现蓝绿发布,灰度发布
https://gitee.com/yangzheng1/springcloud-deploy