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) 方式。
在这里插入图片描述
图中的所有过滤器和拦截器,虽然名称不同,但是作用相同。这部分的主要作用就是 获取或传递路由规则,例如,可以实现基于 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: 10.10.10.0/24
    
  2. Spring Cloud Gateway 的 GlobalFilter 根据请求 token 获取 用户信息,记录用户信息(如 ThreadLocal)。
  3. 负载均衡时,使用 Nacos ConfigService,根据 服务名.user-routes 查询配置信息,同时监听该配置信息,根据这里的配置选择优先路由的服务。
  4. 拦截器根据请求 token 获取 用户信息,记录用户信息(如 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 拦截器设置token或用户信息,将操作用户传递下去。
  10. 和第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 注解,指定 groupCommonConstants.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 注解,指定 groupCommonConstants.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请求头实现,配置信息保留在浏览器中,切换用户后仍然有效,这种方式比基于用户更简单,而且传递相对更方便。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

isea533

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值