ribbon灰度发布

背景:使用ribbon完成 服务->服务 的灰度发布

思路:不同的用户根据ribbon的rule规则匹配到不同的服务

结构图如下:

ribbon灰度发布

 

 

服务调用者api-passenger

pom.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.7.RELEASE</version>
        <relativePath/> 
    </parent>
    <groupId>com.dandan</groupId>
    <artifactId>api-passenger</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>api-passenger</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

server:
  port: 8080

eureka:
  client:
    service-url:
      # 默认从第一个server拉取注册表,失败后找第二台,重试次数为3次,配置的第四个server无效
      # 建议每个服务的server顺序不一致,防止第一个server压力过大
      defaultZone: http://localhost:7900/eureka
      #,http://localhost:7901/eureka,http://localhost:7902/eureka
    # 从server拉取注册表的间隔时间
    registry-fetch-interval-seconds: 30
    # 是否向eureka服务器注册信息,默认是true
    enabled: true
  instance:
    # client续约的间隔时间,默认是30s
    lease-renewal-interval-in-seconds: 30

spring:
  application:
    name: api-passenger

logging:
  root:
    level: info

启动类ApiPassengerApplication 

package com.dandan.apipassenger;

import com.dandan.apipassenger.gray.GrayRibbonConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;

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

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

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

规则配置类GrayRibbonConfiguration

package com.dandan.apipassenger.gray;

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

public class GrayRibbonConfiguration {

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

构造threadLocal

package com.dandan.apipassenger.gray;

import org.springframework.stereotype.Component;

/**
 * 线程内传参
 */
@Component
public class RibbonParameters {

    private static final ThreadLocal local = new ThreadLocal();

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

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

controller请求类

package com.dandan.apipassenger.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/test")
public class TestCallServiceSmsController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/call")
    public String testCall(){

        return restTemplate.getForObject("http://service-sms/test/sms-test",String.class);
    }
}

AOP拦截器类 RequestAspect

package com.dandan.apipassenger.gray;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;


@Aspect
@Component
public class RequestAspect {

    @Pointcut("execution(* com.dandan.apipassenger.controller..*Controller*.*(..))")
    private void anyMehtod(){

    }

    @Before(value = "anyMehtod()")
    public void before(JoinPoint joinPoint){

        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
        String version = request.getHeader("version");

        Map<String,String> map = new HashMap<>();
        map.put("version",version);

        RibbonParameters.set(map);
    }
}

具体规则匹配类 GrayRule

package com.dandan.apipassenger.gray;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;

import java.util.List;
import java.util.Map;

/**
 * 根据token解析用户,然后根据用户规则表找到对应的metadata
 */
public class GrayRule extends AbstractLoadBalancerRule {

    /**
     * 根据用户选出一个服务
     * @param iClientConfig
     * @return
     */
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {}

    @Override
    public Server choose(Object key) {

        return choose(getLoadBalancer(),key);
    }

    public Server choose(ILoadBalancer lb, Object key){

        System.out.println("灰度  rule");
        Server server = null;
        // 获取所有 可达的服务
        List<Server> reachableServers = lb.getReachableServers();

        // 获取 当前线程的参数 用户id verion=1
        Map<String,String> map = RibbonParameters.get();
        String version = "";
        if (map != null && map.containsKey("version")){
            version = map.get("version");
        }
        System.out.println("当前rule version:"+version);

        // 根据用户选服务
        for (int i = 0; i < reachableServers.size(); i++) {
            server = reachableServers.get(i);
            // 用户的version我知道了,服务的自定义meta我不知道。


            // eureka:
            //  instance:
            //    metadata-map:
            //      version: v2
            // 不能调另外 方法实现 当前 类 应该实现的功能,尽量不要乱尝试

            Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();

            String version1 = metadata.get("version");

            // 服务的meta也有了,用户的version也有了。
            if (version.trim().equals(version1)){
                return server;
            }
        }
        // 怎么让server 取到合适的值。
        return null;
    }
}

service-sms和cloud-eureka配置跟 使用网关zuul完成灰度发布 一致

配置完成后启动

cloud-eureka
api-passenger
service-sms(开启两个服务)

访问 

GET localhost:8080/test/call
header传参:{version:v1}
对应找到service-sms:8091

GET localhost:8080/test/call
header传参:{version:v2}
对应找到service-sms:8092

 

上一篇:服务调用 Ribbon


下一篇:Nacos下 Ribbon 原理分析