当前位置:   article > 正文

面试官:如何实现微服务全链路灰度发布?

实现全链路灰度

d62189932c36b1e21df2d53dfc1a4fa9.png

作者 | 磊哥

来源 |公众号:Java中文社群

转载请联系授权(微信ID:GG_Stone)

灰度发布(Gray Release,也称为灰度发布或金丝雀发布)是指在软件或服务发布过程中,将新版本的功能或服务以较小的比例引入到生产环境中,仅向部分用户或节点提供新功能的一种发布策略。

在传统的全量发布中,新版本的功能会一次性全部部署到所有的用户或节点上。然而,这种方式潜在的风险是,如果新版本存在缺陷或问题,可能会对所有用户或节点产生严重的影响,导致系统崩溃或服务不可用。

相比之下,灰度发布采用较小的规模,并逐步将新版本的功能引入到生产环境中,仅向一小部分用户或节点提供新功能。通过持续监测和评估,可以在发现问题时及时回滚或修复。这种逐步引入新版本的方式可以降低风险,并提高系统的稳定性和可靠性。

1.实现思路

灰色发布的常见实现思路有以下几种:

  • 根据用户划分:根据用户标识或用户组进行划分,在整个用户群体中只选择一小部分用户获得新功能。

  • 根据地域划分:在不同地区或不同节点上进行划分,在其中的一小部分地区或节点进行新功能的发布。

  • 根据流量划分:根据流量的百分比或请求次数进行划分,只将一部分请求流量引导到新功能上。

而在生产环境中,比较常用的是根据用户标识来实现灰色发布,也就是说先让一小部分用户体验新功能,以发现新服务中可能存在的某种缺陷或不足。

2.具体实现

Spring Cloud 全链路灰色发布的关键实现思路如下图所示:026761e012735b28a32d26f1d064ea84.png灰度发布的具体实现步骤如下:

  1. 前端程序在灰度测试的用户 Header 头中打上标签,例如在 Header 中添加“grap-tag: true”,其表示要进行灰常测试(访问灰度服务),而其他则为访问正式服务。

  2. 在负载均衡器 Spring Cloud LoadBalancer 中,拿到 Header 中的“grap-tag”进行判断,如果此标签不为空,并等于“true”的话,表示要访问灰度发布的服务,否则只访问正式的服务。

  3. 在网关 Spring Cloud Gateway 中,将 Header 标签“grap-tag: true”继续往下一个调用服务中传递。

  4. 在后续的调用服务中,需要实现以下两个关键功能:

    1. 在负载均衡器 Spring Cloud LoadBalancer 中,判断灰度发布标签,将请求分发到对应服务。

    2. 将灰度发布标签(如果存在),继续传递给下一个调用的服务。

经过第四步的反复传递之后,整个 Spring Cloud 全链路的灰度发布就完成了。

3.核心实现思路和代码

灰度发布的关键实现技术和代码如下。

3.1 区分正式服务和灰度服务

在灰度发布的执行流程中,有一个核心的问题,如果在 Spring Cloud LoadBalancer 进行服务调用时,区分正式服务和灰度服务呢?

这个问题的解决方案是:在灰度服务既注册中心的 MetaData(元数据)中标识自己为灰度服务即可,而元数据中没有标识(灰度服务)的则为正式服务,以 Nacos 为例,它的设置如下:

  1. spring:
  2.   application:
  3.     name: canary-user-service
  4.   cloud:
  5.     nacos:
  6.       discovery:
  7.         username: nacos
  8.         password: nacos
  9.         server-addr: localhost:8848
  10.         namespace: public
  11.         register-enabled: true 
  12.         metadata: { "grap-tag":"true" } # 标识自己为灰度服务

3.2 负载均衡调用灰度服务

Spring Cloud LoadBalancer 判断并调用灰度服务的关键实现代码如下:

  1. private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,
  2.                                                           Request request) {
  3.         // 实例为空
  4.         if (instances.isEmpty()) {
  5.             if (log.isWarnEnabled()) {
  6.                 log.warn("No servers available for service: " + this.serviceId);
  7.             }
  8.             return new EmptyResponse();
  9.         } else { // 服务不为空
  10.             RequestDataContext dataContext = (RequestDataContext) request.getContext();
  11.             HttpHeaders headers = dataContext.getClientRequest().getHeaders();
  12.             // 判断是否为灰度发布(请求)
  13.             if (headers.get(GlobalVariables.GRAY_KEY) != null &&
  14.                     headers.get(GlobalVariables.GRAY_KEY).get(0).equals("true")) {
  15.                 // 灰度发布请求,得到新服务实例列表
  16.                 List<ServiceInstance> findInstances = instances.stream().
  17.                         filter(s -> s.getMetadata().get(GlobalVariables.GRAY_KEY) != null &&
  18.                                 s.getMetadata().get(GlobalVariables.GRAY_KEY).equals("true"))
  19.                         .toList();
  20.                 if (findInstances.size() > 0) { // 存在灰度发布节点
  21.                     instances = findInstances;
  22.                 }
  23.             } else { // 查询非灰度发布节点
  24.                 // 灰度发布测试请求,得到新服务实例列表
  25.                 instances = instances.stream().
  26.                         filter(s -> s.getMetadata().get(GlobalVariables.GRAY_KEY) == null ||
  27.                                 !s.getMetadata().get(GlobalVariables.GRAY_KEY).equals("true"))
  28.                         .toList();
  29.             }
  30.             // 随机正数值 ++i( & 去负数)
  31.             int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
  32.             // ++i 数值 % 实例数 取模 -> 轮询算法
  33.             int index = pos % instances.size();
  34.             // 得到服务实例方法
  35.             ServiceInstance instance = (ServiceInstance) instances.get(index);
  36.             return new DefaultResponse(instance);
  37.         }
  38.     }

以上代码为自定义负载均衡器,并使用了轮询算法。如果 Header 中有灰度标签,则只查询灰度服务的节点实例,否则则查询出所有的正式节点实例(以供服务调用或服务转发)。

3.3 网关传递灰度标识

要在网关 Spring Cloud Gateway 中传递灰度标识,只需要在 Gateway 的全局自定义过滤器中设置 Response 的 Header 即可,具体实现代码如下:

  1. package com.example.gateway.config;
  2. import com.loadbalancer.canary.common.GlobalVariables;
  3. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  4. import org.springframework.cloud.gateway.filter.GlobalFilter;
  5. import org.springframework.core.Ordered;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.server.reactive.ServerHttpRequest;
  8. import org.springframework.http.server.reactive.ServerHttpResponse;
  9. import org.springframework.stereotype.Component;
  10. import org.springframework.web.server.ServerWebExchange;
  11. import reactor.core.publisher.Mono;
  12. @Component
  13. public class LoadBalancerFilter implements GlobalFilter {
  14.     @Override
  15.     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  16.         // 得到 request、response 对象
  17.         ServerHttpRequest request = exchange.getRequest();
  18.         ServerHttpResponse response = exchange.getResponse();
  19.         if (request.getQueryParams().getFirst(GlobalVariables.GRAY_KEY) != null) {
  20.             // 设置金丝雀标识
  21.             response.getHeaders().set(GlobalVariables.GRAY_KEY,
  22.                     "true");
  23.         }
  24.         // 此步骤正常,执行下一步
  25.         return chain.filter(exchange);
  26.     }
  27. }

3.4 Openfeign 传递灰度标签

HTTP 调用工具 Openfeign 传递灰度标签的实现代码如下:

  1. import feign.RequestInterceptor;
  2. import feign.RequestTemplate;
  3. import jakarta.servlet.http.HttpServletRequest;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.web.context.request.RequestContextHolder;
  6. import org.springframework.web.context.request.ServletRequestAttributes;
  7. import java.util.Enumeration;
  8. import java.util.LinkedHashMap;
  9. import java.util.Map;
  10. @Component
  11. public class FeignRequestInterceptor implements RequestInterceptor {
  12.     @Override
  13.     public void apply(RequestTemplate template) {
  14.         // 从 RequestContextHolder 中获取 HttpServletRequest
  15.         ServletRequestAttributes attributes = (ServletRequestAttributes)
  16.                 RequestContextHolder.getRequestAttributes();
  17.         // 获取 RequestContextHolder 中的信息
  18.         Map<String, String> headers = getHeaders(attributes.getRequest());
  19.         // 放入 openfeign 的 RequestTemplate 中
  20.         for (Map.Entry<String, String> entry : headers.entrySet()) {
  21.             template.header(entry.getKey(), entry.getValue());
  22.         }
  23.     }
  24.     /**
  25.      * 获取原请求头
  26.      */
  27.     private Map<String, String> getHeaders(HttpServletRequest request) {
  28.         Map<String, String> map = new LinkedHashMap<>();
  29.         Enumeration<String> enumeration = request.getHeaderNames();
  30.         if (enumeration != null) {
  31.             while (enumeration.hasMoreElements()) {
  32.                 String key = enumeration.nextElement();
  33.                 String value = request.getHeader(key);
  34.                 map.put(key, value);
  35.             }
  36.         }
  37.         return map;
  38.     }
  39. }

小结

灰度发布是微服务时代保证生产环境安全的必备措施,而其关键实现思路是:

1、注册中心区分正常服务和灰度服务;

2、负载均衡正确转发正常服务和灰度服务;

3、网关和 HTTP 工具传递灰度标签。

这样,我们就完整的实现 Spring Cloud 全链路灰度发布功能了。


说件大事

磊哥一直在做的事,为了让大家找到更好的工作,所以有着 13 年工作经验的我,开发了一门《Java 面试突击训练营》。

整个课程为期一个月,带着大家把 16 万的图文 + 3000 多分钟的视频课系统的学完,并且录屏支持永久更新和观看。 

整个课程从 Java 基础到微服务 Spring Cloud 应用尽有,包含以下 16 个大的模块:

5ede3341b0e0f66a74413c7bb59e1c36.png

训练营带你高效的搞定面试问题,学完之后可以应对当前市面上绝大部分公司的面试,得到高薪 Offer。

加我微信咨询吧:GG_Stone【备注:训练营】

6a77c0997719f2f4fe153758ad21b8b7.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/494563
推荐阅读
相关标签
  

闽ICP备14008679号