服务实例
- eureka-server
- zuul -server
- Apollo-config
- provider-test
启动两个服务实例,一个带有版本号信息,一个没有port:7770 version:无
port: 7771 version:v1
- consumer-test
启动两个服务实例,一个带有版本号信息,一个没有任何信息.分别为:port:8880 version:无
port:8881 version: v1
场景分析
公司采用的是SpringCloud微服务体系,注册中心为eureka。我们知道,对于eureka-server
而言,其他服务都是eureka-client
的存在,我们在业务服务中只需要引入@EnableDiscoveryClient
即可实现注册.
比如我们想要调用order-service
的服务,只需要通过resttemplate
或者fegin
的方式指定该服务的服务名即可完成调用。这是为什么呢?这一切还得从Ribbon
这个背后的男人说起,因为ribbon会根据order-service
的服务实例名获取该服务实例的列表,这些列表中包含了每个服务实例的IP和端口号,Ribbon
会从中根据定义的负载均衡算法选取一个作为返回.
看到这里一切都有点拨云见雾了,那么意味着是不是只要能够让Ribbon用我们自定义的负载均衡算法就可以为所欲为了呢?显然,是的!
简单分析下我们在调用过程中的集中情况:
外部调用:
- 请求==>zuul==>服务
zuul在转发请求的时候,也会根据Ribbon
从服务实例列表中选择一个对应的服务,然后选择转发.
内部调用:
- 服务==>Resttemplate==>服务
- 服务==>Fegin==>服务
无论是通过Resttemplate
还是Fegin
的方式进行服务间的调用,他们都会从Ribbon
选择一个服务实例返回.
上面几种调用方式应该涵盖了我们平时调用中的场景,无论是通过哪种方式调用(排除直接ip:port调用),最后都会通过Ribbon
,然后返回服务实例.
设计思路
eureka预备知识
eureka元数据:
- 标准元数据:主机名,IP地址,端口号,状态页健康检查等信息
- 自定义元数据:通过
eureka.instance.metadata-map
配置
更改元数据:
- 源码地址:
com.netflix.eureka.resources.InstanceResource.updateMetadata()
- 接口地址:
/eureka/apps/appID/instanceID/metadata?key=value
- 调用方式:
PUTE
流程解析:
1.在需要灰度发布的服务实例配置中添加eureka.instance.metadata-map.version=v1
,注册成功后该信息后保存在eureka
中.配置如下:eureka.instance.metadata-map.version=v1
2.自定义zuul
拦截器GrayFilter
。当请求带着token
经过zuul
时,根据token得到userId,然后从分布式配置中心Apollo中获取灰度用户列表,并判断该用户是否在该列表中(Apollo非必要配置,由于管理端比较完善所以笔者这里选择采用).
若在列表中,则把version
信息存在ThreadLocal
中,从而使Ribbon
中我们自定义的Rule
能够拿到该信息;若不在,则不做任何处理。 但是我们知道hystrix
是用线程池做隔离的,线程池中是无法拿到ThreadLocal
中的信息的! 所以这个时候我们可以参考Sleuth
做分布式链路追踪的思路或者使用阿里开源的TransmittableThreadLocal
.
为了方便继承,这里采用Sleuth
的方式做处理。Sleuth
能够完整记录一条跨服务调用请求的每个服务请求耗时和总的耗时,它有一个全局traceId和对每个服务的SpanId.利用Sleuth全局traceId的思路解决我们的跨线程调用,所以这里可以使用HystrixRequestVariableDefault
实现跨线程池的线程变量传递效果.
3.zuul在转发之前会先到我们自定义的Rule中,默认Ribbon
的Rule为ZoneAvoidanceRule
.自定义编写的Rule只需要继承ZoneAvoidanceRule
并且覆盖其父类的PredicateBasedRule#choose()
方法即可.该方法是返回具体server,源码如下
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
public abstract AbstractServerPredicate getPredicate();
@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;
}
}
}
这里就完成了zuul转发到具体服务的灰度流程了,流程图如下
4.上面完成了zuul-->服务的灰度过程,接下来就是解决服务与服务间的调用.服务与服务间的调用依然可以用Sleuth的方式解决,只需要把信息添加到header里面即可.在使用RestTemplate
调用的时候,可以添加它的拦截器ClientHttpRequestInterceptor
,这样可以在调用之前就把header信息加入到请求中.
restTemplate.getInterceptors().add(YourHttpRequestInterceptor());
5.到这里基本上整个请求流程就比较完整了,但是我们怎么使用自定义的Ribbon的Rule了?这里其实非常简单,只需要在properties
文件中配置一下代码即可.
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工作机制的理解
6.这里就完成了灰度服务的正确路由,若灰度服务已经发布测试完毕想要把它变成正常服务,则只需要通过eureka
的RestFul接口把它在eureka
的metadata-map
存储的version
信息置空即可.
7.到这里基本上整个请求流程就比较完整了,上述例子中是以用户ID作为灰度的维度,当然这里可以实现更多的灰度策略,比如IP等,基本上都可以基于此方式做扩展