首页 > 其他分享 >面试官:说说微服务灰度发布的底层实现?

面试官:说说微服务灰度发布的底层实现?

时间:2024-03-06 16:48:56浏览次数:31  
标签:面试官 服务 Spring 灰度 org import Cloud 底层

微服务中的灰度发布(又称为金丝雀发布)是一种持续部署策略,它允许在正式环境的小部分用户群体上先部署新版本的应用程序或服务,而不是一次性对所有用户同时发布全新的版本。

这种方式有助于在生产环境中逐步验证新版本的稳定性和兼容性,同时最小化潜在风险,不影响大部分用户的正常使用。

1.灰度发布关键步骤

在 Spring Cloud 微服务架构中,实现灰度发布通常涉及到以下几个方面:

  1. 流量分割
    • 根据一定的策略(如用户 ID、请求头信息、IP 地址等)将流入的请求分配给不同版本的服务实例。
    • 使用 Spring Cloud Gateway、Zuul 等 API 网关组件实现路由规则,将部分请求定向至新版本的服务节点。
  2. 版本标识
    • 新版本服务启动时会注册带有特定版本标签的服务实例到服务注册中心(如 Eureka 或 Nacos)。
    • 请求在路由时可以根据版本标签选择相应版本的服务实例。
  3. 监控与评估
    • 在灰度发布的阶段,运维团队会对新版本服务的性能、稳定性以及用户体验等方面进行实时监控和评估。
    • 如果新版本表现良好,则可以逐渐扩大灰度范围直至全面替换旧版本。
  4. 故障恢复与回滚:若新版本出现问题,可通过快速撤销灰度发布策略,使所有流量恢复到旧版本服务,实现快速回滚,确保服务整体可用性。

通过 Spring Cloud 的扩展组件和自定义路由策略,开发人员可以轻松实现灰度发布功能,确保在微服务架构中安全、平滑地进行版本迭代升级。

2.实现思路

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

  • 根据用户划分:根据用户标识或用户组进行划分,在整个用户群体中只选择一小部分用户获得新功能。
  • 根据地域划分:在不同地区或不同节点上进行划分,在其中的一小部分地区或节点进行新功能的发布。
  • 根据流量划分:根据流量的百分比或请求次数进行划分,只将一部分请求流量引导到新功能上。

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

3.底层实现

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

  1. 前端程序在灰度测试的用户 Header 头中打上标签,例如在 Header 中添加“gray-tag: true”,其表示要进行灰常测试(访问灰度服务),而其他则为访问正式服务。
  2. 在负载均衡器 Spring Cloud LoadBalancer 中,拿到 Header 中的“gray-tag”进行判断,如果此标签不为空,并等于“true”的话,表示要访问灰度发布的服务,否则只访问正式的服务。
  3. 在网关 Spring Cloud Gateway 中,将 Header 标签“gray-tag: true”继续往下一个调用服务中传递。
  4. 在后续的调用服务中,需要实现以下两个关键功能:
    1. 在负载均衡器 Spring Cloud LoadBalancer 中,判断灰度发布标签,将请求分发到对应服务。
    2. 将灰度发布标签(如果存在),继续传递给下一个调用的服务。

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

4.具体实现

4.1 版本标识

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

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

spring:
  application:
    name: gray-user-service
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        server-addr: localhost:8848
        namespace: public
        register-enabled: true 
        metadata: { "gray-tag":"true" } # 标识自己为灰度服务

4.2 负载均衡调用灰度服务

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

private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,
                                                          Request request) {
        // 实例为空
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + this.serviceId);
            }
            return new EmptyResponse();
        } else { // 服务不为空
            RequestDataContext dataContext = (RequestDataContext) request.getContext();
            HttpHeaders headers = dataContext.getClientRequest().getHeaders();
            // 判断是否为灰度发布(请求)
            if (headers.get(GlobalVariables.GRAY_KEY) != null &&
                    headers.get(GlobalVariables.GRAY_KEY).get(0).equals("true")) {
                // 灰度发布请求,得到新服务实例列表
                List<ServiceInstance> findInstances = instances.stream().
                        filter(s -> s.getMetadata().get(GlobalVariables.GRAY_KEY) != null &&
                                s.getMetadata().get(GlobalVariables.GRAY_KEY).equals("true"))
                        .toList();
                if (findInstances.size() > 0) { // 存在灰度发布节点
                    instances = findInstances;
                }
            } else { // 查询非灰度发布节点
                // 灰度发布测试请求,得到新服务实例列表
                instances = instances.stream().
                        filter(s -> s.getMetadata().get(GlobalVariables.GRAY_KEY) == null ||
                                !s.getMetadata().get(GlobalVariables.GRAY_KEY).equals("true"))
                        .toList();
            }
            // 随机正数值 ++i( & 去负数)
            int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
            // ++i 数值 % 实例数 取模 -> 轮询算法
            int index = pos % instances.size();
            // 得到服务实例方法
            ServiceInstance instance = (ServiceInstance) instances.get(index);
            return new DefaultResponse(instance);
        }
    }

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

4.3 网关传递灰度标识

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

package com.example.gateway.config;

import com.loadbalancer.canary.common.GlobalVariables;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class LoadBalancerFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 得到 request、response 对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        if (request.getQueryParams().getFirst(GlobalVariables.GRAY_KEY) != null) {
            // 设置金丝雀标识
            response.getHeaders().set(GlobalVariables.GRAY_KEY,
                    "true");
        }
        // 此步骤正常,执行下一步
        return chain.filter(exchange);
    }
}

4.4 微服务中传递灰度标签

HTTP 调用工具 Openfeign,我们需要在微服务间继续传递灰度标签,它的实现代码如下:

import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

@Component
public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 从 RequestContextHolder 中获取 HttpServletRequest
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        // 获取 RequestContextHolder 中的信息
        Map<String, String> headers = getHeaders(attributes.getRequest());
        // 放入 openfeign 的 RequestTemplate 中
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            template.header(entry.getKey(), entry.getValue());
        }
    }

    /**
     * 获取原请求头
     */
    private Map<String, String> getHeaders(HttpServletRequest request) {
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        if (enumeration != null) {
            while (enumeration.hasMoreElements()) {
                String key = enumeration.nextElement();
                String value = request.getHeader(key);
                map.put(key, value);
            }
        }
        return map;
    }
}

课后思考

说说负载均衡的底层实现?为什么在网关和 Openfeign 中要传递灰度标签?

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

标签:面试官,服务,Spring,灰度,org,import,Cloud,底层
From: https://www.cnblogs.com/vipstone/p/18056928

相关文章

  • MyBatis 底层工作原理
    引言SqlSession是MyBatis提供的面向用户的操作数据库API。那么MyBatis底层是如何工作的呢?为了解开MyBatis的神秘面纱,我们需要了解一下MyBatis的其他几个比较核心的组件及这些组件的作用。MyBatis核心组件MyBatis的执行流程及核心组件如图:Configuration:用于描述MyBati......
  • 面试官:说说SpringAOP实现原理?
    AOP(Aspect-OrientedProgramming,面向切面编程)是一种编程技术,它允许开发者在不改变现有代码的情况下,增加新的功能或行为,这些功能或行为被称为“切面”。AOP可以通过预编译方式和运行期动态代理的方式来实现,它的主要目的是降低业务逻辑的耦合性,提高程序的可重用性和开发效率。AOP......
  • 【C++】【OpenCV-4.9.0】灰度图取反(Mat属性的使用)
    此次我们将一张图像转灰度后再进行灰度取反,即黑的变白的,白的变黑的,所以我们需要获取每个像素点上的灰度级,cv中提供了一个函数at,但是这个函数还有11个重载函数,太多了,我们只用这次需要用到的,即通过读取像素点的位置来获取灰度级。◆ at() [3/12]template<typename_Tp>c......
  • 面试官:集合使用时应该注意哪些问题?我:应该注意该注意的问题!
    写在开头面试官:“小伙子,java的集合学过吗?”我:“肯定学过呀!”,这时候的我自信满满,手撕集合八股文嘛,早已背的滚瓜烂熟了呀。面试官:“那你来讲讲集合使用时,应该注意哪些问题吧”我:“额,这,我想想哈。”,什么!这面试官不按套路出牌,上来就问注意事项,打我一个措手不及啊。我:“嗯~,我觉得......
  • 在K8S中,svc底层是如何实现的?
    在Kubernetes(K8s)中,Service是集群内部的一个抽象层,用于定义一组Pod的逻辑分组,并提供统一的访问入口点,同时还可以对这些Pod提供负载均衡和网络代理功能。Service底层的实现主要包括以下几个关键组件和技术:标签选择器(LabelSelectors):Kubernetes中的Service并不直接指向具体......
  • 面试官上来就让手撕HashMap的7种遍历方式,当场愣住,最后只写出了3种
    写在开头今天有个小伙伴私信诉苦,说面试官上来就让他手撕HashMap的7种遍历方式,最终只写出3种常用的,怀疑面试官是在故意***难。这个问题大家怎么看?反正我个人感觉这肯定不是***难,“手撕遍历方式”算是一个比较简单的考验方式了,而且集合的遍历又是日常开发的必备!至于要一下写出7......
  • pythonUI自动化之POM底层框架
    1.pythonUI自动化之组成结构python+pytest+allure+POMpython:编程语言pytest:单元测试框架allure:测试报告POM:页面对象模型 2.pythonUI自动化之POM测试框架目录1.base文件夹:基类,用于配置元素操作,如获取元素、输入、点击、等待等函数2.pages_obje......
  • 面试官:如何设计一个高并发系统?
    面试题如何设计一个高并发系统?面试官心理剖析面试官对高并发系统的深入询问,实际上是对求职者专业技能和经验的精准检验。在许多公司发布的职位描述(JD)中,高并发经验被视作一项重要的加分项,这反映了当前互联网行业对高并发处理能力的极高要求。对于那些真正在互联网公司中负责过......
  • 面试官:说一下红锁RedLock的实现原理?
    RedLock是一种分布式锁的实现算法,由Redis的作者SalvatoreSanfilippo(也称为Antirez)提出,主要用于解决在分布式系统中实现可靠锁的问题。在Redis单独节点的基础上,RedLock使用了多个独立的Redis实例(通常建议是奇数个,比如5个),共同协作来提供更强健的分布式锁服务。RedLo......
  • 面试官:如何实现10亿数据判重?
    From: https://mp.weixin.qq.com/s/l2yVtL5siHpxzNLuxJ3nkQ当数据量比较大时,使用常规的方式来判重就不行了。例如,使用MySQL数据库判重,或使用List.contains()或Set.contains()判重就不可行,因为MySQL在数据量大时查询就会非常慢,而数据库又是及其珍贵的全局数据库资源。......