Spring Cloud灰度发布方案(自定义路由规则)请移步:https://blog.csdn.net/han949417140/article/details/121420529
蓝绿部署
蓝绿部署的模型中包含两个集群A和B
1、在没有上线的正常情况下,集群A和集群B的代码版本是一致的,并且同时对外提供服务。
2、在系统升级的时候下,我们首先把一个集群(比如集群A)从负载列表中摘除,进行新版本的部署。集群B仍然继续提供服务。
3、当集群A升级完毕,我们把负载均衡重新指向集群A,再把集群B从负载列表中摘除,进行新版本的部署。集群A重新提供服务。
4、最后,当集群B也升级完成,我们把集群B也恢复到负载列表当中。这个时候,两个集群的版本都已经升级,并且对外的服务几乎没有间断过。
详细介绍请参考:https://www.cnblogs.com/aaron911/p/11299422.html
滚动部署
和蓝绿部署不同的是,滚动部署对外提供服务的版本并不是非此即彼,而是在更细的粒度下平滑完成版本的升级。
滚动部署只需要一个集群,集群下的不同节点可以独立进行版本升级。比如在一个16节点的集群中,我们选择每次升级4个节点,过程如下图:
灰度发布(金丝雀发布)
金丝雀发布,与蓝绿部署不同的是,它不是非黑即白的部署方式,所以又称为灰度发布。它能够缓慢的将修改推广到一小部分用户,验证没有问题后,再推广到全部用户,以降低生产环境引入新功能带来的风险。
灰度发布的重点就是制定引流策略,将请求分发到不同版本服务中。比如内部测试人员的请求分发到金丝雀服务,其他用户分发到旧服务中。测试通过之后在推广到全部用户。
部署方式 | 优势 | 劣势 | 描述 |
---|---|---|---|
蓝绿部署 | 同一时间对外服务的只有一个版本,容易定位问题。升级和回滚一集群为粒度,操作相对简单 | 需要维护两个集群,机器成本要求高 | 两套环境交替升级,旧版本保留一定时间便于回滚。 |
滚动部署 | 只需维护一个集群,成本低 | 上线过程中,两个版本同时对外服务,不易定位问题,且容易造成数据错乱。升级和回滚操作相对复杂 | 按批次停止老版本实例,启动新版本实例。 |
灰度发布 | 新版本出现问题影响范围很小,允许失败,风险较小 | 只能适用于兼容迭代的方式,如果是大版本不兼容的场景,就没办法使用这种方式了 | 根据比例将老版本升级,例如80%用户访问是老版本,20%用户访问是新版本。 |
请求名称 | 请求方式 | HTTP地址 | 请求描述 |
---|---|---|---|
注册新服务 | POST | /eureka/apps/{appID} | 传递JSON或者XML格式参数内容,HTTP code为204时表示成功 |
删除注册服务 | DELETE | /eureka/apps/{appID}/{instanceID} | |
发送服务心跳 | PUT | /eureka/apps/{appID}/{instanceID} | |
查询所有服务 | GET | /eureka/apps | |
查询指定appID的服务列表 | GET | /eureka/apps/{appID} | |
查询指定appID&instanceID | GET | /eureka/apps/{appID}/{instanceID} | 获取指定appID以及InstanceId的服务信息 |
查询指定instanceID服务列表 | GET | /eureka/apps/instances/{instanceID} | 获取指定instanceID的服务列表 |
变更服务状态 | PUT | /eureka/apps/{appID}/{instanceID}/status?value=DOWN | 服务上线、服务下线等状态变动 |
变更元数据 | PUT | /eureka/apps/{appID}/{instanceID}/metadata?key=value | 更新eurekametadata元数据 |
灰度发布的核心就是路由转发,如果我们能够自定义网关==>服务a、服务a==>服务b中间的路由策略,就可以实现用户引流,灰度发布。
服务名 | 端口 | eureka元数据 | 描述 |
---|---|---|---|
zuul-server | 9000 | 网关服务 | |
abTest | 8083 | version: v2 | 新版本金丝雀服务 |
abTest | 8084 | version: v1 | 老版本服务 |
abTest | 8085 | version: v1 | 老版本旧服务 |
provider-server | 8093 | version: v2 | 新版本金丝雀服务 |
provider-server | 8094 | version: v1 | 老版本服务 |
provider-server | 8095 | version: v1 | 老版本旧服务 |
# 用户表
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户昵称',
`head_image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'head_image',
`city` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '城市',
`gender` int(2) DEFAULT NULL COMMENT '性别 0:男 1:女',
`user_type` int(2) DEFAULT 0 COMMENT '用户类型(0:普通用户 1:vip)',
`mobile` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户手机号',
`status` int(2) DEFAULT 1 COMMENT '用户状态 0:冻结 1:正常',
`token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '登录token',
`token_expires_time` datetime(0) DEFAULT NULL COMMENT 'token过期时间',
`create_time` datetime(0) DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
INSERT INTO `t_user` VALUES (1, 'hld', NULL, NULL, 1, 0, 'xxxx', 1, 'nm4p2ouy9ckl20bnnd62acev3bnasdmb', '2021-12-01 15:31:09', '2021-08-31 15:31:18', '2021-09-01 16:15:25');
INSERT INTO `t_user` VALUES (2, 'xxx', NULL, NULL, 1, 0, 'xxxxx', 1, 'lskeu9s8df7sdsue7re890er343rtolzospw', '2021-12-01 15:31:09', '2021-08-31 15:31:18', '2021-09-01 16:15:25');
INSERT INTO `t_user` VALUES (3, 'www', NULL, NULL, 1, 0, 'wwww', 1, 'pamsnxs917823skshwienmal2m3n45mz', '2021-12-01 15:31:09', '2021-08-31 15:31:18', '2021-09-01 16:15:25');
# 灰度路由规则配置表
CREATE TABLE `ab_test` (
`id` int(11) NOT NULL,
`application_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '服务名',
`version` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '版本',
`userId` int(11) DEFAULT NULL COMMENT '用户id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `ab_test` VALUES (1, 'abTest', 'v1', 1);
INSERT INTO `ab_test` VALUES (2, 'abTest', 'v2', 3);
灰度服务eureka.instance.metadata-map元数据信息version: v2。 正常服务设置元数据信息version: v1
使用框架实现根据元数据信息指定路由服务。
本demo使用zuul作为网关层,框架实现根据元数据信息指定路由服务。
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
package com.hanergy.out.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hanergy.out.config.GrayHolder;
import com.hanergy.out.entity.AbTest;
import com.hanergy.out.entity.TUser;
import com.hanergy.out.service.AbTestService;
import com.hanergy.out.service.TUserService;
import com.hanergy.out.util.R;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import io.jmnarloch.spring.cloud.ribbon.api.RibbonFilterContext;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
@Component
public class AbTestFilter extends ZuulFilter {
Logger log = LoggerFactory.getLogger(AbTestFilter.class);
public final static String ACCESS_TOKEN = "access_token";
@Autowired
private AbTestService abTestService;
@Autowired
private TUserService userService;
/**
* 过滤器类型: 前置过滤器
* @return
*/
@Override
public String filterType() {
return PRE_TYPE;
}
/**
* 过滤器顺序,越小越先执行
* @return
*/
@Override
public int filterOrder() {
return 50;
}
/**
* 过滤器是否生效
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 业务逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
//请求请求头token信息
String token = request.getHeader("token");
// 根据token获取用户信息
TUser user = userService.getOne(new QueryWrapper<TUser>()
.lambda()
.eq(TUser::getToken, token));
// token异常
if (user == null){
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
// 查询灰度发布配置表,判断此用户是否灰度用户
AbTest abTest = abTestService.getOne(new QueryWrapper<AbTest>()
.lambda()
.eq(AbTest::getUserid, user.getId()));
// v1:正常服务 v2:灰度服务
if (abTest != null && "v2".equals(abTest.getVersion())){
RibbonFilterContextHolder.getCurrentContext().add("version","v2");
} else {
RibbonFilterContextHolder.getCurrentContext().add("version","v1");
}
return null;
}
}
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
package com.hanergy.out.config;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hanergy.out.entity.AbTest;
import com.hanergy.out.entity.TUser;
import com.hanergy.out.service.AbTestService;
import com.hanergy.out.service.TUserService;
import com.hanergy.out.utils.RibbonParam;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.aopalliance.intercept.Joinpoint;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.RequestContext;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* @description:
* @author: Han LiDong
* @create: 2021/11/18 16:31
* @update: 2021/11/18 16:31
*/
@Aspect
@Component
public class ReqestAspect {
@Autowired
private TUserService userService;
@Autowired
private AbTestService abTestService;
@Before("execution(* com.hanergy.out.controller.*.*(..))")
public void before(){
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("token");
// 根据token获取用户信息
TUser user = userService.getOne(new QueryWrapper<TUser>()
.lambda()
.eq(TUser::getToken, token));
if (user == null){
throw new RuntimeException("token异常");
}
// 查询灰度发布配置表,判断此用户是否灰度用户
AbTest abTest = abTestService.getOne(new QueryWrapper<AbTest>()
.lambda()
.eq(AbTest::getUserid, user.getId()));
// v1:正常服务 v2:灰度服务
if (abTest != null && "v2".equals(abTest.getVersion())){
RibbonFilterContextHolder.getCurrentContext().add("version","v2");
} else {
RibbonFilterContextHolder.getCurrentContext().add("version","v1");
}
}
}
调用链:用户==》zuul网关==>abTest服务==>provider-server服务
@Slf4j
@RestController
@RequestMapping("/v1/test")
public class TestController {
@Value("${server.port}")
private Integer port;
@ApiOperation(value="获取端口号",notes="获取端口号")
@GetMapping("/getPort")
public HttpResult<Integer> getPort(){
return HttpResult.successResult(port);
}
}
@FeignClient(value = "provider-server",fallback = ManagerPreFallbackImpl.class)
public interface RemoteManagerPreService {
@ApiOperation(value="获取端口号",notes="获取端口号")
@GetMapping("/v1/test/getPort")
public HttpResult<Integer> getPort();
}
@Slf4j
@Component
public class ManagerPreFallbackImpl implements RemoteManagerPreService {
@Override
public HttpResult<Integer> getPort() {
log.error("获取provider服务端口异常");
return null;
}
}
@Slf4j
@RestController
@RequestMapping("/v1/test")
public class TestController {
@Value("${server.port}")
private Integer port;
@ApiOperation(value="获取provider服务端口号",notes="获取provider服务端口号")
@GetMapping("/getProviderPort")
public HttpResult<Integer> getProviderPort(){
// feign服务间调用
HttpResult<Integer> res = remoteManagerPreService.getPort();
Integer providerPort = res.getData();
return HttpResult.successResult("port: "+ port + ",providerPort:" + providerPort);
}
}
abTest分别使用8083、8084、8085端口启动,其中8083端口设置元数据信息为: version: v2, 8084、8085端口设置源数据信息为:version: v1
provider-server分别使用8093、8094、8095端口启动,其中8093端口设置元数据信息为: version: v2, 8094、8095端口设置源数据信息为:version: v1
那么灰度用户的接口请求路由为:zuul==》8083端口服务==》8093端口服务
正常用户接口请求路由为:zuul==》8084/8085端口服务==》8094/8095端口服务
//修改8083端口abTest服务元数据信息
PUT 182.92.xxx.xxx:8761/eureka/apps/ABTEST/192.168.199.1:abTest:8083/metadata?version=v2
//修改8093端口provider-server服务元数据信息
PUT 182.92.219.202:8761/eureka/apps/PROVIDER-SERVER/192.168.199.1:provider-server:8093/metadata?version=v2
至此灰度发布验证完成.