暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

重写 spring-cloud-consul 服务发现逻辑

Bishion 2018-06-03
427

需求

覆盖 spring-cloud-consul (以下简称 consul,注意区分它并不是日常说的 HashiCorp consul ) 的服务发现逻辑,以满足特殊场景需求
不允许直接改源码

分析

consul 中有两处用到了服务发现逻辑

  1. ConsulDiscoveryClient: 这个类用的场景比较少,一般是别的组件或者系统需要自己获取服务注册的一些信息时,手动注入该类,然后直接使用

  2. ConsulServerList: 我们日常通过 feign 或者 ribbon 间接使用服务发现时,调用的就是这个类的 getServers() 方法
    我们今天重点看一下ConsulServerList
    不出意外,ConsulServerList 是被注入到 Spring 中使用的,而且注入条件是 ConditionalOnMissingBean:

  1. @Configuration

  2. public class ConsulRibbonClientConfiguration {

  3.    @Bean

  4.    @ConditionalOnMissingBean

  5.    public ServerList<?> ribbonServerList(IClientConfig config, ConsulDiscoveryProperties properties) {

  6.        ConsulServerList serverList = new ConsulServerList(client, properties);

  7.        serverList.initWithNiwsConfig(config);

  8.        return serverList;

  9.    }

  10. }

解决办法一

自己实现 ServerList,并且让它在原生 ConsulServerList 初始化前被注入

  1. @Configuration

  2. public class MyConsulRibbonClientConfiguration {

  3.    @Bean

  4.    public ServerList<?> ribbonServerList(IClientConfig config, ConsulDiscoveryProperties properties) {

  5.        MyConsulServerList serverList = new MyConsulServerList(client, properties);

  6.        serverList.initWithNiwsConfig(config);

  7.        return serverList;

  8.    }

  9. }

  10. public class MyConsulServerList extends AbstractServerList<ConsulServer> {

  11.       ..... 自定义逻辑

  12. }

运行结果

虽然我们已经定义好了 MyConsulServerList, 但是程序运行时,还是去初始化 ConsulServerList
而且我们发现原生 ConsulServerList 对于serviceId 的处理很奇怪

  1. public class ConsulServerList extends AbstractServerList<ConsulServer> {

  2.    ....

  3.    private String serviceId; // 服务名,下同,不再备注

  4.    @Override

  5.    public void initWithNiwsConfig(IClientConfig clientConfig) {

  6.        this.serviceId = clientConfig.getClientName();

  7.    }

  8.    ....

  9. }

对的,你没有看错,serviceId 是放在成员变量里的
这就有一个问题,绝大多数系统都是不止一个服务提供方,那 serverId 放在成员变量里,就证明 ConsulServerList 不可能是单例
可是也没有看到明显的 scope 配置,那这个 Bean 是什么时候被初始化的呢

多例的 ConsulServerList

经过断点可知,每当请求一个新的 service 时,ConsulRibbonClientConfiguration.ribbonServerList() 就会被执行一次
而触发该方法的并不是 consul, 而是 ribbon

  1. LoadBalancerFeignClient.execute() -> IClientConfig requestConfig = getClientConfig(options, clientName);

  2. LoadBalancerFeignClient.getClientConfig() -> requestConfig = this.clientFactory.getClientConfig(clientName);

  3. SpringClientFactory.getClientConfig() -> getInstance(name, IClientConfig.class);

  4. SpringClientFactory.getInstance() -> super.getContext(name);

  5. NamedContextFactory.getContext() -> createContext(name);


  6. NamedContextFactory.createContext():

  7. protected AnnotationConfigApplicationContext createContext(String name) { // name 亦即服务名

  8.    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

  9.    ......

  10.    // 重新注册 RibbonClientConfiguration,这里 defaultConfigType 就是 RibbonClientConfiguration

  11.    context.register(PropertyPlaceholderAutoConfiguration.class,this.defaultConfigType);

  12.    .....

  13.    context.refresh(); // 刷新 RibbonClientConfiguration, 其下的 serverList 也被重新初始化

  14.    return context;

  15. }

核心调用链和代码如上文所示。大致步骤如下:

  1. feign 拿着 serviceId 去问 ribbon 要服务端配置

  2. ribbon 根据 serviceId 去 RibbonClientConfiguration 的上下文中找是否存在这个名称的 ConsulServerList bean

  3. 如果存在就返回

  4. 如果不存在,就注册一个,然后刷新上下文初始化

spring 怎么知道要刷新哪个上下文

它是通过 @RibbonClient 注解来定位的
这里就不再展开,具体可以参考代码:RibbonClientConfigurationRegistrar.class

解决办法二

  1. 禁用原有的 ConsulRibbonClientConfiguration: spring.cloud.consul.ribbon.enabled:true

  2. 自定义自己的 ConsulRibbonClientConfiguration: MyConsulRibbonClientConfiguration

  3. 自定义自己的 ConsulServerList: MyConsulServerList

  4. 给自己的逻辑添加入口: @ConditionalOnExpression("${spring.cloud.consul.ribbon.enabled:true}==false")

  5. 具体实现代码参考: https://github.com/bishion/microService/tree/master/myConsulTool

思考

为什么 ribbon 要这么大费周章地去给每个服务方定义一个单独的 ServerList 呢? 明明在调用的时候,将 serviceId 传给服务发现逻辑就可以了
这是因为 ribbon 的一个需求决定的: 服务配置个性化
ribbon 支持对某一个服务单独配置负载,比如负载算法,是否重试等,当然也包括服务发现逻辑
为每一个服务实例化一个服务发现逻辑,可以最大化地将自由交给实现方。只是目前的 consul 并未用到这个功能而已

后记

  1. 为每个服务懒加载一个独有的实例,ribbon 的实现很巧妙,值得学习

  2. 关于 ribbon 对于服务个性化配置的实现,找机会另开一题

  3. 如有错误或不当之处,欢迎留言指正


文章转载自Bishion,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论