Spring Cloud Alibaba 本地调试方案

Posted isea533

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud Alibaba 本地调试方案相关的知识,希望对你有一定的参考价值。


1 本地调试介绍

本地调试: 这里是指在开发环境中,部署了一整套的某个项目或者产品的服务,开发人员开发时,本地会起一个或多个服务,这些服务和开发环境中部署的服务是相同的,这种情况下,一个服务就会有多个实例,大多数微服务中的默认负载均衡策略都是轮询,这些实例会轮流被调用。

为了方便 本地调试,需要提供一种策略,可以指定在负载均衡时,选择哪个实例进行调用。在使用 Nacos 作为注册中心时,可以通过 上线和下线 的方式来选择使用哪个实例,但是这种方式只能强制调用某个实例,如果开发环境还有其他人在调试,自己程序 设置断点 时会阻塞所有调用,非常不利于多人调试的协调。

为了解决 本地调试 的问题,本文实现了一种简单实用的策略,可以通过 Nacos 动态配置服务路由,还可以基于用户,部门,组织等级别配置服务路由,实现 本地调试 的同时,实际上也实现 灰度发布

2 框架环境

本文基于 Spring Cloud Alibaba 框架,和 Spring Cloud 相比增加了一部分针对 Dubbo 的方案,因此本文适合以下框架参考:

  • Spring Cloud Alibaba
  • Spring Cloud
  • Spring Cloud Gateway
  • Spring Cloud Ribbon
  • Dubbo

下图是 Spring Cloud Alibaba 框架中,一次方法调用的可能情况,Ailbaba 这部分多的是图中 ​​ServiceA -> ServiceB​​ 部分使用 Dubbo 协议。Spring Cloud 框架中,用的是 ​​ServiceA -> ServiceC​​ 这种 Feign(HTTP) 方式。

Spring


图中的所有过滤器和拦截器,虽然名称不同,但是作用相同。这部分的主要作用就是 获取或传递路由规则,例如,可以实现基于 HTTP Header 设置路由规则的配置,可以基于 HTTP 和 token 实现基于用户的路由规则配置,这部分的实现和需求有关,没有统一的实现。

3 方案设计

这里以这两种场景简单举个例子。

3.1 基于 HTTP Header 的本地调试方案

在这个方案中,按照上面的流程图叙述一遍。

  1. 用户调用服务前,在 Header 中设置调用规则,比如增加​​service-route​​​ 请求头,请求头的内容为​​servicea:10.10.10.130;serviceb:10.10.10.100;servicec:10.10.10.0/24​​,在请求头中指明需要控制路由的服务信息(不需要控制的直接省略走默认)。
  2. 通过 Spring Cloud Gateway 的 GlobalFilter 实现提取请求头信息,将配置信息记录下来(如​​ThreadLocal​​)
  3. 负载均衡时,根据这里的配置选择优先路由的服务,调用 ServiceA 时,仍然是 HTTP 请求,请求头会传递过去。
  4. 拦截器获取请求头中的路由规则,这一步和1类似,但是属于 Spring MVC 的拦截器,获取路由规则后记录下来(如​​ThreadLocal​​)
  5. ServiceA 调用 ServiceB 是 Dubbo 协议的路径,和 7,8 Feign 方式没有先后顺序,是两个分支。在 4 这一步通过 Dubbo 的 Consumer Filter 过滤器和​​RpcContext​​​ 将路由信息记录到​​attachment​​ 中,这样可以把路由配置传递到 ServiceB,如果 ServiceB 还需要调用其他服务,路由仍然会起到作用。
  6. 在 Dubbo 的 LoadBalance 实现中,根据被调用服务​​remote.application​​ 配置的规则进行调用。
  7. Dubbo 的 Provider Filter 从​​RpcContext​​​ 获取路由配置,记录下来(如​​ThreadLocal​​​),如果后续调用其他服务,逻辑和 4,5,6一样。在 6 这一步的 Provider Filter 结束调用的时候,注意清空路由信息(如​​ThreadLocal.clear()​​),避免对其他调用产生污染。
  8. 这一步和4,5,6没有顺序关系,是纯 Spring Cloud 方式的调用,在 ServiceA 调用时,通过自定义 Ribbon 中的​​IRule​​ 实现基于自己路由规则的调用。
  9. 在最终调用 ServiceC 之前,通过 Feign 的​​RequestInterceptor​​​ 拦截器添加​​service-route​​ 头,将服务路由传递下去。
  10. 和第3步相同,通过 Spring MVC 拦截器获取服务路由记录下来。后续在调用其他服务时,Dubbo服务走4,5,6,Feign方式走7,8,9。

3.2 基于操作用户的本地调试方案

基于操作用户的方案中,和上面类似,但是不需要在每次请求的时候设置 HTTP Header,但是需要一种方式存取服务路由的配置。

这里以使用 Nacos 配置管理实现服务路由配置的存取。

  1. 根据自己使用的用户在 Nacos 配置服务路由,配置名规则如​​服务名.user-routes​​​,使用 Spring Cloud Alibaba 的默认组​​dubbo​​,用户服务路由的配置规则可以自己定义,这里举个简单例子:
enabled: true # 启用,停用
ip: 10.10.0.0/24 # 默认优先IP或网段,所有IP都支持具体IP和网段
userIps: # Map<Long, String>,优先级最高,针对用户配置 IP 优先
# userId: IP
1: 10.10.0.100
2: 10.10.0.101
# 这部分定义根据自己需要设计
deptIps: # 针对部门配置
# deptId: IP
1: 10.10.0.0/24
orgIps: # 针对组织配置
# orgId: IP
1:
  1. Spring Cloud Gateway 的 GlobalFilter 根据请求 token 获取用户信息,记录用户信息(如​​ThreadLocal​​)。
  2. 负载均衡时,使用 Nacos​​ConfigService​​​,根据​​服务名.user-routes​​ 查询配置信息,同时监听该配置信息,根据这里的配置选择优先路由的服务。
  3. 拦截器根据请求 token 获取用户信息,记录用户信息(如​​ThreadLocal​​)。
  4. ServiceA 调用 ServiceB 是 Dubbo 协议的路径,和 7,8 Feign 方式没有先后顺序,是两个分支。在 4 这一步通过 Dubbo 的 Consumer Filter 过滤器和​​RpcContext​​​ 将用户信息记录到​​attachment​​ 中,这样可以把用户信息传递到 ServiceB,如果 ServiceB 还需要调用其他服务,用户信息仍然会起到作用。
  5. 在 Dubbo 的 LoadBalance 实现中,根据被调用服务​​remote.application​​ 配置的规则进行调用。
  6. Dubbo 的 Provider Filter 从​​RpcContext​​​ 获取用户信息,记录下来(如​​ThreadLocal​​​),如果后续调用其他服务,逻辑和 4,5,6一样。在 6 这一步的 Provider Filter 结束调用的时候,注意清空用户信息(如​​ThreadLocal.clear()​​),避免对其他调用产生污染。
  7. 这一步和4,5,6没有顺序关系,是纯 Spring Cloud 方式的调用,在 ServiceA 调用时,通过自定义 Ribbon 中的​​IRule​​ 实现基于自己路由规则的调用。
  8. 在最终调用 ServiceC 之前,通过 Feign 的​​RequestInterceptor​​ 拦截器设置token或用户信息,将操作用户传递下去。
  9. 和第3步相同,通过 Spring MVC 拦截器获取用户信息记录下来。后续在调用其他服务时,Dubbo服务走4,5,6,Feign方式走7,8,9。

本文选择第 2 种方案,针对 1~9 步,分别讲解需要实现的接口和接口应用(生效)的配置。

4 实现要点

上面提到的 ​​ThreadLocal​​​,实现时使用一个 ​​static​​ 变量存储,提供相应的存取清空的静态方法,方便跨接口的 用户信息 传递。

4.1 Spring Cloud Gateway 全局过滤器

假设有一个 ​​UserGlobalFilter​​​,该过滤器根据 ​​token​​ 获取并缓存用户信息,在请求完成后需要清空缓存的用户信息。

Spring Cloud Gateway 中的过滤器,直接在 ​​@Configuration​​​ 的配置类中用 ​​@Bean​​ 提供即可。

4.2 Ribbon 负载均衡

实现 ribbon-loadbalancer 中的 ​​com.netflix.loadbalancer.IRule​​​ 接口,将来调用具体服务时通过 ​​choose​​ 接口返回符合条件的实例。

实现这个接口之后,需要特殊的方式注册该接口,在启动类增加注解 ​​@RibbonClients(defaultConfiguration = UserRuleConfiguration.class)​​​,
注解中指定了一个配置类,这个类一定不要添加 @Configuration 注解!!!

在这个类中,通过 ​​@Bean​​​ 注解返回一个 ​​IRule​​ 接口的实现。

在 Ribbon 中,会创建一个新的 ApplicationContext 来初始化这些配置,在这个新的 ApplicationContext 中,配置的 ​​IRule​​ 实现会被使用。

4.3 Spring MVC 拦截器

实现 ​​HandlerInterceptor​​ 拦截器,从请求获取用户信息并记录下来。

拦截器想要生效,需要提供一个配置类,继承 ​​WebMvcConfigurer​​​ 接口,实现 ​​addInterceptors​​ 方法,在这个方法实现中添加拦截器的实现类。

4.4 Dubbo Consumer Filter 过滤器

实现Dubbo 的Filter接口,通过 ​​RpcContext​​ 传递前面记录的用户信息。

可以在实现类添加 ​​@Activate​​​ 注解,指定 ​​group​​​ 为 ​​CommonConstants.CONSUMER​​。

按照 dubbo SPI 要求,添加 ​​META-INF/dubbo/org.apache.dubbo.rpc.Filter​​ 文件,写上实现类。

4.5 Dubbo LoadBalance 负载均衡

实现负载均衡接口,然后配置 ​​LoadBalance​​ 的 SPI 配置文件。

负载均衡想要生效,还需要配置使用,可以通过 ​​dubbo.consumer.loadbalance​​ 配置调用其他服务时,使用自己定义的负载均衡实现。

本方案中配置信息使用的 config-center,因此在实现中,可以使用下面的方式读取和监听配置

GovernaceRuleRepository repository = ExtensionLoader. getExtensionLoader(GovernanceRuleRepository.class).getDefaultExtension(); //获取配置方式 String config = repository.getRule("配置名", "group,默认dubbo"); //监听配置变更 repository.addListener("配置名", 监听实现);

如果使用 nacos,可以增加配置 ​​dubbo.config-center.address=nacos://ip:port​

4.6 Dubbo Provider Filter 过滤器

实现Dubbo 的Filter接口,通过 ​​RpcContext​​ 获取传递过来的用户信息。

可以在实现类添加 ​​@Activate​​​ 注解,指定 ​​group​​​ 为 ​​CommonConstants.PROVIDER​​。

按照 dubbo SPI 要求,添加 ​​META-INF/dubbo/org.apache.dubbo.rpc.Filter​​ 文件,写上实现类。

这个实现类可以和 4.4 的放一个 Filter 实现中,需要自己区分当前是 consumer 还是 provider 实现不同的逻辑。

4.7 Ribbon 负载均衡,同 4.2

这一步的实现和 4.2 一样,4.2 是用在 Spring Cloud Gateway 中,这里是配置到具体的服务中。配置方式一样。

4.8 Feign RequestInterceptor 拦截器

首先实现 ​​RequestInterceptor​​ 接口,在实现中往 requst 的 Header 中放置要传递的数据。

接口想要生效,需要和 Ribbon 类似的配置。

在 ​​@EnableFeignClients​​​ 的注解中,通过 ​​defaultConfiguration​​​ 设置一个 Feign 的配置类。在这个配置中通过 ​​@Bean​​​ 提供 ​​RequestInterceptor​​ 接口的实现。

4.9 Spring MVC 拦截器,同 4.3

4.3 中是网关调用服务,4.9是服务通过 Feign (或resttemplate)调用服务,对被调用的服务来说都是 HTTP 请求,因此都会执行 Spring MVC 的拦截器,所以这里的实现是一样的。

5. 总结

本文提供了本地调试的方案和主要的实现要点,可以根据文中的关键指引和自己的实际需求实现自己的方案。关于本地调试如果有更好的方案,欢迎留言讨论。

附:工具方法

判断IP是否相等或输入子网IP的方法:

public static boolean ipInRange(String ip, String cidr) 
if(cidr.indexOf(/) < 0)
return ip.equals(cidr);

int ipAddr = ipToInt(ip);
int type = Integer.parseInt(cidr.replaceAll(".*/", ""));
String cidrIp = cidr.replaceAll("/.*", "");
if(type == 32)
return ip.equals(cidrIp);

int cidrIpAddr = ipToInt(cidrIp);
int mask = 0xFFFFFFFF << (32 - type);
return (ipAddr & mask) == (cidrIpAddr & mask);


public static int ipToInt(String ip)
String[] ips = ip.split("\\\\.");
return (Integer.parseInt(ips[0] << 24) |
Integer.parseInt(ips[1] << 16) |
Integer.parseInt(ips[2] << 8) |
Integer.parseInt(ips[3]));

更新日志

2021-7-14

  • 修改 4.5 Router 路由方式为 LoadBalance 负载均衡
    Router 方式中无法获取被调用的服务(provider信息,有反射方案,不够稳定),改为 LoadBalance 负载均衡后,可以通过​​​remote.application​​ 获取被调用的服务,这种方案比 Router 含义更准确(最初选择 Router 是受官方条件路由影响,没选LB是因为这种方案太简单)。

2021-11-7

  • 第一版实现了基于用户的本地联调,使用不够方便,不推荐这种用法。
  • 第二版基于http请求头实现,配置信息保留在浏览器中,切换用户后仍然有效,这种方式比基于用户更简单,而且传递相对更方便。


以上是关于Spring Cloud Alibaba 本地调试方案的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud Alibaba 本地调试方案

Spring Cloud Alibaba - 13 OpenFeign应用篇

Spring Cloud Alibaba 分布式服务调用篇

Spring Cloud Alibaba系列使用feign进行服务调用

Spring Cloud Alibaba系列使用feign进行服务调用

Spring Cloud Alibaba Seata 分布式事务使用快速入门,Nacos做Seata的注册中心和配置中心