乐趣区

SpringCloud灰度发布实践

服务实例

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 等, 基本上都可以基于此方式做扩展

退出移动版