On the use of gray routing in spring cloud

Keywords: Database MySQL Spring Cloud server

In microservices, for high availability, the same service is often deployed in cluster mode, that is, there are several same services at the same time, and the core of gray level is routing, which calls the target service line through our specific strategy

1 Introduction to gray routing

Grayscale publishing (also known as Canary Publishing) refers to a publishing method that can make a smooth transition between black and white. A/B testing can be conducted on it, that is, some users continue to use product feature a and some users start to use product feature B. if users have no objection to B, gradually expand the scope and migrate all users to B. Gray distribution can ensure the stability of the whole system. Problems can be found and adjusted at the initial gray level to ensure its influence

For the gray-scale publishing implementation of SpringCloud microservices + nacos, first of all, the calls between microservices usually use Feign and Resttemplate (less used). Therefore, we need to specify the calls between services. First, we need to add unique identifiers to each service. We can use some special tags, such as version number, version, etc. Second, To interfere with the default polling call mechanism of Ribbon in microservices, we need to call it according to the version of microservices. Finally, we need to transfer the information of the call link between services. We can add the information of the call link in the request header

The arrangement idea is:

1 add call link information to the request header

2. When calling between microservices, use feign interceptor to enhance the request header

3. When microservice invocation is selected, the specified service is obtained from nacos according to the specified policy (such as unique identification version, etc.)

2. Use of gray routing

Case list

Basic services

A parent service and a tool service

Parent service

pom dependency

   <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <!--spring cloud edition-->
    <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
  </properties>

  <dependencies>

    <!--nacos-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      <version>0.2.2.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba.nacos</groupId>
      <artifactId>nacos-client</artifactId>
      <version>1.1.0</version>
    </dependency>


    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
    
    <!--feign-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>


    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      <exclusions>
        <exclusion>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </exclusion>
      </exclusions>
    </dependency>


    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-loadbalancer</artifactId>
    </dependency>


  </dependencies>

Tool services

feign interceptor

@Slf4j
public class FeignInterceptor implements RequestInterceptor {

    /**
     * feign Interface interception, add gray routing request header
     * @param template
     */
    @Override
    public void apply(RequestTemplate template) {

        String header = null;

        try {
            header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest().getHeader("gray-route");
            if (null == header || header.isEmpty()) {
                return;
            }
        } catch (Exception e) {
            log.info("Request header acquisition failed, The error message is: {}", e.getMessage());
        }
        template.header("gray-route", header);

    }
}

Gray routing attribute class

@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery.metadata.gray-route", ignoreUnknownFields = false)
@Data
@RefreshScope
public class GrayRouteProp {

    /**
     * comma
     */
    public final static String COMMA_SEP = ".";
    /**
     * Gray routing
     */
    public final static String GRAY_ROUTE = "gray-route";
    /**
     * edition
     */
    public final static String VERSION = "version";
    /**
     * Full link version
     */
    public final static String ALL = "all";
    /**
     * User defined version
     */
    public final static String CUSTOM = "custom";

    /**
     * Version key, which can be stored in Redis, etc
     */
    public final static String VERSION_KEY = GRAY_ROUTE + COMMA_SEP + VERSION;


    /**
     * Enable gray routing
     */
    private boolean enable = false;
    /**
     * Version of this service
     */
    private String version;

    /**
     * Version routing rules from this service to next hop service
     */
    private RouteProp route;

}

Routing attribute class

@Data
@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery.metadata.gray-route.route", ignoreUnknownFields = false)
@RefreshScope
public class RouteProp {

    /**
     * The uniform version number of all services directly called by this service
     */
    private String all;

    /**
     * Specifies the version of the calling service. serviceA: v1 means that only v1 version service will be called when calling
     */
    private Map<String,String> custom;

}

Grayscale routing rule class (inheriting ZoneAvoidanceRule class)

After the micro service is intercepted, the Ribbon component will obtain an implementation from the service instance list for forwarding, and the Ribbon default rule is the ZoneAvoidanceRule class. We define our own rules. We only need to inherit this class and override the choose method

@Slf4j
public class GrayRouteRule extends ZoneAvoidanceRule {

    @Autowired
    protected GrayRouteProp grayRouteProperties;

    /**
     * Reference {@ link PredicateBasedRule#choose(Object)}
     *
     */
    @Override
    public Server choose(Object key) {
        // According to the gray-scale routing rules, filter out the service this.getServers() that meets the rules
        // Then, according to the load balancing policy, the unavailable and poor performance services are filtered out, and then the remaining services are polled getpredict(). Chooseround Robin afterfiltering()
        Optional<Server> server = getPredicate()
                .chooseRoundRobinAfterFiltering(this.getServers(), key);
        return server.isPresent() ? server.get() : null;
    }

    /**
     * Gray level routing filtering service instance
     *
     * If the expected version is set, filter out all the expected versions, and then go to the default polling. If there is no expected version instance, do not filter, downgrade to the original rules, and conduct all service polling. (grayscale routing failure) if the desired version is not set
     * Then do not follow the gray routing, and poll all users according to the original polling mechanism
     */
    protected List<Server> getServers() {
        // Get spring cloud default load balancer
        ZoneAwareLoadBalancer lb = (ZoneAwareLoadBalancer) getLoadBalancer();
        // Get the gray-scale routing rules in effect for this request
        RouteProp routeRule = this.getGrayRoute();
        // Obtain the expected service version number of this request
        String version = getDesiredVersion(routeRule, lb.getName());
        // Get all services to be selected
        List<Server> allServers = lb.getAllServers();
        if (CollectionUtils.isEmpty(allServers)) {
            return new ArrayList<>();
        }
        // If the version to be accessed is not set, it will not be filtered, all will be returned, and the original default polling mechanism will be followed
        if (StringUtils.isEmpty(version)) {
            return allServers;
        }

        // Start gray rule matching filtering
        List<Server> filterServer = new ArrayList<>();
        for (Server server : allServers) {
            // Get the metadata of the service instance on the registry
            Map<String, String> metadata = ((NacosServer) server).getMetadata();
            // If the version label of the service on the registry is consistent with the expected version, the grayscale route matching is successful
            if (null != metadata && version.equals(metadata.get(GrayRouteProp.VERSION_KEY))) {
                filterServer.add(server);
            }
        }
        // If the desired version instance service is not matched, in order to ensure service availability and invalidate the gray rule, the original mechanism of polling all available services is adopted
        if (CollectionUtils.isEmpty(filterServer)) {
            log.warn(String.format("Version not found version[%s]Services[%s],Gray routing rules are degraded to the original polling mechanism!", version,
                    lb.getName()));
            filterServer = allServers;
        }
        return filterServer;
    }

    /**
     * Obtain the expected service version number of this request
     *
     * @param routeRule Effective configuration rules
     * @param appName service name
     */
    protected String getDesiredVersion(RouteProp routeRule, String appName) {
        // Get the version number of the microservice to be accessed specified in the routing rule
        String version = null;
        if (routeRule != null) {
            if (routeRule.getCustom() != null) {
                // The version specified in custom is preferred
                version = routeRule.getCustom().get(appName);
            } else {
                // If it is not specified in custom, find the unified version set in all
                version = routeRule.getAll();
            }
        }
        return version;
    }

    /**
     * Gets the set gray routing rule
     */
    protected RouteProp getGrayRoute() {
        // Determine routing rules (request header first, yml configuration second)
        RouteProp routeRule;
        String route_header = null;

        try {
            route_header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest().getHeader(GrayRouteProp.GRAY_ROUTE);
        } catch (Exception e) {
            log.error("Gray level routing gets the routing request header from the context. Exception!");
        }

        if (!StringUtils.isEmpty(route_header)) {//header
            routeRule = JSONObject.parseObject(route_header, RouteProp.class);
        } else {
            // yml configuration
            routeRule = grayRouteProperties.getRoute();
        }
        return routeRule;
    }

}

Business services

A client service; Two consumer services, version v1 and v2; Two provider services, v1 and v2

client services

Controller controller

@RestController
@Slf4j
public class ACliController {

    @Autowired
    private ConsumerFeign consumerFeign;

    @GetMapping("/client")
    public String list() {
        String info = "I'm a client,8000  ";
        log.info(info);
        String result = consumerFeign.list();
        return JSON.toJSONString(info + result);
    }

}

Feign interface

@FeignClient(value = "consumer-a")
public interface ConsumerFeign {

    @ResponseBody
    @GetMapping("/consumer")
    String list();

}

Application launcher

@SpringBootApplication
@EnableFeignClients({"com.cf.client.feign"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

application.yml

server:
  port: 8000
spring:
  cloud:
    nacos:
      discovery:
        server-addr:  127.0.0.1:8848 # Configure nacos server address
        namespace: public
        metadata:
          # Gray route is the beginning of gray route configuration
          gray-route:
            enable: true
            version: v1
  application:
    name: client-test # Service name

pom dependency

  <!--custom commons tool kit-->
  <dependencies>
    <dependency>
      <groupId>com.cf</groupId>
      <artifactId>commons</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </dependency>
  </dependencies>

consumer1 service

Controller controller

@RestController
@Slf4j
public class AConController {

    @Autowired
    private ProviderFeign providerFeign;

    @GetMapping("/consumer")
    public String list() {
        String info = "I am consumerA,8081    ";
        log.info(info);
        String result = providerFeign.list();
        return JSON.toJSONString(info + result);
    }

}

Feign interface

@FeignClient(value = "provider-a")
public interface ProviderFeign {

    @ResponseBody
    @GetMapping("/provider")
    String list();

}

Application startup class

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients({"com.cf.consumer.feign"})
public class AConsumerApplication {

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

application.yml

server:
  port: 8081
spring:
  cloud:
    nacos:
      discovery:
        server-addr:  127.0.0.1:8848 # Configure nacos server address
        namespace: public
        metadata:
          # Gray route is the beginning of gray route configuration
          gray-route:
            enable: true
            version: v1
  application:
    name: consumer-a # Service name

pom dependency

  <dependencies>
    <dependency>
      <groupId>com.cf</groupId>
      <artifactId>commons</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </dependency>
  </dependencies>

consumer2 service

The consumer2 service is the same as the consumer1 service, except that the grayscale routing version is different (the ports of the same server are also different)

application.yml

server:
  port: 8082
spring:
  cloud:
    nacos:
      discovery:
        server-addr:  127.0.0.1:8848 # Configure nacos server address
        namespace: public
        metadata:
          # Gray route is the beginning of gray route configuration
          gray-route:
            enable: true
            version: v2
  application:
    name: consumer-a # Service name

provider1 service

Controller controller

@RestController
@Slf4j
public class AProController {

    @GetMapping("/provider")
    public String list() {
        String info = "I am providerA,9091  ";
        log.info(info);
        return JSON.toJSONString(info);
    }
}

Application startup class

@EnableDiscoveryClient
@SpringBootApplication
public class AProviderApplication {

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

application.yml

server:
  port: 9091
spring:
  cloud:
    nacos:
      discovery:
        server-addr:  127.0.0.1:8848 # Configure nacos server address
        namespace: public
        metadata:
          # Gray route is the beginning of gray route configuration
          gray-route:
            enable: true
            version: v1
  application:
    name: provider-a # Service name

provider2 service

Compared with provider1 service, provider2 service has different gray routing versions (the ports of the same server are also different)

application.yml

server:
  port: 9091
spring:
  cloud:
    nacos:
      discovery:
        server-addr:  127.0.0.1:8848 # Configure nacos server address
        namespace: public
        metadata:
          # Gray route is the beginning of gray route configuration
          gray-route:
            enable: true
            version: v2
  application:
    name: provider-a # Service name

Verification test

1 start the local nacos service

2 start five project services

At this time, in nacos, there are three services in the existing service list: client test service (1), provider-a service (2 instances), and consumer-a service (2 instances)

3 test with postman

1 do not specify request header routing

"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""

"I'm a client,8000  \"I am consumerB,8082    \\\"I am providerA,9091  \\\"\""

"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerB,9092     \\\"\""

"I'm a client,8000  \"I am consumerB,8082    \\\"I am providerB,9092     \\\"\""

Call four times, using the default polling strategy in the Ribbon

2 specify the request header gray-scale routing

Set gray route = {"all": "V1"}

"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""

Four test results show that each service is v1 version, and the gray routing takes effect

Set {custom ": {consumer-A": "V1", "provider-a": "V1"}} in the request header

"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""

Four test results show that each service is v1 version, and the gray routing takes effect

Set {custom ": {consumer-A": "V1", "provider-a": "V2"}} in the request header

"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerB,9092     \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerB,9092     \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerB,9092     \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerB,9092     \\\"\""

The four test results show that the consumer service is v1 and the provider service is version 2. The gray routing takes effect

Set {custom ": {consumer-A": "V1"}} in the request header

"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerB,9092     \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerA,9091  \\\"\""
"I'm a client,8000  \"I am consumerA,8081    \\\"I am providerB,9092     \\\"\""

The four test results show that the consumer service is v1 version, and the provider service is not specified, so the default polling mechanism is adopted, and the gray routing takes effect

reference material:

https://segmentfault.com/a/1190000017412946

https://www.cnblogs.com/linyb-geek/p/12774014.html

Posted by Lyleyboy on Thu, 25 Nov 2021 13:27:20 -0800