基于springcloud实现的灰度发布
端口:6007,方便起见直接读取配置文件,生产环境可以读取git。先启动配置中心,所有服务的配置(包括注册中心的地址)均从配置中心读取。
调用服务提供者和服务提供者,验证是否进入灰度服务。
核心jar包,所有微服务均引用该包,用于负载自定义策略规则,是实现灰度发布的核心架包。
端口:6006,用于统筹各个注册服务。
端口:4002,拉取灰度策略,进行请求的把标签操作。
通过灰度版本的控制,实现符合灰度策略的对象,优先进入灰度服务进行体验。
例如,根据不同的策略,有根据不同的渠道、地域、门店、品牌等,优先使用不同的服务。例如,广州地域的用户,仅能使用基于广州部署的微服务。
例如,业务场景,根据不同的渠道和来源进行下单。微信的下单,仅能调用微信的order-service服务;官网下单,仅能调用官网的order--service下单; 通过这样的方式,上层业务无须调用何种具体服务统一底层进行负载调用,实现业务的解耦和服务的可插拔配置;
根据标签的控制,我们当然放到之前写的Ribbon的**CustomMetadataRule
中,每个实例配置的不同规则也是跟之前一样放到注册中心的metadata中,关键是标签数据如何传过来。自定义规则[CustomMetadataRule
]的实现思路里面有答案,请求都通过gray-api-gateway
进来,因此我们可以在zuul里面给请求打标签,基于用户,IP或其他看你的需求,然后将标签信息放入Thystrix。hystrix的原理,为了做到故障隔离,hystrix启用了自己的线程。另外使用sleuth方案,他的链路跟踪就能够将spam传递下去,翻翻sleuth源码,找找其他资料,发现可以使用HystrixRequestVariableDefault
,这里不建议直接使用HystrixConcurrencyStrategy
,会和sleuth的strategy冲突。代码参见CoreHeaderInterceptor
**。现在可以测试zuul里面的rule,看能否拿到标签内容了。
这里还不是终点,解决了zuul的路由,服务A调服务B这里的路由怎么处理呢?zuul算出来的标签如何往后面依次传递下去呢,我们还是抄sleuth:把标签放入header,服务A调服务B时,将服务A header里面的标签放到服务B的header里,依次传递下去。这里的关键点就是:内部的微服务在接收到发来的请求时(gateway-->A,A-->B都是这种情况)。
总结一下:zuul依据用户或IP等计算标签,并将标签放入header里向后传递,后续的微服务通过拦截器,将header里的标签放入RestTemplate请求的header里继续向后接力传递。将灰度标识放入(HystrixRequestVariableDefault
),使Ribbon Rule可以使用。
自定义的规则,该处可以实现针对不同的策略,使用不同的负载机制[ 轮询、随机、权重随机 ]
public class CustomMetadataRule extends ZoneAvoidanceRule {
// 检测灰度开关是否启动
private HttpResult checkGraySwitch() {
String url = "http://10.200.102.136:6015/eureka/apps/switch";
HttpResult result = new HttpResult();
result.statusCode = 500;
try {
result = HttpClient.get(url, null);
} catch (Exception e1) {
e1.printStackTrace();
}
return result;
}
@Override
public Server choose(Object key) {
// 获取是否存在存活的服务可调用
List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers());
// 获取不到服务
if (CollectionUtils.isEmpty(serverList)) {
return null;
}
// 获取灰度开关是否启动
HttpResult result = checkGraySwitch();
// 灰度开关被设置成关闭状态,默认走空metadata或者是特定标识是正常的服务,轮询访问
Boolean isOpen = Boolean.parseBoolean(JSONObject.parseObject(result.content).getString("errorMsg"));
if (result.statusCode == 200 && !isOpen) {
isOpen = true;
return RoundRobinRuleBySelf.getInstance().choose(this.getLoadBalancer(), key,isOpen);
}
// 灰度发布启动状态,未被设置成灰度对象,默认走空metadata或者是特定标识是正常的服务,轮询访问
if (StringUtils.isEmpty(CoreHeaderInterceptor.label.get())) {
isOpen = false;
return RoundRobinRuleBySelf.getInstance().choose(this.getLoadBalancer(), key,isOpen);
}
// 灰度发布启动状态,被设置成灰度对象,走空特定标识的服务,轮询访问
return RoundRobinRuleBySelf.getInstance().choose(this.getLoadBalancer(), key,!isOpen);
}
}
feignClient 调用flag位透传的问题
public class CoreFeignRequestInterceptor implements RequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(CoreHttpRequestInterceptor.class);
@Override
public void apply(RequestTemplate template) {
String header = StringUtils.collectionToDelimitedString(CoreHeaderInterceptor.label.get(),CoreHeaderInterceptor.HEADER_LABEL_SPLIT);
String tag = CoreHeaderInterceptor.tag.get();
template.header(CoreHeaderInterceptor.HEADER_LABEL, header).header(CoreHeaderInterceptor.HEADER_TAG, tag);
logger.info("label: " + header + " tag : " + tag);
}
}
HttpRequest 调用flag位透传的问题
public class CoreHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(CoreHttpRequestInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
String header = StringUtils.collectionToDelimitedString(CoreHeaderInterceptor.label.get(), CoreHeaderInterceptor.HEADER_LABEL_SPLIT);
String tag = CoreHeaderInterceptor.tag.get();
logger.info("label: "+header + " tag : " + tag);
HttpHeaders headers = requestWrapper.getHeaders();
headers.add(CoreHeaderInterceptor.HEADER_LABEL, header);
headers.add(CoreHeaderInterceptor.HEADER_TAG, tag);
return execution.execute(requestWrapper, body);
}
}
配置生效
@Configuration
@EnableWebMvc
public class CoreAutoConfiguration extends WebMvcConfigurerAdapter {
@Bean
public DefaultPropertiesFactory defaultPropertiesFactory() {
return new DefaultPropertiesFactory();
}
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new CoreHttpRequestInterceptor());
return restTemplate;
}
//用于配置feignClient透传生效
@Bean
public Feign.Builder feignBuilder() {
return Feign.builder().requestInterceptor(new CoreFeignRequestInterceptor());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CoreHeaderInterceptor());
}
}
测试/验证流程说明:
1.先启动 order服务;
1.1 未标识灰度服务时,前端每一次访问都是随机的情况
访问url:http://127.0.0.1:4002/order/inner/order/getOrderInfoListByUserName?userName=liulianyuan
1.2 分别启动灰度服务和正常服务。一开始启动的时候是无差别的。我们可以在 metadata.html 进行动态配置,指定某个服务是灰度的。
以下,设置了order-service2 是灰度服务。
访问url:http://127.0.0.1:4002/order/inner/order/getOrderInfoListByUserName?userName=liulianyuan
1.启动 order服务 和 user服务;
1.1 user-service是正常服务,标识user-service2为灰度服务;order-service 均是正常服务。 1.2 启动所有服务。一开始启动的时候是无差别的。我们可以在 metadata.html 进行动态配置,指定某个服务是灰度的。
请求url: http://127.0.0.1:4002/order/test?userName=liulianyuan
该处是随机访问正常的order-service服务的。多试几次,就可以看见order-service和order-service2的出现。【该处如果需要做成轮需,需要改代码】
1.启动 order服务 和 user服务;
1.1 user-service是正常服务,标识user-service2为灰度服务;order-service 均是正常服务。
1.2 启动所有服务。一开始启动的时候是无差别的。我们可以在 metadata.html 进行动态配置,指定某个服务是灰度的。
请求url: http://127.0.0.1:4002/user/getOrderInfo?userName=liulianyuan
该处是随机访问正常的order-service服务的。多试几次,就可以看见order-service和order-service2的出现。【该处如果需要做成轮需,需要改代码】
1.启动 order服务 和 user服务;
1.1 user-service是正常服务,标识user-service2为灰度服务;order-service 均是正常服务。
1.2 启动所有服务。一开始启动的时候是无差别的。我们可以在 metadata.html 进行动态配置,指定某个服务是灰度的。
请求url: http://127.0.0.1:4002/order/test?userName=liulianyuan
1.启动 order服务 和 user服务;
1.1 user-service是正常服务,标识user-service2为灰度服务;order-service 均是正常服务。
1.2 启动所有服务。一开始启动的时候是无差别的。我们可以在 metadata.html 进行动态配置,指定某个服务是灰度的。
请求url: http://127.0.0.1:4002/order/test?userName=lly