• Spring Cloud灰度发布方案


    目录

    不停机部署服务策略介绍

    蓝绿部署

    滚动部署

    灰度发布(金丝雀发布)

    eureka RestFul接口

    灰度发布流程及实现思路

    调用链分析

    请求解析

    资源准备

    代码实现

    网关路由(zuul-server服务)

    服务间调用路由(abTest服务)

    灰度接口测试

    provider-server服务提供测试接口

    abTest服务提供测试接口

    验证


    注意:本文参考  Spring Cloud灰度发布方案----ribbon框架

    Spring Cloud灰度发布方案----自定义路由规则_易佰菜鸟的博客-CSDN博客_springboot灰度发布

    不停机部署服务策略介绍

    蓝绿部署

    蓝绿部署的模型中包含两个集群A和B

    1、在没有上线的正常情况下,集群A和集群B的代码版本是一致的,并且同时对外提供服务。

    2、在系统升级的时候下,我们首先把一个集群(比如集群A)从负载列表中摘除,进行新版本的部署。集群B仍然继续提供服务。

    3、当集群A升级完毕,我们把负载均衡重新指向集群A,再把集群B从负载列表中摘除,进行新版本的部署。集群A重新提供服务。

    4、最后,当集群B也升级完成,我们把集群B也恢复到负载列表当中。这个时候,两个集群的版本都已经升级,并且对外的服务几乎没有间断过。

    详细介绍请参考:什么是蓝绿部署? - Mr.Aaron - 博客园

    滚动部署

    和蓝绿部署不同的是,滚动部署对外提供服务的版本并不是非此即彼,而是在更细的粒度下平滑完成版本的升级。

    滚动部署只需要一个集群,集群下的不同节点可以独立进行版本升级。比如在一个16节点的集群中,我们选择每次升级4个节点,过程如下图:

    灰度发布(金丝雀发布)

    金丝雀发布,与蓝绿部署不同的是,它不是非黑即白的部署方式,所以又称为灰度发布。它能够缓慢的将修改推广到一小部分用户,验证没有问题后,再推广到全部用户,以降低生产环境引入新功能带来的风险。

    灰度发布的重点就是制定引流策略,将请求分发到不同版本服务中。比如内部测试人员的请求分发到金丝雀服务,其他用户分发到旧服务中。测试通过之后在推广到全部用户。 

    部署方式优势劣势描述
    蓝绿部署同一时间对外服务的只有一个版本,容易定位问题。升级和回滚一集群为粒度,操作相对简单需要维护两个集群,机器成本要求高两套环境交替升级,旧版本保留一定时间便于回滚。
    滚动部署只需维护一个集群,成本低上线过程中,两个版本同时对外服务,不易定位问题,且容易造成数据错乱。升级和回滚操作相对复杂按批次停止老版本实例,启动新版本实例。
    灰度发布新版本出现问题影响范围很小,允许失败,风险较小只能适用于兼容迭代的方式,如果是大版本不兼容的场景,就没办法使用这种方式了根据比例将老版本升级,例如80%用户访问是老版本,20%用户访问是新版本。

    eureka RestFul接口

    请求名称请求方式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&instanceIDGET/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元数据

    灰度发布流程及实现思路

    调用链分析

    用户请求==>zuul网关==>服务a==>服务b

    1、首先用户发送请求

    2、经过网关分发请求到具体服务a

    3、服务a 调用服务b接口

    灰度发布的核心就是路由转发,如果我们能够自定义网关==>服务a、服务a==>服务b中间的路由策略,就可以实现用户引流,灰度发布。

    请求解析

     

    1 新服务A、旧服务A、新服务B、旧服务B均需要注册meta-map元数据version信息。新服务注册version: v2。旧服务注册version: v1

    2 用户请求过来之后,zuul自定义一个过滤器,根据请求头token信息解析出用户信息,再去获取用户灰度配置规则信息。

    3 网关根据灰度配置规则路由到不同新/旧服务A。

    4 请求到达新/旧服务A之后,服务A可能还需要调用服务B相关接口,在服务A中使用AOP在调用方法前进行加强。根据请求头token信息解析出用户信息,再去获取用户灰度配置规则信息。

    5 服务间调用根据灰度配置规则路由到不同新/旧服务B

    资源准备

    spring cloud微服务准备

    调用链路:用户==>zuul-server==>abTest==> provider-server

    服务名端口eureka元数据描述
    zuul-server9000网关服务
    abTest8083version: v2新版本金丝雀服务
    abTest8084version: v1老版本服务
    abTest8085version: v1老版本旧服务
    provider-server8093version: v2新版本金丝雀服务
    provider-server8094version: v1老版本服务
    provider-server8095version: v1老版本旧服务

    路由规则库表


     

    1. # 用户表
    2. CREATE TABLE `t_user` (
    3. `id` int(11) NOT NULL AUTO_INCREMENT,
    4. `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户昵称',
    5. `head_image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'head_image',
    6. `city` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '城市',
    7. `gender` int(2) DEFAULT NULL COMMENT '性别 0:男 1:女',
    8. `user_type` int(2) DEFAULT 0 COMMENT '用户类型(0:普通用户 1:vip)',
    9. `mobile` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户手机号',
    10. `status` int(2) DEFAULT 1 COMMENT '用户状态 0:冻结 1:正常',
    11. `token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '登录token',
    12. `token_expires_time` datetime(0) DEFAULT NULL COMMENT 'token过期时间',
    13. `create_time` datetime(0) DEFAULT NULL COMMENT '创建时间',
    14. `update_time` datetime(0) DEFAULT NULL COMMENT '更新时间',
    15. PRIMARY KEY (`id`) USING BTREE
    16. ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
    17. 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');
    18. 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');
    19. 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');
    20. # 灰度路由规则配置表
    21. CREATE TABLE `ab_test` (
    22. `id` int(11) NOT NULL,
    23. `application_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '服务名',
    24. `version` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '版本',
    25. `userId` int(11) DEFAULT NULL COMMENT '用户id',
    26. PRIMARY KEY (`id`) USING BTREE
    27. ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    28. INSERT INTO `ab_test` VALUES (1, 'abTest', 'v1', 1);
    29. INSERT INTO `ab_test` VALUES (2, 'abTest', 'v2', 3);

    代码实现

    灰度服务eureka.instance.metadata-map元数据信息version: v2。 正常服务设置元数据信息version: v1
    使用框架实现根据元数据信息指定路由服务。

    网关路由(zuul-server服务)

    本demo使用zuul作为网关层,框架实现根据元数据信息指定路由服务。

    pom.xml

    1. <dependency>
    2. <groupId>io.jmnarloch</groupId>
    3. <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
    4. <version>2.1.0</version>
    5. </dependency>

    网关拦截用户请求

    1. package com.hanergy.out.controller;
    2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    3. import com.hanergy.out.config.GrayHolder;
    4. import com.hanergy.out.entity.AbTest;
    5. import com.hanergy.out.entity.TUser;
    6. import com.hanergy.out.service.AbTestService;
    7. import com.hanergy.out.service.TUserService;
    8. import com.hanergy.out.util.R;
    9. import com.netflix.zuul.ZuulFilter;
    10. import com.netflix.zuul.context.RequestContext;
    11. import com.netflix.zuul.exception.ZuulException;
    12. import io.jmnarloch.spring.cloud.ribbon.api.RibbonFilterContext;
    13. import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
    14. import org.slf4j.Logger;
    15. import org.slf4j.LoggerFactory;
    16. import org.springframework.beans.factory.annotation.Autowired;
    17. import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter;
    18. import org.springframework.http.HttpStatus;
    19. import org.springframework.stereotype.Component;
    20. import javax.servlet.http.HttpServletRequest;
    21. import java.util.List;
    22. import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
    23. @Component
    24. public class AbTestFilter extends ZuulFilter {
    25. Logger log = LoggerFactory.getLogger(AbTestFilter.class);
    26. public final static String ACCESS_TOKEN = "access_token";
    27. @Autowired
    28. private AbTestService abTestService;
    29. @Autowired
    30. private TUserService userService;
    31. /** * 过滤器类型: 前置过滤器 * @return */
    32. @Override
    33. public String filterType() {
    34. return PRE_TYPE;
    35. }
    36. /** * 过滤器顺序,越小越先执行 * @return */
    37. @Override
    38. public int filterOrder() {
    39. return 50;
    40. }
    41. /** * 过滤器是否生效 * @return */
    42. @Override
    43. public boolean shouldFilter() {
    44. return true;
    45. }
    46. /** * 业务逻辑 * @return * @throws ZuulException */
    47. @Override
    48. public Object run() throws ZuulException {
    49. RequestContext requestContext = RequestContext.getCurrentContext();
    50. HttpServletRequest request = requestContext.getRequest();
    51. //请求请求头token信息
    52. String token = request.getHeader("token");
    53. // 根据token获取用户信息
    54. TUser user = userService.getOne(new QueryWrapper<TUser>()
    55. .lambda()
    56. .eq(TUser::getToken, token));
    57. // token异常
    58. if (user == null){
    59. requestContext.setSendZuulResponse(false);
    60. requestContext.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
    61. }
    62. // 查询灰度发布配置表,判断此用户是否灰度用户
    63. AbTest abTest = abTestService.getOne(new QueryWrapper<AbTest>()
    64. .lambda()
    65. .eq(AbTest::getUserid, user.getId()));
    66. // v1:正常服务 v2:灰度服务
    67. if (abTest != null && "v2".equals(abTest.getVersion())){
    68. RibbonFilterContextHolder.getCurrentContext().add("version","v2");
    69. } else {
    70. RibbonFilterContextHolder.getCurrentContext().add("version","v1");
    71. }
    72. return null;
    73. }
    74. }

    服务间调用路由(abTest服务)

    pom.xml

    1. <dependency>
    2. <groupId>io.jmnarloch</groupId>
    3. <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
    4. <version>2.1.0</version>
    5. </dependency>

    aop拦截请求,获取灰度信息

    1. package com.hanergy.out.config;
    2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    3. import com.hanergy.out.entity.AbTest;
    4. import com.hanergy.out.entity.TUser;
    5. import com.hanergy.out.service.AbTestService;
    6. import com.hanergy.out.service.TUserService;
    7. import com.hanergy.out.utils.RibbonParam;
    8. import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault;
    9. import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
    10. import org.aopalliance.intercept.Joinpoint;
    11. import org.apache.commons.lang3.StringUtils;
    12. import org.aspectj.lang.JoinPoint;
    13. import org.aspectj.lang.annotation.Aspect;
    14. import org.aspectj.lang.annotation.Before;
    15. import org.aspectj.lang.annotation.Pointcut;
    16. import org.springframework.beans.factory.annotation.Autowired;
    17. import org.springframework.http.HttpRequest;
    18. import org.springframework.http.HttpStatus;
    19. import org.springframework.http.client.support.HttpRequestWrapper;
    20. import org.springframework.stereotype.Component;
    21. import org.springframework.web.context.request.RequestContextHolder;
    22. import org.springframework.web.context.request.ServletRequestAttributes;
    23. import org.springframework.web.servlet.support.RequestContext;
    24. import javax.servlet.http.HttpServletRequest;
    25. import java.util.HashMap;
    26. import java.util.Map;
    27. /** * @description: * @author: Han LiDong * @create: 2021/11/18 16:31 * @update: 2021/11/18 16:31 */
    28. @Aspect
    29. @Component
    30. public class ReqestAspect {
    31. @Autowired
    32. private TUserService userService;
    33. @Autowired
    34. private AbTestService abTestService;
    35. @Before("execution(* com.hanergy.out.controller.*.*(..))")
    36. public void before(){
    37. HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
    38. String token = request.getHeader("token");
    39. // 根据token获取用户信息
    40. TUser user = userService.getOne(new QueryWrapper<TUser>()
    41. .lambda()
    42. .eq(TUser::getToken, token));
    43. if (user == null){
    44. throw new RuntimeException("token异常");
    45. }
    46. // 查询灰度发布配置表,判断此用户是否灰度用户
    47. AbTest abTest = abTestService.getOne(new QueryWrapper<AbTest>()
    48. .lambda()
    49. .eq(AbTest::getUserid, user.getId()));
    50. // v1:正常服务 v2:灰度服务
    51. if (abTest != null && "v2".equals(abTest.getVersion())){
    52. RibbonFilterContextHolder.getCurrentContext().add("version","v2");
    53. } else {
    54. RibbonFilterContextHolder.getCurrentContext().add("version","v1");
    55. }
    56. }
    57. }

    灰度接口测试

    调用链:用户==》zuul网关==>abTest服务==>provider-server服务

    provider-server服务提供测试接口

    1. @Slf4j
    2. @RestController
    3. @RequestMapping("/v1/test")
    4. public class TestController {
    5. @Value("${server.port}")
    6. private Integer port;
    7. @ApiOperation(value="获取端口号",notes="获取端口号")
    8. @GetMapping("/getPort")
    9. public HttpResult<Integer> getPort(){
    10. return HttpResult.successResult(port);
    11. }
    12. }

    abTest服务提供测试接口

    feign服务间调用

    1. @FeignClient(value = "provider-server",fallback = ManagerPreFallbackImpl.class)
    2. public interface RemoteManagerPreService {
    3. @ApiOperation(value="获取端口号",notes="获取端口号")
    4. @GetMapping("/v1/test/getPort")
    5. public HttpResult<Integer> getPort();
    6. }

    hystrix断路器

    1. @Slf4j
    2. @Component
    3. public class ManagerPreFallbackImpl implements RemoteManagerPreService {
    4. @Override
    5. public HttpResult<Integer> getPort() {
    6. log.error("获取provider服务端口异常");
    7. return null;
    8. }
    9. }

    服务间调用

    1. @Slf4j
    2. @RestController
    3. @RequestMapping("/v1/test")
    4. public class TestController {
    5. @Value("${server.port}")
    6. private Integer port;
    7. @ApiOperation(value="获取provider服务端口号",notes="获取provider服务端口号")
    8. @GetMapping("/getProviderPort")
    9. public HttpResult<Integer> getProviderPort(){
    10. // feign服务间调用
    11. HttpResult<Integer> res = remoteManagerPreService.getPort();
    12. Integer providerPort = res.getData();
    13. return HttpResult.successResult("port: "+ port + ",providerPort:" + providerPort);
    14. }
    15. }

    验证

    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端口服务

    1 启动所需服务

    启动eureka注册中心、zuul网关、abtest(8083、8084、8085)、provider-server(8093、8094、8095)

    2 调用eureka RestFul接口修改元数据信息

    通过此种方法更改server的元数据后,由于ribbon会缓存实例列表,所以在测试改变服务信息时,ribbon并不会马上从eureka拉去最新信息,需等待一段时间。

    1. //修改8083端口abTest服务元数据信息
    2. PUT 182.92.xxx.xxx:8761/eureka/apps/ABTEST/192.168.199.1:abTest:8083/metadata?version=v2
    3. //修改8093端口provider-server服务元数据信息
    4. PUT 182.92.219.202:8761/eureka/apps/PROVIDER-SERVER/192.168.199.1:provider-server:8093/metadata?version=v2

    3 验证eureka元数据信息是否已添加

    4  灰度用户调用测试

    5 正常用户请求测试

     

  • 相关阅读:
    Elasticsearch 通配符查询
    2024年天津中德应用技术大学专升本机械电子工程专业课考试大纲
    k线图形态这样记(口诀篇)
    在面试了些外包以后,我有了些自己的思考
    CentOS7上安装Hadoop2.9.2集群
    halcon学习和实践(hdev脚本学习技巧)
    Redis(主从复制、哨兵模式、集群)概述及部署
    操作系统-进程与线程(同步互斥典型模型-读者写者模型,哲学家进餐问题)
    SpringMVC工作流程
    mysql索引最左匹配原则的理解?(绝对牛逼)
  • 原文地址:https://blog.csdn.net/xushiyu1996818/article/details/124784216