Spring Cloud灰度发布方案
关注我们,设为星标,每天7:30不见不散,架构路上与您共享 回复"架构师"获取资源
前言
调用链分析
外部调用
请求==>zuul==>服务
zuul在转发请求的时候,也会根据 Ribbon
从服务实例列表中选择一个对应的服务,然后选择转发.
内部调用
请求==>zuul==>服务Resttemplate调用==>服务
请求==>zuul==>服务Fegin调用==>服务
无论是通过Resttemplate
还是Fegin
的方式进行服务间的调用,他们都会从Ribbon
选择一个服务实例返回.
预备知识
eureka元数据
标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
eureka RestFul接口
请求名称 | 请求方式 | HTTP地址 | 请求描述 |
---|---|---|---|
注册新服务 | POST | /eureka/apps/{appID} | 传递JSON或者XML格式参数内容,HTTP code为204时表示成功 |
取消注册服务 | DELETE | /eureka/apps/{appID} /{instanceID} | HTTP code为200时表示成功 |
发送服务心跳 | PUT | /eureka/apps/{appID} /{instanceID} | HTTP code为200时表示成功 |
查询所有服务 | GET | /eureka/apps | HTTP code为200时表示成功,返回XML/JSON数据内容 |
查询指定appID的服务列表 | GET | /eureka/apps/{appID} | HTTP code为200时表示成功,返回XML/JSON数据内容 |
查询指定appID&instanceID | GET | /eureka/apps/{appID} /{instanceID} | 获取指定appID以及InstanceId的服务信息,HTTP code为200时表示成功,返回XML/JSON数据内容 |
查询指定instanceID服务列表 | GET | /eureka/apps/instances/{instanceID} | 获取指定instanceID的服务列表,HTTP code为200时表示成功,返回XML/JSON数据内容 |
变更服务状态 | PUT | /eureka/apps/{appID} /{instanceID} /status?value=DOWN | 服务上线、服务下线等状态变动,HTTP code为200时表示成功 |
变更元数据 | PUT | /eureka/apps/{appID} /{instanceID} /metadata?key=value | HTTP code为200时表示成功 |
更改自定义元数据
eureka.instance.metadata-map.version = v1
PUT /eureka/apps/{appID}/{instanceID}/metadata?key=value
实现流程
用户请求首先到达Nginx然后转发到网关
zuul
,此时zuul
拦截器会根据用户携带请求token
解析出对应的userId
网关从Apollo配置中心拉取灰度用户列表,然后根据灰度用户策略判断该用户是否是灰度用户。如是,则给该请求添加请求头及线程变量添加信息
version=xxx
;若不是,则不做任何处理放行在
zuul
拦截器执行完毕后,zuul
在进行转发请求时会通过负载均衡器Ribbon。负载均衡Ribbon被重写。当请求到达时候,Ribbon会取出
zuul
存入线程变量值version
。于此同时,Ribbon还会取出所有缓存的服务列表(定期从eureka刷新获取最新列表)及其该服务的metadata-map
信息。然后取出服务metadata-map
的version
信息与线程变量version
进行判断对比,若值一直则选择该服务作为返回。若所有服务列表的version信息与之不匹配,则返回null,此时Ribbon选取不到对应的服务则会报错!zuul
通过Ribbon将请求转发到consumer服务后,可能还会通过fegin
或resttemplate
调用其他服务,如provider服务。但是无论是通过fegin
还是resttemplate
,他们最后在选取服务转发的时候都会通过Ribbon
。那么在通过
fegin
或resttemplate
调用另外一个服务的时候需要设置一个拦截器,将请求头version=xxx
给带上,然后存入线程变量。在经过
fegin
或resttemplate
的拦截器后最后会到Ribbon,Ribbon会从线程变量里面取出version
信息。然后重复步骤(4)和(5)
设计思路
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
// 略....
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
public class GrayMetadataRule extends ZoneAvoidanceRule {
// 略....
@Override
public Server choose(Object key) {
//1.从线程变量获取version信息
String version = HystrixRequestVariableDefault.get();
//2.获取服务实例列表
List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);
//3.循环serverList,选择version匹配的服务并返回
for (Server server : serverList) {
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String metaVersion = metadata.get("version);
if (!StringUtils.isEmpty(metaVersion)) {
if (metaVersion.equals(hystrixVer)) {
return server;
}
}
}
}
}
此处,我们可以通过在 步骤2中,让zuul添加添加线程变量的时候也在请求头中添加信息。然后,再自定义HandlerInterceptorAdapter
拦截器,使之在到达服务之前将请求头中的信息存入到线程变量HystrixRequestVariableDefault中。
public class CoreHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
String hystrixVer = CoreHeaderInterceptor.version.get();
requestWrapper.getHeaders().add(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
return execution.execute(requestWrapper, body);
}
}
public class CoreFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String hystrixVer = CoreHeaderInterceptor.version.get();
logger.debug("====>fegin version:{} ",hystrixVer);
template.header(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
}
}
yourServiceId.ribbon.NFLoadBalancerRuleClassName=自定义的负载均衡策略类
public PropertiesFactory() {
classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
classToProperty.put(ServerList.class, "NIWSServerListClassName");
classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
}
灰度使用
spring.application.name = provide-test
server.port = 7770
eureka.client.service-url.defaultZone = http://localhost:1111/eureka/
#启动后直接将该元数据信息注册到eureka
#eureka.instance.metadata-map.version = v1
测试案例
[x] zuul-server
[x] provider-test
port:7770 version:无
port: 7771 version:v1
[x] consumer-test
通过此种方法更改server的元数据后,由于ribbon会缓存实力列表,所以在测试改变服务信息时,ribbon并不会立马从eureka拉去最新信息m,这个拉取信息的时间可自行配置。
测试演示
用户andy为灰度用户。
1.测试灰度用户andy,是否路由到灰度服务provider-test:7771
2.测试非灰度用户andyaaa(任意用户)是否能被路由到普通服务provider-test:7770
以同样的方式再启动两个consumer-test服务,这里不再截图演示。
自动化配置
本文链接:
https://blog.csdn.net/dupengcheng1/article/details/89187452