Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor NacosWatch Class #2868

Closed
steverao opened this issue Oct 27, 2022 · 14 comments
Closed

Refactor NacosWatch Class #2868

steverao opened this issue Oct 27, 2022 · 14 comments
Assignees
Labels
contribution welcome kind/discussion Mark as discussion issues/pr

Comments

@steverao
Copy link
Collaborator

com.alibaba.cloud.nacos.discovery.NacosWatch 类目前的核心逻辑如下:

@Override
public void start() {
  if (this.running.compareAndSet(false, true)) {
    EventListener eventListener = listenerMap.computeIfAbsent(buildKey(),
        event -> new EventListener() {
          @Override
          public void onEvent(Event event) {
            if (event instanceof NamingEvent) {
              List<Instance> instances = ((NamingEvent) event)
                  .getInstances();
              Optional<Instance> instanceOptional = selectCurrentInstance(
                  instances);
              instanceOptional.ifPresent(currentInstance -> {
                resetIfNeeded(currentInstance);
              });
            }
          }
        });

    NamingService namingService = nacosServiceManager.getNamingService();
    try {
      namingService.subscribe(properties.getService(), properties.getGroup(),
          Arrays.asList(properties.getClusterName()), eventListener);
    }
    catch (Exception e) {
      log.error("namingService subscribe failed, properties:{}", properties, e);
    }

    this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
        this::nacosServicesWatch, this.properties.getWatchDelay());
  }
}

它目前做了2件事:

  1. 应用启动时,通过namingService.subscribe()到Nacos中订阅自身实例变化情况,当有变化时会将本地的metadata信息更新为Nacos Server中实例对应metadata信息。
  2. 通过this.taskScheduler.scheduleWithFixedDelay()启动一个定时任务定时发送特定的事件,然后其他的网关比如,Spring Cloud Gateway监听到对应的事件后,到Nacos中拉取相关的服务列表信息更新本地路由列表,达到动态路由效果,部分用户也有这样来使用网关达到动态路由效果的。

其中,第1个效果经过社区周会讨论,应用场景不清晰,反而造成应用启动时订阅自己造成部分用户使用出现报错现象,因此考虑删除。
第2个效果经过社区内外同学讨论,其实并不是动态路由的最佳使用方式,网关路由信息是一份相关稳定的数据,每30s发送事件到注册中心拉取服务列表转换成默认路由,会造成网关的资源被大量消耗,特别是服务量大时,所以也不建议大家这样来用。
综上,社区考虑重构相关类,目前考虑将NacosWatch作为默认关闭的一个功能。欢迎大家对这个问题进行留言:)

@steverao steverao added the kind/discussion Mark as discussion issues/pr label Oct 27, 2022
@ruansheng8
Copy link
Collaborator

ruansheng8 commented Oct 27, 2022

namingService.subscribe() 可以考虑单独一个配置开关 (默认关闭)
this.taskScheduler.scheduleWithFixedDelay() 可能需要保留 (默认开启) , 避免其他用户升级没有看对应的文档导致线上故障 , 可以在开启这个功能的时候打一条警告日志 , 提醒用户该功能存在的风险

@HaojunRen
Copy link
Collaborator

HaojunRen commented Oct 27, 2022

第1件事情,可以考虑移除,30秒更新一次元数据,意义不太大
第2件事情,其实,应该拉服务列表就可以了,考虑DiscoveryClient.getServices()? 不拉具体实例的属性,是不是可以节省大量资源?默认也可以关闭,但要特别提醒用户,有些小公司的用户可能意识不到

@ruansheng8
Copy link
Collaborator

ruansheng8 commented Oct 27, 2022

第1件事情,可以考虑移除,30秒更新一次元数据,意义不太大 第2件事情,其实,应该拉服务列表就可以了,考虑DiscoveryClient.getServices()? 不拉具体实例的属性,是不是可以节省大量资源?

第二点主要在服务实例过多的时候会引发以下问题 #1967
在开启动态路由的场景中 , 引导用户实现让spring cloud gateway 主动定时去获取实例列表获取是比较好的一个选择

@HaojunRen
Copy link
Collaborator

所以,不要去拉实例列表,只拉服务列表即可

@ruansheng8
Copy link
Collaborator

ruansheng8 commented Oct 27, 2022

所以,不要去拉实例列表,只拉服务列表即可

NacosReactiveDiscoveryClient 获取实例列表逻辑

	@Override
	public Flux<ServiceInstance> getInstances(String serviceId) {

		return Mono.justOrEmpty(serviceId).flatMapMany(loadInstancesFromNacos())
				.subscribeOn(Schedulers.boundedElastic());
	}

	private Function<String, Publisher<ServiceInstance>> loadInstancesFromNacos() {
		return serviceId -> {
			try {
				return Mono.justOrEmpty(serviceDiscovery.getInstances(serviceId))
						.flatMapMany(instances -> {
							ServiceCache.setInstances(serviceId, instances);
							return Flux.fromIterable(instances);
						});
			}
			catch (NacosException e) {
				log.error("get service instance[{}] from nacos error!", serviceId, e);
				return failureToleranceEnabled
						? Flux.fromIterable(ServiceCache.getInstances(serviceId))
						: Flux.empty();
			}
		};
	}

NacosReactiveDiscoveryClient 拉取实例有用缓存 , 不会每次都远程拉 , 不过HeartbeatEvent会一直触发(默认30秒发一次 , 基于NacosWatch 的delay值) , 导致 DiscoveryClientRouteDefinitionLocator 不停的在刷新路由

Gateway 创建路由逻辑 ( DiscoveryClientRouteDefinitionLocator )

	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {

                // ... 

		return serviceInstances.filter(instances -> !instances.isEmpty()).flatMap(Flux::fromIterable)
				.filter(includePredicate).collectMap(ServiceInstance::getServiceId)
				// remove duplicates
				.flatMapMany(map -> Flux.fromIterable(map.values())).map(instance -> {
					RouteDefinition routeDefinition = buildRouteDefinition(urlExpr, instance);

					final ServiceInstance instanceForEval = new DelegatingServiceInstance(instance, properties);

					for (PredicateDefinition original : this.properties.getPredicates()) {
						PredicateDefinition predicate = new PredicateDefinition();
						predicate.setName(original.getName());
						for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
							String value = getValueFromExpr(evalCtxt, parser, instanceForEval, entry);
							predicate.addArg(entry.getKey(), value);
						}
						routeDefinition.getPredicates().add(predicate);
					}

					for (FilterDefinition original : this.properties.getFilters()) {
						FilterDefinition filter = new FilterDefinition();
						filter.setName(original.getName());
						for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
							String value = getValueFromExpr(evalCtxt, parser, instanceForEval, entry);
							filter.addArg(entry.getKey(), value);
						}
						routeDefinition.getFilters().add(filter);
					}

					return routeDefinition;
				});
	}

@HaojunRen
Copy link
Collaborator

HaojunRen commented Oct 31, 2022

几个建议

  • 去掉spring.cloud.nacos.discovery.watch.enabled开关,Nacos Watch开启和关闭跟spring.cloud.gateway.discovery.locator.enabled绑定,通过这个开关控制,那么就意味着Nacos Watch专属于Spring Cloud Gateway网关
  • 拉取服务名列表。只拉取全部服务名列表,不拉取潜在可能会冲击性能的全部实例列表(里面包含IP地址和端口,元数据等对于网关路由没有意义的数据),有两种实现方式
    • 通过discoveryClient.getServices(),定时30秒拉一次
    • 监控Nacos缓存,不需要定时器,这样更加实时。这个方案没经过详细调研,不确定
  • 动态更新元数据功能和网关自动路由功能,拆分成两个Bean,分别开关控制

@steverao
Copy link
Collaborator Author

几个建议

  • 去掉spring.cloud.nacos.discovery.watch.enabled开关,Nacos Watch开启和关闭跟spring.cloud.gateway.discovery.locator.enabled绑定,通过这个开关控制,那么就意味着Nacos Watch专属于Spring Cloud Gateway网关

  • 拉取服务名列表。只拉取全部服务名列表,不拉取潜在可能会冲击性能的全部实例列表(里面包含IP地址和端口,元数据等对于网关路由没有意义的数据),有两种实现方式

    • 通过discoveryClient.getServices(),定时30秒拉一次
    • 监控Nacos缓存,不需要定时器,这样更加实时。这个方案没经过详细调研,不确定
  • 动态更新元数据功能和网关自动路由功能,拆分成两个Bean,分别开关控制

我觉得拆分为2个bean来分别进行处理比较合适,定时发送Heartbeat事件的bean通过spring.cloud.gateway.discovery.locator.enabled来控制是否加载。
更新本地元数据,还是继续spring.cloud.nacos.discovery.watch.enabled开关来控制,不过改成默认关闭比较合适,因为该功能目前来看用户诉求不是太多,以便对不使用该功能的用户造成干扰。

@ruansheng8
Copy link
Collaborator

ruansheng8 commented Oct 31, 2022

@@i will solve it@@

@yuhuangbin
Copy link
Collaborator

yuhuangbin commented Nov 1, 2022

我觉得拆分为2个bean来分别进行处理比较合适,定时发送Heartbeat事件的bean通过spring.cloud.gateway.discovery.locator.enabled来控制是否加载。 更新本地元数据,还是继续spring.cloud.nacos.discovery.watch.enabled开关来控制,不过改成默认关闭比较合适,因为该功能目前来看用户诉求不是太多,以便对不使用该功能的用户造成干扰。

赞成

ruansheng8 added a commit to ruansheng8/spring-cloud-alibaba that referenced this issue Nov 11, 2022
yuhuangbin pushed a commit that referenced this issue Nov 16, 2022
* Refactor NacosWatch (#2868)

* GatewayHeartBeatPublisher support zuul

* Add GatewayHeartBeatPublisher tests

* Update readme doc

* Fix nacosWatch property hint
@zhangbinhub
Copy link
Contributor

zhangbinhub commented May 8, 2023

几个建议

  • 去掉spring.cloud.nacos.discovery.watch.enabled开关,Nacos Watch开启和关闭跟spring.cloud.gateway.discovery.locator.enabled绑定,通过这个开关控制,那么就意味着Nacos Watch专属于Spring Cloud Gateway网关

  • 拉取服务名列表。只拉取全部服务名列表,不拉取潜在可能会冲击性能的全部实例列表(里面包含IP地址和端口,元数据等对于网关路由没有意义的数据),有两种实现方式

    • 通过discoveryClient.getServices(),定时30秒拉一次
    • 监控Nacos缓存,不需要定时器,这样更加实时。这个方案没经过详细调研,不确定
  • 动态更新元数据功能和网关自动路由功能,拆分成两个Bean,分别开关控制

我觉得拆分为2个bean来分别进行处理比较合适,定时发送Heartbeat事件的bean通过spring.cloud.gateway.discovery.locator.enabled来控制是否加载。 更新本地元数据,还是继续spring.cloud.nacos.discovery.watch.enabled开关来控制,不过改成默认关闭比较合适,因为该功能目前来看用户诉求不是太多,以便对不使用该功能的用户造成干扰。

这样还是有问题。分两个bean的思路没问题,但是Heartbeat事件不仅gateway的locator会用到,spring boot admin也是通过订阅该事件去更新服务状态的,能否考虑将控制Heartbeat事件的开关名称换一个?#3258

@zhangbinhub
Copy link
Contributor

几个建议

  • 去掉spring.cloud.nacos.discovery.watch.enabled开关,Nacos Watch开启和关闭跟spring.cloud.gateway.discovery.locator.enabled绑定,通过这个开关控制,那么就意味着Nacos Watch专属于Spring Cloud Gateway网关

  • 拉取服务名列表。只拉取全部服务名列表,不拉取潜在可能会冲击性能的全部实例列表(里面包含IP地址和端口,元数据等对于网关路由没有意义的数据),有两种实现方式

    • 通过discoveryClient.getServices(),定时30秒拉一次
    • 监控Nacos缓存,不需要定时器,这样更加实时。这个方案没经过详细调研,不确定
  • 动态更新元数据功能和网关自动路由功能,拆分成两个Bean,分别开关控制

我觉得拆分为2个bean来分别进行处理比较合适,定时发送Heartbeat事件的bean通过spring.cloud.gateway.discovery.locator.enabled来控制是否加载。 更新本地元数据,还是继续spring.cloud.nacos.discovery.watch.enabled开关来控制,不过改成默认关闭比较合适,因为该功能目前来看用户诉求不是太多,以便对不使用该功能的用户造成干扰。

这样还是有问题。分两个bean的思路没问题,但是Heartbeat事件不仅gateway的locator会用到,spring boot admin也是通过订阅该事件去更新服务状态的,能否考虑将控制Heartbeat事件的开关名称换一个?

@ruansheng8
Copy link
Collaborator

@zhangbinhub 可以考虑使用多条件,满足其一就装配。例如引入Spring Boot Admin 服务端依赖时判断该项目中的一个类存在则默认装配

@zhangbinhub
Copy link
Contributor

@ruansheng8 帮看看这个pr的思路如何?#3303
这样是不是感觉稍微灵活一些?望指正

@ruansheng8
Copy link
Collaborator

Refer #3308

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution welcome kind/discussion Mark as discussion issues/pr
Projects
None yet
Development

No branches or pull requests

5 participants