代码GITjava
在平时的业务开发过程当中,后端服务与服务之间的调用每每经过fegin
或者resttemplate
两种方式。可是咱们在调用服务的时候每每只须要写服务名就能够作到路由到具体的服务,这其中的原理相比你们都知道是SpringCloud
的ribbon
组件帮咱们作了负载均衡的功能。 git
灰度的核心就是路由,若是咱们可以重写ribbon默认的负载均衡算法是否是就意味着咱们可以控制服务的转发呢?是的!github
zuul在转发请求的时候,也会根据
Ribbon
从服务实例列表中选择一个对应的服务,而后选择转发.
不管是经过Resttemplate
仍是Fegin
的方式进行服务间的调用,他们都会从Ribbon
选择一个服务实例返回.
上面几种调用方式应该涵盖了咱们平时调用中的场景,不管是经过哪一种方式调用(排除直接ip:port调用),最后都会经过Ribbon
,而后返回服务实例.算法
Eureka的元数据有两种,分别为标准元数据和自定义元数据。spring
标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。自定义元数据:自定义元数据可使用
eureka.instance.metadata-map
配置,这些元数据能够在远程客户端中访问,可是通常不会改变客户端的行为,除非客户端知道该元数据的含义segmentfault
请求名称 | 请求方式 | 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
原图连接app
zuul
,此时zuul
拦截器会根据用户携带请求token
解析出对应的userId
version=xxx
;若不是,则不作任何处理放行zuul
拦截器执行完毕后,zuul
在进行转发请求时会经过负载均衡器Ribbon。zuul
存入线程变量值version
。于此同时,Ribbon还会取出全部缓存的服务列表(按期从eureka刷新获取最新列表)及其该服务的metadata-map
信息。而后取出服务metadata-map
的version
信息与线程变量version
进行判断对比,若值一直则选择该服务做为返回。若全部服务列表的version信息与之不匹配,则返回null,此时Ribbon选取不到对应的服务则会报错!当服务为非灰度服务,即没有version信息时,此时Ribbon会收集全部非灰度服务列表,而后利用Ribbon默认的规则从这些非灰度服务列表中返回一个服务。负载均衡
zuul
经过Ribbon将请求转发到consumer服务后,可能还会经过fegin
或resttemplate
调用其余服务,如provider服务。可是不管是经过fegin
仍是resttemplate
,他们最后在选取服务转发的时候都会经过Ribbon
。fegin
或resttemplate
调用另一个服务的时候须要设置一个拦截器,将请求头version=xxx
给带上,而后存入线程变量。fegin
或resttemplate
的拦截器后最后会到Ribbon,Ribbon会从线程变量里面取出version
信息。而后重复步骤(4)和(5)首先,咱们经过更改服务在eureka的元数据标识该服务为灰度服务,笔者这边用的元数据字段为version
。
1.首先更改服务元数据信息,标记其灰度版本。经过eureka RestFul接口或者配置文件添加以下信息eureka.instance.metadata-map.version=v1
2.自定义zuul
拦截器GrayFilter
。此处笔者获取的请求头为token,而后将根据JWT的思想获取userId,而后获取灰度用户列表及其灰度版本信息,判断该用户是否为灰度用户。
若为灰度用户,则将灰度版本信息version
存放在线程变量里面。此处不能用Threadlocal
存储线程变量,由于SpringCloud用hystrix作线程池隔离,而线程池是没法获取到ThreadLocal中的信息的! 因此这个时候咱们能够参考Sleuth
作分布式链路追踪的思路或者使用阿里开源的TransmittableThreadLocal
方案。此处使用HystrixRequestVariableDefault
实现跨线程池传递线程变量。
3.zuul拦截器处理完毕后,会通过ribbon组件从服务实例列表中获取一个实例选择转发。Ribbon默认的Rule为
ZoneAvoidanceRule`。而此处咱们继承该类,重写了其父类选择服务实例的方法。
如下为Ribbon源码:
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; } } } } }
4.此时,只是已经完成了 请求==》zuul==》zuul拦截器==》自定义ribbon负载均衡算法==》灰度服务这个流程,并无涉及到 服务==》服务的调用。
服务到服务的调用不管是经过resttemplate仍是fegin,最后也会走ribbon的负载均衡算法,即服务==》Ribbon 自定义Rule==》服务。由于此时自定义的GrayMetadataRule
并不能从线程变量中取到version,由于已经到了另一个服务里面了。
5.此时依然能够参考Sleuth
的源码org.springframework.cloud.sleuth.Span
,这里不作赘述只是大体讲一下该类的实现思想。 就是在请求里面添加请求头,以便下个服务可以从请求头中获取信息。
此处,咱们能够经过在 步骤2中,让zuul添加添加线程变量的时候也在请求头中添加信息。而后,再自定义HandlerInterceptorAdapter
拦截器,使之在到达服务以前将请求头中的信息存入到线程变量HystrixRequestVariableDefault中。而后服务再调用另一个服务以前,设置resttemplate和fegin的拦截器,添加头信息。
resttemplate拦截器
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); } }
fegin拦截器
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); } }
6.到这里基本上整个请求流程就比较完整了,可是咱们怎么让Ribbon使用自定义的Rule?这里其实很是简单,只须要在服务的配置文件中配置一下代码便可.
yourServiceId.ribbon.NFLoadBalancerRuleClassName=自定义的负载均衡策略类
可是这样配置须要指定服务名,意味着须要在每一个服务的配置文件中这么配置一次,因此须要对此作一下扩展.打开源码org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration
类,该类是Ribbon的默认配置类.能够清楚的发现该类注入了一个PropertiesFactory
类型的属性,能够看到PropertiesFactory
类的构造方法
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"); }
因此,咱们能够继承该类从而实现咱们的扩展,这样一来就不用配置具体的服务名了.至于Ribbon是如何工做的,这里有一篇方志明的文章(传送门)能够增强对Ribbon工做机制的理解
7.到这里基本上整个请求流程就比较完整了,上述例子中是以用户ID做为灰度的维度,固然这里能够实现更多的灰度策略,好比IP等,基本上均可以基于此方式作扩展
配置文件示例
spring.application.name = provide-test server.port = 7770 eureka.client.service-url.defaultZone = http://localhost:1111/eureka/ #启动后直接将该元数据信息注册到eureka #eureka.instance.metadata-map.version = v1
分别启动四个测试实例,有version表明灰度服务,无version则为普通服务。当灰度服务测试没问题的时候,经过PUT请求eureka接口将version信息去除,使其变成普通服务.
实例列表:
port:7770 version:无
port: 7771 version:v1
port:8880 version:无
port: 8881 version:v1
修改服务信息
服务在eureka的元数据信息可经过接口http://localhost:1111/eureka/apps访问到。
服务信息实例:
访问接口查看信息http://localhost:1111/eureka/apps/PROVIDE-TEST
注意事项
经过此种方法更改server的元数据后,因为ribbon会缓存实力列表,因此在测试改变服务信息时,ribbon并不会立马从eureka拉去最新信息m,这个拉取信息的时间可自行配置。同时,当服务重启时服务会从新将配置文件的version信息注册上去。
zuul==>provider服务
用户andy为灰度用户。
1.测试灰度用户andy,是否路由到灰度服务provider-test:7771
2.测试非灰度用户andyaaa(任意用户)是否能被路由到普通服务provider-test:7770
zuul==>consumer服务>provider服务
以一样的方式再启动两个consumer-test服务,这里再也不截图演示。请求从zuul==>consumer-test==>provider-test,经过
fegin
和resttemplate
两种请求方式测试
Resttemplate请求方式
fegin请求方式
与Apollo实现整合,避免手动调用接口。实现配置监听,完成灰度。详情见下篇文章