Nacos 如何适应 Spring Cloud 规范


我们知道在 Java 微服务生态体系的发展过程中,有两种主流的解决方案逐渐受到行业内的关注,一种是基于 Spring 技术体系,由 Spring 官方团队推出的 Spring Cloud 微服务一体式解决方案可以为我们方便地搭建一个微服务架构,而且基于 Spring 良好的粘合剂的特性我们可以方便地将各类第三方框架集成到该体系中。另一个方面,由阿里所推出的 Dubbo 从早期单纯的一个 RPC 框架自从阿里宣布重新启动对 Dubbo 的维护后,逐渐自成一派,融合了如 Nacos, Sentinel, Seata, RocketMQ 一类的中间件技术,成为全新一套完整的微服务解决方案,为企业构建自身的架构提供了不同的选择。很多时候我们在面临选择拥抱哪一种生态的时候,往往会权衡利弊,有所犹豫,直到 Spring Cloud Alibaba 的推出,这一切又变得迥然不同。面对 Spring Cloud 体系的蓬勃发展并受到大众的广泛接受,Dubbo 体系选择拥抱 Spring Cloud,基于 Spring 体系强大的包容性和扩展性推出了适用 Spring Cloud 规范而生的 Spring Cloud Alibaba,如今在 Spring 官网我们已经看到 Spring Cloud Alibaba 的英文文档,意味着这种方案已经被 Spring 官方所认可和推崇。

Nacos 如何适应 Spring Cloud 中的服务注册与发现


DiscoveryClient 服务发现客户端

在微服务架构中 Consumer 服务消费者必须具有服务发现的功能,基于此 Consumer 才能从 Registry 服务注册中心获取 Provider 服务提供者的地址列表,通过某一种 LoadBalance 负载均衡的策略选取一个地址进行 RPC 通信访问。在这个过程中诸如 Registry, LoadBalance 这样的实现方式也许是不同,但可以从中抽象出几个重要的实体,整个过程只是这几个实体之间的交互。而 org.springframework.cloud.client.discovery.DiscoveryClient 正是 Spring Cloud 对于服务发现者的抽象,在微服务架构中每个 Consumer 都必须是一个 DiscoveryClient 服务发现者。那么 Nacos 对这个接口必然有一个实现 com.alibaba.cloud.nacos.discovery.NacosDiscoveryClient ,其中的核心方法就是 getServices() 用于获取所有的 Provider 列表。

public List<String> getServices() {
    try {
        ListView<String> services = discoveryProperties.namingServiceInstance()
                .getServicesOfServer(1, Integer.MAX_VALUE);
        return services.getData();
    } catch (Exception e) {
        log.error("get service name from nacos server fail,", e);
        return Collections.emptyList();
    }
}

我们看到请求被委托给了 NamingServiceInstance ,这个所谓的 NamingService 是什么呢?这是 Nacos 内部的一个命名服务,通过跟踪代码我们发现这个实例时通过反射创建的,它的真正实现是 com.alibaba.nacos.client.naming.NacosNamingService,这个请求最终调用的是 NamingProxy#getServiceList() 方法,通过 HTTP 方式从 Nacos 服务获取服务列表。也就是说 Spring Cloud 提供的 DiscoveryClient 只是一个规范,或者说一层包装,而内部真正是实现是第三方框架所提供的,如 Spring Cloud 默认提供的 Eureka 服务注册中心是 Netflix 所提供的实现。这是 Spring 家族贯穿始终的一种设计思想,框架层面提供抽象性的设计,而具体的实现是可扩展,可自定义的,个人认为这是 Spring 成为 Java 生态粘合剂的根本指导思想。

服务自动注册

在微服务架构中 Provider 服务提供者在启动后就应该自动向注册中心进行注册,以实现微服务架构中的一个重要特性,服务动态扩容。com.alibaba.cloud.nacos.registry.NacosAutoServiceRegistration 负责自动将服务信息自动注册到 Nacos,这是一个较为简易的实现,核心逻辑已经被 Spring Cloud 抽象实现了,因为服务自动注册是一个普遍性功能,与具体实现无关,只是各框架的注册方式不同而已。org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration 就是 Spring Cloud 提供的抽象实现。

public abstract class AbstractAutoServiceRegistration<R extends Registration>
		implements AutoServiceRegistration, ApplicationContextAware,
		ApplicationListener<WebServerInitializedEvent> {
    //...
}

我们可以看到它实现了 ApplicationListener<WebServerInitializedEvent>,也就是它本身是一个事件监听器,会对 WebServerInitializedEvent 服务初始化事件作出反应。

@Override
@SuppressWarnings("deprecation")
public void onApplicationEvent(WebServerInitializedEvent event) {
	bind(event);
}

@Deprecated
public void bind(WebServerInitializedEvent event) {
	ApplicationContext context = event.getApplicationContext();
	if (context instanceof ConfigurableWebServerApplicationContext) {
		if ("management".equals(((ConfigurableWebServerApplicationContext) context)
				.getServerNamespace())) {
			return;
		}
	}
	this.port.compareAndSet(0, event.getWebServer().getPort());
	this.start();
}

服务一启动便执行了 bind() 逻辑,这里了主要是设置端口号,接着执行 start() 逻辑。

public void start() {
	if (!isEnabled()) {
		if (logger.isDebugEnabled()) {
			logger.debug("Discovery Lifecycle disabled. Not starting");
		}
		return;
	}

	// only initialize if nonSecurePort is greater than 0 and it isn't already running
	// because of containerPortInitializer below
	if (!this.running.get()) {
		this.context.publishEvent(
				new InstancePreRegisteredEvent(this, getRegistration()));
		register();
		if (shouldRegisterManagement()) {
			registerManagement();
		}
		this.context.publishEvent(
				new InstanceRegisteredEvent<>(this, getConfiguration()));
		this.running.compareAndSet(false, true);
	}

}

这里首先判断服务自动注册是否被禁用,接着利用了一个 AtomicBoolean 避免重复注册,同时向上下文发布了服务注册前后的 InstancePreRegisteredEventInstanceRegisteredEvent 两个事件,如果我们有业务需要在服务注册前后嵌入相关逻辑,可以通过监听这两个事件来实现,其中核心便是执行 register() 方法。

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    if (instance.isEphemeral()) {
        BeatInfo beatInfo = new BeatInfo();
        beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
        beatInfo.setIp(instance.getIp());
        beatInfo.setPort(instance.getPort());
        beatInfo.setCluster(instance.getClusterName());
        beatInfo.setWeight(instance.getWeight());
        beatInfo.setMetadata(instance.getMetadata());
        beatInfo.setScheduled(false);
        long instanceInterval = instance.getInstanceHeartBeatInterval();
        beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);

        beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
    }

    serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}

通过跟踪代码可以发现 registry 请求依然是委托给 NacosNamingService 执行的,可以说这个类是 Nacos 实现服务注册发现的核心。同时这里通过 BeatReactor 添加了一个心跳任务,因为 Nacos 采用心跳机制判断向自己注册的服务是否依然存活,以此动态维护服务列表,这一点与 Eurake 相似。BeatReactor 中持有了一个 ScheduledExecutorService 用以周期性地执行心跳任务,默认间隔为五秒,每次心跳任务会上报该服务的元数据信息表示自身依然存活。

Nacos 如何适应 Spring Cloud Config 配置中心


PropertySourceLocator 配置源定位

org.springframework.cloud.bootstrap.config.PropertySourceLocator 是 Spring Cloud 抽象的一个接口,用于从 Environment 中定位到 PropertySource 配置资源,在微服务架构中,由于配置存储在远程的配置中心,我们需要从远程配置中心进行拉取。

@Override
public PropertySource<?> locate(Environment env) {

	ConfigService configService = nacosConfigProperties.configServiceInstance();

	if (null == configService) {
		log.warn("no instance of config service found, can't load config from nacos");
		return null;
	}
	long timeout = nacosConfigProperties.getTimeout();
	nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
			timeout);
	String name = nacosConfigProperties.getName();

	String dataIdPrefix = nacosConfigProperties.getPrefix();
	if (StringUtils.isEmpty(dataIdPrefix)) {
		dataIdPrefix = name;
	}

	if (StringUtils.isEmpty(dataIdPrefix)) {
		dataIdPrefix = env.getProperty("spring.application.name");
	}

	CompositePropertySource composite = new CompositePropertySource(
			NACOS_PROPERTY_SOURCE_NAME);

	loadSharedConfiguration(composite);
	loadExtConfiguration(composite);
	loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

	return composite;
}

com.alibaba.cloud.nacos.client.NacosPropertySourceLocator 是 Nacos 对该接口的实现,其中的 locate() 方法是其核心逻辑。可以看到其中调用的三个私有方法分别为我们加载了三个层级的配置信息,因为Nacos为我们提供了三个层级的配置,分别是

  • Shared 共享配置信息,这个层级一般存放一些所有服务共有的默认配置信息,优先级最低。
  • Ext 扩展配置信息,这个层级可以存放可供选择的公用配置信息,如一个项目集成了 Redis,才需要 Redis 相关的配置。
  • Application 应用配置信息,这是服务独有的配置信息,并支持环境隔离。
    这三种配置的加载顺序也决定了他们的优先级,加载越迟,配置优先级越高。

Nacos 配置动态刷新

在微服务架构中,当我们修改配置中心的配置信息后,我们希望这些变化能实时地被系统中的各服务获取,反映到我们业务中的逻辑变化。所以 Nacos 通过一些机制实现了该特性。com.alibaba.cloud.nacos.refresh.NacosContextRefresher 中我们可以看到其核心逻辑。

public class NacosContextRefresher
		implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
		//...
}

同样它也是一个事件监听器,当 ApplicationReadyEvent 这个生命周期事件被派发后,便开始向 Nacos 注册相应的监听器负责监听配置信息的变化。

private void registerNacosListenersForApplications() {
	if (refreshProperties.isEnabled()) {
		for (NacosPropertySource nacosPropertySource : NacosPropertySourceRepository
				.getAll()) {

			if (!nacosPropertySource.isRefreshable()) {
				continue;
			}

			String dataId = nacosPropertySource.getDataId();
			registerNacosListener(nacosPropertySource.getGroup(), dataId);
		}
	}
}

private void registerNacosListener(final String group, final String dataId) {

	Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
		@Override
		public void receiveConfigInfo(String configInfo) {
			refreshCountIncrement();
			String md5 = "";
			if (!StringUtils.isEmpty(configInfo)) {
				try {
					MessageDigest md = MessageDigest.getInstance("MD5");
					md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
							.toString(16);
				}
				catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
					log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
				}
			}
			refreshHistory.add(dataId, md5);
			applicationContext.publishEvent(
					new RefreshEvent(this, null, "Refresh Nacos config"));
			if (log.isDebugEnabled()) {
				log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
			}
		}

		@Override
		public Executor getExecutor() {
			return null;
		}
	});

	try {
		configService.addListener(dataId, group, listener);
	}
	catch (NacosException e) {
		e.printStackTrace();
	}
}

循环当前服务中的每一个 NacosPropertySource 配置源,分别为其注册 Nacos 监听器,listenerMap 是一个 ConcurrentHashMap 作为监听器缓存,同时避免重复注册。Nacos 的监听器功能与 zookeeperWatcher 类似,非常适合用于分布式系统中的数据协调。当配置信息发生改变时,receiveConfigInfo() 方法会被回调,加入一条配置刷新历史,同时向上下文中派发一个 RefreshEvent 事件,这个事件可以触发刷新 Spring 上下文中的 PropertySource,之后我们从 Environment 中获取到的 Property 已经是更新后的。


文章作者: Ethan Zhang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethan Zhang !
评论
 上一篇
响应式编程之浅见 响应式编程之浅见
在计算中,响应式编程或反应式编程(英语:Reactive programming)是一种面向数据流和变化传播的声明式编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。 以
2020-06-28 Ethan Zhang
下一篇 
微服务网关 Spring Cloud Gateway 之限流 微服务网关 Spring Cloud Gateway 之限流
Spring Cloud Gateway 作为 Spring 官方全新推出的微服务网关解决方案,用于替代原有的 Netflix Zuul 框架,相比于 Zuul,Spring Cloud Gateway 最大的不同点在于它是基于 Spri
2020-04-08 Ethan Zhang