灰度发布

最近更新时间:2021-01-20 10:12:23

使用kmse实现服务间的灰度发布功能
通过一个简单的实例说明开发者如何通过kmse进行服务间的灰度发布功能。kmse的灰度发布功能可分为基于流量灰度和全链路灰度。

一、准备客户端和服务端两个demo 
这里演示如何快速实践服务路由功能。假如现在有两个微服务 client 和 server,想实现 client 调用 server 时,通过灰度规则对服务间的请求流量做定向路由。

参考服务开发文档,下载server和client两个demo。

灰度发布

灰度发布

查看依赖,实践服务鉴权只需要依赖以下maven组件,调用端和被调用端都只需要如下依赖。

<dependency>
    <groupId>com.ksyun.kmse</groupId>
    <artifactId>spring-cloud-kmse-starter-route</artifactId>
    <version>1.0-release</version>
</dependency>

准备测试的java代码,因为需要灰度功能,所以我们在controller加入特殊的逻辑用来体现灰度功能,server端提供服务的controller:

package com.cn.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;

@RequestMapping({"/server"})
@RestController
@Configuration
public class ServerController {

    private static final Logger log = LoggerFactory.getLogger(ServerController.class);

    public ServerController() {
    }

    @GetMapping({"/{id}"})
    public String account(@PathVariable("id") Integer id) {
        System.out.println(appName);
        System.out.println(instance);
        return version + "#" + instance;
    }

    @Value("${spring.cloud.consul.discovery.tags:version=v1}")
    private String tag;

    private String version;

    @Value("${spring.cloud.consul.discovery.instanceId:1}")
    private String instance;

    @GetMapping
    public String routeServer() {
        System.out.println(appName);
        System.out.println(instance);
        return version + "#" + instance;
    }

    @PostConstruct
    private void parseVersion() {
        System.out.println(String.format("configName={%s}", configName));
        String[] split = tag.split(",");
        for (String s : split) {
            if (s.startsWith("version=")) {
                version = s.substring(s.indexOf("version=") + 8);
                break;
            }
        }
    }
}

client端提供的远程调用client:

package com.cn.controller;

import com.cn.client.Client;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RequestMapping({"/client"})
@RestController
public class ClientController {

    private static final Logger log = LoggerFactory.getLogger(ClientController.class);

    @Autowired
    private Client client;

    public ClientController() {
    }

    @GetMapping({"/{id}"})
    public String account(@PathVariable("id") Integer id) {
        log.info("调用client " + id);
        Map<String, Integer> mapVersion = new HashMap<>(), mapInstance = new HashMap<>();

        StringBuilder lbResult = new StringBuilder();
        lbResult.append("\n");
        for (int i = 0; i < id; i++) {
            String result = client.getById(i);
            String[] split = result.split("#");
            String version = split[0];
            String instance = split[1];

            if (!mapVersion.containsKey(version)) {             mapVersion.put(version, 1);
            } else {
                mapVersion.put(version, mapVersion.get(version) + 1);
            }
            if (!mapInstance.containsKey(instance)) {
                mapInstance.put(instance, 1);
            } else {
                mapInstance.put(instance, mapInstance.get(instance) + 1);
            }
            lbResult.append(instance).append("\n");
        }
        log.info("版本统计 = {}", mapVersion.toString());
        log.info("实例统计 = {}", mapInstance.toString());
        log.info("负载均衡顺序 = {}", lbResult.toString());
        return mapVersion.toString() + "     " + mapInstance.toString() + " " + lbResult.toString();
    }
}

至此两个测试应用的java代码准备完毕。

二、对基于流量的灰度进行测试
将server服务用maven(mvn package)命令打包,然后在本地启动consul作为注册中心。

使用如下两条命令分别运行server的v1和v2版本:

java -Dspring.cloud.consul.discovery.tags=namespace=ns1,version=v1  -Dserver.port=8081 -jar server.jar
java -Dspring.cloud.consul.discovery.tags=namespace=ns1,version=v2  -Dserver.port=8082 -jar server.jar

运行成功后,在consul-ui上看到如下的注册信息,可以看到分别有version不同的tag:

灰度发布

因为client是调用方,所以在client的resources文件夹中新增application.yaml文件,写入灰度配置。配置的意思是该应用访问server的流量,90%的流量发送到v2版本,10%的流量发送到v1版本。

ksyun:
  cloud:
    route:
      rule:
        client:
          virtualService:
            - route:
                - destination:
                    host: server
                    subset: v1
                  weight: 10
                - destination:
                    host: server
                    subset: v2
                  weight: 90

因为本地调试的特殊性,所以还需要加入一个命名空间的参数,如下图:

灰度发布

然后运行client,访问测试接口:

http://127.0.0.1:8080/client/100

从请求返回体和日志都可以看到,v1、v2的流量确实是按照规则来分配的。

三、对全链路的灰度进行测试
1. 简单演示
这里采用uri来进行演示

ksyun:
  cloud:
    route:
      rule:
        mytest:
          virtualService:
          - match:
            - uri:
                endUser:
                  prefix: /client/10
            route:
            - destination:
                host: server
                subset: v1

这个配置的含义是"uri前缀等于/client/10的请求流量全部路由到v1版本"。 重启client后,再次调用测试接口。我们期望形如"/client/10"的请求全部打到v1版本。

http://127.0.0.1:8080/client/10

从请求返回体和日志都可以看到,流量全部路由到了v1版本,这就符合了我们的预期。

灰度发布

这里我们继续刚才的配置测试后缀拦截,将client的配置改为如下,这种类型的配置会将形如以下的请求流量都达到server的v2。

  • /a/client/10
  • /b/client/10
ksyun:
  cloud:
    route:
      rule:
        mytest:
          virtualService:
            - match:
                - uri:
                    endUser:
                      suffix: /client/10
              route:
                - destination:
                    host: server
                    subset: v2

2. 更多的场景展示
由于参数类型,匹配规则,逻辑关系三者都能组合出众多的规则,所以下文例举了一些请求场景来作为参考,可以根据实际业务中的场景来做自定义扩展。

2.1 header前缀匹配
如果场景中有headers: name前缀匹配zhangsan 的请求,那么这种类型的配置会将形如以下的请求流量都路由到server的v1。

  • curl --location --request GET '127.0.0.1/aa' --header 'name: zhangsan1'
  • curl --location --request GET '127.0.0.1/aa/bb' --header 'name: zhangsanaaa'
ksyun:
  cloud:
    route:
      rule:
        mytest:
          virtualService:
            - match:
                - headers:
                    endUser:
                      prefix: "zhangsan"
                      param: name 
              route:
                - destination:
                    host: server
                    subset: v1

2.2 uri正则匹配
如果场景中有queryParams:type正则匹配-?[1-9]*)$ 的请求,那么这种类型的配置会将形如以下的请求流量都路由到server的v1。

  • curl --location --request GET '127.0.0.1/a?type=123'
  • curl --location --request GET '127.0.0.1/b/c?type=456&name=zhangsan'
ksyun:
  cloud:
    route:
      rule:
        test:
          virtualService:
          - match:
            - queryParams:
                endUser:
                  regular: ^(-?[1-9]\d*)$
                  param: type
            route:
            - destination:
                host: server
                subset: v1

2.3 多规则匹配,"且"逻辑关系
如果场景中有headers:name=zhangsan, 且headers:age=20的请求,那么这种类型的配置会将形如以下的请求流量都路由到server的v1。

  • curl --location --request GET '127.0.0.1/aa' --header 'name: zhangsan' --header 'age: 20'
  • curl --location --request GET '127.0.0.1/aa/bb' --header 'name: zhangsan' --header 'age: 20' --header 'role: student'
ksyun:
  cloud:
    route:
      rule:
        test:
          virtualService:
          - match:
            - headers:
                endUser:
                  exact: "20"
                  param: age
            - headers:
                endUser:
                  exact: zhangsan
                  param: name
            route:
            - destination:
                host: server
                subset: v1

2.4 多规则匹配,"或"逻辑关系
如果场景中有headers:age前缀匹配20, 或headers:name后缀匹配zhangsan 的请求,那么这种类型的配置会将形如以下的请求流量都路由到server的v1中。

  • curl --location --request GET '127.0.0.1/aa' --header 'age: 200'
  • curl --location --request GET '127.0.0.1/aa/bb' --header 'name: zhangsan1' --header 'role: student'
  • curl --location --request GET '127.0.0.1/aa/cc' --header 'name: zhangsan' --header 'age: 20'
ksyun:
  cloud:
    route:
      rule:
        test:
          virtualService:
          - match:
            - headers:
                endUser:
                  prefix: "20"
                  param: age
            route:
            - destination:
                host: server
                subset: v1
          - match:
            - headers:
                endUser:
                  suffix: zhangsan
                  param: name
            route:
            - destination:
                host: server
                subset: v1

金山云,开启您的云计算之旅

免费注册