Grayscale publishing (aka Canary Publishing)

A smooth transition between black and white. A/B test can be carried out on it (AB test is to make two (A/B) or multiple (A/B/ N) versions for the Web or App interface or process. In the same dimension at the same time, groups of visitors (target population) with the same (similar) composition can access these versions randomly, and user experience data and business data of each group can be collected. Finally, the best version is analyzed and evaluated, and officially adopted.) That is, let some users continue to use product feature A and some users start to use product feature B. If users have no objection to PRODUCT feature B, then gradually expand the scope and migrate all users to B. Grayscale publishing can ensure the stability of the whole system, and problems can be found and adjusted at the beginning of the grayscale to ensure its impact.

Today we will talk about how to implement canary publishing in a microservice environment, including the gateway forwarding to the corresponding service according to the specified rules and the microservice request downstream service according to the specified rules.

1. The gateway layer routes services based on specified rules. The zuul gateway is used as an example.

Firstly, cloud-Eureka, Cloud-Zuul and SMS-service are prepared. Eureka is the registry, and Cloud-Zuul and Gray-Service are registered with Cloud-Eureka as services. We will register two instances of SMS-service to the registry (so that we can forward service requests to specific service cases later). We can implement multiple instances of idea for the same service through the following configuration: 1. Modify the application.yml file

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
Copy the code

2. Modify the IDEA configuration

At this point, select the configuration you want to start to complete the multi-instance start, and then accesshttp://localhost:7900/You can see the service instances registered by the registry:You can see that there are two services registered in the registry, of which there are two instances of service-SMS. Modify the service and create a new controller under smS-Servie:

@RestController
@Slf4j
public class RibbonLoadBalanceController {

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

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

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

Restart both smS-Servie service instances, wait for the service to register with the registry to start testing, request SMS service through the gateway: http://localhost:9100/service-sms/load-balance can find 8003: service – SMS and 8004: the service – SMS appear alternately, we now realize for gateway routing requests to the specified services according to specified rules. To distinguish different versions of services, you can configure the meta-data of the service, for example, modify the application. Yml configuration file of smS-service as follows:

spring:
  profiles: 8004Eureka: instance: metadata-map: # key values are user-defined version: v2 server: port:8004

---

spring:
  profiles: 8003Eureka: instance: metadata-map: # key value all user-defined version: v1 server: port:8003
Copy the code

After modification is complete, restart through accesshttp://localhost:7900/eureka/appsYou can see the instance information registered to Eureka:Of course, if you do not want to restart the service, you can also do this through the request interfacemetadataData update:PUT /eureka/v2/apps/appID/instanceID/metadata? key=value, see the official website for related interface informationhttps://github.com/Netflix/eureka/wiki/Eureka-REST-operations. Now that you can distinguish instance information of different versions of the same service (because the instance metadata is different), how do you route requests through the gateway? We know that the gateway can do a lot of things, such as authentication, traffic limiting and so on can be implemented by inheriting ZuulFilter. In fact, the gateway can forward requests according to specified rules in a similar way. I’m using a three-way component here,pom.xmladd

<dependency>
      <groupId>io.jmnarloch</groupId>
      <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
      <version>2.1. 0</version>
</dependency>
Copy the code

New GrayFilter

@Component
public class GrayFilter extends ZuulFilter {

    @Override
    public String filterType(a) {
        // Specify filter type to select route here
        return FilterConstants.ROUTE_TYPE;
    }

    @Override
    public int filterOrder(a) {
        // The order in which multiple filters are executed. The smaller the value, the higher the execution order
        return 0;
    }

    @Override
    public boolean shouldFilter(a) {
        // Specify the items that need to be filtered (you can implement the filtering according to your own business requirements, by default, all filters are filtered)
        return true;
    }

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

        String userId = request.getHeader("userId");
        if (StringUtils.isBlank(userId)) {
            return null;
        }
        // Query rules (where rules can be stored in db, etc.)
        if (Integer.valueOf(userId) == 1) {
            // Requests that meet the rule are routed to the specified service
            RibbonFilterContextHolder.getCurrentContext().add("version"."v1");
        }
        return null; }}Copy the code

Where we specify the user with userId 1 to route to the service with version value v1 in the metadata. Restart two instances of cloud-Zuul and SMS-service to start testing: open postman,get request requesthttp://localhost:9100/service-sms/load-balanceAnd add userId=1 to the headerThrough the test, when userId=1, all requests are forwarded to 8003 service with metadata version=v1. If the request header does not add the userId parameter or the userId is not equal to 1, the request will still be forwarded to 8003 and 8004 service.That’s what we need.

2. The services are routed based on the specified rule

We have implemented the routing of the request to the specified service through the gateway. How can we implement the routing of the service according to the specified rule without the invocation of the service through the gateway? Create a Grey-Serivce service and register it with Eureka. Create a RequestSmsController to test service-SMS sending requests.

@RestController
@Slf4j
public class RequestSmsController {

    private final RestTemplate restTemplate;

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

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

To use RestTemplate we need to declare a bean and use the @loadBalanced annotation.

@Configuration
public class RestTemplateConfig {

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

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory(a) {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(180000);
        factory.setConnectTimeout(5000);
        returnfactory; }}Copy the code

By requesting http://localhost:8080/request-sms test we found that the service – SMS: 8003 and service – SMS: 8004 will be forwarded to the service. From the example above, we found that to implement routing by specified rules, there must be a parameter in the process to identify the request, and then we can get it to route by specified rules. What can be the declaration cycle of a thread? ThreadLocal 1. Create RibbonParameters to create a thread-to-thread variable copy

@Component
public class RibbonParameters {

    private static final ThreadLocal local = new ThreadLocal();

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

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

2. Create a new slice, inject parameters where necessary, and pass them through the threadLocal process.

@Aspect
@Component
public class RibbonParameterAspect {
    
    /** * declare pointcut *@return {@link void}
     */
    @Pointcut("execution(* com.info.grayservice.controller.. *Controller*.* (..) )"
    private void ribbonParameterPoint(a) {};/** * specifically enhances logic *@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) {
            // Inject routing rule parameters
            map.put("version"."v2"); RibbonParameters.set(map); }}}Copy the code

3. Create a GrayRule routing rule

/** * User-defined routing rules between services */

@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"))) {
                    returnserver; }}}while (server == null);
        // If no match is found, select one at random to avoid invocation errors
        return reachableServers.get(newRandom().nextInt(reachableServers.size())); }}Copy the code

4. Of course, in order for our routing rules to take effect, we need to declare this custom bean

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

public class GrayRibbonConfiguration {

    @Bean
    public IRule grayRule(a) {
        return newGrayRule(); }}Copy the code

5. Add @RibbonClient annotation to the main class to make our newly defined grayRule take effect

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

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

Restart the test, add userId=1 to the request header, requesthttp://localhost:8080/request-sms, we found that all the services to be forwarded to the 8004 version of v2 in the metadata service. Of course, the ribbon- Discovery-filter-spring-cloud-starter we mentioned earlier can be used to quickly implement this effect. Many of the details we have implemented above have been encapsulated. Add maven coordinates to POM.xml:

<dependency>
       <groupId>io.jmnarloch</groupId>
       <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
       <version>2.1. 0</version>
</dependency>
Copy the code

Modify RibbonParameterAspect. Java is as follows

@Aspect
@Component
public class RibbonParameterAspect {

    /** * declare the pointcut */
    @Pointcut("execution(* com.info.apipassenger.controller.. *Controller*.* (..) )"
    private void ribbonParameterPoint(a) {};/** * specifically enhances logic */
    @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"); }}}Copy the code

At this time, GrayRule, GrayRibbonConfiguration and RibbonParameters are no longer needed. Through the test, we find that the required effect can also be achieved.So far, we have realized the routing service by finger rule through gateway and the routing between services by specified rule, the end of the whole article, thank you for watching. My ability is limited, if there is a description of understanding is not in place, please forgive me, a lot of advice, thank you.