alibaba / spring-cloud-alibaba

Spring Cloud Alibaba provides a one-stop solution for application development for the distributed solutions of Alibaba middleware.
https://sca.aliyun.com
Apache License 2.0
27.47k stars 8.19k forks source link

为什么nacos不采用像apollo那样动态发布配置后 在客户端使用反射去更改配置值的 方式去动态更改配置? #2557

Closed DeBruyne2020 closed 1 year ago

DeBruyne2020 commented 2 years ago

问下各位大佬, nacos现在动态发布配置的机制是依赖spring的RefreshScope注解,RefreshScope注解配在配置所在的类头上,类似于这样: image

服务在线的情况下,发布这个配置,这会导致spring上下文重新加载,导致服务重启,表现为在注册中心(如eureka)上先down一下然后再up(虽然这个过程可能非常快),,为什么nacos不采用类似apollo的动态发布方式,也就是通过beanFactory找到对应的配置所在的类,然后用反射的方式修改具体配置的值,这样整个spring上下文是不重启的,服务不会重启。

这个说是springcloud/springcloudalibaba的标准导致目前的这个现状,所以在这里问问

steverao commented 2 years ago

这个问题我们在2021.x分支上做了部分优化的,具体可以见#2437,可以做到只刷新特定改动的配置,但是还不支持你说的不重启spring上下文。这块有兴趣可以一起来建设哈:)

DeBruyne2020 commented 2 years ago

这个问题我们在2021.x分支上做了部分优化的,具体可以见#2437,可以做到只刷新特定改动的配置,但是还不支持你说的不重启spring上下文。这块有兴趣可以一起来建设哈:)

好的,谢谢大佬,
这块很有兴趣, 请问大佬我可以在您说的这个2021.x分支上或者另外的分支上做代码提交以此来优化改进这个问题吗?或者可以先商量讨论下改动方法改动思路什么的?

steverao commented 2 years ago

这个问题我们在2021.x分支上做了部分优化的,具体可以见#2437,可以做到只刷新特定改动的配置,但是还不支持你说的不重启spring上下文。这块有兴趣可以一起来建设哈:)

好的,谢谢大佬, 这块很有兴趣, 请问大佬我可以在您说的这个2021.x分支上或者另外的分支上做代码提交以此来优化改进这个问题吗?或者可以先商量讨论下改动方法改动思路什么的?

嗯嗯,是的,在2021.x分支上改动就好了,可以先在这个issues下面留言说一下你这边详细的方案思路,我们可以一起看一下。

DeBruyne2020 commented 2 years ago

这个问题我们在2021.x分支上做了部分优化的,具体可以见#2437,可以做到只刷新特定改动的配置,但是还不支持你说的不重启spring上下文。这块有兴趣可以一起来建设哈:)

好的,谢谢大佬, 这块很有兴趣, 请问大佬我可以在您说的这个2021.x分支上或者另外的分支上做代码提交以此来优化改进这个问题吗?或者可以先商量讨论下改动方法改动思路什么的?

嗯嗯,是的,在2021.x分支上改动就好了,可以先在这个issues下面留言说一下你这边详细的方案思路,我们可以一起看一下。

好的 👌 , 谢谢大佬

DanielLiu1123 commented 2 years ago
  1. 使用 @RefreshScope 动态刷新并不是一种优雅的做法,推荐封装成类使用 @ConfigurationProperties 完成动态刷新功能。
  2. 采用反射对于基本的数据结构是可取的,但是如果是复杂类型数据结构会带来较大的解析成本,降低稳定性。
DeBruyne2020 commented 2 years ago
  1. 使用 @RefreshScope 动态刷新并不是一种优雅的做法,推荐封装成类使用 @ConfigurationProperties 完成动态刷新功能。
  2. 采用反射对于基本的数据结构是可取的,但是如果是复杂类型数据结构会带来较大的解析成本,降低稳定性。

@DanielLiu1123 大佬好~,如果用@ConfigurationProperties去刷新配置,那有办法做到不重启spring上下文么?

yujunchengg commented 2 years ago

它那个refreshEvent里头那个refresh处理是很慢的,打个断点卡老半天

DeBruyne2020 commented 2 years ago

这个问题我们在2021.x分支上做了部分优化的,具体可以见#2437,可以做到只刷新特定改动的配置,但是还不支持你说的不重启spring上下文。这块有兴趣可以一起来建设哈:)

好的,谢谢大佬, 这块很有兴趣, 请问大佬我可以在您说的这个2021.x分支上或者另外的分支上做代码提交以此来优化改进这个问题吗?或者可以先商量讨论下改动方法改动思路什么的?

嗯嗯,是的,在2021.x分支上改动就好了,可以先在这个issues下面留言说一下你这边详细的方案思路,我们可以一起看一下。

@steverao @DanielLiu1123 大佬好,我有两个改动思路如下: 思路1:

step 1 定义一个注解@RefreshValue ,加在包含有想能够动态刷新的配置的类的类头上 step2 定义一个类implements BeanPostProcessor, ApplicationListener 在spring初始化的时候聚集好所有加了@RefreshValue注解的类 step 3 在com.alibaba.cloud.nacos.refresh.NacosContextRefresher的registerNacosListener方法里 ,把applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config")); 去掉(这样就不重启spring上下文了) 改成从step2中的类里获取含有想要动态刷新的配置值的类,并且通过反射找到具体的属性,通过反射正常set赋值就可以了

思路2: 和思路1的区别就是无需自定义注解,就是在spring启动的时候找到所有含有配置值(如@Value注解)的类,写到一个map里,key是属性key,value是类,在com.alibaba.cloud.nacos.refresh.NacosContextRefresher的registerNacosListener方法里 ,把applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config")); 去掉(这样就不重启spring上下文了),然后通过上面的map就找到对应的类及具体的属性,正常通过反射去set赋值就可以了

上面两种思路,其中思路1已经写好代码,并且试验是成功的,spring上下文不重启,配置值也实时刷新了

大佬看上面的方案思路可以吗?大佬觉得哪种思路更好? 方案思路可以的话我就提交代码了

DanielLiu1123 commented 2 years ago

欢迎提交 PR ~

DanielLiu1123 commented 2 years ago

这里有一个很致命的问题,如果不发送 RefreshEvent 事件,那么很多依赖于该事件的功能都将不起作用,这是一个 breaking change

DeBruyne2020 commented 2 years ago

这里有一个很致命的问题,如果不发送 RefreshEvent 事件,那么很多依赖于该事件的功能都将不起作用,这是一个 breaking change

@DanielLiu1123 “那么很多依赖于该事件的功能都将不起作用”,大佬能举个例子么, 比如具体一个什么样的功能?我没有完全get到,现在是只是改了nacos的配置值,那么最终目的不是让用到nacos配置值的地方刷新成最新的值就可以么?,“依赖于该事件的功能” 是指有哪些?

然后第二个问题是如果发送RefreshEvent事件,有什么办法能spring上下文不重启吗?

DeBruyne2020 commented 2 years ago

@steverao @DanielLiu1123 大佬 问下哈 , springcloudalibaba有微信群QQ群钉钉群之类的么?

steverao commented 2 years ago

@steverao @DanielLiu1123 大佬 问下哈 , springcloudalibaba有微信群QQ群钉钉群之类的么?

钉钉群:34351718

DeBruyne2020 commented 2 years ago

@steverao @DanielLiu1123 大佬 问下哈 , springcloudalibaba有微信群QQ群钉钉群之类的么?

钉钉群:34351718

感谢大佬

DanielLiu1123 commented 2 years ago

这里有一个很致命的问题,如果不发送 RefreshEvent 事件,那么很多依赖于该事件的功能都将不起作用,这是一个 breaking change

@DanielLiu1123 “那么很多依赖于该事件的功能都将不起作用”,大佬能举个例子么, 比如具体一个什么样的功能?我没有完全get到,现在是只是改了nacos的配置值,那么最终目的不是让用到nacos配置值的地方刷新成最新的值就可以么?,“依赖于该事件的功能” 是指有哪些?

然后第二个问题是如果发送RefreshEvent事件,有什么办法能spring上下文不重启吗?

  1. Sentinel CircuitBreakerRuleChangeListener, Gateway RouteRefreshListener 都会间接依赖于该事件,如果不触发 RefreshEvent,这些功能都会失效,存在极高的风险(配置在 Nacos 的路由,熔断配置都不会刷新)。
  2. 在 Spring Boot 2.4 以前会启动一个无头服务来刷新环境,确实成本比较高;但是 Spring Boot 2.4 以后采取了更轻量的实现方式。see org.springframework.cloud.context.refresh.ContextRefresher
DeBruyne2020 commented 2 years ago

这里有一个很致命的问题,如果不发送 RefreshEvent 事件,那么很多依赖于该事件的功能都将不起作用,这是一个 breaking change

@DanielLiu1123 “那么很多依赖于该事件的功能都将不起作用”,大佬能举个例子么, 比如具体一个什么样的功能?我没有完全get到,现在是只是改了nacos的配置值,那么最终目的不是让用到nacos配置值的地方刷新成最新的值就可以么?,“依赖于该事件的功能” 是指有哪些? 然后第二个问题是如果发送RefreshEvent事件,有什么办法能spring上下文不重启吗?

  1. Sentinel CircuitBreakerRuleChangeListener, Gateway RouteRefreshListener 都会间接依赖于该事件,如果不触发 RefreshEvent,这些功能都会失效,存在极高的风险(配置在 Nacos 的路由,熔断配置都不会刷新)。
  2. 在 Spring Boot 2.4 以前会启动一个无头服务来刷新环境,确实成本比较高;但是 Spring Boot 2.4 以后采取了更轻量的实现方式。see org.springframework.cloud.context.refresh.ContextRefresher

@DanielLiu1123 收到,感谢大佬的指点🙏,感谢!

我现在正在看spring源码,还不确定您说的springBoot2.4之后采取更轻量的实现是具体有多轻量,不知是否可以做到服务在eureka注册中心上不重启?

另外还有下面一个思路您看看如何:定义一个环境变量“SpringCloudAlibabaNacosMode”,在com.alibaba.cloud.nacos.refresh.NacosContextRefresher#registerNacosListener方法里识别下这个环境变量,默认都走您说的publishEvent RefreshEvent , 只有当这个环境变量配成指定值时,才不走publishEvent RefreshEvent,走单独用反射去更改属性值的代码逻辑分支。 这样做的目的是保证默认都走发送RefreshEvent事件,保证您说的诸如Sentinel CircuitBreakerRuleChangeListener, Gateway `RouteRefreshListener这样的功能不受影响,但是当服务没有用到这些功能,服务用nacos配置都只是在业务代码里做业务配置用的时候, 则可以给使用者一个选择渠道,选择切到不触发RefreshEvent事件,用最轻量的反射更改属性值的方式, 您看这个思路如何~?

fuxiuzhan commented 2 years ago

可以结合nacos和apollo的优势,使用Apollo的方式管理@value注解,使用事件机制管理@ConfigurationProperties,但是不用RefreshEvent事件,这会引起通过新建spring容器使RefreshScope的bean重建,代价比较大,可以使用EnvironmentChangeEvent 事件触发只重新绑定properties就行了。有个比较完善并且可实际使用的例子,并且完美支持配置加解密:https://github.com/fuxiuzhan/fuled-component/tree/master/fuled-config-starter

fuxiuzhan commented 2 years ago

其实nacos本身是依赖RefreshEvent事件来完成配置的更新的,nacos-config每次有RefreshEvent触发时要把所有配置全部请求一遍,把新的配置加到spring的environment 中。如果不触发RefreshEvent事件,nacos只能知道变更了什么东西,但无法把变更加到environment中,变量重新绑定拿到的依然是旧值。每次变更全量请求这块代价也是比较大的。在上边提到的项目中做到了变更了那个值就精确的更新那个值,然后只刷新对应的@value和properties,更快速且代价非常小。

liqi19950722 commented 2 years ago

其实这个项目是可以支持局部刷新的,不知道为啥没引入 nacos-spring-project issue bilibili介绍 4年前就没听明白,现在也还是不明白。。。

目的可能是这样:

  1. RefreshEvent 是以SpringCloud的方式来刷新应用配置
  2. nacos-spring-project 单独刷新字段或者Bean配置 在项目中同时引入,各取所需。
liqi19950722 commented 1 year ago

其实这个项目是可以支持局部刷新的,不知道为啥没引入 nacos-spring-project issue bilibili介绍 4年前就没听明白,现在也还是不明白。。。

目的可能是这样:

  1. RefreshEvent 是以SpringCloud的方式来刷新应用配置
  2. nacos-spring-project 单独刷新字段或者Bean配置 在项目中同时引入,各取所需。

利用 nacos-spring-project 实现了一下 https://github.com/liqi19950722/demo/

DanielLiu1123 commented 1 year ago

SCA is not suitable for implementing this feature, bring uncertain risk.

kimmking commented 1 year ago

其实这个项目是可以支持局部刷新的,不知道为啥没引入 nacos-spring-project issue bilibili介绍 4年前就没听明白,现在也还是不明白。。。 目的可能是这样:

  1. RefreshEvent 是以SpringCloud的方式来刷新应用配置
  2. nacos-spring-project 单独刷新字段或者Bean配置 在项目中同时引入,各取所需。

利用 nacos-spring-project 实现了一下 https://github.com/liqi19950722/demo/

L47 clear method is not safe.