端口:8888,方便起见直接读取配置文件,生产环境可以读取 git。application-dev.properties 为全局配置。先启动配置中心,所有服务的配置(包括注册中心的地址)均从配置中心读取。
端口:18090,调用服务提供者,为了演示 header 传递。
核心 jar 包,所有微服务均引用该包,使用 AutoConfig 实现免配置,模拟生产环境下 spring-cloud 的使用。
端口:8761,/metadata 端点实现 metadata 信息配置。
端口:18090,服务提供者,无特殊逻辑。
端口:8080,演示解析 token 获得 label 并放入 header 往后传递
我本人是从 dubbo 转过来的,经常看到社区里面拿 dubbo 和 spring-cloud 做对比,一对比就提到 dubbo 所谓的降级、限流功能。spring-cloud 默认没有这个能力,让我们来扩展 spring-cloud,使她具备比 dubbo 更牛逼的各种能力。
所谓的降级、限流、滚动、灰度、AB、金丝雀等等等等,在我看来无非就是扩展了服务路由能力而已。这里说的服务降级,说的是服务 A 部署多个实例,实例级别的降级限流。如果要做整个服务 A 的降级,直接采用 docker 自动扩容缩容即可。
我们先来看应用场景:服务 A 发布了 1.0 版,部署了 3 个实例 A1、A2、A3,现在要对服务 A 进行升级,由 1.0 升级到 2.0。先将 A1 服务流量关闭,使 A2、A3 负担;升级 A1 代码版本到 2.0;将 A1 流量调整为 1%,观察新版本运行情况,如果运行稳定,则逐步提升流量 5%、10% 直到完全放开流量控制。A2、A3 重复上述步骤。
在上述步骤中,我们想让特别的人使用 2.0,其他人还是使用 1.0 版,稳定后再全员开放。
我们想不依赖 sleuth 做链路跟踪,想自己实现一套基于 ELK 的链路跟踪。
我们还有各种千奇百怪的想法。。。
要实现这些想法,我们需要对 spring-cloud 的各个组件、数据流非常熟悉,这样才能知道该在哪里做扩展。一个典型的调用:
- 外网 --> Zuul网关 --> 服务A --> 服务B --> ...
spring-cloud 跟 dubbo 一样都是客户端负载均衡,所有调用均由 Ribbon 来做负载均衡选择服务器,所有调用前后会套一层 hystrix 做隔离、熔断。服务间调用均用带 LoadBalanced 注解的 RestTemplate 发出。
- RestTemplate --> Ribbon --> hystrix
通过上述分析我们可以看到,我们的扩展点就在 Ribbon,Ribbon 根据我们的规则,选择正确的服务器即可。
我们先来一个 dubbo 自带的功能:基于权重的流量控制。dubbo 自带的控制台可以设置服务实例粒度的半权,倍权。其实就是在客户端负载均衡时,选择服务器带上权重即可,spring-cloud 默认是 ZoneAvoidanceRule,优先选择相同 Zone 下的实例,实例间采用轮询方式做负载均衡。我们的想把基于轮询改为基于权重即可。接下来的问题是,每个实例的权重信息保存在哪里?从哪里取?dubbo 放在 zookeeper 中,spring-cloud 放在 eureka 中。我们只需从 eureka 拿每个实例的权重信息,然后根据权重来选择服务器即可。具体代码 LabelAndWeightMetadataRule(先忽略里面的优先匹配 label 相关代码)。
LabelAndWeightMetadataRule 写好了,那么我们如何使用它,使之生效呢?有 3 种方式。
1)写个 AutoConfig 将 LabelAndWeightMetadataRule 声明成 @Bean,用来替换默认的 ZoneAvoidanceRule。这种方式在技术验证、开发测试阶段使用短平快。但是这种方式是强制全局设置,无法个性化。
2)由于 spring-cloud 的 Ribbon 并没有实现 netflix Ribbon 的所有配置项。netflix 配置全局 rule 方式为:ribbon.NFLoadBalancerRuleClassName=package.YourRule,spring-cloud 并不支持,spring-cloud 直接到服务粒度,只支持 SERVICE_ID.ribbon.NFLoadBalancerRuleClassName=package.YourRule。我们可以扩展 org.springframework.cloud.netflix.ribbon.PropertiesFactory 修正 spring cloud ribbon 未能完全支持 netflix ribbon 配置的问题。这样我们可以将全局配置写到配置中心的 application-dev.properties 全局配置中,然后各个微服务还可以根据自身情况做个性化定制。但是 PropertiesFactory 属性均为私有,应该是 spring cloud 不建议在此扩展。参见 https://github.com/spring-cloud/spring-cloud-netflix/issues/1741。
3)使用 spring cloud 官方建议的 @RibbonClient 方式。该方式仅存在于 spring-cloud 单元测试中(在我提问后,现在还存在于 spring-cloud issue list)。具体代码参见 DefaultRibbonConfiguration、CoreAutoConfiguration。
依次开启 config eureka provide(开两个实例,通过启动参数 server.port 指定不同端口区分) consumer zuul
访问 http://localhost:8761/metadata.html 这是我手写的一个简单的 metadata 管理界面,分别设置两个 provider 实例的 weight 值(设置完需要一段 2 分钟才能生效),然后访问 http://localhost:8080/provider/user 多刷几次来测试 zuul 是否按权重发送请求,也可以访问 http://localhost:8080/consumer/test 多刷几次来测试 consumer 是否按权重来调用 provide 服务。
基于权重的搞定之后,接下来才是重头戏:基于标签的路由。入口请求含有各种标签,然后我们可以根据标签幻化出各种各样的路由规则。例如只有标注为粉丝的用户才使用新版本(灰度、AB、金丝雀),例如标注为中国的用户请求必须发送到中国的服务器(全球部署),例如标注为写的请求必须发送到专门的写服务实例(读写分离),等等等等,唯一限制你的就是你的想象力。
根据标签的控制,我们当然放到之前写的 Ribbon 的 rule 中,每个实例配置的不同规则也是跟之前一样放到注册中心的 metadata 中,关键是标签数据如何传过来。权重随机的实现思路里面有答案,请求都通过 zuul 进来,因此我们可以在 zuul 里面给请求打标签,基于用户,IP 或其他看你的需求,然后将标签信息放入 ThreadLocal 中,然后在 Ribbon Rule 中从 ThreadLocal 拿出来使用就可以了。然而,按照这个方式去实验时,发现有问题,拿不到 ThreadLocal。原因是有 hystrix 这个东西,回忆下 hystrix 的原理,为了做到故障隔离,hystrix 启用了自己的线程,不在同一个线程 ThreadLocal 失效。那么还有什么办法能够将标签信息一传到底呢,想想之前有没有人实现过类似的东西,没错 sleuth,他的链路跟踪就能够将 spam 传递下去,翻翻 sleuth 源码,找找其他资料,发现可以使用 HystrixRequestVariableDefault,这里不建议直接使用 HystrixConcurrencyStrategy,会和 sleuth 的 strategy 冲突。代码参见 CoreHeaderInterceptor。现在可以测试 zuul 里面的 rule,看能否拿到标签内容了。
这里还不是终点,解决了 zuul 的路由,服务 A 调服务 B 这里的路由怎么处理呢?zuul 算出来的标签如何往后面依次传递下去呢,我们还是抄 sleuth:把标签放入 header,服务 A 调服务 B 时,将服务 A header 里面的标签放到服务 B 的 header 里,依次传递下去。这里的关键点就是:内部的微服务在接收到发来的请求时(zuul-》A,A-》B 都是这种情况)我们将请求放入 ThreadLocal,哦,不对,是 HystrixRequestVariableDefault,还记得上面说的原因么:)。这个容易处理,写一个 spring mvc 拦截器即可,代码参见 CoreHeaderInterceptor。然后发送请求时自动带上这个里面保存的标签信息,参见 RestTemplate 的拦截器 CoreHttpRequestInterceptor。到此为止,技术上全部走通实现。
总结一下:zuul 依据用户或 IP 等计算标签,并将标签放入 header 里向后传递,后续的微服务通过拦截器,将 header 里的标签放入 RestTemplate 请求的 header 里继续向后接力传递。标签的内容通过放入类似于 ThreadLocal 的全局变量(HystrixRequestVariableDefault),使 Ribbon Rule 可以使用。
参见 PreFilter 源码,模拟了几个用户的标签,参见 LabelAndWeightMetadataRule 源码,模拟了 OR AND 两种标签处理策略。依次开启 config eureka provide(开两个实例,通过启动参数 server.port 指定不同端口区分) consumer zuul
访问 http://localhost:8761/metadata.html 设置第一个 provide 实例 orLabel 为 CN,Test 发送请求头带入 Authorization: emt 访问 http://localhost:8080/provider/user 多刷几次,可以看到 zuul 所有请求均路由给了第一个实例。访问 http://localhost:8080/consumer/test 多刷几次,可以看到,consumer 调用均路由给了第一个实例。
设置第二个 provide 实例 andLabel 为 EN,Male 发送请求头带入 Authorization: em 访问 http://localhost:8080/provider/user 多刷几次,可以看到 zuul 所有请求均路由给了第二个实例。访问 http://localhost:8080/consumer/test 多刷几次,可以看到,consumer 调用均路由给了第二个实例。
Authorization 头还可以设置为 PreFilter 里面的模拟 token 来做测试,至此所有内容讲解完毕,技术路线拉通,剩下的就是根据需求来完善你自己的路由策略啦。
如果您有任何想法或问题需要讨论或交流,可进入交流区发表您的想法或问题。
来源: http://www.tuicool.com/articles/3ymY3er