在微服务中如何实现全链路的金丝雀发布?

目录

1. 什么金丝雀发布?它有什么用?

2.如何实现全链路的金丝雀发布

2.1 负载均衡模块

2.2 网关模块

2.3 服务模块

2.3.1 注册为灰色服务实例

2.3.2 设置负载均衡器

2.3.3 传递灰度发布标签

2.4 其他代码

2.4.1 其他业务代码

2.4.2 pom.xml 关键代码

2.4.3 application.yml 相关代码

3. 验证全链路金丝雀发布的实现效果


1. 什么金丝雀发布?它有什么用?

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

        而在传统的全量发布中,新版本将会立即部署到所有用户或节点上。金丝雀发布的核心思想是逐步推进,监测新版本的稳定性和性能,以确保在全面发布之前能够解决潜在的问题。

假设某款在线多人游戏决定上线一个全新的多人模式功能。在传统的全量发布中,它会将这个新功能立即部署到所有玩家的游戏客户端中,然后在全面发布后等待用户的反馈。而使用金丝雀发布,它的发布流程就变成了这样:

  1. 内测阶段

    • 游戏开发团队首先将新多人模式功能引入到游戏的内测版本中,但仅向少数特定的内测玩家提供。
    • 这些内测玩家是经过筛选或自愿参与的,他们了解可能会遇到问题,并愿意分享反馈。
    • 内测玩家可以在一定时间内使用新功能,并向开发团队报告问题、提供建议和反馈意见。
  2. 监测和改进

    • 游戏开发团队密切关注内测玩家的游戏体验、性能和稳定性。
    • 如果在内测期间发现了问题,团队可以及时进行修复和改进,并确保新功能在全面发布前达到高质量标准。
  3. 逐步扩展

    • 在确认新功能在内测阶段表现良好后,开发团队逐步扩展金丝雀发布的范围。
    • 他们可以将新功能提供给更多的玩家,但仍然限制在一小部分,比如10%的玩家。
    • 这一阶段被称为金丝雀发布的初期阶段,新功能仅对一小部分用户可见。
  4. 全面发布

    • 在经过一系列逐步扩展和监测后,开发团队最终将新多人模式功能发布给了所有玩家。
    • 此时,新功能已经通过了多轮测试和改进,用户体验较好,且潜在问题得到了解决。

从上述游戏上线新功能的金丝雀发布流程中能看出,金丝雀发布相比传统的全量发布有以下好处:

  • 逐步引入新功能,降低全面发布的风险。
  • 及时获取内测玩家的反馈,加速问题的修复。
  • 确保新功能在全面发布时达到高质量标准。
  • 提供更好的用户体验,减少潜在问题对所有用户的影响。

2.如何实现全链路的金丝雀发布

Spring Cloud 全链路金丝雀发布的实现思路图如下:

 金丝雀发布的具体实现步骤大致分为以下几步:

  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. 在内部的服务调用过程中,传递灰度发布标签。

由此可见,全链路的灰色发布只需要解决两个大问题:

1. Gateway 中的问题

  • Gateway 的调度转发问题。
  • Gateway 灰色发布标签的传递问题。

2. 内部服务中的问题

  • 服务的灰度转发问题。
  • 服务内部灰色发布标签的传递问题。

【金丝雀发布代码案例】

根据 Spring Cloud 全链路金丝雀发布的实现思路图来编写代码,

创建 Spring 多模块项目,然后准备 7 个模块:user-service、new-user-service、order-service、log-service、new-log-service、gray-loadbalancer、gateway。

2.1 负载均衡模块

操作 gray-loadblancer 模块,这个模块作为一个公共模块,可以不需要启动类。

① 自定义负载均衡器

这里可以参考默认的轮询负载均衡策略里面的实现:

  1. 实现 ReactorServiceInstanceLoadBalancer 接口
  2. 复制其他代码,修改关键地方的类名
  3. 重写 getInstanceResponse 方法
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.loadbalancer.core.*;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 定义灰度发布的负载均衡算法
 */

public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private static final Log log = LogFactory.getLog(GrayLoadBalancer.class);
    private final String serviceId;
    private AtomicInteger position;  // 下标
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.position = new AtomicInteger(new Random().nextInt(1000));
    }

    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map((serviceInstances) -> {
            return this.processInstanceResponse(supplier, serviceInstances,request);
        });
    }

    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
                                                              List<ServiceInstance> serviceInstances,
                                                              Request request) {
        Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances,request);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());
        }

        return serviceInstanceResponse;
    }

    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 {
            // 灰度业务的实现

            // 0.得到 Request 对象 [通过方法参数的传递拿到此对象]

            // 1.从 Request 对象的 Header 中得到灰度标签
            RequestDataContext requestDataContext = (RequestDataContext) request.getContext();
            HttpHeaders headers = requestDataContext.getClientRequest().getHeaders();
            // 获取名为 "gray-tag" 的头部信息的值
            List<String> headersList = headers.get(GlobalVariable.GRAY_TAGE);
            if (headersList != null && !headersList.isEmpty() &&
                    headersList.get(0).equals("true")) { // 灰度请求
                // 灰度列表
                List<ServiceInstance> grayList = instances.stream().
                        filter(i -> i.getMetadata().get(GlobalVariable.GRAY_TAGE) != null &&
                                i.getMetadata().get(GlobalVariable.GRAY_TAGE).equals("true")).
                        toList();
                if(!grayList.isEmpty()) {
                    instances = grayList;
                }
            } else {  // 正式节点
                // 2.将实例进行进行分组 【生产服务列表|灰度服务列表】
                instances = instances.stream(). // 取反
                        filter(i -> i.getMetadata().get(GlobalVariable.GRAY_TAGE) == null ||
                                !i.getMetadata().get(GlobalVariable.GRAY_TAGE).equals("true")).
                        toList();
            }
            // 3.使用负载均衡算法选择上一步列表中的某个节点
            int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
            ServiceInstance instance = instances.get(pos % instances.size());
            return new DefaultResponse(instance);
        }
    }
}
/**
 * 全局变量
 */
public class GlobalVariable {
    public static final String GRAY_TAGE = "gray-tag";
}

② 封装负载均衡器

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

/**
 * 封装灰度发布负载均衡器
 */
public class GrayLoadBalancerConfig {
    @Bean
    public ReactorLoadBalancer<ServiceInstance> grayLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        // 灰度发布的负载均衡器
        return new GrayLoadBalancer(
                loadBalancerClientFactory.getLazyProvider(name,
                        ServiceInstanceListSupplier.class), name);
    }
}

2.2 网关模块

通过全局过滤器,来判断或设置灰度标识,

import com.loadbalancer.gray.GlobalVariable;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
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();

        // 判断灰度标签
        String tag = request.getQueryParams().getFirst(GlobalVariable.GRAY_TAGE);
        if(tag != null) {
            // 设置灰度标识
            response.getHeaders().set(GlobalVariable.GRAY_TAGE,"true");
        }
        // 此步骤正常,执行下一步
        return chain.filter(exchange);
    }
}

2.3 服务模块

2.3.1 注册为灰色服务实例

将测试版的服务,注册为灰色服务实例:new-user-service、new-log-service

spring:
  application:
    name: user-service-gray
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        metadata: {"gray-tag": "true"}  # 金丝雀标识
server:
  port: 0

2.3.2 设置负载均衡器

在服务启动类上设置负载均衡和开启 OpenFeign 服务:user-service、new-user-service、order-service。

import com.loadbalancer.gray.GrayLoadBalancerConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
@LoadBalancerClients(defaultConfiguration =
        GrayLoadBalancerConfig.class)
public class UserServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }

}

 在网关模块中设置负载均衡,

import com.loadbalancer.gray.GrayLoadBalancerConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;

@SpringBootApplication
@LoadBalancerClients(defaultConfiguration =
        GrayLoadBalancerConfig.class)
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

}

2.3.3 传递灰度发布标签

在服务内部传递灰度发布标签:user-service、new-user-service、order-service

方式一:传递request中所有的header,所有的header中就包含了灰度发布标签。

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;

@Component
public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 从 RequestContextHolder 中获取 HttpServletRequest
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 传递所有的 header,就包含了灰度发布标签
        Enumeration<String> headerNames = request.getHeaderNames();
        while(headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            String value = request.getHeader(key);
            requestTemplate.header(key,value);
        }
    }
}

方式二:只传递header中的灰度发布标签

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 requestTemplate) {
        // 从 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()) {
            requestTemplate.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;
    }
}

2.4 其他代码

2.4.1 其他业务代码

① user-service 模块的 controller

import com.example.userservice.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private OrderService orderService;

    @RequestMapping("/getname")
    public String getName() {
        String result = orderService.getOrder();
        return "正式版:User Service getName." +
                result;
    }
}

② user-service 模块的 service

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient("order-service-gray")
@Service
public interface OrderService {
    @RequestMapping("/order/getorder")
    public String getOrder();
}

③ new-user-service 模块的 controller

import com.example.newuserservice.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private OrderService orderService;

    @RequestMapping("/getname")
    public String getName() {
        String result = orderService.getOrder();
        return "测试版:User Service getName." +
                result;
    }
}

④ new-user-service 模块的 service

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient("order-service-gray")
@Service
public interface OrderService {
    @RequestMapping("/order/getorder")
    public String getOrder();
}

⑤ order-service 模块的 controller

import com.example.orderservice.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private LogService logService;

    @RequestMapping("/getorder")
    public String getOrder() {
        String result = logService.getLog();
        return "Do OrderService getOrder Method." +
                result;
    }
}

⑥ order-service 模块的 service

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;

@Service
@FeignClient("log-service-gray")
public interface LogService {
    @RequestMapping("/log/getlog")
    public String getLog();
}

⑦ log-service 模块的 controller

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/log")
public class LogController {
    @RequestMapping("/getlog")
    public String getLog() {
        return "正式版:Log Service getLog";
    }
}

⑧ new-log-service 模块的 controller

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/log")
public class LogController {
    @RequestMapping("/getlog")
    public String getLog() {
        return "测试版:Log Service getLog";
    }
}

2.4.2 pom.xml 关键代码

① 父模块的 pom.xml

modules 中先加载服务调用链中靠后的服务,

<packaging>pom</packaging>

<!-- ....省略 -->

<!-- 注意打包顺序 -->
<modules>
    <module>gray-loadbalancer</module>
    <module>gateway</module>
    <module>new-log-service</module>
    <module>log-service</module>
    <module>order-service</module>
    <module>user-service</module>
    <module>new-user-service</module>
</modules>

<!-- ....省略 -->

<dependencies>
    <!-- nacos服务注册 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- 负载均衡 -->
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>gray-loadbalancer</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

② user-service 模块的 pom.xml

<parent>
    <groupId>com.example</groupId>
    <artifactId>gray-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

③ new-user-service 模块的 pom.xml

<parent>
    <groupId>com.example</groupId>
    <artifactId>gray-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>gray-loadbalancer</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

③ order-service 模块的 pom.xml

<parent>
    <groupId>com.example</groupId>
    <artifactId>gray-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>gray-loadbalancer</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

④ log-service 和 new-log-service 模块的 pom.xml

<parent>
    <groupId>com.example</groupId>
    <artifactId>gray-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

⑤ gray-loadbalancer 模块的 pom.xml

<parent>
    <groupId>com.example</groupId>
    <artifactId>gray-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

⑥ gateway 模块的 pom.xml

<parent>
    <groupId>com.example</groupId>
    <artifactId>gray-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>gray-loadbalancer</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

2.4.3 application.yml 相关代码

① user-service 模块的 application.yml

spring:
  application:
    name: user-service-gray
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
server:
  port: 0

② new-user-servicce 模块的 application.yml

spring:
  application:
    name: user-service-gray
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        metadata: {"gray-tag": "true"}  # 金丝雀标识
server:
  port: 0

③ order-servicce 模块的 application.yml

spring:
  application:
    name: order-service-gray
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
server:
  port: 0

④ log-servicce 模块的 application.yml

spring:
  application:
    name: log-service-gray
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
server:
  port: 0

⑤ new-log-servicce 模块的 application.yml

spring:
  application:
    name: log-service-gray
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        metadata: {"gray-tag": "true"}  # 金丝雀标识
server:
  port: 0

⑥ gateway 模块的 application.yml

spring:
  main:
    web-application-type: reactive  # Spring Web 和 reactive web 冲突
  application:
    name: gateway-gray
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        register-enabled: false  # 网关不需要注册到 nacos
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service-gray
          predicates:
            - Path=/user/**
server:
  port: 10086

3. 验证全链路金丝雀发布的实现效果

按顺序启动 log、order、user 的正式及测试服务,以及 gateway 模块,

使用 Postman 来验证全链路金丝雀发布的实现效果:

1. 请求头中不带 "gray-tag" 灰度标签,访问正式版服务

验证结果:无论访问多少次,不管是否服务集群,只要请求头中不带 "gray-tag" 灰度标签,只能访问到正式版的服务。

2. 请求头中带上 "gray-tag" 灰度标签,并且值为 true,访问测试版服务

验证结果:无论访问多少次,不管是否服务集群,只要请求头中带上 "gray-tag" 灰度标签,并且值为 true ,就只能访问到测试版的服务。


至此,微服务中全链路的金丝雀发布就实现好了~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/276286.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

出现频率高达80%的软件测试常见面试题合集(内附详细答案)

最近看到网上流传着各种面试经验及面试题&#xff0c;往往都是一大堆技术题目贴上去&#xff0c;但是没有答案。 为此我业余时间整理了这份软件测试基础常见的面试题及详细答案&#xff0c;望各路大牛发现不对的地方不吝赐教&#xff0c;留言即可。 01 软件测试理论部分 1.1…

SpingBoot的项目实战--模拟电商【1.首页搭建】

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.项目背景及技术点运用 …

你知道继电保护测试仪的价格是多少吗?

继电保护测试仪是电气设备检测中经常使用的检测仪器。它能准确、快速地检测到每个继电保护装置的一些潜在故障和问题&#xff0c;帮助电力检测工人锁定问题点&#xff0c;使继电保护装置能够正常工作&#xff0c;保护电力需求。继电保护测试仪贵吗&#xff1f;哪些因素影响价格…

链表:如何利用“假头,新指针,双指针”解决链表问题

Java学习面试指南&#xff1a;https://javaxiaobear.cn 链表是一种线性数据结构&#xff0c;其中的每个元素实际上是一个单独的对象&#xff0c;而所有对象都通过每个元素中的引用字段链接在一起。 链表是一种物理存储单元上非连续、非顺序的存储结构&#xff0c;其物理结构不能…

C# Winform教程(二):基础窗口程序

1、介绍 winform应用程序是一种智能客户端技术&#xff0c;我们可以使用winform应用程序帮助我们获得信息或者传输信息等。 2、常用属性 Name&#xff1a;在后台要获得前台的控件对象&#xff0c;需要使用Name属性。 Visible&#xff1a;指示一个控件是否可见、 Enable&…

基于动态窗口的航线规划

MATLAB2016b可以运行 % ------------------------------------------------------------------------- % File : DWA 算法 % Discription : Mobile Robot Motion Planning with Dynamic Window Approach % Author :Yuncheng Jiang % License : Modified BSD Software License A…

MySQL按月分片

一、按照月分片 使用场景为按照自然月来分片,每个自然月为一个分片,但是一年有12个月,是不是要有12个数据节点才行呢?并不是。例如我现在只有三个分片数据库,这样就可以1月在第一个数据分片中,2月在第二个数据分片中,3月在第三个数据分片中,当来到4月的时候,就会重新开…

echarts中给图表X轴和Y轴加单位以及给tooltip(提示框)增加单位

左边没有单位&#xff0c;右图是增加单位的效果。 1.x轴y轴设置单位 增加单位不管是x轴还是y轴都可以设置name字段&#xff0c;设置完name后效果是红色箭头效果。如果想要蓝色箭头效果可以使用x轴y轴的都有的 axisLabel 属性里面有formatter配置项&#xff0c;formatter支持字…

Python【json模块常用函数】

json模块常用函数 json模块是Python标准库中的一个内置模块&#xff0c;用于处理JSON&#xff08;JavaScript Object Notation&#xff09;格式的数据。它提供了一组函数来解析、序列化和操作JSON数据。 下面是json模块中常用的几个函数&#xff1a; .loads() 用于将JSON字…

YOLOv5-Lite 树莓派4B 15帧教程

【前言】 由于v5Lite仓库遗漏了不少历史问题&#xff0c;最大的问题是毕业后卷起来了&#xff0c;找不到时间更新。 上面是这篇博客的背景&#xff0c;那么先说下结论&#xff0c;使用 v5lite-e 模型&#xff0c;在 树莓派4B&#xff08;4G内存&#xff09; 上&#xff0c;有三…

C#高级 02异步编程

基础知识 1.什么是异步任务 包含了异步任务的各种状态的一个引用类型 1)正在运行、完成、结果、报错等 2)另有ValueTask值类型版本对于异步任务的抽象 1)开启异步任务后&#xff0c;当前线程并不会阻塞&#xff0c;而是可以去做其他事情 2)异步任务&#xff08;默认&#xff…

两张图片沿着斜对角线合并成一张图片

在图像融合领域&#xff0c;论文中的对比算法可视化&#xff0c;需要将红外图像和可见光图像沿着斜对角线合并成一张图片。 红外与可见光图像举例&#xff1a; 然后做出这样的效果&#xff1a; 用Python的PIL库&#xff0c;将两张图片沿着斜对角线合并成一张图片。 from PIL …

【Python基础篇】【19.异常处理】(附案例,源码)

异常处理 异常处理常见异常elsefinallyraise获取异常信息sys.exc_info()traceback 处理异常基本原则assert断点调试两种方式Debugger窗口各图标的含义1.Show Execution Point &#xff08;Alt F10&#xff09;2.Step Over&#xff08;F8&#xff09;3.Step Into &#xff08;F…

GBASE南大通用常用错误代码

错误代码为 GBASE南大通用Server 返回给应用的错误编号&#xff0c;用于唯一的标识一个错误。错误码在 GBaseErrorCode 枚举中定义。 下表仅提供通过 GBASE南大通用数据库返回给应用的常用错误码及错误描述的参考&#xff0c; 具体错误码请参考 GBase 数据库相关手册。

活动回顾 (下) | 机器学习系统趋势研判,大咖金句汇总

作者&#xff1a;三羊、李宝珠、李玮栋、Yudi、xixi 编辑&#xff1a;李宝珠 在大模型时代的浪潮中&#xff0c;机器学习系统正经历着前所未有的变革。模型规模的急剧膨胀&#xff0c;让我们见证了 AI 能力的巨大提升&#xff0c;然而这种提升不仅为各个领域带来了新的机遇&…

汇编语言学习中的Dosbox自动配置方法

学到期末才发现可以自动配置 一、先找到dosbox的下载/安装路径 二、打开其下的Dosbox *.**(这里是版本号) Options.bat 三、在其打开的文件的最下面输入你经常打开dosbox要输入的内容 例如&#xff1a; mount c e:\masm c:

UEFI模拟环境搭建——windows+EDKII

目录 0 说明 1 安装软件 1.1 VS2019的安装 1.2 Python的安装 1.3 IASL的安装 1.4 NASM的安装 1.5 git的下载 2 EDKII的下载 3 配置环境 0 说明 个人感觉UEFI的环境搭建非常复杂&#xff0c;在经过很长一段折磨后&#xff0c;终于还是搭建成功&#xff0c;写下来记录一…

MS761比较器可兼容MAX9030

MS761/762 是一款低噪声&#xff0c;低输入失调电压的高精度比较器&#xff0c;输入失调电压室温下典型值为 200μV&#xff0c;整个温度范围内最大为 1mV。可兼容MAX9030。MS761 有关断脚可以关闭整个器件&#xff0c;减小电流消耗。 MS761/762 具有 CMOS 输入及推挽输出&…

Python入门知识点分享——(十一)if条件语句

if条件语句是一种编程语言中用于控制程序流程的结构。它根据一个条件的真假来决定执行不同的代码块。 if条件语句通常由if关键字、一个条件表达式和一个代码块组成。条件表达式可以是一个返回布尔值的表达式&#xff0c;如果条件为真&#xff0c;则执行代码块中的代码&#xf…

C# 操作非持久化内存映射文件

目录 写在前面 代码实现 进程A 进程B 进程C 调用示例 总结 写在前面 多个进程之间&#xff0c;通过操作未映射到磁盘上现有文件的内存映射文件&#xff0c;可以实现数据共享和类似进程间通讯的功能。以下示例展示了使用三个独立进程&#xff0c;先各自将布尔值写入内存…