ribbon灰度发布
背景:使用ribbon完成 服务->服务 的灰度发布
思路:不同的用户根据ribbon的rule规则匹配到不同的服务
结构图如下:
服务调用者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