微服务之-----灰度发布(金丝雀发布)

灰度发布(又名金丝雀发布)

是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B 测试(AB测试即为Web或App界面或流程制作两个(A/B)或多个(A/B/n)版本,在同一时间维度,分别让组成成分相同(相似)的访客群组(目标人群)随机的访问这些版本,收集各群组的用户体验数据和业务数据,最后分析、评估出最好版本,正式采用。),即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

今天来说说微服务环境下怎么实现金丝雀发布,包括网关按指定规则转发到对应服务和微服务之间按指定规则请求下游服务。

1.网关层按指定规则路由服务,本例以zuul网关为例。

首先准备需要的微服务环境cloud-eurekacloud-zuulsms-service,其中eureka是注册中心,cloud-zuulgray-service作为服务注册到cloud-eureka。其中 sms-service 我们会注册两个实例到注册中心(方便我们后期转发服务请求到指定服务案例),可以通过以下配置实现idea同一服务启动多个实例:
1.修改application.yml文件

spring:
  application:
    #应用名称
    name: service-sms
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7900/eureka/
    registry-fetch-interval-seconds: 30
    enabled: true
  instance:
    lease-renewal-interval-in-seconds: 30

---

spring:
  profiles: 8003
server:
  port: 8003

---
spring:
  profiles: 8004
server:
  port: 8004

2.修改idea配置
微服务之-----灰度发布(金丝雀发布)_第1张图片

至此选中需要启动的配置即可完成多实例启动,然后访问http://localhost:7900/即可看到注册中心所注册的服务实例:
在这里插入图片描述
可以看到注册中心共注册了两个服务,其中service-sms有两个实例。
修改服务,在sms-servie服务下新建controller:

@RestController
@Slf4j
public class RibbonLoadBalanceController {

    @Value("${server.port}")
    private Integer port;

    @Value("${spring.application.name}")
    private String applicationName;

    @GetMapping("/load-balance")
    public String loadBalance() {
        return port + ":" + applicationName;
    }
}

重启sms-servie两个服务实例,等服务注册到注册中心开始测试,通过网关请求sms服务:http://localhost:9100/service-sms/load-balance可以发现 8003:service-sms8004:service-sms交替出现,现在我们来实现让网关按照指定规则路由请求到指定服务。
首先需要区分不同版本的服务,可以通过配置服务的meta-data来实现,如修改sms-serviceapplication.yml配置文件如下:

spring:
  profiles: 8004
eureka:
  instance:
    metadata-map:
      # key value均为自定义
      version: v2
server:
  port: 8004

---

spring:
  profiles: 8003
eureka:
  instance:
    metadata-map:
      # key value均为自定义
      version: v1
server:
  port: 8003

修改完成后重启,通过访问 http://localhost:7900/eureka/apps 即可看到注册到eureka的实例信息:
微服务之-----灰度发布(金丝雀发布)_第2张图片
当然如果不想重启服务,也可以通过请求接口来完成metadata数据的更新:
PUT /eureka/v2/apps/appID/instanceID/metadata?key=value,相关接口信息见官网 https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
至此已经可以区分同一服务不同版本的实例信息了(因为实例的metadata不同),怎么通过网关来路由转发请求呢?
我们知道网关可以做很多事情,例如鉴权、限流等等都可以通过继承ZuulFilter来实现,其实通过网关按指定规则转发请求也是通过类似方式来实现。
这里用到一个三方组件,pom.xml中添加

<dependency>
      <groupId>io.jmnarloch</groupId>
      <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
      <version>2.1.0</version>
</dependency>

新建GrayFilter

@Component
public class GrayFilter extends ZuulFilter {

    @Override
    public String filterType() {
        // 指定过滤器类型  这里选择路由
        return FilterConstants.ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 多个过滤器执行顺序,值越小,执行顺序越靠前
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        // 指定需要过滤的请(可以按自己业务需求实现,这里默认全部过滤)
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();

        String userId = request.getHeader("userId");
        if (StringUtils.isBlank(userId)) {
            return null;
        }
        // 查询规则(这里的规则可以储存到db等地方)
        if (Integer.valueOf(userId) == 1) {
            // 满足规则的请求路由到指定服务
            RibbonFilterContextHolder.getCurrentContext().add("version", "v1");
        }
        return null;
    }
}

在其中我们指定userId为1的用户规定路由到metadata中version值为v1的服务。
重启cloud-zuul以及sms-service服务两个实例开始测试:
打开postman,get请求请求 http://localhost:9100/service-sms/load-balance,同时在header中添加userId=1微服务之-----灰度发布(金丝雀发布)_第3张图片
通过测试我们现在当userId=1时,所以的请求都被转发到了metadata为version=v1的8003服务上,当请求头不添加userId参数或者userId不等于1时,请求依然会被转发到8003和8004两个服务中。
微服务之-----灰度发布(金丝雀发布)_第4张图片
这样就实现了我们的需求。

2.服务间按指定规则路由服务

上面我们已经实现了通过网关路由满足要求的请求到指定服务,那么服务之间调用不经过网关怎么实现按指定规则路由服务呢?
新建gray-serivce服务,并注册到eureka,新建RequestSmsController对service-sms发送请求进行测试。

@RestController
@Slf4j
public class RequestSmsController {

    private final RestTemplate restTemplate;

    public RequestSmsController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/request-sms")
    public String requestSms() {
        log.info("request-sms....");
        String url = "http://service-sms/gray";
        return restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<Object>(null, null), String.class).getBody();
    }
}

使用RestTemplate我们需要申明一个bean并使用@LoadBalanced注解。

@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate(simpleClientHttpRequestFactory());
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(180000);
        factory.setConnectTimeout(5000);
        return factory;
    }
}

通过请求 http://localhost:8080/request-sms 测试我们发现,service-sms:8003 和 service-sms:8004 服务均会被转发到。
通过上面通过网关路由服务的案例我们发现,要实现按指定规则路由服务,在这一个流程中肯定要有一个参数标识这个请求是谁的,然后我们才能拿到他去按指定规则进行路由,那什么可以贯穿一个线程的声明周期呢?ThreadLocal
1.新建RibbonParameters建立一个线程一对一的变量副本

@Component
public class RibbonParameters {

    private static final ThreadLocal local = new ThreadLocal();

    public static <T> T get() {
        return (T) local.get();
    }

    public static <T> void set(T data) {
        local.set(data);
    }
}

2.新建切面,在需要的地方注入参数,通过threadLocal进程传递。

@Aspect
@Component
public class RibbonParameterAspect {
    
    /**
     * 声明切入点
     * @return {@link void}
     */
    @Pointcut("execution(* com.info.grayservice.controller..*Controller*.* (..))")
    private void ribbonParameterPoint() { };
    
    /**
     * 具体增强逻辑
     * @return {@link void}
     */
    @Before("ribbonParameterPoint()")
    public void before(JoinPoint joinPoint) {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String userId = request.getHeader("userId");
        if (StringUtils.isBlank(userId)) {
            return;
        }
        Map<String, String> map = new HashMap<>();
        if (Integer.valueOf(userId) == 1) {
            // 注入路由规则参数
            map.put("version", "v2");
            RibbonParameters.set(map);
        }
    }
}

3.新建路由规则GrayRule

/**
 * 自定义服务间路由规则
 */

@Slf4j
public class GrayRule extends AbstractLoadBalancerRule {


    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    private Server choose(ILoadBalancer lb, Object key) {
        Server server = null;
        Map<String, String> threadLocalMap = RibbonParameters.get();
        List<Server> reachableServers;
        log.info("lb = {}, key = {}, threadLocalMap = {}", lb, key, threadLocalMap);
        do {
            reachableServers = lb.getReachableServers();
            for (Server reachableServer : reachableServers) {
                server = reachableServer;
                InstanceInfo instanceInfo = ((DiscoveryEnabledServer) (server)).getInstanceInfo();
                Map<String, String> metadata = instanceInfo.getMetadata();
                String version = metadata.get("version");
                if (StringUtils.isBlank(version)) {
                    continue;
                }
                if (version.equals(threadLocalMap.get("version"))) {
                    return server;
                }
            }
        } while (server == null);
        // 匹配不到则随机选取一个,避免调用出错
        return reachableServers.get(new Random().nextInt(reachableServers.size()));
    }
}

4.当然,为了使我们的路由规则生效,我们需要声明这个自定义的bean

import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;

public class GrayRibbonConfiguration {

    @Bean
    public IRule grayRule() {
        return new GrayRule();
    }
}

5.主类添加@RibbonClient注解,使我们新定义的grayRule生效

@SpringBootApplication
@RibbonClient(name = "service-sms", configuration = GrayRibbonConfiguration.class)
public class GrayServiceApplication {

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

重启开始测试,在请求头添加userId=1,请求 http://localhost:8080/request-sms,我们发现所有的服务都被转发到了metadata中version为v2的服务8004。
微服务之-----灰度发布(金丝雀发布)_第5张图片
当然,通过我们之前提到过的ribbon-discovery-filter-spring-cloud-starter可以快速的实现这个效果,我们上面实现的很多细节都已经被封装好了。
pom.xml添加maven坐标:

<dependency>
       <groupId>io.jmnarloch</groupId>
       <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
       <version>2.1.0</version>
</dependency>

修改RibbonParameterAspect.java如下

@Aspect
@Component
public class RibbonParameterAspect {

    /**
     * 声明切入点
     */
    @Pointcut("execution(* com.info.apipassenger.controller..*Controller*.* (..))")
    private void ribbonParameterPoint() {
    }

    ;

    /**
     * 具体增强逻辑
     */
    @Before("ribbonParameterPoint()")
    public void before(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String userId = request.getHeader("userId");
        if (StringUtils.isBlank(userId)) {
            return;
        }
        if (Integer.valueOf(userId) == 1) {
            RibbonFilterContextHolder.getCurrentContext().add("version", "v2");
        }
    }
}

这时,GrayRule、GrayRibbonConfiguration、RibbonParameters都不需要了,通过测试我们发现同样可以达到需要的效果。
微服务之-----灰度发布(金丝雀发布)_第6张图片
至此,我们实现了通过网关按指规则路由服务以及服务间按指定规则路由,全篇结束,谢谢您的观看。本人能力有限,如有描述理解不到位的还请多多包涵,多多指教,感谢。

你可能感兴趣的:(spring,cloud,java)