浅谈Ribbon之实现对网关服务的上下线平滑感知

简介

这篇文章是在生产环境遇到的问题实现的解决方案。

在使用GateWay网关做路由转发时,要转发的服务启动过后,网关总是不能及时去感知上线或下线的服务,导致服务会有一小段空白期不可访问。
针对这个问题,本人通过了解了Ribbon的执行过程,在网关层最终最大程度上解决了这个问题。
用到的组件:

注册中心:Nacos 1.4.1。
网关:Spring Cloud Gateway 2.2.5.RELEASE。

版本别整错了!

解决办法: 基于Nacos事件推送立即刷新客户端服务列表。

1.开始

先简单分析一下ribbon的配置类:

  • RibbonClientConfiguration:默认的配置
  • NacosRibbonClientConfiguration:Nacos定制化的配置

RibbonClientConfiguration:

public class RibbonClientConfiguration {

	@RibbonClientName
	private String name = "client";

	// 自定义文件配置的时候,配置读取工厂类
	@Autowired
	private PropertiesFactory propertiesFactory;

	@Bean
	@ConditionalOnMissingBean
	public IClientConfig ribbonClientConfig() {
		// IClientConfig 就代表每个 Ribbon Client ,也就是每个微服务
		DefaultClientConfigImpl config = new DefaultClientConfigImpl();
		config.loadProperties(this.name);
		config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
		config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
		config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
		return config;
	}
	
	// 这个就是负载均衡规则
	@Bean
	@ConditionalOnMissingBean
	public IRule ribbonRule(IClientConfig config) {
		if (this.propertiesFactory.isSet(IRule.class, name)) {
			return this.propertiesFactory.get(IRule.class, config, name);
		}
		ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
		rule.initWithNiwsConfig(config);
		return rule;
	}
	
	@Bean
	@ConditionalOnMissingBean
	public IPing ribbonPing(IClientConfig config) {
		if (this.propertiesFactory.isSet(IPing.class, name)) {
			return this.propertiesFactory.get(IPing.class, config, name);
		}
		return new DummyPing();
	}
	
	// ribbon 服务拉取的地方
	@Bean
	@ConditionalOnMissingBean
	@SuppressWarnings("unchecked")
	public ServerList<Server> ribbonServerList(IClientConfig config) {
		if (this.propertiesFactory.isSet(ServerList.class, name)) {
			return this.propertiesFactory.get(ServerList.class, config, name);
		}
		ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
		serverList.initWithNiwsConfig(config);
		return serverList;
	}
	
	// 重要:内部维护了一个线程,定时去拉取服务列表
	@Bean
	@ConditionalOnMissingBean
	public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
		return new PollingServerListUpdater(config);
	}
	
	// 重要:把上面的所有类都传了进来
	@Bean
	@ConditionalOnMissingBean
	public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
			ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
			IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
		if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
			return this.propertiesFactory.get(ILoadBalancer.class, config, name);
		}
		return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
				serverListFilter, serverListUpdater);
	}

	@Bean
	@ConditionalOnMissingBean
	@SuppressWarnings("unchecked")
	public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
		if (this.propertiesFactory.isSet(ServerListFilter.class, name)) {
			return this.propertiesFactory.get(ServerListFilter.class, config, name);
		}
		ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
		filter.initWithNiwsConfig(config);
		return filter;
	}
}

这个是Ribbon提供的默认实现。

NacosRibbonClientConfiguration:

public class NacosRibbonClientConfiguration {

	@Autowired
	private PropertiesFactory propertiesFactory;
	
	// Nacos 定制了 ServerList,从nacos里面拉取。
	@Bean
	@ConditionalOnMissingBean
	public ServerList<?> ribbonServerList(IClientConfig config,
			NacosDiscoveryProperties nacosDiscoveryProperties) {
		if (this.propertiesFactory.isSet(ServerList.class, config.getClientName())) {
			ServerList serverList = this.propertiesFactory.get(ServerList.class, config,
					config.getClientName());
			return serverList;
		}
		NacosServerList serverList = new NacosServerList(nacosDiscoveryProperties);
		serverList.initWithNiwsConfig(config);
		return serverList;
	}

}

nacos 定制了其中的一些组件。

我们简要对Nacos 定制的 ServerList 做简单的分析:

public class NacosServerList extends AbstractServerList<NacosServer> {

	private String serviceId;

	@Override
	public List<NacosServer> getInitialListOfServers() {
		return getServers();
	}

	@Override
	public List<NacosServer> getUpdatedListOfServers() {
		return getServers();
	}

	private List<NacosServer> getServers() {
		String group = discoveryProperties.getGroup();
		// 核心就这一句,向Nacos Server 端发送Http请求,获取要拉取的服务实例信息
		List<Instance> instances = discoveryProperties.namingServiceInstance()
				.selectInstances(serviceId, group, true);
		return instancesToServerList(instances);
	}

	@Override
	public void initWithNiwsConfig(IClientConfig iClientConfig) {
		this.serviceId = iClientConfig.getClientName();
	}
}

很简单,感兴趣的可以自己接着点进去看看。

我们在看和这个ServerList有关的其他组件:

  • ServerListUpdater:Ribbon提供的client的默认配置,默认实现是PollingServerListUpdater,见明知意,负责ServerList的更新。

简要分析一下 ServerListUpdater :
最核心的就是 这个 start 方法,启动了一个定时任务 执行 updateAction.doUpdate()

@Override
  public synchronized void start(final UpdateAction updateAction) {
      if (isActive.compareAndSet(false, true)) {
          final Runnable wrapperRunnable = new Runnable() {
              @Override
              public void run() {
                  if (!isActive.get()) {
                      if (scheduledFuture != null) {
                          scheduledFuture.cancel(true);
                      }
                      return;
                  }
                  try {
                      updateAction.doUpdate();
                      lastUpdated = System.currentTimeMillis();
                  } catch (Exception e) {
                      logger.warn("Failed one update cycle", e);
                  }
              }
          };

          scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                  wrapperRunnable,
                  initialDelayMs,
                  refreshIntervalMs,
                  TimeUnit.MILLISECONDS
          );
      } else {
          logger.info("Already active, no-op");
      }
  }

start() 方法何时被调用的,以及 updateAction 是什么,这里就要用到另外的一个Ribbon核心组件:LoadBalancer,其默认实现是:ZoneAwareLoadBalancer。
简要分析一下这个核心类:
在这里插入图片描述
可以明确的是这是个实现负载均衡的类,代码很多,这里只分析核心的部分:

 public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule,
                              IPing ping, ServerList<T> serverList, ServerListFilter<T> filter,
                              ServerListUpdater serverListUpdater) {
     super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
 }

public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,ServerList<T> serverList, ServerListFilter<T> filter,ServerListUpdater serverListUpdater) {
     super(clientConfig, rule, ping);
     this.serverListImpl = serverList;
     this.filter = filter;
     this.serverListUpdater = serverListUpdater;
     if (filter instanceof AbstractServerListFilter) {
         ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
     }
     // 这一步
     restOfInit(clientConfig);
 }

void restOfInit(IClientConfig clientConfig) {
    boolean primeConnection = this.isEnablePrimingConnections();
    this.setEnablePrimingConnections(false);
    // 这一步 调用 上面提到的 ServerListUpdater 的 start()方法
    enableAndInitLearnNewServersFeature();
    // 立即拉取服务
    updateListOfServers();
}

// updateAction 是个内部定义的字段,内部类的方式
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            // 内部调用了updateListOfServers()
            updateListOfServers();
        }
    };

public void enableAndInitLearnNewServersFeature() {
    LOGGER.info("Using serverListUpdater {}", serverListUpdater.getClass().getSimpleName());
    // start()
    serverListUpdater.start(updateAction);
}

/*
 * 上面的地方都调用了这个方法,
 * 这个方法就是 拉取服务 
 */
 @VisibleForTesting
 public void updateListOfServers() {
     List<T> servers = new ArrayList<T>();
     if (serverListImpl != null) {
     	 // serverListImpl 就是 Nacos 实现的 ServerList
         servers = serverListImpl.getUpdatedListOfServers();
         if (filter != null) {
             servers = filter.getFilteredListOfServers(servers);
         }
     }
     // 更新内部的实例列表
     updateAllServerList(servers);
 }

上面就是实现了Ribbon对服务实例更新的分析。

接下来,我们再来看一个比较核心且重要的类:SpringClientFactory
在这里插入图片描述
这个类里面有几个核心的方法:

  • public <C extends IClient<?, ?>> C getClient(String name, Class clientClass);
  • public ILoadBalancer getLoadBalancer(String name)
  • public IClientConfig getClientConfig(String name)

它们的第一个参数都是name,name是什么呢?name其实就是注册在nacos中的服务名
我们选择一个比较重要的分析:

public ILoadBalancer getLoadBalancer(String name) {
	return getInstance(name, ILoadBalancer.class);
}

@Override
public <C> C getInstance(String name, Class<C> type) {
	// 父类的 super.getInstance()
	C instance = super.getInstance(name, type);
	if (instance != null) {
		return instance;
	}
	IClientConfig config = getInstance(name, IClientConfig.class);
	return instantiateWithConfig(getContext(name), type, config);
}


public <T> T getInstance(String name, Class<T> type) {
	// 通过name 获得一个Spring容器,对你没看错就是获取一个Spring容器
	AnnotationConfigApplicationContext context = getContext(name);
	if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
			type).length > 0) {
			// 从容器里面get出这个实现类
		return context.getBean(type);
	}
	return null;
}

protected AnnotationConfigApplicationContext getContext(String name) {
	// this.contexts 是个map结构
	if (!this.contexts.containsKey(name)) {
		synchronized (this.contexts) {
			if (!this.contexts.containsKey(name)) {
				// 获取不到这个spring容器就直接创建
				this.contexts.put(name, createContext(name));
			}
		}
	}
	return this.contexts.get(name);
}

/*
* 比较重要的就是这个方法了
*/
protected AnnotationConfigApplicationContext createContext(String name) {
   // 手动创建一个容器
	AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
	if (this.configurations.containsKey(name)) {
		for (Class<?> configuration : this.configurations.get(name)
				.getConfiguration()) {
			context.register(configuration);
		}
	}
	// 这个地方会注入进来两个配置类,就是上面说到的配置类
	// - RibbonClientConfiguration, NacosRibbonClientConfiguration
	for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
		if (entry.getKey().startsWith("default.")) {
			for (Class<?> configuration : entry.getValue().getConfiguration()) {
				context.register(configuration);
			}
		}
	}
	// 注入一些必要的组件类
	context.register(PropertyPlaceholderAutoConfiguration.class,
			this.defaultConfigType);
	context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
			this.propertySourceName,
			Collections.<String, Object>singletonMap(this.propertyName, name)));
	if (this.parent != null) {
		// Uses Environment from parent as well as beans
		// 设置父容器为当前项目启动的主容器
		context.setParent(this.parent);
		// jdk11 issue
		// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
		context.setClassLoader(this.parent.getClassLoader());
	}
	context.setDisplayName(generateDisplayName(name));
	// 刷新容器
	context.refresh();
	return context;
}

上面这段代码算是最核心的部分了:

  • ribbon会为每个服务单独创建一个容器,容器里面注入了这个ribbon client必要的组件。

SpringClientFactory是在我们项目启动的主容器全局注入的,是单例的, 我们能够直接获取到的。

我们下面的事件感知过后的服务刷新也是利用了这个特性,这点非常重要。

了解完了上面的部分内容,才会能看懂下面链接中代码的意思,代码我直接放到gitee上,下面给出仓库地址链接:

https://gitee.com/shouhengR/ribbon-watch.git

码字不易,代码中也都写了注释,不懂的可以给我留言,最后,感谢岁月,感谢青春!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值